From af3d6c96ee82e85b30d7e45bc867d499aaec0eac Mon Sep 17 00:00:00 2001 From: sindoring Date: Sun, 2 Aug 2026 18:58:14 +0400 Subject: [PATCH] Update vendored mod-playerbots to ba46fcd --- .../.github/workflows/check_pr_source.yml | 1 + .../.github/workflows/codestyle_cpp.yml | 18 +- .../.github/workflows/core_build.yml | 2 +- .../workflows/label_translation-pr.yml | 42 + .../.github/workflows/macos_build.yml | 2 +- .../.github/workflows/windows_build.yml | 139 +- modules/mod-playerbots/AUTHORS.md | 121 + modules/mod-playerbots/LICENSE | 884 +- .../mod-playerbots/PULL_REQUEST_TEMPLATE.md | 178 +- modules/mod-playerbots/README.md | 52 +- modules/mod-playerbots/banner.png | Bin 0 -> 208820 bytes .../mod-playerbots/conf/playerbots.conf.dist | 805 +- .../playerbots/base/ai_playerbot_texts.sql | 3740 +++---- ..._ai_playerbot_multi_pvp_text_variables.sql | 39 +- .../2026_02_13_00_ai_playerbot_texts.sql | 1 + ..._00_ai_playerbot_wait_for_attack_texts.sql | 104 + .../2026_03_26_ai_playerbot_logout_texts.sql | 59 + ...03_28_00_ai_playerbot_focus_heal_texts.sql | 240 + .../2026_04_12_00_ai_playerbot_pull_texts.sql | 102 + .../2026_04_28_00_playerbots_bis_gear.sql | 7972 ++++++++++++++ .../2026_04_28_01_ai_playerbot_bis_texts.sql | 183 + ...2026_05_19_00_ai_playerbot_audit_texts.sql | 489 + ...0_ai_playerbot_send_mail_disabled_text.sql | 13 + ...i_playerbot_missing_group_buff_reagent.sql | 29 + ...26_06_19_00_ai_playerbot_cache_cleanup.sql | 5 + modules/mod-playerbots/icon.png | Bin 208820 -> 127630 bytes .../src/Ai/Base/ActionContext.h | 40 +- .../AcceptBattlegroundInvitationAction.cpp | 5 +- .../AcceptBattlegroundInvitationAction.h | 9 +- .../src/Ai/Base/Actions/AcceptDuelAction.cpp | 5 +- .../src/Ai/Base/Actions/AcceptDuelAction.h | 9 +- .../Base/Actions/AcceptInvitationAction.cpp | 8 +- .../Ai/Base/Actions/AcceptInvitationAction.h | 9 +- .../src/Ai/Base/Actions/AcceptQuestAction.cpp | 21 +- .../src/Ai/Base/Actions/AcceptQuestAction.h | 9 +- .../Ai/Base/Actions/AcceptResurrectAction.cpp | 5 +- .../Ai/Base/Actions/AcceptResurrectAction.h | 9 +- .../src/Ai/Base/Actions/AddLootAction.cpp | 5 +- .../src/Ai/Base/Actions/AddLootAction.h | 9 +- .../src/Ai/Base/Actions/AreaTriggerAction.cpp | 14 +- .../src/Ai/Base/Actions/AreaTriggerAction.h | 9 +- .../src/Ai/Base/Actions/ArenaTeamActions.cpp | 14 +- .../src/Ai/Base/Actions/ArenaTeamActions.h | 9 +- .../src/Ai/Base/Actions/AttackAction.cpp | 90 +- .../src/Ai/Base/Actions/AttackAction.h | 9 +- .../AutoMaintenanceOnLevelupAction.cpp | 49 +- .../Actions/AutoMaintenanceOnLevelupAction.h | 9 +- .../src/Ai/Base/Actions/BankAction.cpp | 11 +- .../src/Ai/Base/Actions/BankAction.h | 9 +- .../Base/Actions/BattleGroundJoinAction.cpp | 30 +- .../Ai/Base/Actions/BattleGroundJoinAction.h | 9 +- .../Ai/Base/Actions/BattleGroundTactics.cpp | 15 +- .../src/Ai/Base/Actions/BattleGroundTactics.h | 9 +- .../src/Ai/Base/Actions/BossAuraActions.cpp | 5 +- .../src/Ai/Base/Actions/BossAuraActions.h | 9 +- .../src/Ai/Base/Actions/BuffAction.cpp | 5 +- .../src/Ai/Base/Actions/BuffAction.h | 9 +- .../src/Ai/Base/Actions/BuyAction.cpp | 13 +- .../src/Ai/Base/Actions/BuyAction.h | 9 +- .../Ai/Base/Actions/CancelChannelAction.cpp | 5 +- .../src/Ai/Base/Actions/CancelChannelAction.h | 9 +- .../Ai/Base/Actions/CastCustomSpellAction.cpp | 16 +- .../Ai/Base/Actions/CastCustomSpellAction.h | 13 +- .../src/Ai/Base/Actions/ChangeChatAction.cpp | 5 +- .../src/Ai/Base/Actions/ChangeChatAction.h | 9 +- .../Ai/Base/Actions/ChangeStrategyAction.cpp | 71 +- .../Ai/Base/Actions/ChangeStrategyAction.h | 9 +- .../Ai/Base/Actions/ChangeTalentsAction.cpp | 5 +- .../src/Ai/Base/Actions/ChangeTalentsAction.h | 9 +- .../Ai/Base/Actions/ChatShortcutActions.cpp | 55 +- .../src/Ai/Base/Actions/ChatShortcutActions.h | 16 +- .../src/Ai/Base/Actions/CheatAction.cpp | 5 +- .../src/Ai/Base/Actions/CheatAction.h | 10 +- .../src/Ai/Base/Actions/CheckMailAction.cpp | 5 +- .../src/Ai/Base/Actions/CheckMailAction.h | 9 +- .../Ai/Base/Actions/CheckMountStateAction.cpp | 142 +- .../Ai/Base/Actions/CheckMountStateAction.h | 14 +- .../src/Ai/Base/Actions/CheckValuesAction.cpp | 5 +- .../src/Ai/Base/Actions/CheckValuesAction.h | 9 +- .../Ai/Base/Actions/ChooseRpgTargetAction.cpp | 14 +- .../Ai/Base/Actions/ChooseRpgTargetAction.h | 9 +- .../Ai/Base/Actions/ChooseTargetActions.cpp | 13 +- .../src/Ai/Base/Actions/ChooseTargetActions.h | 18 +- .../Base/Actions/ChooseTravelTargetAction.cpp | 22 +- .../Base/Actions/ChooseTravelTargetAction.h | 9 +- .../src/Ai/Base/Actions/CombatActions.cpp | 5 +- .../src/Ai/Base/Actions/CombatActions.h | 9 +- .../Base/Actions/CustomStrategyEditAction.cpp | 5 +- .../Base/Actions/CustomStrategyEditAction.h | 9 +- .../src/Ai/Base/Actions/DebugAction.cpp | 9 +- .../src/Ai/Base/Actions/DebugAction.h | 9 +- .../src/Ai/Base/Actions/DelayAction.cpp | 5 +- .../src/Ai/Base/Actions/DelayAction.h | 9 +- .../src/Ai/Base/Actions/DestroyItemAction.cpp | 5 +- .../src/Ai/Base/Actions/DestroyItemAction.h | 9 +- .../src/Ai/Base/Actions/DropQuestAction.cpp | 86 +- .../src/Ai/Base/Actions/DropQuestAction.h | 9 +- .../src/Ai/Base/Actions/EmoteAction.cpp | 5 +- .../src/Ai/Base/Actions/EmoteAction.h | 9 +- .../src/Ai/Base/Actions/EquipAction.cpp | 15 +- .../src/Ai/Base/Actions/EquipAction.h | 9 +- .../src/Ai/Base/Actions/EquipGlyphsAction.cpp | 5 +- .../src/Ai/Base/Actions/EquipGlyphsAction.h | 9 +- .../src/Ai/Base/Actions/FishingAction.cpp | 9 +- .../src/Ai/Base/Actions/FishingAction.h | 9 +- .../src/Ai/Base/Actions/FlagAction.cpp | 5 +- .../src/Ai/Base/Actions/FlagAction.h | 9 +- .../src/Ai/Base/Actions/FollowActions.cpp | 197 +- .../src/Ai/Base/Actions/FollowActions.h | 9 +- .../src/Ai/Base/Actions/GenericActions.cpp | 16 +- .../src/Ai/Base/Actions/GenericActions.h | 9 +- .../src/Ai/Base/Actions/GenericBuffUtils.cpp | 145 - .../src/Ai/Base/Actions/GenericBuffUtils.h | 63 - .../Ai/Base/Actions/GenericSpellActions.cpp | 444 +- .../src/Ai/Base/Actions/GenericSpellActions.h | 123 +- .../src/Ai/Base/Actions/GiveItemAction.cpp | 7 +- .../src/Ai/Base/Actions/GiveItemAction.h | 9 +- .../src/Ai/Base/Actions/GoAction.cpp | 5 +- .../src/Ai/Base/Actions/GoAction.h | 9 +- .../src/Ai/Base/Actions/GossipHelloAction.cpp | 5 +- .../src/Ai/Base/Actions/GossipHelloAction.h | 9 +- .../src/Ai/Base/Actions/GreetAction.cpp | 5 +- .../src/Ai/Base/Actions/GreetAction.h | 9 +- .../src/Ai/Base/Actions/GuildAcceptAction.cpp | 15 +- .../src/Ai/Base/Actions/GuildAcceptAction.h | 9 +- .../src/Ai/Base/Actions/GuildBankAction.cpp | 7 +- .../src/Ai/Base/Actions/GuildBankAction.h | 9 +- .../Ai/Base/Actions/GuildCreateActions.cpp | 10 +- .../src/Ai/Base/Actions/GuildCreateActions.h | 9 +- .../Base/Actions/GuildManagementActions.cpp | 5 +- .../Ai/Base/Actions/GuildManagementActions.h | 9 +- .../src/Ai/Base/Actions/HelpAction.cpp | 5 +- .../src/Ai/Base/Actions/HelpAction.h | 9 +- .../src/Ai/Base/Actions/HireAction.cpp | 5 +- .../src/Ai/Base/Actions/HireAction.h | 9 +- .../src/Ai/Base/Actions/ImbueAction.cpp | 5 +- .../src/Ai/Base/Actions/ImbueAction.h | 9 +- .../src/Ai/Base/Actions/InventoryAction.cpp | 43 +- .../src/Ai/Base/Actions/InventoryAction.h | 9 +- .../Actions/InventoryChangeFailureAction.cpp | 5 +- .../Actions/InventoryChangeFailureAction.h | 9 +- .../Ai/Base/Actions/InviteToGroupAction.cpp | 27 +- .../src/Ai/Base/Actions/InviteToGroupAction.h | 18 +- .../src/Ai/Base/Actions/LeaveGroupAction.cpp | 12 +- .../src/Ai/Base/Actions/LeaveGroupAction.h | 9 +- .../src/Ai/Base/Actions/LfgActions.cpp | 10 +- .../src/Ai/Base/Actions/LfgActions.h | 9 +- .../src/Ai/Base/Actions/ListQuestsActions.cpp | 5 +- .../src/Ai/Base/Actions/ListQuestsActions.h | 9 +- .../src/Ai/Base/Actions/ListSpellsAction.cpp | 5 +- .../src/Ai/Base/Actions/ListSpellsAction.h | 9 +- .../src/Ai/Base/Actions/LogLevelAction.cpp | 5 +- .../src/Ai/Base/Actions/LogLevelAction.h | 9 +- .../src/Ai/Base/Actions/LootAction.cpp | 3 +- .../src/Ai/Base/Actions/LootAction.h | 9 +- .../src/Ai/Base/Actions/LootRollAction.cpp | 85 +- .../src/Ai/Base/Actions/LootRollAction.h | 11 +- .../Ai/Base/Actions/LootStrategyAction.cpp | 6 +- .../src/Ai/Base/Actions/LootStrategyAction.h | 9 +- .../src/Ai/Base/Actions/MailAction.cpp | 15 +- .../src/Ai/Base/Actions/MailAction.h | 9 +- .../Ai/Base/Actions/MoveToRpgTargetAction.cpp | 5 +- .../Ai/Base/Actions/MoveToRpgTargetAction.h | 9 +- .../Base/Actions/MoveToTravelTargetAction.cpp | 22 +- .../Base/Actions/MoveToTravelTargetAction.h | 9 +- .../src/Ai/Base/Actions/MovementActions.cpp | 121 +- .../src/Ai/Base/Actions/MovementActions.h | 17 +- .../src/Ai/Base/Actions/NonCombatActions.cpp | 58 +- .../src/Ai/Base/Actions/NonCombatActions.h | 9 +- .../src/Ai/Base/Actions/OpenItemAction.cpp | 6 + .../src/Ai/Base/Actions/OpenItemAction.h | 9 +- .../src/Ai/Base/Actions/OutfitAction.cpp | 69 +- .../src/Ai/Base/Actions/OutfitAction.h | 9 +- .../Actions/PassLeadershipToMasterAction.cpp | 7 +- .../Actions/PassLeadershipToMasterAction.h | 9 +- .../Ai/Base/Actions/PetitionSignAction.cpp | 5 +- .../src/Ai/Base/Actions/PetitionSignAction.h | 9 +- .../src/Ai/Base/Actions/PetsAction.cpp | 5 +- .../src/Ai/Base/Actions/PetsAction.h | 10 +- .../src/Ai/Base/Actions/PositionAction.cpp | 5 +- .../src/Ai/Base/Actions/PositionAction.h | 9 +- .../src/Ai/Base/Actions/PullActions.cpp | 331 + .../src/Ai/Base/Actions/PullActions.h | 100 + .../Ai/Base/Actions/QueryItemUsageAction.cpp | 5 +- .../Ai/Base/Actions/QueryItemUsageAction.h | 9 +- .../src/Ai/Base/Actions/QueryQuestAction.cpp | 6 +- .../src/Ai/Base/Actions/QueryQuestAction.h | 9 +- .../src/Ai/Base/Actions/QuestAction.cpp | 12 +- .../src/Ai/Base/Actions/QuestAction.h | 9 +- .../Base/Actions/QuestConfirmAcceptAction.cpp | 11 +- .../Base/Actions/QuestConfirmAcceptAction.h | 9 +- .../Ai/Base/Actions/RandomBotUpdateAction.cpp | 5 +- .../Ai/Base/Actions/RandomBotUpdateAction.h | 9 +- .../src/Ai/Base/Actions/RangeAction.cpp | 5 +- .../src/Ai/Base/Actions/RangeAction.h | 9 +- .../Ai/Base/Actions/ReachTargetActions.cpp | 5 +- .../src/Ai/Base/Actions/ReachTargetActions.h | 9 +- .../src/Ai/Base/Actions/ReadyCheckAction.cpp | 17 +- .../src/Ai/Base/Actions/ReadyCheckAction.h | 9 +- .../Ai/Base/Actions/ReleaseSpiritAction.cpp | 17 +- .../src/Ai/Base/Actions/ReleaseSpiritAction.h | 11 +- .../Ai/Base/Actions/RememberTaxiAction.cpp | 7 +- .../src/Ai/Base/Actions/RememberTaxiAction.h | 9 +- .../src/Ai/Base/Actions/RemoveAuraAction.cpp | 5 +- .../src/Ai/Base/Actions/RemoveAuraAction.h | 9 +- .../src/Ai/Base/Actions/RepairAllAction.cpp | 5 +- .../src/Ai/Base/Actions/RepairAllAction.h | 9 +- .../src/Ai/Base/Actions/ResetAiAction.cpp | 20 +- .../src/Ai/Base/Actions/ResetAiAction.h | 9 +- .../Ai/Base/Actions/ResetInstancesAction.cpp | 12 +- .../Ai/Base/Actions/ResetInstancesAction.h | 9 +- .../Actions/RevealGatheringItemAction.cpp | 5 +- .../Base/Actions/RevealGatheringItemAction.h | 9 +- .../Base/Actions/ReviveFromCorpseAction.cpp | 12 +- .../Ai/Base/Actions/ReviveFromCorpseAction.h | 9 +- .../src/Ai/Base/Actions/RewardAction.cpp | 5 +- .../src/Ai/Base/Actions/RewardAction.h | 9 +- .../src/Ai/Base/Actions/RtiAction.cpp | 5 +- .../src/Ai/Base/Actions/RtiAction.h | 9 +- .../src/Ai/Base/Actions/RtscAction.cpp | 5 +- .../src/Ai/Base/Actions/RtscAction.h | 9 +- .../src/Ai/Base/Actions/SaveManaAction.cpp | 5 +- .../src/Ai/Base/Actions/SaveManaAction.h | 9 +- .../src/Ai/Base/Actions/SayAction.cpp | 16 +- .../src/Ai/Base/Actions/SayAction.h | 17 +- .../Ai/Base/Actions/SecurityCheckAction.cpp | 11 +- .../src/Ai/Base/Actions/SecurityCheckAction.h | 9 +- .../src/Ai/Base/Actions/SeeSpellAction.cpp | 7 +- .../src/Ai/Base/Actions/SeeSpellAction.h | 9 +- .../src/Ai/Base/Actions/SellAction.cpp | 5 +- .../src/Ai/Base/Actions/SellAction.h | 9 +- .../src/Ai/Base/Actions/SendMailAction.cpp | 59 +- .../src/Ai/Base/Actions/SendMailAction.h | 9 +- .../src/Ai/Base/Actions/SetCraftAction.cpp | 39 +- .../src/Ai/Base/Actions/SetCraftAction.h | 9 +- .../Actions/SetFocusHealTargetsAction.cpp | 220 + .../Base/Actions/SetFocusHealTargetsAction.h | 22 + .../src/Ai/Base/Actions/SetHomeAction.cpp | 31 +- .../src/Ai/Base/Actions/SetHomeAction.h | 9 +- .../src/Ai/Base/Actions/ShareQuestAction.cpp | 28 +- .../src/Ai/Base/Actions/ShareQuestAction.h | 9 +- .../Ai/Base/Actions/SkipSpellsListAction.cpp | 5 +- .../Ai/Base/Actions/SkipSpellsListAction.h | 9 +- .../src/Ai/Base/Actions/StatsAction.cpp | 5 +- .../src/Ai/Base/Actions/StatsAction.h | 9 +- .../src/Ai/Base/Actions/StayActions.cpp | 6 +- .../src/Ai/Base/Actions/StayActions.h | 9 +- .../Ai/Base/Actions/SuggestWhatToDoAction.cpp | 8 +- .../Ai/Base/Actions/SuggestWhatToDoAction.h | 9 +- .../Base/Actions/TalkToQuestGiverAction.cpp | 9 +- .../Ai/Base/Actions/TalkToQuestGiverAction.h | 9 +- .../src/Ai/Base/Actions/TameAction.cpp | 103 +- .../src/Ai/Base/Actions/TameAction.h | 9 +- .../src/Ai/Base/Actions/TaxiAction.cpp | 35 +- .../src/Ai/Base/Actions/TaxiAction.h | 9 +- .../src/Ai/Base/Actions/TeleportAction.cpp | 5 +- .../src/Ai/Base/Actions/TeleportAction.h | 9 +- .../Ai/Base/Actions/TellCastFailedAction.cpp | 5 +- .../Ai/Base/Actions/TellCastFailedAction.h | 9 +- .../src/Ai/Base/Actions/TellEmblemsAction.cpp | 40 + .../src/Ai/Base/Actions/TellEmblemsAction.h | 22 + .../src/Ai/Base/Actions/TellGlyphsAction.cpp | 5 +- .../src/Ai/Base/Actions/TellGlyphsAction.h | 9 +- .../Ai/Base/Actions/TellItemCountAction.cpp | 5 +- .../src/Ai/Base/Actions/TellItemCountAction.h | 9 +- .../src/Ai/Base/Actions/TellLosAction.cpp | 6 +- .../src/Ai/Base/Actions/TellLosAction.h | 9 +- .../src/Ai/Base/Actions/TellMasterAction.cpp | 5 +- .../src/Ai/Base/Actions/TellMasterAction.h | 9 +- .../Ai/Base/Actions/TellPvpStatsAction.cpp | 5 +- .../src/Ai/Base/Actions/TellPvpStatsAction.h | 11 +- .../Ai/Base/Actions/TellReputationAction.cpp | 94 +- .../Ai/Base/Actions/TellReputationAction.h | 15 +- .../src/Ai/Base/Actions/TellTargetAction.cpp | 22 +- .../src/Ai/Base/Actions/TellTargetAction.h | 9 +- .../src/Ai/Base/Actions/TradeAction.cpp | 7 +- .../src/Ai/Base/Actions/TradeAction.h | 9 +- .../src/Ai/Base/Actions/TradeStatusAction.cpp | 74 +- .../src/Ai/Base/Actions/TradeStatusAction.h | 9 +- .../Actions/TradeStatusExtendedAction.cpp | 11 +- .../Base/Actions/TradeStatusExtendedAction.h | 10 +- .../src/Ai/Base/Actions/TradeValues.cpp | 5 +- .../src/Ai/Base/Actions/TradeValues.h | 9 +- .../src/Ai/Base/Actions/TrainerAction.cpp | 463 +- .../src/Ai/Base/Actions/TrainerAction.h | 29 +- .../src/Ai/Base/Actions/TravelAction.cpp | 11 +- .../src/Ai/Base/Actions/TravelAction.h | 9 +- .../src/Ai/Base/Actions/UnequipAction.cpp | 5 +- .../src/Ai/Base/Actions/UnequipAction.h | 9 +- .../src/Ai/Base/Actions/UnlockItemAction.cpp | 6 + .../src/Ai/Base/Actions/UnlockItemAction.h | 10 +- .../Base/Actions/UnlockTradedItemAction.cpp | 6 + .../Ai/Base/Actions/UnlockTradedItemAction.h | 10 +- .../src/Ai/Base/Actions/UseItemAction.cpp | 66 +- .../src/Ai/Base/Actions/UseItemAction.h | 9 +- .../Ai/Base/Actions/UseMeetingStoneAction.cpp | 61 +- .../Ai/Base/Actions/UseMeetingStoneAction.h | 9 +- .../src/Ai/Base/Actions/VehicleActions.cpp | 5 +- .../src/Ai/Base/Actions/VehicleActions.h | 9 +- .../Ai/Base/Actions/WaitForAttackAction.cpp | 184 + .../src/Ai/Base/Actions/WaitForAttackAction.h | 41 + .../src/Ai/Base/Actions/WhoAction.cpp | 7 +- .../src/Ai/Base/Actions/WhoAction.h | 9 +- .../src/Ai/Base/Actions/WipeAction.cpp | 11 +- .../src/Ai/Base/Actions/WipeAction.h | 9 +- .../src/Ai/Base/Actions/WorldBuffAction.cpp | 5 +- .../src/Ai/Base/Actions/WorldBuffAction.h | 9 +- .../src/Ai/Base/Actions/WtsAction.cpp | 5 +- .../src/Ai/Base/Actions/WtsAction.h | 9 +- .../src/Ai/Base/Actions/XpGainAction.cpp | 5 +- .../src/Ai/Base/Actions/XpGainAction.h | 9 +- .../src/Ai/Base/ChatActionContext.h | 27 +- .../src/Ai/Base/ChatTriggerContext.h | 39 +- .../src/Ai/Base/SharedValueContext.h | 11 +- .../Ai/Base/Strategy/AggressiveStrategy.cpp | 21 + .../src/Ai/Base/Strategy/AggressiveStrategy.h | 23 + .../Strategy/AttackEnemyPlayersStrategy.cpp | 5 +- .../Strategy/AttackEnemyPlayersStrategy.h | 9 +- .../Ai/Base/Strategy/BattlegroundStrategy.cpp | 7 +- .../Ai/Base/Strategy/BattlegroundStrategy.h | 13 +- .../src/Ai/Base/Strategy/CastTimeStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/CastTimeStrategy.h | 9 +- .../Strategy/ChatCommandHandlerStrategy.cpp | 138 +- .../Strategy/ChatCommandHandlerStrategy.h | 13 +- .../src/Ai/Base/Strategy/CombatStrategy.cpp | 11 +- .../src/Ai/Base/Strategy/CombatStrategy.h | 9 +- .../Ai/Base/Strategy/ConserveManaStrategy.cpp | 5 +- .../Ai/Base/Strategy/ConserveManaStrategy.h | 9 +- .../src/Ai/Base/Strategy/DeadStrategy.cpp | 9 +- .../src/Ai/Base/Strategy/DeadStrategy.h | 13 +- .../src/Ai/Base/Strategy/DebugStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/DebugStrategy.h | 9 +- .../Ai/Base/Strategy/DpsAssistStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/DpsAssistStrategy.h | 9 +- .../src/Ai/Base/Strategy/DuelStrategy.cpp | 11 +- .../src/Ai/Base/Strategy/DuelStrategy.h | 13 +- .../src/Ai/Base/Strategy/EmoteStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/EmoteStrategy.h | 9 +- .../src/Ai/Base/Strategy/FleeStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/FleeStrategy.h | 9 +- .../Ai/Base/Strategy/FocusTargetStrategy.h | 21 + .../Ai/Base/Strategy/FollowMasterStrategy.cpp | 7 +- .../Ai/Base/Strategy/FollowMasterStrategy.h | 9 +- .../src/Ai/Base/Strategy/GrindingStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/GrindingStrategy.h | 9 +- .../src/Ai/Base/Strategy/GroupStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/GroupStrategy.h | 9 +- .../src/Ai/Base/Strategy/GuardStrategy.cpp | 7 +- .../src/Ai/Base/Strategy/GuardStrategy.h | 9 +- .../src/Ai/Base/Strategy/GuildStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/GuildStrategy.h | 9 +- .../src/Ai/Base/Strategy/KiteStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/KiteStrategy.h | 9 +- .../src/Ai/Base/Strategy/LfgStrategy.cpp | 7 +- .../src/Ai/Base/Strategy/LfgStrategy.h | 13 +- .../Base/Strategy/LootNonCombatStrategy.cpp | 5 +- .../Ai/Base/Strategy/LootNonCombatStrategy.h | 9 +- .../Ai/Base/Strategy/MaintenanceStrategy.cpp | 7 +- .../Ai/Base/Strategy/MaintenanceStrategy.h | 9 +- .../src/Ai/Base/Strategy/MarkRtiStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/MarkRtiStrategy.h | 9 +- .../Ai/Base/Strategy/MeleeCombatStrategy.cpp | 5 +- .../Ai/Base/Strategy/MeleeCombatStrategy.h | 9 +- .../Base/Strategy/MoveFromGroupStrategy.cpp | 5 +- .../Ai/Base/Strategy/MoveFromGroupStrategy.h | 9 +- .../Ai/Base/Strategy/NonCombatStrategy.cpp | 7 +- .../src/Ai/Base/Strategy/NonCombatStrategy.h | 11 +- .../Ai/Base/Strategy/PassThroughStrategy.cpp | 16 + .../Ai/Base/Strategy/PassThroughStrategy.h | 26 + .../Ai/Base/Strategy/PassTroughStrategy.cpp | 15 - .../src/Ai/Base/Strategy/PassTroughStrategy.h | 25 - .../src/Ai/Base/Strategy/PassiveStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/PassiveStrategy.h | 9 +- .../src/Ai/Base/Strategy/PullStrategy.cpp | 240 +- .../src/Ai/Base/Strategy/PullStrategy.h | 76 +- .../src/Ai/Base/Strategy/QuestStrategies.cpp | 9 +- .../src/Ai/Base/Strategy/QuestStrategies.h | 13 +- .../src/Ai/Base/Strategy/RTSCStrategy.cpp | 7 +- .../src/Ai/Base/Strategy/RTSCStrategy.h | 9 +- .../src/Ai/Base/Strategy/RacialsStrategy.cpp | 128 +- .../src/Ai/Base/Strategy/RacialsStrategy.h | 9 +- .../Ai/Base/Strategy/RangedCombatStrategy.cpp | 5 +- .../Ai/Base/Strategy/RangedCombatStrategy.h | 9 +- .../src/Ai/Base/Strategy/ReturnStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/ReturnStrategy.h | 9 +- .../src/Ai/Base/Strategy/RunawayStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/RunawayStrategy.h | 9 +- .../src/Ai/Base/Strategy/SayStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/SayStrategy.h | 9 +- .../src/Ai/Base/Strategy/StayStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/StayStrategy.h | 9 +- .../Ai/Base/Strategy/TankAssistStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/TankAssistStrategy.h | 9 +- .../Ai/Base/Strategy/TellTargetStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/TellTargetStrategy.h | 9 +- .../src/Ai/Base/Strategy/ThreatStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/ThreatStrategy.h | 9 +- .../src/Ai/Base/Strategy/TravelStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/TravelStrategy.h | 9 +- .../src/Ai/Base/Strategy/UseFoodStrategy.cpp | 5 +- .../src/Ai/Base/Strategy/UseFoodStrategy.h | 9 +- .../Ai/Base/Strategy/UsePotionsStrategy.cpp | 9 +- .../src/Ai/Base/Strategy/UsePotionsStrategy.h | 9 +- .../Base/Strategy/WaitForAttackStrategy.cpp | 95 + .../Ai/Base/Strategy/WaitForAttackStrategy.h | 40 + .../Strategy/WorldPacketHandlerStrategy.cpp | 11 +- .../Strategy/WorldPacketHandlerStrategy.h | 17 +- .../src/Ai/Base/StrategyContext.h | 20 +- .../src/Ai/Base/Trigger/BossAuraTriggers.cpp | 22 +- .../src/Ai/Base/Trigger/BossAuraTriggers.h | 9 +- .../Ai/Base/Trigger/ChatCommandTrigger.cpp | 5 +- .../src/Ai/Base/Trigger/ChatCommandTrigger.h | 9 +- .../src/Ai/Base/Trigger/CureTriggers.cpp | 5 +- .../src/Ai/Base/Trigger/CureTriggers.h | 9 +- .../src/Ai/Base/Trigger/FishingTriggers.cpp | 5 +- .../src/Ai/Base/Trigger/FishingTriggers.h | 9 +- .../src/Ai/Base/Trigger/GenericTriggers.cpp | 207 +- .../src/Ai/Base/Trigger/GenericTriggers.h | 153 +- .../src/Ai/Base/Trigger/GuildTriggers.cpp | 14 +- .../src/Ai/Base/Trigger/GuildTriggers.h | 9 +- .../src/Ai/Base/Trigger/HealthTriggers.cpp | 14 +- .../src/Ai/Base/Trigger/HealthTriggers.h | 18 +- .../src/Ai/Base/Trigger/LfgTriggers.cpp | 5 +- .../src/Ai/Base/Trigger/LfgTriggers.h | 9 +- .../src/Ai/Base/Trigger/LootTriggers.cpp | 21 +- .../src/Ai/Base/Trigger/LootTriggers.h | 9 +- .../src/Ai/Base/Trigger/PullTriggers.cpp | 72 + .../src/Ai/Base/Trigger/PullTriggers.h | 45 + .../src/Ai/Base/Trigger/PvpTriggers.cpp | 7 +- .../src/Ai/Base/Trigger/PvpTriggers.h | 9 +- .../src/Ai/Base/Trigger/RangeTriggers.cpp | 10 +- .../src/Ai/Base/Trigger/RangeTriggers.h | 9 +- .../src/Ai/Base/Trigger/RpgTriggers.cpp | 52 +- .../src/Ai/Base/Trigger/RpgTriggers.h | 11 +- .../src/Ai/Base/Trigger/RtiTriggers.cpp | 23 +- .../src/Ai/Base/Trigger/RtiTriggers.h | 18 +- .../src/Ai/Base/Trigger/StuckTriggers.cpp | 10 +- .../src/Ai/Base/Trigger/StuckTriggers.h | 9 +- .../src/Ai/Base/Trigger/TravelTriggers.cpp | 5 +- .../src/Ai/Base/Trigger/TravelTriggers.h | 9 +- .../Ai/Base/Trigger/WaitForAttackTriggers.h | 50 + .../src/Ai/Base/Trigger/WithinAreaTrigger.cpp | 5 +- .../src/Ai/Base/Trigger/WithinAreaTrigger.h | 9 +- .../Ai/Base/Trigger/WorldPacketTrigger.cpp | 5 +- .../src/Ai/Base/Trigger/WorldPacketTrigger.h | 9 +- .../src/Ai/Base/TriggerContext.h | 41 +- .../src/Ai/Base/Util/GenericBuffUtils.cpp | 324 + .../src/Ai/Base/Util/GenericBuffUtils.h | 59 + .../src/Ai/Base/Value/ActiveSpellValue.cpp | 5 +- .../src/Ai/Base/Value/ActiveSpellValue.h | 9 +- .../Ai/Base/Value/AggressiveTargetValue.cpp | 67 + .../src/Ai/Base/Value/AggressiveTargetValue.h | 23 + .../src/Ai/Base/Value/AlwaysLootListValue.cpp | 5 +- .../src/Ai/Base/Value/AlwaysLootListValue.h | 9 +- .../src/Ai/Base/Value/AoeHealValues.cpp | 5 +- .../src/Ai/Base/Value/AoeHealValues.h | 9 +- .../src/Ai/Base/Value/AoeValues.cpp | 5 +- .../src/Ai/Base/Value/AoeValues.h | 9 +- .../src/Ai/Base/Value/Arrow.cpp | 16 +- .../mod-playerbots/src/Ai/Base/Value/Arrow.h | 16 +- .../src/Ai/Base/Value/AttackerCountValues.cpp | 7 +- .../src/Ai/Base/Value/AttackerCountValues.h | 9 +- .../Value/AttackerWithoutAuraTargetValue.cpp | 17 +- .../Value/AttackerWithoutAuraTargetValue.h | 9 +- .../src/Ai/Base/Value/AttackersValue.cpp | 57 +- .../src/Ai/Base/Value/AttackersValue.h | 9 +- .../src/Ai/Base/Value/AvailableLootValue.cpp | 5 +- .../src/Ai/Base/Value/AvailableLootValue.h | 9 +- .../src/Ai/Base/Value/BudgetValues.cpp | 63 +- .../src/Ai/Base/Value/BudgetValues.h | 9 +- .../src/Ai/Base/Value/CcTargetValue.cpp | 7 +- .../src/Ai/Base/Value/CcTargetValue.h | 9 +- .../src/Ai/Base/Value/ChatValue.h | 9 +- .../src/Ai/Base/Value/CollisionValue.cpp | 5 +- .../src/Ai/Base/Value/CollisionValue.h | 9 +- .../src/Ai/Base/Value/CraftValue.h | 14 +- .../Ai/Base/Value/CurrentCcTargetValue.cpp | 7 +- .../src/Ai/Base/Value/CurrentCcTargetValue.h | 9 +- .../src/Ai/Base/Value/CurrentTargetValue.cpp | 5 +- .../src/Ai/Base/Value/CurrentTargetValue.h | 9 +- .../src/Ai/Base/Value/DistanceValue.cpp | 5 +- .../src/Ai/Base/Value/DistanceValue.h | 9 +- .../src/Ai/Base/Value/DpsTargetValue.cpp | 96 +- .../src/Ai/Base/Value/DpsTargetValue.h | 9 +- .../src/Ai/Base/Value/DuelTargetValue.cpp | 5 +- .../src/Ai/Base/Value/DuelTargetValue.h | 9 +- .../Ai/Base/Value/EnemyHealerTargetValue.cpp | 5 +- .../Ai/Base/Value/EnemyHealerTargetValue.h | 9 +- .../src/Ai/Base/Value/EnemyPlayerValue.cpp | 39 +- .../src/Ai/Base/Value/EnemyPlayerValue.h | 9 +- .../Ai/Base/Value/EstimatedLifetimeValue.cpp | 6 + .../Ai/Base/Value/EstimatedLifetimeValue.h | 9 +- .../src/Ai/Base/Value/FishValues.cpp | 5 +- .../src/Ai/Base/Value/FishValues.h | 9 +- .../src/Ai/Base/Value/Formations.cpp | 5 +- .../src/Ai/Base/Value/Formations.h | 9 +- .../src/Ai/Base/Value/GrindTargetValue.cpp | 9 +- .../src/Ai/Base/Value/GrindTargetValue.h | 9 +- .../src/Ai/Base/Value/GroupLeaderValue.cpp | 5 +- .../src/Ai/Base/Value/GroupLeaderValue.h | 9 +- .../src/Ai/Base/Value/GroupValues.cpp | 5 +- .../src/Ai/Base/Value/GroupValues.h | 10 +- .../src/Ai/Base/Value/GuildValues.cpp | 5 +- .../src/Ai/Base/Value/GuildValues.h | 9 +- .../Ai/Base/Value/HasAvailableLootValue.cpp | 5 +- .../src/Ai/Base/Value/HasAvailableLootValue.h | 9 +- .../src/Ai/Base/Value/HasTotemValue.cpp | 5 +- .../src/Ai/Base/Value/HasTotemValue.h | 9 +- .../src/Ai/Base/Value/InvalidTargetValue.cpp | 5 +- .../src/Ai/Base/Value/InvalidTargetValue.h | 9 +- .../src/Ai/Base/Value/IsBehindValue.cpp | 5 +- .../src/Ai/Base/Value/IsBehindValue.h | 9 +- .../src/Ai/Base/Value/IsFacingValue.cpp | 5 +- .../src/Ai/Base/Value/IsFacingValue.h | 9 +- .../src/Ai/Base/Value/IsMovingValue.cpp | 5 +- .../src/Ai/Base/Value/IsMovingValue.h | 9 +- .../src/Ai/Base/Value/ItemCountValue.cpp | 7 +- .../src/Ai/Base/Value/ItemCountValue.h | 11 +- .../src/Ai/Base/Value/ItemForSpellValue.cpp | 22 +- .../src/Ai/Base/Value/ItemForSpellValue.h | 9 +- .../src/Ai/Base/Value/ItemUsageValue.cpp | 102 +- .../src/Ai/Base/Value/ItemUsageValue.h | 9 +- .../src/Ai/Base/Value/LastMovementValue.cpp | 9 +- .../src/Ai/Base/Value/LastMovementValue.h | 9 +- .../src/Ai/Base/Value/LastSaidValue.h | 9 +- .../Ai/Base/Value/LastSpellCastTimeValue.h | 9 +- .../src/Ai/Base/Value/LastSpellCastValue.cpp | 5 +- .../src/Ai/Base/Value/LastSpellCastValue.h | 9 +- .../src/Ai/Base/Value/LeastHpTargetValue.cpp | 11 +- .../src/Ai/Base/Value/LeastHpTargetValue.h | 9 +- .../src/Ai/Base/Value/LfgValues.h | 9 +- .../src/Ai/Base/Value/LineTargetValue.cpp | 5 +- .../src/Ai/Base/Value/LineTargetValue.h | 9 +- .../src/Ai/Base/Value/LogLevelValue.h | 9 +- .../src/Ai/Base/Value/LootStrategyValue.cpp | 7 +- .../src/Ai/Base/Value/LootStrategyValue.h | 9 +- .../src/Ai/Base/Value/LootValues.cpp | 5 +- .../src/Ai/Base/Value/LootValues.h | 9 +- .../src/Ai/Base/Value/MaintenanceValues.cpp | 10 +- .../src/Ai/Base/Value/MaintenanceValues.h | 17 +- .../src/Ai/Base/Value/ManaSaveLevelValue.h | 9 +- .../src/Ai/Base/Value/NearestAdsValue.cpp | 5 +- .../src/Ai/Base/Value/NearestAdsValue.h | 9 +- .../src/Ai/Base/Value/NearestCorpsesValue.cpp | 10 +- .../src/Ai/Base/Value/NearestCorpsesValue.h | 9 +- .../Value/NearestFriendlyPlayersValue.cpp | 5 +- .../Base/Value/NearestFriendlyPlayersValue.h | 9 +- .../src/Ai/Base/Value/NearestGameObjects.cpp | 5 +- .../src/Ai/Base/Value/NearestGameObjects.h | 9 +- .../Base/Value/NearestNonBotPlayersValue.cpp | 5 +- .../Ai/Base/Value/NearestNonBotPlayersValue.h | 9 +- .../src/Ai/Base/Value/NearestNpcsValue.cpp | 5 +- .../src/Ai/Base/Value/NearestNpcsValue.h | 9 +- .../src/Ai/Base/Value/NearestUnitsValue.cpp | 5 +- .../src/Ai/Base/Value/NearestUnitsValue.h | 9 +- .../Ai/Base/Value/NewPlayerNearbyValue.cpp | 5 +- .../src/Ai/Base/Value/NewPlayerNearbyValue.h | 9 +- .../src/Ai/Base/Value/OutfitListValue.cpp | 5 +- .../src/Ai/Base/Value/OutfitListValue.h | 9 +- .../Value/PartyMemberSnaredTargetValue.cpp | 65 + .../Base/Value/PartyMemberSnaredTargetValue.h | 25 + .../src/Ai/Base/Value/PartyMemberToDispel.cpp | 7 +- .../src/Ai/Base/Value/PartyMemberToDispel.h | 9 +- .../src/Ai/Base/Value/PartyMemberToHeal.cpp | 81 +- .../src/Ai/Base/Value/PartyMemberToHeal.h | 18 +- .../Ai/Base/Value/PartyMemberToResurrect.cpp | 5 +- .../Ai/Base/Value/PartyMemberToResurrect.h | 9 +- .../src/Ai/Base/Value/PartyMemberValue.cpp | 7 +- .../src/Ai/Base/Value/PartyMemberValue.h | 9 +- .../Value/PartyMemberWithoutAuraValue.cpp | 7 +- .../Base/Value/PartyMemberWithoutAuraValue.h | 13 +- .../Value/PartyMemberWithoutItemValue.cpp | 7 +- .../Base/Value/PartyMemberWithoutItemValue.h | 9 +- .../src/Ai/Base/Value/PetTargetValue.cpp | 5 +- .../src/Ai/Base/Value/PetTargetValue.h | 9 +- .../src/Ai/Base/Value/PositionValue.cpp | 5 +- .../src/Ai/Base/Value/PositionValue.h | 13 +- .../Ai/Base/Value/PossibleRpgTargetsValue.cpp | 35 +- .../Ai/Base/Value/PossibleRpgTargetsValue.h | 11 +- .../Ai/Base/Value/PossibleTargetsValue.cpp | 5 +- .../src/Ai/Base/Value/PossibleTargetsValue.h | 9 +- .../src/Ai/Base/Value/PvpValues.cpp | 11 +- .../src/Ai/Base/Value/PvpValues.h | 9 +- .../src/Ai/Base/Value/QuestValues.cpp | 7 +- .../src/Ai/Base/Value/QuestValues.h | 9 +- .../src/Ai/Base/Value/RTSCValues.cpp | 5 +- .../src/Ai/Base/Value/RTSCValues.h | 9 +- .../src/Ai/Base/Value/RandomBotUpdateValue.h | 9 +- .../src/Ai/Base/Value/RangeValues.cpp | 5 +- .../src/Ai/Base/Value/RangeValues.h | 9 +- .../src/Ai/Base/Value/RtiTargetValue.cpp | 5 +- .../src/Ai/Base/Value/RtiTargetValue.h | 9 +- .../src/Ai/Base/Value/RtiValue.cpp | 5 +- .../src/Ai/Base/Value/RtiValue.h | 9 +- .../src/Ai/Base/Value/SelfTargetValue.cpp | 5 +- .../src/Ai/Base/Value/SelfTargetValue.h | 9 +- .../src/Ai/Base/Value/SkipSpellsListValue.cpp | 5 +- .../src/Ai/Base/Value/SkipSpellsListValue.h | 9 +- .../src/Ai/Base/Value/SnareTargetValue.cpp | 6 +- .../src/Ai/Base/Value/SnareTargetValue.h | 9 +- .../Ai/Base/Value/SpellCastUsefulValue.cpp | 17 +- .../src/Ai/Base/Value/SpellCastUsefulValue.h | 9 +- .../src/Ai/Base/Value/SpellIdValue.cpp | 15 +- .../src/Ai/Base/Value/SpellIdValue.h | 9 +- .../src/Ai/Base/Value/Stances.cpp | 5 +- .../src/Ai/Base/Value/Stances.h | 9 +- .../src/Ai/Base/Value/StatsValues.cpp | 7 +- .../src/Ai/Base/Value/StatsValues.h | 9 +- .../src/Ai/Base/Value/TankTargetValue.cpp | 68 +- .../src/Ai/Base/Value/TankTargetValue.h | 15 +- .../src/Ai/Base/Value/TargetValue.cpp | 51 +- .../src/Ai/Base/Value/TargetValue.h | 41 +- .../src/Ai/Base/Value/ThreatValues.cpp | 7 +- .../src/Ai/Base/Value/ThreatValues.h | 9 +- .../Ai/Base/Value/WaitForAttackTimeValue.h | 33 + .../mod-playerbots/src/Ai/Base/ValueContext.h | 72 +- .../src/Ai/Base/WorldPacketActionContext.h | 9 +- .../src/Ai/Base/WorldPacketTriggerContext.h | 9 +- .../src/Ai/Class/Dk/Action/DKActions.cpp | 50 - .../src/Ai/Class/Dk/DKActions.cpp | 103 + .../src/Ai/Class/Dk/{Action => }/DKActions.h | 16 +- .../src/Ai/Class/Dk/DKAiObjectContext.cpp | 28 +- .../src/Ai/Class/Dk/DKAiObjectContext.h | 9 +- .../Ai/Class/Dk/{Trigger => }/DKTriggers.cpp | 5 +- .../Ai/Class/Dk/{Trigger => }/DKTriggers.h | 15 +- .../Ai/Class/Dk/Strategy/BloodDKStrategy.cpp | 37 +- .../Ai/Class/Dk/Strategy/BloodDKStrategy.h | 9 +- .../Dk/Strategy/DeathKnightPullStrategy.cpp | 32 + .../Dk/Strategy/DeathKnightPullStrategy.h | 20 + .../Ai/Class/Dk/Strategy/FrostDKStrategy.cpp | 6 +- .../Ai/Class/Dk/Strategy/FrostDKStrategy.h | 9 +- .../Strategy/GenericDKNonCombatStrategy.cpp | 46 +- .../Dk/Strategy/GenericDKNonCombatStrategy.h | 18 +- .../Class/Dk/Strategy/GenericDKStrategy.cpp | 137 +- .../Ai/Class/Dk/Strategy/GenericDKStrategy.h | 9 +- .../Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp | 18 +- .../Ai/Class/Dk/Strategy/UnholyDKStrategy.h | 9 +- .../Ai/Class/Druid/Action/DruidActions.cpp | 234 +- .../src/Ai/Class/Druid/Action/DruidActions.h | 119 +- .../Class/Druid/Action/DruidBearActions.cpp | 5 +- .../Ai/Class/Druid/Action/DruidBearActions.h | 21 +- .../Ai/Class/Druid/Action/DruidCatActions.cpp | 6 + .../Ai/Class/Druid/Action/DruidCatActions.h | 90 +- .../Druid/Action/DruidShapeshiftActions.cpp | 14 +- .../Druid/Action/DruidShapeshiftActions.h | 77 +- .../Ai/Class/Druid/DruidAiObjectContext.cpp | 155 +- .../src/Ai/Class/Druid/DruidAiObjectContext.h | 9 +- .../src/Ai/Class/Druid/DruidTriggers.cpp | 126 + .../src/Ai/Class/Druid/DruidTriggers.h | 454 + .../Druid/Strategy/BalanceDruidStrategy.cpp | 205 + .../Druid/Strategy/BalanceDruidStrategy.h | 26 + .../Druid/Strategy/BearDruidStrategy.cpp | 163 + .../Class/Druid/Strategy/BearDruidStrategy.h | 25 + .../Druid/Strategy/BearTankDruidStrategy.cpp | 256 - .../Druid/Strategy/BearTankDruidStrategy.h | 24 - .../Druid/Strategy/CasterDruidStrategy.cpp | 254 - .../Druid/Strategy/CasterDruidStrategy.h | 45 - .../Druid/Strategy/CatDpsDruidStrategy.cpp | 314 - .../Druid/Strategy/CatDpsDruidStrategy.h | 35 - .../Class/Druid/Strategy/CatDruidStrategy.cpp | 380 + .../Class/Druid/Strategy/CatDruidStrategy.h | 38 + .../Druid/Strategy/DruidPullStrategy.cpp | 36 + .../Class/Druid/Strategy/DruidPullStrategy.h | 21 + .../Druid/Strategy/FeralDruidStrategy.cpp | 46 +- .../Class/Druid/Strategy/FeralDruidStrategy.h | 26 +- .../GenericDruidNonCombatStrategy.cpp | 21 +- .../Strategy/GenericDruidNonCombatStrategy.h | 9 +- .../Druid/Strategy/GenericDruidStrategy.cpp | 214 +- .../Druid/Strategy/GenericDruidStrategy.h | 18 +- .../Druid/Strategy/HealDruidStrategy.cpp | 101 - .../Class/Druid/Strategy/HealDruidStrategy.h | 24 - .../Druid/Strategy/MeleeDruidStrategy.cpp | 32 - .../Class/Druid/Strategy/MeleeDruidStrategy.h | 21 - .../Strategy/OffhealDruidCatStrategy.cpp | 307 - .../Druid/Strategy/OffhealDruidCatStrategy.h | 27 - .../Druid/Strategy/RestoDruidStrategy.cpp | 104 + .../Class/Druid/Strategy/RestoDruidStrategy.h | 43 + .../Ai/Class/Druid/Trigger/DruidTriggers.cpp | 63 - .../Ai/Class/Druid/Trigger/DruidTriggers.h | 283 - .../Hunter/{Action => }/HunterActions.cpp | 59 +- .../Class/Hunter/{Action => }/HunterActions.h | 230 +- .../Ai/Class/Hunter/HunterAiObjectContext.cpp | 68 +- .../Ai/Class/Hunter/HunterAiObjectContext.h | 9 +- .../Hunter/{Trigger => }/HunterTriggers.cpp | 93 +- .../Hunter/{Trigger => }/HunterTriggers.h | 31 +- .../Strategy/BeastMasteryHunterStrategy.cpp | 39 +- .../Strategy/BeastMasteryHunterStrategy.h | 9 +- .../GenericHunterNonCombatStrategy.cpp | 62 +- .../Strategy/GenericHunterNonCombatStrategy.h | 9 +- .../Hunter/Strategy/GenericHunterStrategy.cpp | 63 +- .../Hunter/Strategy/GenericHunterStrategy.h | 18 +- .../Hunter/Strategy/HunterBuffStrategies.cpp | 63 +- .../Hunter/Strategy/HunterBuffStrategies.h | 36 +- .../Strategy/MarksmanshipHunterStrategy.cpp | 43 +- .../Strategy/MarksmanshipHunterStrategy.h | 9 +- .../Strategy/SurvivalHunterStrategy.cpp | 78 +- .../Hunter/Strategy/SurvivalHunterStrategy.h | 9 +- .../Class/Mage/{Action => }/MageActions.cpp | 55 +- .../Ai/Class/Mage/{Action => }/MageActions.h | 36 +- .../src/Ai/Class/Mage/MageAiObjectContext.cpp | 7 +- .../src/Ai/Class/Mage/MageAiObjectContext.h | 9 +- .../Class/Mage/{Trigger => }/MageTriggers.cpp | 75 +- .../Class/Mage/{Trigger => }/MageTriggers.h | 52 +- .../Mage/Strategy/ArcaneMageStrategy.cpp | 27 +- .../Class/Mage/Strategy/ArcaneMageStrategy.h | 9 +- .../Class/Mage/Strategy/FireMageStrategy.cpp | 33 +- .../Ai/Class/Mage/Strategy/FireMageStrategy.h | 10 +- .../Mage/Strategy/FrostFireMageStrategy.cpp | 33 +- .../Mage/Strategy/FrostFireMageStrategy.h | 9 +- .../Class/Mage/Strategy/FrostMageStrategy.cpp | 40 +- .../Class/Mage/Strategy/FrostMageStrategy.h | 9 +- .../Strategy/GenericMageNonCombatStrategy.cpp | 25 +- .../Strategy/GenericMageNonCombatStrategy.h | 9 +- .../Mage/Strategy/GenericMageStrategy.cpp | 134 +- .../Class/Mage/Strategy/GenericMageStrategy.h | 9 +- .../Class/Paladin/Action/PaladinActions.cpp | 502 - .../Class/Paladin/Actions/PaladinActions.cpp | 606 ++ .../{Action => Actions}/PaladinActions.h | 82 +- .../Actions/PaladinGreaterBlessingAction.cpp | 1202 +++ .../Actions/PaladinGreaterBlessingAction.h | 268 + .../Class/Paladin/PaladinAiObjectContext.cpp | 89 +- .../Ai/Class/Paladin/PaladinAiObjectContext.h | 9 +- .../src/Ai/Class/Paladin/PaladinHelper.h | 46 + .../src/Ai/Class/Paladin/PaladinTriggers.cpp | 107 + .../Paladin/{Trigger => }/PaladinTriggers.h | 123 +- .../Paladin/Strategy/DpsPaladinStrategy.cpp | 49 +- .../Paladin/Strategy/DpsPaladinStrategy.h | 9 +- .../GenericPaladinNonCombatStrategy.cpp | 25 +- .../GenericPaladinNonCombatStrategy.h | 9 +- .../Strategy/GenericPaladinStrategy.cpp | 42 +- .../Paladin/Strategy/GenericPaladinStrategy.h | 9 +- .../GenericPaladinStrategyActionNodeFactory.h | 154 +- .../Paladin/Strategy/HealPaladinStrategy.cpp | 13 +- .../Paladin/Strategy/HealPaladinStrategy.h | 9 +- .../Strategy/OffhealRetPaladinStrategy.cpp | 27 +- .../Strategy/OffhealRetPaladinStrategy.h | 9 +- .../Strategy/PaladinBuffStrategies.cpp | 5 +- .../Paladin/Strategy/PaladinBuffStrategies.h | 17 +- .../Paladin/Strategy/PaladinPullStrategy.cpp | 36 + .../Paladin/Strategy/PaladinPullStrategy.h | 21 + .../Paladin/Strategy/TankPaladinStrategy.cpp | 39 +- .../Paladin/Strategy/TankPaladinStrategy.h | 9 +- .../Class/Paladin/Trigger/PaladinTriggers.cpp | 32 - .../Priest/{Action => }/PriestActions.cpp | 5 +- .../Class/Priest/{Action => }/PriestActions.h | 63 +- .../Ai/Class/Priest/PriestAiObjectContext.cpp | 24 +- .../Ai/Class/Priest/PriestAiObjectContext.h | 9 +- .../Priest/{Trigger => }/PriestTriggers.cpp | 41 +- .../Priest/{Trigger => }/PriestTriggers.h | 69 +- .../Priest/Strategy/GenericPriestStrategy.cpp | 5 +- .../Priest/Strategy/GenericPriestStrategy.h | 9 +- .../GenericPriestStrategyActionNodeFactory.h | 22 +- .../Priest/Strategy/HealPriestStrategy.cpp | 6 +- .../Priest/Strategy/HealPriestStrategy.h | 9 +- .../Priest/Strategy/HolyPriestStrategy.cpp | 5 +- .../Priest/Strategy/HolyPriestStrategy.h | 9 +- .../Strategy/PriestNonCombatStrategy.cpp | 13 +- .../Priest/Strategy/PriestNonCombatStrategy.h | 9 +- ...PriestNonCombatStrategyActionNodeFactory.h | 25 +- .../Priest/Strategy/ShadowPriestStrategy.cpp | 13 +- .../Priest/Strategy/ShadowPriestStrategy.h | 9 +- .../ShadowPriestStrategyActionNodeFactory.h | 17 +- .../Ai/Class/Rogue/Action/RogueActions.cpp | 21 +- .../src/Ai/Class/Rogue/Action/RogueActions.h | 15 +- .../Class/Rogue/Action/RogueComboActions.cpp | 5 +- .../Ai/Class/Rogue/Action/RogueComboActions.h | 9 +- .../Rogue/Action/RogueFinishingActions.h | 9 +- .../Rogue/Action/RogueOpeningActions.cpp | 5 +- .../Class/Rogue/Action/RogueOpeningActions.h | 9 +- .../Ai/Class/Rogue/RogueAiObjectContext.cpp | 7 +- .../src/Ai/Class/Rogue/RogueAiObjectContext.h | 9 +- .../Rogue/{Trigger => }/RogueTriggers.cpp | 21 +- .../Class/Rogue/{Trigger => }/RogueTriggers.h | 9 +- .../Strategy/AssassinationRogueStrategy.cpp | 16 +- .../Strategy/AssassinationRogueStrategy.h | 9 +- .../Class/Rogue/Strategy/DpsRogueStrategy.cpp | 76 +- .../Class/Rogue/Strategy/DpsRogueStrategy.h | 9 +- .../GenericRogueNonCombatStrategy.cpp | 5 +- .../Strategy/GenericRogueNonCombatStrategy.h | 9 +- .../Ai/Class/Shaman/Action/ShamanActions.h | 667 -- .../Shaman/{Action => }/ShamanActions.cpp | 32 +- .../src/Ai/Class/Shaman/ShamanActions.h | 712 ++ .../Ai/Class/Shaman/ShamanAiObjectContext.cpp | 203 +- .../Ai/Class/Shaman/ShamanAiObjectContext.h | 9 +- .../Shaman/{Trigger => }/ShamanTriggers.cpp | 25 +- .../Shaman/{Trigger => }/ShamanTriggers.h | 149 +- .../Strategy/ElementalShamanStrategy.cpp | 38 +- .../Shaman/Strategy/ElementalShamanStrategy.h | 9 +- .../Strategy/EnhancementShamanStrategy.cpp | 22 +- .../Strategy/EnhancementShamanStrategy.h | 9 +- .../Shaman/Strategy/GenericShamanStrategy.cpp | 66 +- .../Shaman/Strategy/GenericShamanStrategy.h | 9 +- .../Shaman/Strategy/RestoShamanStrategy.cpp | 102 +- .../Shaman/Strategy/RestoShamanStrategy.h | 9 +- .../Strategy/ShamanNonCombatStrategy.cpp | 115 +- .../Shaman/Strategy/ShamanNonCombatStrategy.h | 9 +- .../Shaman/Strategy/TotemsShamanStrategy.cpp | 55 +- .../Shaman/Strategy/TotemsShamanStrategy.h | 13 +- .../Strategy/AfflictionWarlockStrategy.cpp | 43 +- .../Strategy/AfflictionWarlockStrategy.h | 9 +- .../Strategy/DemonologyWarlockStrategy.cpp | 51 +- .../Strategy/DemonologyWarlockStrategy.h | 9 +- .../Strategy/DestructionWarlockStrategy.cpp | 47 +- .../Strategy/DestructionWarlockStrategy.h | 9 +- .../GenericWarlockNonCombatStrategy.cpp | 5 +- .../GenericWarlockNonCombatStrategy.h | 9 +- .../Strategy/GenericWarlockStrategy.cpp | 31 +- .../Warlock/Strategy/GenericWarlockStrategy.h | 9 +- .../Warlock/Strategy/TankWarlockStrategy.cpp | 32 +- .../Warlock/Strategy/TankWarlockStrategy.h | 9 +- .../Warlock/{Action => }/WarlockActions.cpp | 14 +- .../Warlock/{Action => }/WarlockActions.h | 11 +- .../Class/Warlock/WarlockAiObjectContext.cpp | 5 +- .../Ai/Class/Warlock/WarlockAiObjectContext.h | 9 +- .../Warlock/{Trigger => }/WarlockTriggers.cpp | 27 +- .../Warlock/{Trigger => }/WarlockTriggers.h | 20 +- .../Warrior/Strategy/ArmsWarriorStrategy.cpp | 49 +- .../Warrior/Strategy/ArmsWarriorStrategy.h | 9 +- .../Warrior/Strategy/FuryWarriorStrategy.cpp | 24 +- .../Warrior/Strategy/FuryWarriorStrategy.h | 9 +- .../GenericWarriorNonCombatStrategy.cpp | 29 +- .../GenericWarriorNonCombatStrategy.h | 11 +- .../Strategy/GenericWarriorStrategy.cpp | 26 +- .../Warrior/Strategy/GenericWarriorStrategy.h | 9 +- .../Warrior/Strategy/TankWarriorStrategy.cpp | 43 +- .../Warrior/Strategy/TankWarriorStrategy.h | 9 +- .../Warrior/Strategy/WarriorPullStrategy.cpp | 21 + .../Warrior/Strategy/WarriorPullStrategy.h | 20 + .../Class/Warrior/Trigger/WarriorTriggers.cpp | 121 - .../Warrior/{Action => }/WarriorActions.cpp | 96 +- .../Warrior/{Action => }/WarriorActions.h | 19 +- .../Class/Warrior/WarriorAiObjectContext.cpp | 9 +- .../Ai/Class/Warrior/WarriorAiObjectContext.h | 9 +- .../src/Ai/Class/Warrior/WarriorTriggers.cpp | 166 + .../Warrior/{Trigger => }/WarriorTriggers.h | 18 +- .../src/Ai/Dungeon/AC/ACActionContext.h | 40 + .../src/Ai/Dungeon/AC/ACActions.cpp | 123 + .../src/Ai/Dungeon/AC/ACActions.h | 37 + .../src/Ai/Dungeon/AC/ACMultipliers.cpp | 51 + .../src/Ai/Dungeon/AC/ACMultipliers.h | 19 + .../src/Ai/Dungeon/AC/ACStrategy.cpp | 27 + .../src/Ai/Dungeon/AC/ACStrategy.h | 25 + .../src/Ai/Dungeon/AC/ACTriggerContext.h | 41 + .../src/Ai/Dungeon/AC/ACTriggers.cpp | 40 + .../src/Ai/Dungeon/AC/ACTriggers.h | 44 + .../AKActionContext.h} | 12 +- .../AKActions.cpp} | 8 +- .../OldKingdomActions.h => AK/AKActions.h} | 12 +- .../AKMultipliers.cpp} | 12 +- .../AKMultipliers.h} | 10 +- .../AKStrategy.cpp} | 10 +- .../OldKingdomStrategy.h => AK/AKStrategy.h} | 10 +- .../AKTriggerContext.h} | 12 +- .../AKTriggers.cpp} | 8 +- .../OldKingdomTriggers.h => AK/AKTriggers.h} | 10 +- .../ANActionContext.h} | 12 +- .../ANActions.cpp} | 8 +- .../AzjolNerubActions.h => AN/ANActions.h} | 12 +- .../ANMultipliers.cpp} | 12 +- .../src/Ai/Dungeon/AN/ANMultipliers.h | 21 + .../ANStrategy.cpp} | 10 +- .../AzjolNerubStrategy.h => AN/ANStrategy.h} | 10 +- .../ANTriggerContext.h} | 12 +- .../ANTriggers.cpp} | 8 +- .../AzjolNerubTriggers.h => AN/ANTriggers.h} | 10 +- .../Multiplier/AzjolNerubMultipliers.h | 15 - .../CoSActionContext.h} | 12 +- .../CoSActions.cpp} | 8 +- .../CoSActions.h} | 12 +- .../CoSMultipliers.cpp} | 12 +- .../src/Ai/Dungeon/CoS/CoSMultipliers.h | 21 + .../CoSStrategy.cpp} | 10 +- .../CoSStrategy.h} | 10 +- .../CoSTriggerContext.h} | 12 +- .../CoSTriggers.cpp} | 8 +- .../CoSTriggers.h} | 10 +- .../CullingOfStratholmeMultipliers.h | 15 - .../DTKActionContext.h} | 12 +- .../DTKActions.cpp} | 8 +- .../DTKActions.h} | 12 +- .../DTKMultipliers.cpp} | 12 +- .../DTKMultipliers.h} | 10 +- .../DTKStrategy.cpp} | 10 +- .../DTKStrategy.h} | 10 +- .../DTKTriggerContext.h} | 12 +- .../DTKTriggers.cpp} | 8 +- .../DTKTriggers.h} | 10 +- .../src/Ai/Dungeon/DungeonStrategyContext.h | 44 +- .../src/Ai/Dungeon/DungeonStrategyUtils.h | 10 +- .../FoSActionContext.h} | 12 +- .../FoSActions.cpp} | 12 +- .../FoSActions.h} | 12 +- .../FoSMultipliers.cpp} | 14 +- .../FoSMultipliers.h} | 10 +- .../FoSStrategy.cpp} | 10 +- .../FoSStrategy.h} | 10 +- .../FoSTriggerContext.h} | 12 +- .../FoSTriggers.cpp} | 8 +- .../FoSTriggers.h} | 10 +- .../GDActionContext.h} | 12 +- .../GundrakActions.cpp => GD/GDActions.cpp} | 9 +- .../GundrakActions.h => GD/GDActions.h} | 12 +- .../GDMultipliers.cpp} | 12 +- .../GDMultipliers.h} | 10 +- .../GundrakStrategy.cpp => GD/GDStrategy.cpp} | 10 +- .../GundrakStrategy.h => GD/GDStrategy.h} | 10 +- .../GDTriggerContext.h} | 12 +- .../GundrakTriggers.cpp => GD/GDTriggers.cpp} | 8 +- .../GundrakTriggers.h => GD/GDTriggers.h} | 10 +- .../src/Ai/Dungeon/HallsOfReflection/TODO | 0 .../HoLActionContext.h} | 12 +- .../HoLActions.cpp} | 8 +- .../HoLActions.h} | 12 +- .../HoLMultipliers.cpp} | 12 +- .../HoLMultipliers.h} | 10 +- .../HoLStrategy.cpp} | 10 +- .../HoLStrategy.h} | 10 +- .../HoLTriggerContext.h} | 12 +- .../HoLTriggers.cpp} | 8 +- .../HoLTriggers.h} | 10 +- .../HoSActionContext.h} | 12 +- .../HoSActions.cpp} | 8 +- .../HoSActions.h} | 12 +- .../HoSMultipliers.cpp} | 12 +- .../HoSMultipliers.h} | 10 +- .../HoSStrategy.cpp} | 10 +- .../HoSStrategy.h} | 10 +- .../HoSTriggerContext.h} | 12 +- .../HoSTriggers.cpp} | 8 +- .../HoSTriggers.h} | 10 +- .../NexActionContext.h} | 12 +- .../NexusActions.cpp => Nex/NexActions.cpp} | 8 +- .../NexusActions.h => Nex/NexActions.h} | 12 +- .../NexMultipliers.cpp} | 12 +- .../NexMultipliers.h} | 10 +- .../NexusStrategy.cpp => Nex/NexStrategy.cpp} | 10 +- .../NexusStrategy.h => Nex/NexStrategy.h} | 10 +- .../NexTriggerContext.h} | 12 +- .../NexusTriggers.cpp => Nex/NexTriggers.cpp} | 8 +- .../NexusTriggers.h => Nex/NexTriggers.h} | 10 +- .../OCActionContext.h} | 12 +- .../OculusActions.cpp => OC/OCActions.cpp} | 10 +- .../Action/OculusActions.h => OC/OCActions.h} | 10 +- .../OCMultipliers.cpp} | 14 +- .../OCMultipliers.h} | 10 +- .../OculusStrategy.cpp => OC/OCStrategy.cpp} | 10 +- .../OculusStrategy.h => OC/OCStrategy.h} | 10 +- .../OCTriggerContext.h} | 12 +- .../OculusTriggers.cpp => OC/OCTriggers.cpp} | 8 +- .../OculusTriggers.h => OC/OCTriggers.h} | 10 +- .../PoSActionContext.h} | 12 +- .../PoSActions.cpp} | 21 +- .../PitOfSaronActions.h => PoS/PoSActions.h} | 12 +- .../PoSMultipliers.cpp} | 14 +- .../PoSMultipliers.h} | 10 +- .../PoSStrategy.cpp} | 10 +- .../PoSStrategy.h} | 10 +- .../PoSTriggerContext.h} | 12 +- .../PoSTriggers.cpp} | 8 +- .../PoSTriggers.h} | 10 +- .../TOCActionContext.h} | 12 +- .../TOCActions.cpp} | 12 +- .../TOCActions.h} | 12 +- .../src/Ai/Dungeon/TOC/TOCMultipliers.cpp | 15 + .../src/Ai/Dungeon/TOC/TOCMultipliers.h | 21 + .../TOCStrategy.cpp} | 10 +- .../TOCStrategy.h} | 12 +- .../TOCTriggerContext.h} | 12 +- .../TOCTriggers.cpp} | 8 +- .../TOCTriggers.h} | 10 +- .../src/Ai/Dungeon/TbcDungeonActionContext.h | 12 + .../src/Ai/Dungeon/TbcDungeonTriggerContext.h | 12 + .../TrialOfTheChampionMultipliers.cpp | 9 - .../TrialOfTheChampionMultipliers.h | 15 - .../UKActionContext.h} | 12 +- .../UKActions.cpp} | 8 +- .../UtgardeKeepActions.h => UK/UKActions.h} | 12 +- .../UKMultipliers.cpp} | 12 +- .../UKMultipliers.h} | 10 +- .../UKStrategy.cpp} | 10 +- .../UtgardeKeepStrategy.h => UK/UKStrategy.h} | 10 +- .../UKTriggerContext.h} | 12 +- .../UKTriggers.cpp} | 8 +- .../UtgardeKeepTriggers.h => UK/UKTriggers.h} | 10 +- .../UPActionContext.h} | 12 +- .../UPActions.cpp} | 8 +- .../UPActions.h} | 12 +- .../UPMultipliers.cpp} | 12 +- .../UPMultipliers.h} | 10 +- .../UPStrategy.cpp} | 10 +- .../UPStrategy.h} | 10 +- .../UPTriggerContext.h} | 12 +- .../UPTriggers.cpp} | 8 +- .../UPTriggers.h} | 10 +- .../VHActionContext.h} | 12 +- .../VHActions.cpp} | 8 +- .../VioletHoldActions.h => VH/VHActions.h} | 12 +- .../VHMultipliers.cpp} | 12 +- .../VHMultipliers.h} | 10 +- .../VHStrategy.cpp} | 10 +- .../VioletHoldStrategy.h => VH/VHStrategy.h} | 10 +- .../VHTriggerContext.h} | 12 +- .../VHTriggers.cpp} | 8 +- .../VioletHoldTriggers.h => VH/VHTriggers.h} | 10 +- .../Ai/Dungeon/WotlkDungeonActionContext.h | 40 +- .../Ai/Dungeon/WotlkDungeonTriggerContext.h | 40 +- .../src/Ai/Raid/Aq20/Action/RaidAq20Actions.h | 14 - ...q20ActionContext.h => Aq20ActionContext.h} | 12 +- .../RaidAq20Actions.cpp => Aq20Actions.cpp} | 10 +- .../src/Ai/Raid/Aq20/Aq20Actions.h | 20 + .../src/Ai/Raid/Aq20/Aq20Strategy.cpp | 17 + .../RaidAq20Strategy.h => Aq20Strategy.h} | 12 +- .../src/Ai/Raid/Aq20/Aq20TriggerContext.h | 25 + .../RaidAq20Triggers.cpp => Aq20Triggers.cpp} | 10 +- .../src/Ai/Raid/Aq20/Aq20Triggers.h | 20 + .../{Util/RaidAq20Utils.cpp => Aq20Utils.cpp} | 8 +- .../src/Ai/Raid/Aq20/Aq20Utils.h | 21 + .../src/Ai/Raid/Aq20/RaidAq20TriggerContext.h | 20 - .../Raid/Aq20/Strategy/RaidAq20Strategy.cpp | 11 - .../Ai/Raid/Aq20/Trigger/RaidAq20Triggers.h | 14 - .../src/Ai/Raid/Aq20/Util/RaidAq20Utils.h | 15 - .../src/Ai/Raid/BT/BTActionContext.h | 417 + .../src/Ai/Raid/BT/BTActions.cpp | 3052 ++++++ .../mod-playerbots/src/Ai/Raid/BT/BTActions.h | 566 + .../src/Ai/Raid/BT/BTHelpers.cpp | 469 + .../mod-playerbots/src/Ai/Raid/BT/BTHelpers.h | 217 + .../src/Ai/Raid/BT/BTMultipliers.cpp | 787 ++ .../src/Ai/Raid/BT/BTMultipliers.h | 268 + .../src/Ai/Raid/BT/BTStrategy.cpp | 254 + .../src/Ai/Raid/BT/BTStrategy.h | 23 + .../src/Ai/Raid/BT/BTTriggerContext.h | 417 + .../src/Ai/Raid/BT/BTTriggers.cpp | 846 ++ .../src/Ai/Raid/BT/BTTriggers.h | 519 + .../src/Ai/Raid/BWL/BWLActionContext.h | 49 + .../src/Ai/Raid/BWL/BWLActions.cpp | 368 + .../src/Ai/Raid/BWL/BWLActions.h | 96 + .../src/Ai/Raid/BWL/BWLHelpers.cpp | 61 + .../src/Ai/Raid/BWL/BWLHelpers.h | 55 + .../src/Ai/Raid/BWL/BWLMultipliers.cpp | 91 + .../src/Ai/Raid/BWL/BWLMultipliers.h | 36 + .../src/Ai/Raid/BWL/BWLStrategy.cpp | 51 + .../src/Ai/Raid/BWL/BWLStrategy.h | 21 + .../src/Ai/Raid/BWL/BWLTriggerContext.h | 48 + .../src/Ai/Raid/BWL/BWLTriggers.cpp | 99 + .../src/Ai/Raid/BWL/BWLTriggers.h | 87 + .../BlackwingLair/Action/RaidBwlActions.cpp | 35 - .../BlackwingLair/Action/RaidBwlActions.h | 33 - .../Raid/BlackwingLair/RaidBwlActionContext.h | 24 - .../BlackwingLair/RaidBwlTriggerContext.h | 22 - .../Strategy/RaidBwlStrategy.cpp | 15 - .../BlackwingLair/Strategy/RaidBwlStrategy.h | 18 - .../BlackwingLair/Trigger/RaidBwlTriggers.cpp | 24 - .../BlackwingLair/Trigger/RaidBwlTriggers.h | 22 - .../EoEActionContext.h} | 12 +- .../RaidEoEActions.cpp => EoE/EoEActions.cpp} | 10 +- .../RaidEoEActions.h => EoE/EoEActions.h} | 10 +- .../EoEMultipliers.cpp} | 12 +- .../src/Ai/Raid/EoE/EoEMultipliers.h | 21 + .../EoEStrategy.cpp} | 10 +- .../RaidEoEStrategy.h => EoE/EoEStrategy.h} | 12 +- .../EoETriggerContext.h} | 13 +- .../EoETriggers.cpp} | 8 +- .../RaidEoETriggers.h => EoE/EoETriggers.h} | 10 +- .../Multiplier/RaidEoEMultipliers.h | 16 - .../src/Ai/Raid/Gruul/GruulActionContext.h | 105 + .../src/Ai/Raid/Gruul/GruulActions.cpp | 609 ++ .../src/Ai/Raid/Gruul/GruulActions.h | 122 + .../src/Ai/Raid/Gruul/GruulHelpers.cpp | 107 + .../src/Ai/Raid/Gruul/GruulHelpers.h | 50 + .../src/Ai/Raid/Gruul/GruulMultipliers.cpp | 162 + .../src/Ai/Raid/Gruul/GruulMultipliers.h | 76 + .../GruulStrategy.cpp} | 49 +- .../GruulStrategy.h} | 11 +- .../src/Ai/Raid/Gruul/GruulTriggerContext.h | 105 + .../src/Ai/Raid/Gruul/GruulTriggers.cpp | 124 + .../src/Ai/Raid/Gruul/GruulTriggers.h | 116 + .../Action/RaidGruulsLairActions.cpp | 652 -- .../GruulsLair/Action/RaidGruulsLairActions.h | 112 - .../Multiplier/RaidGruulsLairMultipliers.cpp | 103 - .../Multiplier/RaidGruulsLairMultipliers.h | 48 - .../GruulsLair/RaidGruulsLairActionContext.h | 49 - .../GruulsLair/RaidGruulsLairTriggerContext.h | 49 - .../Trigger/RaidGruulsLairTriggers.cpp | 159 - .../Trigger/RaidGruulsLairTriggers.h | 97 - .../GruulsLair/Util/RaidGruulsLairHelpers.cpp | 204 - .../GruulsLair/Util/RaidGruulsLairHelpers.h | 45 - .../src/Ai/Raid/Hyjal/HyjalActionContext.h | 225 + .../src/Ai/Raid/Hyjal/HyjalActions.cpp | 1209 +++ .../src/Ai/Raid/Hyjal/HyjalActions.h | 278 + .../src/Ai/Raid/Hyjal/HyjalMultipliers.cpp | 296 + .../src/Ai/Raid/Hyjal/HyjalMultipliers.h | 126 + .../src/Ai/Raid/Hyjal/HyjalStrategy.cpp | 138 + .../src/Ai/Raid/Hyjal/HyjalStrategy.h | 23 + .../src/Ai/Raid/Hyjal/HyjalTriggerContext.h | 225 + .../src/Ai/Raid/Hyjal/HyjalTriggers.cpp | 359 + .../src/Ai/Raid/Hyjal/HyjalTriggers.h | 272 + .../src/Ai/Raid/Hyjal/Util/HyjalHelpers.cpp | 269 + .../src/Ai/Raid/Hyjal/Util/HyjalHelpers.h | 144 + .../src/Ai/Raid/Hyjal/Util/HyjalScripts.cpp | 212 + .../src/Ai/Raid/ICC/Action/ICCActions.h | 849 ++ .../src/Ai/Raid/ICC/Action/ICCActions_BPC.cpp | 764 ++ .../src/Ai/Raid/ICC/Action/ICCActions_BQL.cpp | 1294 +++ .../src/Ai/Raid/ICC/Action/ICCActions_DBS.cpp | 381 + .../Ai/Raid/ICC/Action/ICCActions_Dogs.cpp | 71 + .../src/Ai/Raid/ICC/Action/ICCActions_FG.cpp | 526 + .../src/Ai/Raid/ICC/Action/ICCActions_GSB.cpp | 1030 ++ .../src/Ai/Raid/ICC/Action/ICCActions_LDW.cpp | 983 ++ .../src/Ai/Raid/ICC/Action/ICCActions_LK.cpp | 4406 ++++++++ .../src/Ai/Raid/ICC/Action/ICCActions_LM.cpp | 536 + .../src/Ai/Raid/ICC/Action/ICCActions_PP.cpp | 1670 +++ .../src/Ai/Raid/ICC/Action/ICCActions_RF.cpp | 1001 ++ .../src/Ai/Raid/ICC/Action/ICCActions_SG.cpp | 1585 +++ .../src/Ai/Raid/ICC/Action/ICCActions_SS.cpp | 67 + .../src/Ai/Raid/ICC/Action/ICCActions_VT.cpp | 1229 +++ .../ICCActionContext.h} | 40 +- .../src/Ai/Raid/ICC/ICCMultipliers.cpp | 1222 +++ .../ICCMultipliers.h} | 25 +- .../src/Ai/Raid/ICC/ICCScripts.cpp | 196 + .../src/Ai/Raid/ICC/ICCScripts.h | 149 + .../src/Ai/Raid/ICC/ICCShared.cpp | 301 + .../src/Ai/Raid/ICC/ICCShared.h | 101 + .../ICCStrategy.cpp} | 61 +- .../RaidIccStrategy.h => ICC/ICCStrategy.h} | 13 +- .../ICCTriggerContext.h} | 47 +- .../src/Ai/Raid/ICC/ICCTriggers.cpp | 1101 ++ .../RaidIccTriggers.h => ICC/ICCTriggers.h} | 172 +- .../Raid/Icecrown/Action/RaidIccActions.cpp | 9334 ----------------- .../Ai/Raid/Icecrown/Action/RaidIccActions.h | 669 -- .../Multiplier/RaidIccMultipliers.cpp | 874 -- .../Raid/Icecrown/Trigger/RaidIccTriggers.cpp | 1189 --- .../Ai/Raid/Icecrown/Util/RaidIccScripts.h | 6 - .../KaraActionContext.h} | 221 +- .../KaraActions.cpp} | 181 +- .../KaraActions.h} | 10 +- .../KaraHelpers.cpp} | 33 +- .../KaraHelpers.h} | 15 +- .../KaraMultipliers.cpp} | 52 +- .../KaraMultipliers.h} | 26 +- .../KaraStrategy.cpp} | 12 +- .../KaraStrategy.h} | 11 +- .../KaraTriggerContext.h} | 223 +- .../KaraTriggers.cpp} | 70 +- .../KaraTriggers.h} | 10 +- .../MCActionContext.h} | 14 +- .../RaidMcActions.cpp => MC/MCActions.cpp} | 52 +- .../Action/RaidMcActions.h => MC/MCActions.h} | 19 +- .../mod-playerbots/src/Ai/Raid/MC/MCHelpers.h | 31 + .../MCMultipliers.cpp} | 12 +- .../MCMultipliers.h} | 10 +- .../RaidMcStrategy.cpp => MC/MCStrategy.cpp} | 15 +- .../RaidMcStrategy.h => MC/MCStrategy.h} | 12 +- .../MCTriggerContext.h} | 15 +- .../RaidMcTriggers.cpp => MC/MCTriggers.cpp} | 15 +- .../RaidMcTriggers.h => MC/MCTriggers.h} | 17 +- .../src/Ai/Raid/Mag/MagActionContext.h | 94 + .../src/Ai/Raid/Mag/MagActions.cpp | 757 ++ .../src/Ai/Raid/Mag/MagActions.h | 125 + .../src/Ai/Raid/Mag/MagMultipliers.cpp | 141 + .../src/Ai/Raid/Mag/MagMultipliers.h | 44 + .../MagStrategy.cpp} | 55 +- .../MagStrategy.h} | 11 +- .../src/Ai/Raid/Mag/MagTriggerContext.h | 94 + .../src/Ai/Raid/Mag/MagTriggers.cpp | 128 + .../src/Ai/Raid/Mag/MagTriggers.h | 108 + .../src/Ai/Raid/Mag/Util/MagHelpers.cpp | 139 + .../src/Ai/Raid/Mag/Util/MagHelpers.h | 91 + .../src/Ai/Raid/Mag/Util/MagScripts.cpp | 75 + .../Action/RaidMagtheridonActions.cpp | 673 -- .../Action/RaidMagtheridonActions.h | 98 - .../Multiplier/RaidMagtheridonMultipliers.cpp | 75 - .../Multiplier/RaidMagtheridonMultipliers.h | 27 - .../RaidMagtheridonActionContext.h | 37 - .../RaidMagtheridonTriggerContext.h | 37 - .../Trigger/RaidMagtheridonTriggers.cpp | 122 - .../Trigger/RaidMagtheridonTriggers.h | 77 - .../Util/RaidMagtheridonHelpers.cpp | 155 - .../Magtheridon/Util/RaidMagtheridonHelpers.h | 69 - .../Ai/Raid/MoltenCore/Util/RaidMcHelpers.h | 22 - .../src/Ai/Raid/Naxx/Action/NaxxActions.h | 332 + .../Naxx/Action/NaxxActions_Anubrekhan.cpp | 83 + .../Raid/Naxx/Action/NaxxActions_Faerlina.cpp | 9 + .../Naxx/Action/NaxxActions_FourHorsemen.cpp | 65 + .../Ai/Raid/Naxx/Action/NaxxActions_Gluth.cpp | 184 + .../Raid/Naxx/Action/NaxxActions_Gothik.cpp | 9 + .../Naxx/Action/NaxxActions_Grobbulus.cpp | 52 + .../Raid/Naxx/Action/NaxxActions_Heigan.cpp | 83 + .../Naxx/Action/NaxxActions_Kelthuzad.cpp | 183 + .../Raid/Naxx/Action/NaxxActions_Loatheb.cpp | 61 + .../Raid/Naxx/Action/NaxxActions_Maexxna.cpp | 9 + .../Ai/Raid/Naxx/Action/NaxxActions_Noth.cpp | 9 + .../Naxx/Action/NaxxActions_Patchwerk.cpp | 37 + .../Naxx/Action/NaxxActions_Razuvious.cpp | 161 + .../Naxx/Action/NaxxActions_Sapphiron.cpp | 109 + .../Raid/Naxx/Action/NaxxActions_Shared.cpp | 24 + .../Raid/Naxx/Action/NaxxActions_Thaddius.cpp | 138 + .../src/Ai/Raid/Naxx/NaxxActionContext.h | 93 + .../src/Ai/Raid/Naxx/NaxxBossHelper.h | 540 + .../src/Ai/Raid/Naxx/NaxxMultipliers.cpp | 328 + .../src/Ai/Raid/Naxx/NaxxMultipliers.h | 120 + .../src/Ai/Raid/Naxx/NaxxSpellIds.h | 172 + .../src/Ai/Raid/Naxx/NaxxStrategy.cpp | 162 + .../src/Ai/Raid/Naxx/NaxxStrategy.h | 21 + .../src/Ai/Raid/Naxx/NaxxTriggerContext.h | 86 + .../src/Ai/Raid/Naxx/NaxxTriggers.cpp | 263 + .../src/Ai/Raid/Naxx/NaxxTriggers.h | 264 + .../OSActionContext.h} | 12 +- .../RaidOsActions.cpp => OS/OSActions.cpp} | 44 +- .../Action/RaidOsActions.h => OS/OSActions.h} | 10 +- .../OSMultipliers.cpp} | 12 +- .../src/Ai/Raid/OS/OSMultipliers.h | 21 + .../RaidOsStrategy.cpp => OS/OSStrategy.cpp} | 10 +- .../RaidOsStrategy.h => OS/OSStrategy.h} | 12 +- .../OSTriggerContext.h} | 13 +- .../RaidOsTriggers.cpp => OS/OSTriggers.cpp} | 8 +- .../RaidOsTriggers.h => OS/OSTriggers.h} | 10 +- .../Multiplier/RaidOsMultipliers.h | 16 - .../OnyActionContext.h} | 12 +- .../OnyActions.cpp} | 14 +- .../RaidOnyxiaActions.h => Ony/OnyActions.h} | 42 +- .../OnyStrategy.cpp} | 10 +- .../OnyStrategy.h} | 9 +- .../OnyTriggerContext.h} | 13 +- .../OnyTriggers.cpp} | 8 +- .../OnyTriggers.h} | 10 +- .../src/Ai/Raid/RS/Action/RSActions.h | 1355 +++ .../src/Ai/Raid/RS/Action/RSActions_ADD.cpp | 415 + .../src/Ai/Raid/RS/Action/RSActions_BAL.cpp | 641 ++ .../src/Ai/Raid/RS/Action/RSActions_HAL.cpp | 938 ++ .../src/Ai/Raid/RS/Action/RSActions_SAV.cpp | 354 + .../src/Ai/Raid/RS/Action/RSActions_ZAR.cpp | 218 + .../src/Ai/Raid/RS/RSActionContext.h | 78 + .../src/Ai/Raid/RS/RSMultipliers.cpp | 378 + .../src/Ai/Raid/RS/RSMultipliers.h | 96 + .../src/Ai/Raid/RS/RSScripts.cpp | 541 + .../mod-playerbots/src/Ai/Raid/RS/RSScripts.h | 113 + .../mod-playerbots/src/Ai/Raid/RS/RSShared.h | 109 + .../src/Ai/Raid/RS/RSStrategy.cpp | 99 + .../src/Ai/Raid/RS/RSStrategy.h | 21 + .../src/Ai/Raid/RS/RSTriggerContext.h | 79 + .../src/Ai/Raid/RS/RSTriggers.cpp | 316 + .../src/Ai/Raid/RS/RSTriggers.h | 297 + .../src/Ai/Raid/RaidBossHelpers.cpp | 248 +- .../src/Ai/Raid/RaidBossHelpers.h | 38 +- .../src/Ai/Raid/RaidStrategyContext.h | 58 +- .../SSCActionContext.h} | 310 +- .../RaidSSCActions.cpp => SSC/SSCActions.cpp} | 1642 ++- .../RaidSSCActions.h => SSC/SSCActions.h} | 37 +- .../RaidSSCHelpers.cpp => SSC/SSCHelpers.cpp} | 327 +- .../RaidSSCHelpers.h => SSC/SSCHelpers.h} | 46 +- .../SSCMultipliers.cpp} | 493 +- .../SSCMultipliers.h} | 18 +- .../SSCStrategy.cpp} | 14 +- .../RaidSSCStrategy.h => SSC/SSCStrategy.h} | 11 +- .../SSCTriggerContext.h} | 300 +- .../SSCTriggers.cpp} | 369 +- .../RaidSSCTriggers.h => SSC/SSCTriggers.h} | 18 +- .../src/Ai/Raid/TK/TKActionContext.h | 300 + .../src/Ai/Raid/TK/TKActions.cpp | 2100 ++++ .../mod-playerbots/src/Ai/Raid/TK/TKActions.h | 419 + .../src/Ai/Raid/TK/TKMultipliers.cpp | 429 + .../src/Ai/Raid/TK/TKMultipliers.h | 156 + .../src/Ai/Raid/TK/TKStrategy.cpp | 168 + .../src/Ai/Raid/TK/TKStrategy.h | 23 + .../src/Ai/Raid/TK/TKTriggerContext.h | 276 + .../src/Ai/Raid/TK/TKTriggers.cpp | 532 + .../src/Ai/Raid/TK/TKTriggers.h | 344 + .../src/Ai/Raid/TK/Util/TKHelpers.cpp | 431 + .../src/Ai/Raid/TK/Util/TKHelpers.h | 164 + .../src/Ai/Raid/TK/Util/TKKaelthasBossAI.h | 60 + .../src/Ai/Raid/TK/Util/TKScripts.cpp | 53 + .../UldActionContext.h} | 15 +- .../UldActions.cpp} | 278 +- .../RaidUlduarActions.h => Uld/UldActions.h} | 14 +- .../UldStrategy.cpp} | 8 +- .../src/Ai/Raid/Uld/UldStrategy.h | 20 + .../UldTriggerContext.h} | 16 +- .../UldTriggers.cpp} | 156 +- .../UldTriggers.h} | 12 +- .../Util/UldBossHelper.cpp} | 8 +- .../Util/UldBossHelper.h} | 10 +- .../src/Ai/Raid/Uld/Util/UldScripts.h | 12 + .../Raid/Ulduar/Strategy/RaidUlduarStrategy.h | 16 - .../Ai/Raid/Ulduar/Util/RaidUlduarScripts.h | 6 - .../Strategy/RaidVoAStrategy.h | 19 - .../VoAActionContext.h} | 15 +- .../RaidVoAActions.cpp => VoA/VoAActions.cpp} | 10 +- .../RaidVoAActions.h => VoA/VoAActions.h} | 10 +- .../VoAStrategy.cpp} | 8 +- .../src/Ai/Raid/VoA/VoAStrategy.h | 20 + .../VoATriggerContext.h} | 16 +- .../VoATriggers.cpp} | 8 +- .../RaidVoATriggers.h => VoA/VoATriggers.h} | 10 +- .../src/Ai/Raid/ZA/ZAActionContext.h | 210 + .../src/Ai/Raid/ZA/ZAActions.cpp | 754 ++ .../mod-playerbots/src/Ai/Raid/ZA/ZAActions.h | 254 + .../src/Ai/Raid/ZA/ZAHelpers.cpp | 191 + .../mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.h | 111 + .../src/Ai/Raid/ZA/ZAMultipliers.cpp | 383 + .../src/Ai/Raid/ZA/ZAMultipliers.h | 168 + .../src/Ai/Raid/ZA/ZAStrategy.cpp | 135 + .../src/Ai/Raid/ZA/ZAStrategy.h | 23 + .../src/Ai/Raid/ZA/ZATriggerContext.h | 212 + .../src/Ai/Raid/ZA/ZATriggers.cpp | 272 + .../src/Ai/Raid/ZA/ZATriggers.h | 250 + .../src/Ai/World/Rpg/Action/NewRpgAction.cpp | 80 +- .../src/Ai/World/Rpg/Action/NewRpgAction.h | 19 +- .../Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 292 +- .../Ai/World/Rpg/Action/NewRpgBaseAction.h | 23 +- .../Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp | 134 + .../Ai/World/Rpg/Action/NewRpgOutdoorPvP.h | 25 + .../Rpg/Action}/RpgAction.cpp | 7 +- .../Actions => World/Rpg/Action}/RpgAction.h | 9 +- .../Rpg/Action}/RpgSubActions.cpp | 92 +- .../Rpg/Action}/RpgSubActions.h | 12 +- .../src/Ai/World/Rpg/NewRpgInfo.cpp | 42 +- .../src/Ai/World/Rpg/NewRpgInfo.h | 27 +- .../{Base/Actions => World/Rpg}/RpgValues.h | 9 +- .../Ai/World/Rpg/Strategy/NewRpgStrategy.cpp | 13 +- .../Ai/World/Rpg/Strategy/NewRpgStrategy.h | 9 +- .../Rpg}/Strategy/RpgStrategy.cpp | 5 +- .../Rpg}/Strategy/RpgStrategy.h | 9 +- .../Ai/World/Rpg/Trigger/NewRpgTrigger.cpp | 6 + .../src/Ai/World/Rpg/Trigger/NewRpgTriggers.h | 10 +- .../mod-playerbots/src/Bot/Cmd/ChatFilter.cpp | 5 +- .../mod-playerbots/src/Bot/Cmd/ChatFilter.h | 9 +- .../mod-playerbots/src/Bot/Cmd/ChatHelper.cpp | 9 +- .../mod-playerbots/src/Bot/Cmd/ChatHelper.h | 11 +- .../src/Bot/Cmd/PlayerbotCommandServer.cpp | 7 +- .../src/Bot/Cmd/PlayerbotCommandServer.h | 9 +- .../src/Bot/Debug/PerfMonitor.cpp | 13 +- .../src/Bot/Debug/PerfMonitor.h | 17 +- .../src/Bot/Engine/Action/Action.cpp | 5 +- .../src/Bot/Engine/Action/Action.h | 13 +- .../src/Bot/Engine/AiObject.cpp | 5 +- .../mod-playerbots/src/Bot/Engine/AiObject.h | 9 +- .../src/Bot/Engine/AiObjectContext.cpp | 132 +- .../src/Bot/Engine/AiObjectContext.h | 9 +- .../Bot/Engine/BuildSharedActionContexts.cpp | 73 + .../Engine/BuildSharedStrategyContexts.cpp | 20 + .../Bot/Engine/BuildSharedTriggerContexts.cpp | 73 + .../Bot/Engine/BuildSharedValueContexts.cpp | 13 + .../mod-playerbots/src/Bot/Engine/Engine.cpp | 15 +- .../mod-playerbots/src/Bot/Engine/Engine.h | 12 +- .../src/Bot/Engine/ExternalEventHelper.cpp | 14 +- .../src/Bot/Engine/ExternalEventHelper.h | 9 +- .../src/Bot/Engine/Multiplier.h | 9 +- .../src/Bot/Engine/NamedObjectContext.cpp | 5 +- .../src/Bot/Engine/NamedObjectContext.h | 9 +- .../src/Bot/Engine/PassiveMultiplier.cpp | 5 +- .../src/Bot/Engine/PassiveMultiplier.h | 9 +- .../src/Bot/Engine/PlayerbotAIAware.h | 9 +- .../src/Bot/Engine/PlayerbotAIBase.cpp | 15 +- .../src/Bot/Engine/PlayerbotAIBase.h | 12 +- .../Bot/Engine/Strategy/CustomStrategy.cpp | 7 +- .../src/Bot/Engine/Strategy/CustomStrategy.h | 9 +- .../src/Bot/Engine/Strategy/Strategy.cpp | 5 +- .../src/Bot/Engine/Strategy/Strategy.h | 21 +- .../src/Bot/Engine/Trigger/Trigger.cpp | 7 +- .../src/Bot/Engine/Trigger/Trigger.h | 10 +- .../src/Bot/Engine/Value/Value.cpp | 9 +- .../src/Bot/Engine/Value/Value.h | 23 +- .../src/Bot/Engine/WorldPacket/Event.cpp | 5 +- .../src/Bot/Engine/WorldPacket/Event.h | 9 +- .../src/Bot/Factory/AiFactory.cpp | 108 +- .../src/Bot/Factory/AiFactory.h | 9 +- .../src/Bot/Factory/PlayerbotFactory.cpp | 1296 ++- .../src/Bot/Factory/PlayerbotFactory.h | 98 +- .../Bot/Factory/RandomPlayerbotFactory.cpp | 43 +- .../src/Bot/Factory/RandomPlayerbotFactory.h | 9 +- .../mod-playerbots/src/Bot/PlayerbotAI.cpp | 1021 +- modules/mod-playerbots/src/Bot/PlayerbotAI.h | 35 +- .../mod-playerbots/src/Bot/PlayerbotMgr.cpp | 177 +- modules/mod-playerbots/src/Bot/PlayerbotMgr.h | 11 +- .../src/Bot/RandomPlayerbotMgr.cpp | 552 +- .../src/Bot/RandomPlayerbotMgr.h | 27 +- .../src/Db/FlightMasterCache.cpp | 39 - .../mod-playerbots/src/Db/FlightMasterCache.h | 36 - .../src/Db/PlayerbotDungeonRepository.cpp | 5 +- .../src/Db/PlayerbotDungeonRepository.h | 9 +- .../src/Db/PlayerbotRepository.cpp | 9 +- .../src/Db/PlayerbotRepository.h | 9 +- .../src/Db/PlayerbotSpellRepository.cpp | 6 + .../src/Db/PlayerbotSpellRepository.h | 9 +- .../src/Mgr/Guild/GuildTaskMgr.cpp | 11 +- .../src/Mgr/Guild/GuildTaskMgr.h | 9 +- .../src/Mgr/Guild/PlayerbotGuildMgr.cpp | 17 +- .../src/Mgr/Guild/PlayerbotGuildMgr.h | 10 +- .../src/Mgr/Item/BisListMgr.cpp | 93 + .../mod-playerbots/src/Mgr/Item/BisListMgr.h | 45 + .../src/Mgr/Item/ItemVisitors.cpp | 5 +- .../src/Mgr/Item/ItemVisitors.h | 30 +- .../src/Mgr/Item/LootObjectStack.cpp | 14 +- .../src/Mgr/Item/LootObjectStack.h | 9 +- .../src/Mgr/Item/RandomItemMgr.cpp | 4620 ++++---- .../src/Mgr/Item/RandomItemMgr.h | 216 +- .../src/Mgr/Item/StatsCollector.cpp | 44 +- .../src/Mgr/Item/StatsCollector.h | 13 +- .../src/Mgr/Item/StatsWeightCalculator.cpp | 371 +- .../src/Mgr/Item/StatsWeightCalculator.h | 17 +- .../src/Mgr/Move/FleeManager.cpp | 5 +- .../mod-playerbots/src/Mgr/Move/FleeManager.h | 9 +- .../src/Mgr/Security/PlayerbotSecurity.cpp | 12 +- .../src/Mgr/Security/PlayerbotSecurity.h | 9 +- .../src/Mgr/Talent/Talentspec.cpp | 15 +- .../src/Mgr/Talent/Talentspec.h | 9 +- .../src/Mgr/Text/PlayerbotTextMgr.cpp | 9 +- .../src/Mgr/Text/PlayerbotTextMgr.h | 9 +- .../src/Mgr/Travel/TravelMgr.cpp | 653 +- .../mod-playerbots/src/Mgr/Travel/TravelMgr.h | 75 +- .../src/Mgr/Travel/TravelNode.cpp | 368 +- .../src/Mgr/Travel/TravelNode.h | 23 +- .../mod-playerbots/src/PlayerbotAIConfig.cpp | 156 +- .../mod-playerbots/src/PlayerbotAIConfig.h | 75 +- .../src/Script/PlayerbotCommandScript.cpp | 17 +- .../src/Script/PlayerbotCommandScript.h | 11 + .../mod-playerbots/src/Script/Playerbots.cpp | 40 +- .../mod-playerbots/src/Script/Playerbots.h | 9 +- .../src/Script/PlayerbotsSecureLogin.cpp | 13 +- .../src/Script/WorldThr/PlayerbotOperation.h | 9 +- .../src/Script/WorldThr/PlayerbotOperations.h | 10 +- .../PlayerbotWorldThreadProcessor.cpp | 5 +- .../WorldThr/PlayerbotWorldThreadProcessor.h | 9 +- .../src/Script/WorldThr/Queue.cpp | 5 +- .../src/Script/WorldThr/Queue.h | 9 +- .../src/Script/playerbots_loader.cpp | 17 +- .../src/Util/BroadcastHelper.cpp | 7 +- .../mod-playerbots/src/Util/BroadcastHelper.h | 11 +- modules/mod-playerbots/src/Util/Helpers.cpp | 5 +- modules/mod-playerbots/src/Util/Helpers.h | 9 +- .../src/Util/LazyCalculatedValue.h | 9 +- .../src/Util/PlaceholderHelper.cpp | 5 +- .../src/Util/PlaceholderHelper.h | 9 +- .../mod-playerbots/src/Util/ServerFacade.cpp | 12 +- .../mod-playerbots/src/Util/ServerFacade.h | 9 +- 1434 files changed, 93851 insertions(+), 36678 deletions(-) create mode 100644 modules/mod-playerbots/.github/workflows/label_translation-pr.yml create mode 100644 modules/mod-playerbots/AUTHORS.md create mode 100644 modules/mod-playerbots/banner.png create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_00_playerbots_bis_gear.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_01_ai_playerbot_bis_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_05_19_00_ai_playerbot_audit_texts.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_05_22_00_ai_playerbot_send_mail_disabled_text.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_06_13_00_ai_playerbot_missing_group_buff_reagent.sql create mode 100644 modules/mod-playerbots/data/sql/playerbots/updates/2026_06_19_00_ai_playerbot_cache_cleanup.sql delete mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/PullActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/PullActions.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/FocusTargetStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Trigger/WaitForAttackTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp create mode 100644 modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h create mode 100644 modules/mod-playerbots/src/Ai/Base/Value/WaitForAttackTimeValue.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Dk/DKActions.cpp rename modules/mod-playerbots/src/Ai/Class/Dk/{Action => }/DKActions.h (95%) rename modules/mod-playerbots/src/Ai/Class/Dk/{Trigger => }/DKTriggers.cpp (87%) rename modules/mod-playerbots/src/Ai/Class/Dk/{Trigger => }/DKTriggers.h (91%) create mode 100644 modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.h rename modules/mod-playerbots/src/Ai/Class/Hunter/{Action => }/HunterActions.cpp (50%) rename modules/mod-playerbots/src/Ai/Class/Hunter/{Action => }/HunterActions.h (50%) rename modules/mod-playerbots/src/Ai/Class/Hunter/{Trigger => }/HunterTriggers.cpp (63%) rename modules/mod-playerbots/src/Ai/Class/Hunter/{Trigger => }/HunterTriggers.h (82%) rename modules/mod-playerbots/src/Ai/Class/Mage/{Action => }/MageActions.cpp (69%) rename modules/mod-playerbots/src/Ai/Class/Mage/{Action => }/MageActions.h (89%) rename modules/mod-playerbots/src/Ai/Class/Mage/{Trigger => }/MageTriggers.cpp (64%) rename modules/mod-playerbots/src/Ai/Class/Mage/{Trigger => }/MageTriggers.h (84%) delete mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.cpp rename modules/mod-playerbots/src/Ai/Class/Paladin/{Action => Actions}/PaladinActions.h (86%) create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/PaladinHelper.h create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.cpp rename modules/mod-playerbots/src/Ai/Class/Paladin/{Trigger => }/PaladinTriggers.h (64%) create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp rename modules/mod-playerbots/src/Ai/Class/Priest/{Action => }/PriestActions.cpp (91%) rename modules/mod-playerbots/src/Ai/Class/Priest/{Action => }/PriestActions.h (81%) rename modules/mod-playerbots/src/Ai/Class/Priest/{Trigger => }/PriestTriggers.cpp (53%) rename modules/mod-playerbots/src/Ai/Class/Priest/{Trigger => }/PriestTriggers.h (61%) rename modules/mod-playerbots/src/Ai/Class/Rogue/{Trigger => }/RogueTriggers.cpp (83%) rename modules/mod-playerbots/src/Ai/Class/Rogue/{Trigger => }/RogueTriggers.h (90%) delete mode 100644 modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.h rename modules/mod-playerbots/src/Ai/Class/Shaman/{Action => }/ShamanActions.cpp (78%) create mode 100644 modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.h rename modules/mod-playerbots/src/Ai/Class/Shaman/{Trigger => }/ShamanTriggers.cpp (95%) rename modules/mod-playerbots/src/Ai/Class/Shaman/{Trigger => }/ShamanTriggers.h (61%) rename modules/mod-playerbots/src/Ai/Class/Warlock/{Action => }/WarlockActions.cpp (96%) rename modules/mod-playerbots/src/Ai/Class/Warlock/{Action => }/WarlockActions.h (97%) rename modules/mod-playerbots/src/Ai/Class/Warlock/{Trigger => }/WarlockTriggers.cpp (90%) rename modules/mod-playerbots/src/Ai/Class/Warlock/{Trigger => }/WarlockTriggers.h (94%) create mode 100644 modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp rename modules/mod-playerbots/src/Ai/Class/Warrior/{Action => }/WarriorActions.cpp (69%) rename modules/mod-playerbots/src/Ai/Class/Warrior/{Action => }/WarriorActions.h (90%) create mode 100644 modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.cpp rename modules/mod-playerbots/src/Ai/Class/Warrior/{Trigger => }/WarriorTriggers.h (86%) create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.h create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.h rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/OldKingdomActionContext.h => AK/AKActionContext.h} (70%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Action/OldKingdomActions.cpp => AK/AKActions.cpp} (91%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Action/OldKingdomActions.h => AK/AKActions.h} (67%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Multiplier/OldKingdomMultipliers.cpp => AK/AKMultipliers.cpp} (83%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Multiplier/OldKingdomMultipliers.h => AK/AKMultipliers.h} (68%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Strategy/OldKingdomStrategy.cpp => AK/AKStrategy.cpp} (81%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Strategy/OldKingdomStrategy.h => AK/AKStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/OldKingdomTriggerContext.h => AK/AKTriggerContext.h} (69%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Trigger/OldKingdomTriggers.cpp => AK/AKTriggers.cpp} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{OldKingdom/Trigger/OldKingdomTriggers.h => AK/AKTriggers.h} (76%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/AzjolNerubActionContext.h => AN/ANActionContext.h} (68%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Action/AzjolNerubActions.cpp => AN/ANActions.cpp} (93%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Action/AzjolNerubActions.h => AN/ANActions.h} (69%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Multiplier/AzjolNerubMultipliers.cpp => AN/ANMultipliers.cpp} (80%) create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.h rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Strategy/AzjolNerubStrategy.cpp => AN/ANStrategy.cpp} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Strategy/AzjolNerubStrategy.h => AN/ANStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/AzjolNerubTriggerContext.h => AN/ANTriggerContext.h} (74%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Trigger/AzjolNerubTriggers.cpp => AN/ANTriggers.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{AzjolNerub/Trigger/AzjolNerubTriggers.h => AN/ANTriggers.h} (84%) delete mode 100644 modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.h rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/CullingOfStratholmeActionContext.h => CoS/CoSActionContext.h} (62%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Action/CullingOfStratholmeActions.cpp => CoS/CoSActions.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Action/CullingOfStratholmeActions.h => CoS/CoSActions.h} (61%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.cpp => CoS/CoSMultipliers.cpp} (55%) create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.h rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.cpp => CoS/CoSStrategy.cpp} (72%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.h => CoS/CoSStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/CullingOfStratholmeTriggerContext.h => CoS/CoSTriggerContext.h} (62%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.cpp => CoS/CoSTriggers.cpp} (74%) rename modules/mod-playerbots/src/Ai/Dungeon/{CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.h => CoS/CoSTriggers.h} (64%) delete mode 100644 modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.h rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/DrakTharonKeepActionContext.h => DTK/DTKActionContext.h} (83%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Action/DrakTharonKeepActions.cpp => DTK/DTKActions.cpp} (95%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Action/DrakTharonKeepActions.h => DTK/DTKActions.h} (83%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.cpp => DTK/DTKMultipliers.cpp} (83%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.h => DTK/DTKMultipliers.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Strategy/DrakTharonKeepStrategy.cpp => DTK/DTKStrategy.cpp} (85%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Strategy/DrakTharonKeepStrategy.h => DTK/DTKStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/DrakTharonKeepTriggerContext.h => DTK/DTKTriggerContext.h} (77%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Trigger/DrakTharonKeepTriggers.cpp => DTK/DTKTriggers.cpp} (85%) rename modules/mod-playerbots/src/Ai/Dungeon/{DraktharonKeep/Trigger/DrakTharonKeepTriggers.h => DTK/DTKTriggers.h} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/ForgeOfSoulsActionContext.h => FoS/FoSActionContext.h} (75%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Action/ForgeOfSoulsActions.cpp => FoS/FoSActions.cpp} (95%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Action/ForgeOfSoulsActions.h => FoS/FoSActions.h} (78%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.cpp => FoS/FoSMultipliers.cpp} (75%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.h => FoS/FoSMultipliers.h} (56%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.cpp => FoS/FoSStrategy.cpp} (71%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.h => FoS/FoSStrategy.h} (57%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/ForgeOfSoulsTriggerContext.h => FoS/FoSTriggerContext.h} (74%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.cpp => FoS/FoSTriggers.cpp} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.h => FoS/FoSTriggers.h} (78%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/GundrakActionContext.h => GD/GDActionContext.h} (69%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Action/GundrakActions.cpp => GD/GDActions.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Action/GundrakActions.h => GD/GDActions.h} (67%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Multiplier/GundrakMultipliers.cpp => GD/GDMultipliers.cpp} (82%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Multiplier/GundrakMultipliers.h => GD/GDMultipliers.h} (57%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Strategy/GundrakStrategy.cpp => GD/GDStrategy.cpp} (76%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Strategy/GundrakStrategy.h => GD/GDStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/GundrakTriggerContext.h => GD/GDTriggerContext.h} (68%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Trigger/GundrakTriggers.cpp => GD/GDTriggers.cpp} (77%) rename modules/mod-playerbots/src/Ai/Dungeon/{Gundrak/Trigger/GundrakTriggers.h => GD/GDTriggers.h} (78%) delete mode 100644 modules/mod-playerbots/src/Ai/Dungeon/HallsOfReflection/TODO rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/HallsOfLightningActionContext.h => HoL/HoLActionContext.h} (84%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Action/HallsOfLightningActions.cpp => HoL/HoLActions.cpp} (94%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Action/HallsOfLightningActions.h => HoL/HoLActions.h} (86%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Multiplier/HallsOfLightningMultipliers.cpp => HoL/HoLMultipliers.cpp} (91%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Multiplier/HallsOfLightningMultipliers.h => HoL/HoLMultipliers.h} (71%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Strategy/HallsOfLightningStrategy.cpp => HoL/HoLStrategy.cpp} (84%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Strategy/HallsOfLightningStrategy.h => HoL/HoLStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/HallsOfLightningTriggerContext.h => HoL/HoLTriggerContext.h} (84%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Trigger/HallsOfLightningTriggers.cpp => HoL/HoLTriggers.cpp} (90%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfLightning/Trigger/HallsOfLightningTriggers.h => HoL/HoLTriggers.h} (88%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/HallsOfStoneActionContext.h => HoS/HoSActionContext.h} (63%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Action/HallsOfStoneActions.cpp => HoS/HoSActions.cpp} (84%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Action/HallsOfStoneActions.h => HoS/HoSActions.h} (59%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Multiplier/HallsOfStoneMultipliers.cpp => HoS/HoSMultipliers.cpp} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Multiplier/HallsOfStoneMultipliers.h => HoS/HoSMultipliers.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Strategy/HallsOfStoneStrategy.cpp => HoS/HoSStrategy.cpp} (78%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Strategy/HallsOfStoneStrategy.h => HoS/HoSStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/HallsOfStoneTriggerContext.h => HoS/HoSTriggerContext.h} (63%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Trigger/HallsOfStoneTriggers.cpp => HoS/HoSTriggers.cpp} (72%) rename modules/mod-playerbots/src/Ai/Dungeon/{HallsOfStone/Trigger/HallsOfStoneTriggers.h => HoS/HoSTriggers.h} (72%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/NexusActionContext.h => Nex/NexActionContext.h} (79%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Action/NexusActions.cpp => Nex/NexActions.cpp} (94%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Action/NexusActions.h => Nex/NexActions.h} (79%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Multiplier/NexusMultipliers.cpp => Nex/NexMultipliers.cpp} (89%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Multiplier/NexusMultipliers.h => Nex/NexMultipliers.h} (73%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Strategy/NexusStrategy.cpp => Nex/NexStrategy.cpp} (89%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Strategy/NexusStrategy.h => Nex/NexStrategy.h} (57%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/NexusTriggerContext.h => Nex/NexTriggerContext.h} (83%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Trigger/NexusTriggers.cpp => Nex/NexTriggers.cpp} (92%) rename modules/mod-playerbots/src/Ai/Dungeon/{Nexus/Trigger/NexusTriggers.h => Nex/NexTriggers.h} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/OculusActionContext.h => OC/OCActionContext.h} (81%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Action/OculusActions.cpp => OC/OCActions.cpp} (97%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Action/OculusActions.h => OC/OCActions.h} (85%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Multiplier/OculusMultipliers.cpp => OC/OCMultipliers.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Multiplier/OculusMultipliers.h => OC/OCMultipliers.h} (79%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Strategy/OculusStrategy.cpp => OC/OCStrategy.cpp} (84%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Strategy/OculusStrategy.h => OC/OCStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/OculusTriggerContext.h => OC/OCTriggerContext.h} (83%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Trigger/OculusTriggers.cpp => OC/OCTriggers.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{Oculus/Trigger/OculusTriggers.h => OC/OCTriggers.h} (92%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/PitOfSaronActionContext.h => PoS/PoSActionContext.h} (61%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Action/PitOfSaronActions.cpp => PoS/PoSActions.cpp} (91%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Action/PitOfSaronActions.h => PoS/PoSActions.h} (69%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp => PoS/PoSMultipliers.cpp} (76%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Multiplier/PitOfSaronMultipliers.h => PoS/PoSMultipliers.h} (56%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Strategy/PitOfSaronStrategy.cpp => PoS/PoSStrategy.cpp} (60%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Strategy/PitOfSaronStrategy.h => PoS/PoSStrategy.h} (57%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/PitOfSaronTriggerContext.h => PoS/PoSTriggerContext.h} (60%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Trigger/PitOfSaronTriggers.cpp => PoS/PoSTriggers.cpp} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{PitOfSaron/Trigger/PitOfSaronTriggers.h => PoS/PoSTriggers.h} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/TrialOfTheChampionActionContext.h => TOC/TOCActionContext.h} (74%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/Action/TrialOfTheChampionActions.cpp => TOC/TOCActions.cpp} (96%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/Action/TrialOfTheChampionActions.h => TOC/TOCActions.h} (81%) create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.h rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp => TOC/TOCStrategy.cpp} (70%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.h => TOC/TOCStrategy.h} (56%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/TrialOfTheChampionTriggerContext.h => TOC/TOCTriggerContext.h} (75%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.cpp => TOC/TOCTriggers.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.h => TOC/TOCTriggers.h} (91%) create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonTriggerContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.h rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/UtgardeKeepActionContext.h => UK/UKActionContext.h} (81%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Action/UtgardeKeepActions.cpp => UK/UKActions.cpp} (91%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Action/UtgardeKeepActions.h => UK/UKActions.h} (77%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Multiplier/UtgardeKeepMultipliers.cpp => UK/UKMultipliers.cpp} (89%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Multiplier/UtgardeKeepMultipliers.h => UK/UKMultipliers.h} (69%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Strategy/UtgardeKeepStrategy.cpp => UK/UKStrategy.cpp} (86%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Strategy/UtgardeKeepStrategy.h => UK/UKStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/UtgardeKeepTriggerContext.h => UK/UKTriggerContext.h} (82%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Trigger/UtgardeKeepTriggers.cpp => UK/UKTriggers.cpp} (89%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardeKeep/Trigger/UtgardeKeepTriggers.h => UK/UKTriggers.h} (88%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/UtgardePinnacleActionContext.h => UP/UPActionContext.h} (69%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Action/UtgardePinnacleActions.cpp => UP/UPActions.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Action/UtgardePinnacleActions.h => UP/UPActions.h} (60%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.cpp => UP/UPMultipliers.cpp} (91%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.h => UP/UPMultipliers.h} (57%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Strategy/UtgardePinnacleStrategy.cpp => UP/UPStrategy.cpp} (74%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Strategy/UtgardePinnacleStrategy.h => UP/UPStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/UtgardePinnacleTriggerContext.h => UP/UPTriggerContext.h} (68%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Trigger/UtgardePinnacleTriggers.cpp => UP/UPTriggers.cpp} (87%) rename modules/mod-playerbots/src/Ai/Dungeon/{UtgardePinnacle/Trigger/UtgardePinnacleTriggers.h => UP/UPTriggers.h} (81%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/VioletHoldActionContext.h => VH/VHActionContext.h} (73%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Action/VioletHoldActions.cpp => VH/VHActions.cpp} (91%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Action/VioletHoldActions.h => VH/VHActions.h} (76%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Multiplier/VioletHoldMultipliers.cpp => VH/VHMultipliers.cpp} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Multiplier/VioletHoldMultipliers.h => VH/VHMultipliers.h} (66%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Strategy/VioletHoldStrategy.cpp => VH/VHStrategy.cpp} (81%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Strategy/VioletHoldStrategy.h => VH/VHStrategy.h} (58%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/VioletHoldTriggerContext.h => VH/VHTriggerContext.h} (77%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Trigger/VioletHoldTriggers.cpp => VH/VHTriggers.cpp} (80%) rename modules/mod-playerbots/src/Ai/Dungeon/{VioletHold/Trigger/VioletHoldTriggers.h => VH/VHTriggers.h} (82%) delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.h rename modules/mod-playerbots/src/Ai/Raid/Aq20/{RaidAq20ActionContext.h => Aq20ActionContext.h} (50%) rename modules/mod-playerbots/src/Ai/Raid/Aq20/{Action/RaidAq20Actions.cpp => Aq20Actions.cpp} (86%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.cpp rename modules/mod-playerbots/src/Ai/Raid/Aq20/{Strategy/RaidAq20Strategy.h => Aq20Strategy.h} (53%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20TriggerContext.h rename modules/mod-playerbots/src/Ai/Raid/Aq20/{Trigger/RaidAq20Triggers.cpp => Aq20Triggers.cpp} (82%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.h rename modules/mod-playerbots/src/Ai/Raid/Aq20/{Util/RaidAq20Utils.cpp => Aq20Utils.cpp} (79%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlActionContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.h rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/RaidEoEActionContext.h => EoE/EoEActionContext.h} (77%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Action/RaidEoEActions.cpp => EoE/EoEActions.cpp} (97%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Action/RaidEoEActions.h => EoE/EoEActions.h} (85%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Multiplier/RaidEoEMultipliers.cpp => EoE/EoEMultipliers.cpp} (85%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.h rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Strategy/RaidEoEStrategy.cpp => EoE/EoEStrategy.cpp} (69%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Strategy/RaidEoEStrategy.h => EoE/EoEStrategy.h} (53%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/RaidEoETriggerContext.h => EoE/EoETriggerContext.h} (57%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Trigger/RaidEoETriggers.cpp => EoE/EoETriggers.cpp} (79%) rename modules/mod-playerbots/src/Ai/Raid/{EyeOfEternity/Trigger/RaidEoETriggers.h => EoE/EoETriggers.h} (83%) delete mode 100644 modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.h rename modules/mod-playerbots/src/Ai/Raid/{GruulsLair/Strategy/RaidGruulsLairStrategy.cpp => Gruul/GruulStrategy.cpp} (51%) rename modules/mod-playerbots/src/Ai/Raid/{GruulsLair/Strategy/RaidGruulsLairStrategy.h => Gruul/GruulStrategy.h} (53%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalScripts.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BPC.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BQL.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_DBS.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_Dogs.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_FG.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_GSB.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LDW.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LK.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LM.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_PP.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_RF.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SG.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SS.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_VT.cpp rename modules/mod-playerbots/src/Ai/Raid/{Icecrown/RaidIccActionContext.h => ICC/ICCActionContext.h} (79%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.cpp rename modules/mod-playerbots/src/Ai/Raid/{Icecrown/Multiplier/RaidIccMultipliers.h => ICC/ICCMultipliers.h} (77%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.h rename modules/mod-playerbots/src/Ai/Raid/{Icecrown/Strategy/RaidIccStrategy.cpp => ICC/ICCStrategy.cpp} (78%) rename modules/mod-playerbots/src/Ai/Raid/{Icecrown/Strategy/RaidIccStrategy.h => ICC/ICCStrategy.h} (53%) rename modules/mod-playerbots/src/Ai/Raid/{Icecrown/RaidIccTriggerContext.h => ICC/ICCTriggerContext.h} (78%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.cpp rename modules/mod-playerbots/src/Ai/Raid/{Icecrown/Trigger/RaidIccTriggers.h => ICC/ICCTriggers.h} (73%) delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/RaidKarazhanActionContext.h => Kara/KaraActionContext.h} (51%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Action/RaidKarazhanActions.cpp => Kara/KaraActions.cpp} (91%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Action/RaidKarazhanActions.h => Kara/KaraActions.h} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Util/RaidKarazhanHelpers.cpp => Kara/KaraHelpers.cpp} (95%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Util/RaidKarazhanHelpers.h => Kara/KaraHelpers.h} (91%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Multiplier/RaidKarazhanMultipliers.cpp => Kara/KaraMultipliers.cpp} (90%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Multiplier/RaidKarazhanMultipliers.h => Kara/KaraMultipliers.h} (82%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Strategy/RaidKarazhanStrategy.cpp => Kara/KaraStrategy.cpp} (94%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Strategy/RaidKarazhanStrategy.h => Kara/KaraStrategy.h} (53%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/RaidKarazhanTriggerContext.h => Kara/KaraTriggerContext.h} (55%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Trigger/RaidKarazhanTriggers.cpp => Kara/KaraTriggers.cpp} (82%) rename modules/mod-playerbots/src/Ai/Raid/{Karazhan/Trigger/RaidKarazhanTriggers.h => Kara/KaraTriggers.h} (96%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/RaidMcActionContext.h => MC/MCActionContext.h} (87%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Action/RaidMcActions.cpp => MC/MCActions.cpp} (82%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Action/RaidMcActions.h => MC/MCActions.h} (78%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/MC/MCHelpers.h rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Multiplier/RaidMcMultipliers.cpp => MC/MCMultipliers.cpp} (91%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Multiplier/RaidMcMultipliers.h => MC/MCMultipliers.h} (67%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Strategy/RaidMcStrategy.cpp => MC/MCStrategy.cpp} (87%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Strategy/RaidMcStrategy.h => MC/MCStrategy.h} (52%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/RaidMcTriggerContext.h => MC/MCTriggerContext.h} (86%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Trigger/RaidMcTriggers.cpp => MC/MCTriggers.cpp} (70%) rename modules/mod-playerbots/src/Ai/Raid/{MoltenCore/Trigger/RaidMcTriggers.h => MC/MCTriggers.h} (71%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.h rename modules/mod-playerbots/src/Ai/Raid/{Magtheridon/Strategy/RaidMagtheridonStrategy.cpp => Mag/MagStrategy.cpp} (68%) rename modules/mod-playerbots/src/Ai/Raid/{Magtheridon/Strategy/RaidMagtheridonStrategy.h => Mag/MagStrategy.h} (54%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagScripts.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Anubrekhan.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Faerlina.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_FourHorsemen.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gluth.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gothik.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Grobbulus.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Heigan.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Kelthuzad.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Loatheb.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Maexxna.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Noth.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Patchwerk.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Razuvious.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Sapphiron.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Shared.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Thaddius.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxBossHelper.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxSpellIds.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.h rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/RaidOsActionContext.h => OS/OSActionContext.h} (79%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Action/RaidOsActions.cpp => OS/OSActions.cpp} (92%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Action/RaidOsActions.h => OS/OSActions.h} (86%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Multiplier/RaidOsMultipliers.cpp => OS/OSMultipliers.cpp} (81%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.h rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Strategy/RaidOsStrategy.cpp => OS/OSStrategy.cpp} (80%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Strategy/RaidOsStrategy.h => OS/OSStrategy.h} (53%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/RaidOsTriggerContext.h => OS/OSTriggerContext.h} (80%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Trigger/RaidOsTriggers.cpp => OS/OSTriggers.cpp} (92%) rename modules/mod-playerbots/src/Ai/Raid/{ObsidianSanctum/Trigger/RaidOsTriggers.h => OS/OSTriggers.h} (92%) delete mode 100644 modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.h rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/RaidOnyxiaActionContext.h => Ony/OnyActionContext.h} (75%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/Action/RaidOnyxiaActions.cpp => Ony/OnyActions.cpp} (91%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/Action/RaidOnyxiaActions.h => Ony/OnyActions.h} (61%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/Strategy/RaidOnyxiaStrategy.cpp => Ony/OnyStrategy.cpp} (76%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/Strategy/RaidOnyxiaStrategy.h => Ony/OnyStrategy.h} (54%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/RaidOnyxiaTriggerContext.h => Ony/OnyTriggerContext.h} (75%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/Trigger/RaidOnyxiaTriggers.cpp => Ony/OnyTriggers.cpp} (92%) rename modules/mod-playerbots/src/Ai/Raid/{Onyxia/Trigger/RaidOnyxiaTriggers.h => Ony/OnyTriggers.h} (71%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ADD.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_BAL.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_HAL.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_SAV.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ZAR.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSShared.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.h rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/RaidSSCActionContext.h => SSC/SSCActionContext.h} (57%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Action/RaidSSCActions.cpp => SSC/SSCActions.cpp} (66%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Action/RaidSSCActions.h => SSC/SSCActions.h} (93%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Util/RaidSSCHelpers.cpp => SSC/SSCHelpers.cpp} (65%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Util/RaidSSCHelpers.h => SSC/SSCHelpers.h} (82%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp => SSC/SSCMultipliers.cpp} (62%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h => SSC/SSCMultipliers.h} (92%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp => SSC/SSCStrategy.cpp} (96%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Strategy/RaidSSCStrategy.h => SSC/SSCStrategy.h} (52%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/RaidSSCTriggerContext.h => SSC/SSCTriggerContext.h} (59%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp => SSC/SSCTriggers.cpp} (61%) rename modules/mod-playerbots/src/Ai/Raid/{SerpentshrineCavern/Trigger/RaidSSCTriggers.h => SSC/SSCTriggers.h} (97%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKTriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/Util/TKKaelthasBossAI.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/TK/Util/TKScripts.cpp rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/RaidUlduarActionContext.h => Uld/UldActionContext.h} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Action/RaidUlduarActions.cpp => Uld/UldActions.cpp} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Action/RaidUlduarActions.h => Uld/UldActions.h} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Strategy/RaidUlduarStrategy.cpp => Uld/UldStrategy.cpp} (97%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.h rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/RaidUlduarTriggerContext.h => Uld/UldTriggerContext.h} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Trigger/RaidUlduarTriggers.cpp => Uld/UldTriggers.cpp} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Trigger/RaidUlduarTriggers.h => Uld/UldTriggers.h} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Util/RaidUlduarBossHelper.cpp => Uld/Util/UldBossHelper.cpp} (97%) rename modules/mod-playerbots/src/Ai/Raid/{Ulduar/Util/RaidUlduarBossHelper.h => Uld/Util/UldBossHelper.h} (97%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldScripts.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h delete mode 100644 modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/RaidVoAActionContext.h => VoA/VoAActionContext.h} (81%) rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/Action/RaidVoAActions.cpp => VoA/VoAActions.cpp} (95%) rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/Action/RaidVoAActions.h => VoA/VoAActions.h} (77%) rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/Strategy/RaidVoAStrategy.cpp => VoA/VoAStrategy.cpp} (80%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.h rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/RaidVoATriggerContext.h => VoA/VoATriggerContext.h} (80%) rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/Trigger/RaidVoATriggers.cpp => VoA/VoATriggers.cpp} (92%) rename modules/mod-playerbots/src/Ai/Raid/{VaultOfArchavon/Trigger/RaidVoATriggers.h => VoA/VoATriggers.h} (77%) create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAActionContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggerContext.h create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.cpp create mode 100644 modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.h create mode 100644 modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp create mode 100644 modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h rename modules/mod-playerbots/src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.cpp (94%) rename modules/mod-playerbots/src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.h (69%) rename modules/mod-playerbots/src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.cpp (80%) rename modules/mod-playerbots/src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.h (94%) rename modules/mod-playerbots/src/Ai/{Base/Actions => World/Rpg}/RpgValues.h (52%) rename modules/mod-playerbots/src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.cpp (93%) rename modules/mod-playerbots/src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.h (65%) create mode 100644 modules/mod-playerbots/src/Bot/Engine/BuildSharedActionContexts.cpp create mode 100644 modules/mod-playerbots/src/Bot/Engine/BuildSharedStrategyContexts.cpp create mode 100644 modules/mod-playerbots/src/Bot/Engine/BuildSharedTriggerContexts.cpp create mode 100644 modules/mod-playerbots/src/Bot/Engine/BuildSharedValueContexts.cpp delete mode 100644 modules/mod-playerbots/src/Db/FlightMasterCache.cpp delete mode 100644 modules/mod-playerbots/src/Db/FlightMasterCache.h create mode 100644 modules/mod-playerbots/src/Mgr/Item/BisListMgr.cpp create mode 100644 modules/mod-playerbots/src/Mgr/Item/BisListMgr.h diff --git a/modules/mod-playerbots/.github/workflows/check_pr_source.yml b/modules/mod-playerbots/.github/workflows/check_pr_source.yml index 5b354f6f7..823019834 100644 --- a/modules/mod-playerbots/.github/workflows/check_pr_source.yml +++ b/modules/mod-playerbots/.github/workflows/check_pr_source.yml @@ -2,6 +2,7 @@ name: Enforce test-staging → master on: pull_request: + types: [opened, synchronize, reopened, edited] branches: - master - test-staging diff --git a/modules/mod-playerbots/.github/workflows/codestyle_cpp.yml b/modules/mod-playerbots/.github/workflows/codestyle_cpp.yml index 065732890..b3d69867c 100644 --- a/modules/mod-playerbots/.github/workflows/codestyle_cpp.yml +++ b/modules/mod-playerbots/.github/workflows/codestyle_cpp.yml @@ -6,10 +6,6 @@ on: - reopened - synchronize - ready_for_review - paths: - - src/** - - "!README.md" - - "!docs/**" concurrency: group: "codestyle-cppcheck-${{ github.event.pull_request.number }}" @@ -22,13 +18,27 @@ jobs: if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + cpp: + - 'src/**' + - '!README.md' + - '!docs/**' + - name: Setup python + if: steps.filter.outputs.cpp == 'true' uses: actions/setup-python@v5 with: python-version: '3.10' - name: AzerothCore codestyle + if: steps.filter.outputs.cpp == 'true' run: python ./apps/codestyle/codestyle-cpp.py + - name: C++ Advanced + if: steps.filter.outputs.cpp == 'true' run: | sudo apt update -y sudo apt install -y cppcheck diff --git a/modules/mod-playerbots/.github/workflows/core_build.yml b/modules/mod-playerbots/.github/workflows/core_build.yml index ac6ee60d8..6021238e6 100644 --- a/modules/mod-playerbots/.github/workflows/core_build.yml +++ b/modules/mod-playerbots/.github/workflows/core_build.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v3 with: repository: 'mod-playerbots/azerothcore-wotlk' - ref: 'Playerbot' + ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }} - name: Set reusable strings id: strings diff --git a/modules/mod-playerbots/.github/workflows/label_translation-pr.yml b/modules/mod-playerbots/.github/workflows/label_translation-pr.yml new file mode 100644 index 000000000..ae5705aaa --- /dev/null +++ b/modules/mod-playerbots/.github/workflows/label_translation-pr.yml @@ -0,0 +1,42 @@ +name: Label translation PRs + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label-translation: + runs-on: ubuntu-latest + steps: + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + fetch-depth: 0 + + - name: Fetch PR head and generate diff + run: | + git fetch origin refs/pull/${{ github.event.pull_request.number }}/head + git diff ${{ github.event.pull_request.base.sha }}...FETCH_HEAD > pr.diff + + - name: Detect ai_playerbot_texts inserts + id: detect + run: | + if grep -E '^\+.*INSERT[[:space:]]+INTO[[:space:]]+`?ai_playerbot_texts`?' pr.diff; then + echo "has_translation=true" >> "$GITHUB_OUTPUT" + else + echo "has_translation=false" >> "$GITHUB_OUTPUT" + fi + + - name: Add label + if: steps.detect.outputs.has_translation == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr edit ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ + --add-label "Added translation" diff --git a/modules/mod-playerbots/.github/workflows/macos_build.yml b/modules/mod-playerbots/.github/workflows/macos_build.yml index ab4d83daa..a7b5a5e13 100644 --- a/modules/mod-playerbots/.github/workflows/macos_build.yml +++ b/modules/mod-playerbots/.github/workflows/macos_build.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 with: repository: 'mod-playerbots/azerothcore-wotlk' - ref: 'Playerbot' + ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }} - name: Checkout Playerbot Module uses: actions/checkout@v4 with: diff --git a/modules/mod-playerbots/.github/workflows/windows_build.yml b/modules/mod-playerbots/.github/workflows/windows_build.yml index 121d97751..59dd11995 100644 --- a/modules/mod-playerbots/.github/workflows/windows_build.yml +++ b/modules/mod-playerbots/.github/workflows/windows_build.yml @@ -1,4 +1,5 @@ name: windows-build + on: push: branches: [ "master", "test-staging" ] @@ -6,7 +7,7 @@ on: branches: [ "master", "test-staging" ] concurrency: - group: "windows-build-${{ github.event.pull_request.number }}" + group: "windows-build-${{ github.event.pull_request.number || github.ref }}" cancel-in-progress: true jobs: @@ -14,36 +15,144 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest] + os: [windows-2022] + runs-on: ${{ matrix.os }} name: ${{ matrix.os }} + env: - BOOST_ROOT: C:\local\boost_1_82_0 + BOOST_ROOT: C:\local\boost_1_87_0 + CMAKE_GENERATOR: Ninja + CTOOLS_BUILD: all + steps: - name: Checkout AzerothCore - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'mod-playerbots/azerothcore-wotlk' - ref: 'Playerbot' - path: 'ac' + ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }} + path: a + - name: Checkout Playerbot Module - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: 'mod-playerbots/mod-playerbots' - #path: 'modules/mod-playerbots' - path: ac/modules/mod-playerbots + path: a/modules/mod-playerbots + + - name: Move source tree to short path + shell: powershell + run: | + if (Test-Path C:\ac) { + Remove-Item C:\ac -Recurse -Force + } + + New-Item -ItemType Directory -Path C:\ac | Out-Null + + robocopy "${{ github.workspace }}\a" "C:\ac" /MIR /NFL /NDL /NJH /NJS /NP + + if ($LASTEXITCODE -le 7) { + exit 0 + } + + exit $LASTEXITCODE + + - name: Install Boost and Ninja + shell: powershell + run: | + choco install ninja -y --no-progress + choco install boost-msvc-14.3 --version=1.87.0 -y --no-progress + + - name: Prepare OpenSSL from runner image + shell: powershell + run: | + $candidates = @( + "C:\Program Files\OpenSSL", + "C:\Program Files\OpenSSL-Win64" + ) + + $opensslRoot = $null + + foreach ($candidate in $candidates) { + if ((Test-Path "$candidate\bin\openssl.exe") -and (Test-Path "$candidate\include\openssl\ssl.h")) { + $opensslRoot = $candidate + break + } + } + + if (-not $opensslRoot) { + Write-Host "OpenSSL candidates checked:" + $candidates | ForEach-Object { Write-Host " - $_" } + throw "OpenSSL development files not found on runner image" + } + + if (Test-Path C:\openssl) { + Remove-Item C:\openssl -Recurse -Force + } + + New-Item -ItemType Junction -Path C:\openssl -Target $opensslRoot | Out-Null + + "OPENSSL_ROOT_DIR=C:\openssl" >> $env:GITHUB_ENV + "OPENSSL_DIR=C:\openssl" >> $env:GITHUB_ENV + "PATH=C:\openssl\bin;$env:PATH" >> $env:GITHUB_ENV + + C:\openssl\bin\openssl.exe version + - name: ccache uses: hendrikmuhs/ccache-action@v1.2.13 - - name: Configure OS + + - name: Create AzerothCore CI config shell: bash - working-directory: ac - env: - CONTINUOUS_INTEGRATION: true + working-directory: C:\ac run: | - ./acore.sh install-deps + mkdir -p conf + + cat > conf/config.sh <<'EOF' + CCOMPILERC="cl" + CCOMPILERCXX="cl" + + CTYPE="Release" + CSCRIPTS="static" + CMODULES="static" + CTOOLS_BUILD="all" + + CCUSTOMOPTIONS="-DBOOST_ROOT=C:/local/boost_1_87_0 -DOPENSSL_ROOT_DIR=C:/openssl -DOPENSSL_USE_STATIC_LIBS=FALSE -DCMAKE_RC_COMPILER=rc -DCMAKE_NINJA_FORCE_RESPONSE_FILE=ON -DCMAKE_NINJA_CMCLDEPS_RC=OFF -DCMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS=ON -DCMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS=ON -DCMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES=ON -DCMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES=ON -DCMAKE_C_USE_RESPONSE_FILE_FOR_LIBRARIES=ON -DCMAKE_CXX_USE_RESPONSE_FILE_FOR_LIBRARIES=ON" + EOF + + cat conf/config.sh + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 + with: + arch: x64 + - name: Build shell: bash - working-directory: ac + working-directory: C:\ac run: | export CTOOLS_BUILD=all + export CMAKE_GENERATOR=Ninja + + export CC=cl + export CXX=cl + export RC=rc + + cmake --version + ninja --version + + echo "CMAKE_GENERATOR=$CMAKE_GENERATOR" + echo "CC=$CC" + echo "CXX=$CXX" + echo "BOOST_ROOT=$BOOST_ROOT" + echo "OPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR" + + which cl || true + which rc || true + which openssl || true + + cl || true + rc || true + openssl version || true + + rm -rf var/build/obj + ./acore.sh compiler build diff --git a/modules/mod-playerbots/AUTHORS.md b/modules/mod-playerbots/AUTHORS.md new file mode 100644 index 000000000..bce8022a3 --- /dev/null +++ b/modules/mod-playerbots/AUTHORS.md @@ -0,0 +1,121 @@ +# List of AUTHORS who contributed over time to the mod-playerbots project + +## Point of current development +The project is currently developed under https://github.com/mod-playerbots/mod-playerbots, as a module for AzerothCore (https://www.azerothcore.org/) + +## History of development +mod-playerbots continues the playerbot AI lineage, which was developed under various umbrellas over time: +* Playerbot AI core by ike3, originally developed for the MaNGOS/TrinityCore family +* CMaNGOS playerbots, located at https://github.com/cmangos/playerbots +* mod-playerbots, 2021-CURRENT, located at https://github.com/mod-playerbots/mod-playerbots + +## Authorship of the code +Authorship is assigned for each commit within the git history, which is stored in this git repository: +* github.com/mod-playerbots/mod-playerbots + +## Authors List: + +*Please inform us, if you find somebody who is missing!* + +* achyles222 +* Aldori +* Alex Dcnh +* Alvin Zhu +* antony +* Atidot3 +* avirar +* AzoraNova +* bash +* BeardBear +* Ben Wagner +* Benjamin Jackson +* blinkysc +* Bobblybook +* Bogir[rus] +* Boidl +* Boxhead78 +* Brian +* Caffarius +* chris +* Chris Lacy +* Christoph Wilk +* Chromie-WoW +* Crow +* Cryo81 +* CrypticAvacado +* Dennis +* dillyns +* Dreathean +* Engardium +* EricksOliveira +* Fiery +* Flashtate98 +* FutterSillo +* Fuzz +* Gabriel Comeau +* Garashan +* Gonzalo +* gtkk +* HennyWilly +* Hokken +* hongyiqin +* HT Cesta +* Iain Donnelly +* Ivan Novokhatski +* Jay +* Jelly +* Jered +* Jgoodwin64 +* jublinq +* Julien MAS +* kadeshar +* Keleborn +* kikiviki +* killerzwelch +* Lichborne +* Mikhail +* mostlynick3 +* mtm84 +* nick +* Nicolas Lebacq +* Noscopezz +* Not +* NotCronky +* NoxMax +* Oiuv +* OlegKungurov +* oskov +* Paulo +* Paulo Oliveira +* privatecore +* qcloud +* qudzy +* Revision +* Rikus Louw +* SaW +* Scarecr0w12 +* sd3420625 +* secahtah +* Spargel +* St0ny +* sudlud +* Tecc +* ThePenguinMan96 +* Thomas Frey +* tomcoffingiii +* Trus3683 +* Type1Error +* UltraNix +* valsan-azerty-boi +* Vanna White +* Veit F. +* Vigerus +* wetbrownsauce +* whipowill +* xSparky911x +* XYUU +* yang +* Yunfan Li +* zeb139 +* ZhengPeiRu21 +* 姜耀 diff --git a/modules/mod-playerbots/LICENSE b/modules/mod-playerbots/LICENSE index dbbe35581..d159169d1 100644 --- a/modules/mod-playerbots/LICENSE +++ b/modules/mod-playerbots/LICENSE @@ -1,620 +1,281 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to this License. - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. - 13. Remote Network Interaction; Use with the GNU General Public License. +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. - 14. Revised Versions of this License. + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. + NO WARRANTY - 15. Disclaimer of Warranty. + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS @@ -626,36 +287,53 @@ free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least +convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. + GNU General Public License for more details. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/modules/mod-playerbots/PULL_REQUEST_TEMPLATE.md b/modules/mod-playerbots/PULL_REQUEST_TEMPLATE.md index d26b1b1cd..3c3b8adb9 100644 --- a/modules/mod-playerbots/PULL_REQUEST_TEMPLATE.md +++ b/modules/mod-playerbots/PULL_REQUEST_TEMPLATE.md @@ -1,124 +1,122 @@ -# Pull Request + -We prioritize **stability, performance, and predictability** over behavioral realism. -Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and -long-term robustness. +## Pull Request Description + -Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all -participants. Because every action and -decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and -negatively affect both players and -world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a -project goal. Increased behavioral -realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. -Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and -maintained continuously as the system evolves. -If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision -model**. More complex behavior should only be -available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. - -Principles: - -- **Stability before intelligence** - A stable system is always preferred over a smarter one. - -- **Performance is a shared resource** - Any increase in bot cost affects all players and all bots. - -- **Simple logic scales better than smart logic** - Predictable behavior under load is more valuable than perfect decisions. - -- **Complexity must justify itself** - If a feature cannot clearly explain its cost, it should not exist. - -- **Defaults must be cheap** - Expensive behavior must always be optional and clearly communicated. - -- **Bots should look reasonable, not perfect** - The goal is believable behavior, not human simulation. - -Before submitting, confirm that this change aligns with those principles. - ---- ## Feature Evaluation + -Please answer the following: + +- Describe the **minimum logic** required to achieve the intended behavior. +- Describe the **processing cost** when this logic executes across many bots. -- Describe the **minimum logic** required to achieve the intended behavior? -- Describe the **cheapest implementation** that produces an acceptable result? -- Describe the **runtime cost** when this logic executes across many bots? ---- ## How to Test the Changes + -- Step-by-step instructions to test the change -- Any required setup (e.g. multiple players, bots, specific configuration) -- Expected behavior and how to verify it -## Complexity & Impact -Does this change add new decision branches? -- - [ ] No -- - [ ] Yes (**explain below**) +## Impact Assessment + +- Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? + - - [ ] No, not at all + - - [ ] Minimal impact (**explain below**) + - - [ ] Moderate impact (**explain below**) -Does this change increase per-bot or per-tick processing? -- - [ ] No -- - [ ] Yes (**describe and justify impact**) -Could this logic scale poorly under load? -- - [ ] No -- - [ ] Yes (**explain why**) ---- -## Defaults & Configuration +- Does this change modify default bot behavior? + - - [ ] No + - - [ ] Yes (**explain why**) + + + +- Does this change add new decision branches or increase maintenance complexity? + - - [ ] No + - - [ ] Yes (**explain below**) -Does this change modify default bot behavior? -- - [ ] No -- - [ ] Yes (**explain why**) -If this introduces more advanced or AI-heavy logic: -- - [ ] Lightweight mode remains the default -- - [ ] More complex behavior is optional and thereby configurable ---- ## AI Assistance - -Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? + +Was AI assistance used while working on this change? - - [ ] No - - [ ] Yes (**explain below**) - + -- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) -- Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) -- Which parts of the change were influenced or generated -- Whether the result was manually reviewed and adapted -AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. -Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest -about what they do and do not understand. ---- +## Code Provenance / Attribution + +Was any code in this PR copied or adapted from a sister / upstream project (e.g. CMaNGOS playerbots, + MaNGOS, another module)? +- - [ ] No, all code in this PR is original +- - [ ] Yes (**name the project and the original author(s) below**) + + + + + ## Final Checklist -- - [ ] Stability is not compromised -- - [ ] Performance impact is understood, tested, and acceptable -- - [ ] Added logic complexity is justified and explained -- - [ ] Documentation updated if needed - ---- +- - [ ] Stability is not compromised. +- - [ ] Performance impact is understood, tested, and acceptable. +- - [ ] Added logic complexity is justified and explained. +- - [ ] Any new bot dialogue lines are translated. +- - [ ] Any code ported/adapted from another project is attributed (project + author(s), `Co-authored-by:` trailer + in-file note). +- - [ ] New source files use the GPLv2 header. +- - [ ] Documentation updated if needed (Conf comments, WiKi commands). +- - [ ] New and modified files do not introduce new compiler warnings. ## Notes for Reviewers - -Anything that significantly improves realism at the cost of stability or performance should be carefully discussed -before merging. + diff --git a/modules/mod-playerbots/README.md b/modules/mod-playerbots/README.md index 3924765ab..1b5205168 100644 --- a/modules/mod-playerbots/README.md +++ b/modules/mod-playerbots/README.md @@ -8,7 +8,7 @@
- Playerbots Icon + Playerbots Banner
@@ -32,13 +32,11 @@ We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can ## Installation -Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. +Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. -**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below. +> **Important:** All `mod-playerbots` installations require a custom fork of AzerothCore: [mod-playerbots/azerothcore-wotlk (Playerbot branch)](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). The standard AzerothCore repository will **not** work. -### Cloning the Repositories - -To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following: +### Quick Start ```bash git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot @@ -46,44 +44,18 @@ cd azerothcore-wotlk/modules git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master ``` -For more information, refer to the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. +Then build the server following the platform-specific instructions in our **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)**. -### Docker Installation +> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues. -Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module: +### Detailed Guides -```bash -git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot -cd azerothcore-wotlk/modules -git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master -``` +| Guide | Description | +|---|---| +| **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)** | Full step-by-step instructions for clean installs, migrating from existing AzerothCore, Docker setup, adding modules, and updating | +| **[Troubleshooting](https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting)** | Solutions to the most common build errors, database issues, configuration mistakes, crashes, and platform-specific problems | -Afterwards, create a `docker-compose.override.yml` file in the `azerothcore-wotlk` directory. This override file allows for mounting the modules directory to the `ac-worldserver` service which is required for it to run. Put the following inside and save: - -```yml -services: - ac-worldserver: - volumes: - - ./modules:/azerothcore/modules:ro -``` - -Additionally, this override file can be used to set custom configuration settings for `ac-worldserver` and any modules you install as environment variables: - -```yml -services: - ac-worldserver: - environment: - AC_RATE_XP_KILL: "1" - AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "1" - volumes: - - ./modules:/azerothcore/modules:ro -``` - -For example, to double the experience gain rate per kill, take the setting `Rate.XP.Kill = 1` from [woldserver.conf](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/src/server/apps/worldserver/worldserver.conf.dist), convert it to an environment variable, and change it to the desired setting in the override file to get `AC_RATE_XP_KILL: "2"`. If you wanted to disable random bots from logging in automatically, take the `AiPlayerbot.RandomBotAutologin = 1` setting from [playerbots.conf](https://github.com/mod-playerbots/mod-playerbots/blob/master/conf/playerbots.conf.dist) and do the same to get `AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "0"`. For more information on how to configure Azerothcore, Playerbots, and other module settings as environment variables in Docker Compose, see the "Configuring AzerothCore in Containers" section in the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) guide. - -Before building, consider setting the database password. One way to do this is to create a `.env` file in the root `azerothcore-wotlk` directory using the [template](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/conf/dist/env.docker). This file also allows you to set the user and group Docker uses for the services in case you run into any permissions issues, which are the most common cause for Docker installation problems. - -Use `docker compose up -d --build` to build and run the server. For more information, including how to create an account and taking backups, refer to the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) page. +For additional references, see the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. ## Documentation diff --git a/modules/mod-playerbots/banner.png b/modules/mod-playerbots/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..addcb75509502903628cf7e15e544364dd786b01 GIT binary patch literal 208820 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

fLPTwvW;KlGk$B&=k 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 E9~eu7zyJUM literal 0 HcmV?d00001 diff --git a/modules/mod-playerbots/conf/playerbots.conf.dist b/modules/mod-playerbots/conf/playerbots.conf.dist index a955c028f..9bc59f24b 100644 --- a/modules/mod-playerbots/conf/playerbots.conf.dist +++ b/modules/mod-playerbots/conf/playerbots.conf.dist @@ -22,7 +22,6 @@ # THRESHOLDS # QUESTS # COMBAT -# GREATER BUFFS STRATEGIES # CHEATS # SPELLS # FLIGHTPATH @@ -32,13 +31,13 @@ # LEVELS # GEAR # QUESTS -# ACTIVITIES +# ACTIVITY # SPELLS # STRATEGIES # RPG STRATEGY # TELEPORTS # BATTLEGROUND & ARENA & PVP -# RANDOM BOT TIMING AND BEHAVIOR +# RANDOMBOT TIMING AND BEHAVIOR # PREMADE SPECS # INFORMATION # WARRIOR @@ -62,15 +61,13 @@ # MAGE # WARLOCK # DRUID +# RAIDS # PLAYERBOTS SYSTEM SETTINGS # DATABASE & CONNECTIONS # DEBUG # CHAT SETTINGS # LOGS -# DEPRECIATED (TEMPORARY) -# -# -# +# DEPRECATED (TEMPORARY) #################################################################################################### ################################### @@ -96,11 +93,21 @@ AiPlayerbot.MinRandomBots = 500 AiPlayerbot.MaxRandomBots = 500 # Randombot accounts -# If you are not using any expansion at all, you may have to set this manually, in which case please -# ensure that RandomBotAccountCount is at least greater than (MaxRandomBots / 10 + AddClassAccountPoolSize) +# Set to 0 to automatically calculate needed accounts based on MaxRandomBots, EnablePeriodicOnlineOffline +# and its ratio, and AddClassAccountPoolSize. Set manually if you want to create more accounts than needed. +# If the manual value is less than the required, the system will override it with the required value. # Default: 0 (automatic) AiPlayerbot.RandomBotAccountCount = 0 +# Addclass accounts +# Number of accounts created for bots reserved for the addclass command. As with randombots, each account has +# 10 bots (or 9 if WotLK is disabled), one bot for each class. +AiPlayerbot.AddClassAccountPoolSize = 50 + +# Prefix for created bot accounts (of any type). +# Do not change this prefix while there are existing bot accounts. +AiPlayerbot.RandomBotAccountPrefix = "rndbot" + # Delete all randombot accounts # To apply this, set the number to 1 and run the Worldserver. Once deletion is complete, if you would # like to recreate randombots, set the number back to 0 and rerun the Worldserver. @@ -127,7 +134,6 @@ AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay = 300 #################################################################################################### # GENERAL # -# # The maximum number of bots that a player can control simultaneously AiPlayerbot.MaxAddedBots = 40 @@ -136,14 +142,11 @@ AiPlayerbot.MaxAddedBots = 40 # Default: 1 (enabled) AiPlayerbot.AddClassCommand = 1 -# Set the addclass command account pool size -AiPlayerbot.AddClassAccountPoolSize = 50 - # Bot group invitation permission level (0 = GM only, 1 = accept based on level, 2 = always accept) # Default: 1 (accept based on level) AiPlayerbot.GroupInvitationPermission = 1 -# Keep alt bots in the party even when the master leaves +# Keep altbots in the party even when the master leaves # 0 = disabled (default behavior) # 1 = enabled (prevents bots from automatically leaving the group) AiPlayerbot.KeepAltsInGroup = 0 @@ -192,15 +195,22 @@ AiPlayerbot.SelfBotLevel = 1 # Default: 0 (non-GM player can use any intialization commands) AiPlayerbot.AutoInitOnly = 0 +# Allow .bot refresh=raid to unbind instances on player-created alt +# bots (non-addclass bots). When 0, refresh=raid is denied on alts +# with "non-addclass bot" error. UnbindInstance is DB-light, no lag +# risk from enabling this. +# Default: 0 +AiPlayerbot.ResetInstanceIdForAltBots = 0 + # The upper limit ratio of bot equipment level for init=auto # Default: 1.0 (same with the player) AiPlayerbot.AutoInitEquipLevelLimitRatio = 1.0 -# Bot automatically trains spells when talking to trainer -# yes = train all available spells as long as the bot has the money, free = auto trains with no money cost, no = only list spells -AiPlayerbot.AutoTrainSpells = yes +# AllowLearnTrainerSpells +# Description: Allow the bot to learn trainers' spells as long as it has the money. +# Default: 1 - (enabled) +AiPlayerbot.AllowLearnTrainerSpells = 1 -# # # #################################################################################################### @@ -208,7 +218,6 @@ AiPlayerbot.AutoTrainSpells = yes #################################################################################################### # SUMMON OPTIONS # -# # Enable/Disable summoning bots when the master is in combat # Default: 1 (enabled) @@ -230,7 +239,6 @@ AiPlayerbot.ReviveBotWhenSummoned = 1 # Default: 1 (enabled) AiPlayerbot.BotRepairWhenSummon = 1 -# # # #################################################################################################### @@ -238,7 +246,6 @@ AiPlayerbot.BotRepairWhenSummon = 1 #################################################################################################### # MOUNT # -# # Defines at what level a bot will naturally use its 60% ground mount # Note: was level 20 during WotLK, 30 during TBC, 40 during Vanilla @@ -260,7 +267,6 @@ AiPlayerbot.UseFlyMountAtMinLevel = 60 # Default: 70 AiPlayerbot.UseFastFlyMountAtMinLevel = 70 -# # # #################################################################################################### @@ -268,9 +274,12 @@ AiPlayerbot.UseFastFlyMountAtMinLevel = 70 #################################################################################################### # GEAR # -# # Show helmet and cloak on randombots (reset required) +# 0 - Helmet or cloak will be visually hidden on all randombots +# 1 - Helmet or cloak will be visually visible on all randombots +# 2 - Helmet or cloak visibility is random when a randombot is randomized +# Default: 1 (helm/cloak visible on all randombots) AiPlayerbot.RandomBotShowHelmet = 1 AiPlayerbot.RandomBotShowCloak = 1 @@ -285,7 +294,6 @@ AiPlayerbot.EquipUpgradeThreshold = 1.1 # Two rounds of equipment initialization to create more suitable gear AiPlayerbot.TwoRoundsGearInit = 0 -# # # #################################################################################################### @@ -293,17 +301,40 @@ AiPlayerbot.TwoRoundsGearInit = 0 #################################################################################################### # LOOTING # -# # Bots keep looting when loot system is set to free for all # Default: 0 (disabled) AiPlayerbot.FreeMethodLoot = 0 -# Bots' loot roll level (0 = pass, 1 = greed, 2 = need) +# Bots' Roll level bots will use for items they Need (0 = pass, 1 = greed, 2 = need) # Default: 1 (greed) -AiPlayerbot.LootRollLevel = 1 +AiPlayerbot.LootNeedRollLevel = 1 + +# Enable bots to roll GREED on items (global toggle) +# If disabled, bots will PASS instead of GREED on all items +# Default: 0 (disabled - bots only NEED or PASS) +AiPlayerbot.LootGreedRollLevel = 0 + +# Enable bots to roll on recipes. Will NEED on learnable profession recipes they don't already know +# Bots will roll GREED on BoE recipes they can't learn if LootRollGreed is enabled. +# Default: 0 (disabled) +AiPlayerbot.LootRollRecipe = 0 + +# Bots with enchanting will roll DISENCHANT instead of GREED on disenchantable items +# If disabled, bots will GREED on disenchantable items instead +# Default: 0 (disabled) +AiPlayerbot.LootRollDisenchant = 0 + +# A list of GameObject GUIDs that bots will not interact with. +# List of default GUIDs: +# QuestItems: +# Blood of Heroes = 176213, Defias Gunpowder = 17155, Waterlogged Envelope = 2656, Baelog's Chest = 123329, Half-Buried Bottle = 2560 +# Chests: +# Large Solid Chest = 74448, Box of Assorted Parts = 19020, Food Crate = 3719, Water Barrel = 3658, Barrel of Milk = 3705, Barrel of sweet Nectar = 3706, Tattered Chest = 105579, Large bettered Chest = 75293, Solid Chest = 2857, Battered Foodlocker = 179490, Witch Doctor's Chest = 141596, Relic Coffer = 160836, Dark Coffer = 160845, Fengus's Chest = 179516, Supply Crate = 176224/181085, Malor's Strongbox = 176112 +# Other: +# Shallow Grave (Zul'Farrak) = 128308/128403, Grim Guzzler Boar (Blackrock Depths) = 165739, Dark Iron Ale Mug (Blackrock Depths) = 165738, Father Flame (Blackrock Spire) = 175245, Unforged Runic Breastplate (Blackrock Spire) = 175970, Blacksmithing Plans (Stratholme) = 176325/176327 +AiPlayerbot.DisallowedGameObjects = 176213,17155,2656,74448,19020,3719,3658,3705,3706,105579,75293,2857,179490,141596,160836,160845,179516,176224,181085,176112,128308,128403,165739,165738,175245,175970,176325,176327,123329,2560 -# # # #################################################################################################### @@ -311,7 +342,6 @@ AiPlayerbot.LootRollLevel = 1 #################################################################################################### # TIMERS # -# # Max AI iterations per tick AiPlayerbot.IterationsPerTick = 10 @@ -359,7 +389,6 @@ AiPlayerbot.SitDelay = 20000 AiPlayerbot.ReturnDelay = 2000 AiPlayerbot.LootDelay = 1000 -# # # #################################################################################################### @@ -367,7 +396,6 @@ AiPlayerbot.LootDelay = 1000 #################################################################################################### # DISTANCES # -# # Distances are in yards AiPlayerbot.FarDistance = 20.0 @@ -388,7 +416,6 @@ AiPlayerbot.RpgDistance = 200 AiPlayerbot.GrindDistance = 75.0 AiPlayerbot.ReactDistance = 150.0 -# # # #################################################################################################### @@ -396,7 +423,6 @@ AiPlayerbot.ReactDistance = 150.0 #################################################################################################### # THRESHOLDS # -# # Health/Mana levels AiPlayerbot.CriticalHealth = 25 @@ -407,7 +433,6 @@ AiPlayerbot.LowMana = 15 AiPlayerbot.MediumMana = 40 AiPlayerbot.HighMana = 65 -# # # #################################################################################################### @@ -415,7 +440,6 @@ AiPlayerbot.HighMana = 65 #################################################################################################### # QUESTS # -# # Bots pick their quest rewards # yes = picks the most useful item, no = list all rewards, ask = pick useful item and lists if multiple @@ -433,7 +457,6 @@ AiPlayerbot.SyncQuestForPlayer = 0 # Default: 1 (enabled) AiPlayerbot.DropObsoleteQuests = 1 -# # # #################################################################################################### @@ -441,7 +464,6 @@ AiPlayerbot.DropObsoleteQuests = 1 #################################################################################################### # COMBAT # -# # Auto add dungeon/raid strategies when entering the instance if implemented AiPlayerbot.ApplyInstanceStrategies = 1 @@ -464,28 +486,35 @@ AiPlayerbot.AutoSaveMana = 1 # Default: 60 (60%) AiPlayerbot.SaveManaThreshold = 60 +# Enable Paladin bots to use greater blessings, with the blessing used being based on the +# number of Paladins in the raid/group and the spec of the recipient. Priorities for each +# spec are hardcoded in GreaterBlessingActions.h. +# 0 = disabled +# 1 = enabled in raid groups only +# 2 = enabled in all groups +# Default: 1 (raid only) +AiPlayerbot.AutoGreaterBlessings = 1 + +# Enable bots to use group reagent buffs: Gift of the Wild, Arcane Brilliance, +# Prayer of Fortitude, Prayer of Spirit, and Prayer of Shadow Protection. +# 0 = disabled +# 1 = enabled in raid groups only +# 2 = enabled in all groups +# Default: 2 (all groups) +AiPlayerbot.AutoPartyBuffs = 2 + +# Bots tell the group when they attempt to use a reagent-based greater blessing or group buff +# but have to fall back to the single-target version because they are out of reagents. +# Default: 1 (enabled) +AiPlayerbot.TellWhenMissingBuffReagents = 1 + +# Cooldown for the missing buff reagent message, in seconds. +# Default: 300 (5 minutes) +AiPlayerbot.MissingBuffReagentMessageCooldown = 300 + # Bots can flee from enemies AiPlayerbot.FleeingEnabled = 1 -# -# -# -#################################################################################################### - -#################################################################################################### -# GREATER BUFFS STRATEGIES -# -# - -# Min group size to use Greater buffs (Paladin, Mage, Druid) -# Default: 3 -AiPlayerbot.MinBotsForGreaterBuff = 3 - -# Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff -# Default: 30 -AiPlayerbot.RPWarningCooldown = 30 - -# # # #################################################################################################### @@ -493,7 +522,6 @@ AiPlayerbot.RPWarningCooldown = 30 #################################################################################################### # CHEATS # -# # Enable/Disable maintenance command # Learn all available spells and skills, assign talent points, refresh consumables, repair, enchant equipment, socket gems, etc. @@ -501,8 +529,8 @@ AiPlayerbot.RPWarningCooldown = 30 # Default: 1 (enabled) AiPlayerbot.MaintenanceCommand = 1 -# Enable/Disable specific maintenance command functionality for alt bots -# Disable to prevent players from giving free bags, spells, skill levels, etc. to their alt bots +# Enable/Disable specific maintenance command functionality for altbots +# Disable to prevent players from giving free bags, spells, skill levels, etc. to their altbots # Default: 1 (enabled) AiPlayerbot.AltMaintenanceAmmo = 1 AiPlayerbot.AltMaintenanceFood = 1 @@ -538,6 +566,17 @@ AiPlayerbot.AutoGearCommand = 1 # Default: 1 (enabled) AiPlayerbot.AutoGearCommandAltBots = 1 +# If 1 (enabled) chat command: autogear bis, gives bots BiS gear from the playerbots_bis_gear table +# whose item-level floor matches AutoGearScoreLimit (e.g. 290 = ICC, 245 = ToC, +# 125 = Kara, 78 = MC). See the AutoGearScoreLimit comment below for the full list. +# chat command: autogear bis x, (x must be positive integer) will give items based on value of x. +# If x is bigger than AutoGearScoreLimit, bis wont be given, if lower it will match x when giving items. +# Commands falls back to autogear when no tier matches or the table is empty for the bot's class/spec. +# Requires AutoGearQualityLimit = 4 and AutoGearCommand = 1. +# If AutoGearCommandAltBots = 1 it will be anbled for alt bots. +# Default: 0 (disabled) +AiPlayerbot.AutoGearBisCommand = 0 + # Equipment quality limitation for autogear command (1 = normal, 2 = uncommon, 3 = rare, 4 = epic, 5 = legendary) # Default: 3 (rare) AiPlayerbot.AutoGearQualityLimit = 3 @@ -562,12 +601,44 @@ AiPlayerbot.AutoGearScoreLimit = 0 # "mana" (bots have infinite mana) # "power" (bots have infinite energy, rage, and runic power) # "taxi" (bots may use all flight paths, though they will not actually learn them) -# "raid" (bots use cheats implemented into raid strategies (currently only for SSC and Ulduar)) +# "raid" (bots use certain cheats implemented into raid strategies) # To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,raid,taxi") # 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. +# 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. # +# Defaults: +# Caverns of Time - Part 1 +# - 10279, To The Master's Lair +# - 10277, The Caverns of Time +# Caverns of Time - Part 2 (Escape from Durnholde Keep) +# - 10282, Old Hillsbrad +# - 10283, Taretha's Diversion +# - 10284, Escape from Durnholde +# - 10285, Return to Andormu +# Caverns of Time - Part 2 (The Black Morass) +# - 10296, The Black Morass +# - 10297, The Opening of the Dark Portal +# - 10298, Hero of the Brood +# Magister's Terrace +# - 11481, Crisis at the Sunwell +# - 11482, Duty Calls +# - 11488, Magisters' Terrace +# - 11490, The Scryer's Scryer +# - 11492, Hard to Kill +# Serpentshrine Cavern +# - 10901, The Cudgel of Kar'desh +# Tempest Keep: The Eye +# - 10888, Trial of the Naaru: Magtheridon +# Battle for Mount Hyjal (Hyjal Summit) +# - 10445, The Vials of Eternity +# Black Temple +# - 10985, A Distraction for Akama +AiPlayerbot.AttunementQuests = 10279,10277,10282,10283,10284,10285,10296,10297,10298,11481,11482,11488,11490,11492,10901,10888,10445,10985 + # # #################################################################################################### @@ -575,7 +646,6 @@ AiPlayerbot.BotCheats = "food,taxi,raid" #################################################################################################### # FLIGHTPATH # -# # Min random delay before the 1st follower bot clicks the flight-master (ms) AiPlayerbot.BotTaxiDelayMinMs = 350 @@ -589,16 +659,20 @@ AiPlayerbot.BotTaxiGapMs = 200 # Extra small randomness added to each gap so launches don’t look robotic (ms) 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 @@ -613,28 +687,23 @@ AiPlayerbot.FishingDistance = 40.0 # Distance from water (in yards) beyond which a bot will remove the 'master fishing' strategy AiPlayerbot.EndFishingWithMaster = 30.0 -# # # #################################################################################################### ####################################### # # -# RANDOMBOT-SPECIFIC SETTINGS # +# RANDOMBOT-SPECIFIC SETTINGS # # # ####################################### #################################################################################################### # GENERAL # -# # Enable/Disable randomly generated password for randombot accounts AiPlayerbot.RandomBotRandomPassword = 0 -# Prefix for account names to create for randombots -AiPlayerbot.RandomBotAccountPrefix = "rndbot" - # Minimum and maximum levels for randombots AiPlayerbot.RandomBotMinLevel = 1 AiPlayerbot.RandomBotMaxLevel = 80 @@ -669,8 +738,8 @@ AiPlayerbot.RandomBotHordeRatio = 50 AiPlayerbot.DisableDeathKnightLogin = 0 # Enable simulated expansion limitation for talents and glyphs -# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 -# and 7 rows plus the middle talent of the 8th row for bots from level 61 until level 71 +# If enabled, limits talent trees to 6 rows plus the middle talent of the 7th row for bots until level 61 +# and 8 rows plus the middle talent of the 9th row for bots from level 61 until level 71 # Default: 0 (disabled) AiPlayerbot.LimitTalentsExpansion = 0 @@ -681,7 +750,11 @@ AiPlayerbot.EnableRandomBotTrading = 1 # Configure message prefixes which will be excluded in analysis in trade action to open trade window AiPlayerbot.TradeActionExcludedPrefixes = "RPLL_H_,DBMv4,{звезда} Questie,{rt1} Questie" -# +# Allow bots to send mail. When disabled, bots will not mail items or money in +# response to chat commands ("send mail"). +# Default: 1 (enabled) +AiPlayerbot.BotSendMailEnabled = 1 + # # #################################################################################################### @@ -689,13 +762,12 @@ AiPlayerbot.TradeActionExcludedPrefixes = "RPLL_H_,DBMv4,{звезда} Questie, #################################################################################################### # LEVELS # -# # Disable randombots being generated with a random level # If disabled, every randombot starts on a specified level (but can still level up by killing mobs and questing) AiPlayerbot.DisableRandomLevels = 0 -# Set randombots' starting level here if "AiPlayerbot.DisableRandomLevels" enabled +# Set randombots' starting level here if "AiPlayerbot.DisableRandomLevels" is enabled AiPlayerbot.RandombotStartingLevel = 1 # Chance randombot has min level on first randomize @@ -714,11 +786,10 @@ AiPlayerbot.RandomBotFixedLevel = 0 # Default: 0 (disabled) AiPlayerbot.DowngradeMaxLevelBot = 0 -# Set XP rate for random bots (Default: 1.0) +# Set XP rate for randombots (Default: 1.0) # Server XP Rate * AiPlayerbot.RandomBotXPRate AiPlayerbot.RandomBotXPRate = 1.0 -# # # #################################################################################################### @@ -726,7 +797,20 @@ AiPlayerbot.RandomBotXPRate = 1.0 #################################################################################################### # GEAR # -# + +# Randombots automatically receive upgraded equipment on levelup +# Default: 1 (enabled) +AiPlayerbot.AutoUpgradeEquip = 1 + +# Enable/disable which (if any) randombots preserve their current equipment and spec during login +# and periodic randomization (i.e., persistence). +# Default: 1 (enabled) +AiPlayerbot.EquipAndSpecPersistence = 1 + +# If EquipAndSpecPersistence is enabled, equipment and talent persistence will affect randombots of this +# level or higher. +# Default: 1 (enable persistence for randombots of at least level 1 (i.e., all levels)) +AiPlayerbot.EquipAndSpecPersistenceLevel = 1 # Equipment quality limitation for randombots (1 = normal, 2 = uncommon, 3 = rare, 4 = epic, 5 = legendary) # This also sets the maximum quality that can be generated by autogear for randombots and altbots @@ -746,9 +830,25 @@ AiPlayerbot.RandomGearQualityLimit = 3 # Default: 0 (no limit) AiPlayerbot.RandomGearScoreLimit = 0 -# If disabled, random bots can only upgrade equipment through looting and quests -# Default: 1 (enabled) -AiPlayerbot.IncrementalGearInit = 1 +# Prefer armor of the class's ideal type: apply 3x score multiplier to class-appropriate armor. +# When enabled, Warriors strongly prefer plate, Shamans prefer mail, etc. +# A truly superior item can still win (no hard filtering), but same-quality +# armor of the preferred type will score 3x higher and be equipped instead. +# +# ARMOR TYPE PREFERENCES: +# Plate: Warriors, Paladins, Death Knights +# Mail: Hunters, Shamans +# Leather: Rogues, Druids +# Cloth: Priests, Mages, Warlocks +# +# Default: 0 (disabled) +AiPlayerbot.PreferClassArmorType = 0 + +# When enabled, bots prefer spec-appropriate weapons based on speed and weapon type during autogear. +# Examples: Arms Warriors favor slow 2H axes/polearms (Axe Specialization), Combat Rogues +# favor a slow MH with a fast OH, and Enhancement Shamans favor synchronized slow 1H weapons. +# Default: 0 (disabled) +AiPlayerbot.PreferredSpecWeapons = 0 # Set minimum level of bots that will enchant and socket gems into their equipment with maintenance # If greater than RandomBotMaxlevel, bots will not automatically enchant equipment or socket gems @@ -769,21 +869,15 @@ AiPlayerbot.LimitGearExpansion = 1 # Set between 0 (0%) and 1 (100%) AiPlayerbot.RandomGearLoweringChance = 0 +# Unobtainable or unusable items (comma-separated list of item IDs) +# Defaults: Chilton Wand (12468), Frenzyheart Insignia of Fury test/on-use row (44869), +# Oracle Talisman of Ablution test/on-use row (44870), Totem of the Earthen Ring (46978) +AiPlayerbot.UnobtainableItems = 12468,44869,44870,46978 + # Randombots check player's gearscore level and deny the group invitation if it's too low # Default: 0 (disabled) AiPlayerbot.GearScoreCheck = 0 -# Enable/Disable bot equipment persistence (stop random initialization) after certain level (EquipmentPersistenceLevel) -# Default: 0 (disabled) -AiPlayerbot.EquipmentPersistence = 0 - -# Default level if enabled: 80 -AiPlayerbot.EquipmentPersistenceLevel = 80 - -# Randombots automatically upgrade their equipment on levelup -# Default: 1 (enabled) -AiPlayerbot.AutoUpgradeEquip = 1 - # Only set wolf pets for hunters for optimal raid performance (0 = disabled, 1 = enabled only for max-level bots, 2 = enabled) # Default: 0 (disabled) AiPlayerbot.HunterWolfPet = 0 @@ -802,38 +896,69 @@ AiPlayerbot.PetChatCommandDebug = 0 # See the creature_family database table for all pet families by ID (note: ID for spiders is 3) AiPlayerbot.ExcludedHunterPetFamilies = "" -# # # #################################################################################################### - #################################################################################################### -# ACTIVITIES +# ACTIVITY # -# -# Specify percent of active bots -# The default is 100% but will be automatically adjusted if botActiveAloneSmartScale -# is enabled. Regardless, this value is only applied to inactive areas where no real players -# are detected. When real players are nearby, the value is always enforced to 100% -AiPlayerbot.BotActiveAlone = 100 -# Force botActiveAlone when bot is within the specified distance of a real player +# BotActiveAlone +# - Controls how many bots are active when no real players are nearby. +# - Think of it as a rough percentage: 10 means approximately 10% of bots will be active. +# Not exact — the actual number may vary slightly per rotation cycle. +# - The active bots rotate: every a different set of bots takes a turn. +# - The real number of active bots will always be higher than this value, because bots in +# combat, dungeons, battlegrounds, LFG queue, groups with real players, etc. are always +# forced active on top of this (see force rules below). +# - Set to 100 (with SmartScale off) = all bots always active. Maximum server load. +# - Set to 0 = only bots that match a force rule below will be active. +# +# BotActiveAloneDurationSeconds +# - How often the active roster rotates (in seconds). A different group of bots wakes up +# and the previous group may go idle. +# - This is a minimum, not exact. If a bot is in combat or meets any force rule when the +# rotation happens, it stays active until those conditions end — it won't be cut off +# mid-fight just because its turn expired. +# +AiPlayerbot.BotActiveAlone = 10 +AiPlayerbot.BotActiveAloneDurationSeconds = 30 + +# Force-active rules (1 = on, 0 = off) +# These override the percentage above. If any of these conditions is true, the bot stays active. +# +# InRadius - A real player is within this many yards (set to 0 to disable). +# InZone - A real player is in the same zone (e.g. Elwynn Forest). +# InMap - A real player is on the same continent (e.g. Eastern Kingdoms). +# IsFriend - A real player has this bot on their friends list. +# InGuild - This bot is in a guild that has a real player in it. +# +# Bots are also always forced active (not configurable) when: +# in combat, inside a dungeon/raid/BG, in a BG or LFG queue, +# grouped with a real player, or controlled by a real player. +# AiPlayerbot.BotActiveAloneForceWhenInRadius = 150 AiPlayerbot.BotActiveAloneForceWhenInZone = 1 AiPlayerbot.BotActiveAloneForceWhenInMap = 0 -AiPlayerbot.BotActiveAloneForceWhenIsFriend = 1 +AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0 AiPlayerbot.BotActiveAloneForceWhenInGuild = 1 -# SmartScale (automatic scaling of percentage of active bots based on latency) -# The default is 1. When enabled (smart) scales the 'BotActiveAlone' value. -# (The scaling will be overruled by the BotActiveAloneForceWhen...rules) +# SmartScale — automatically reduces active bots when the server is struggling. +# Monitors the server's update time (how long each server tick takes in milliseconds). +# When the server slows down, fewer bots are kept active to reduce load. # -# Limitfloor - when DIFF (latency) is above floor, activity scaling begins -# LimitCeiling - when DIFF (latency) is above ceiling, activity is 0% +# Floor (default 50ms) - Below this, no reduction. Server is running fine. +# Ceiling (default 200ms) - At or above this, all non-forced bots are paused. +# Between floor and ceiling, activity scales down gradually. +# Example: BotActiveAlone=10, floor=50, ceiling=200 +# Server at 50ms → ~10% active (no reduction) +# Server at 125ms → ~5% active (half reduction) +# Server at 200ms → 0% active (only forced bots remain) # -# MinLevel - only apply scaling when level is above or equal to min(bot)Level -# MaxLevel - only apply scaling when level is lower or equal of max(bot)Level +# MinLevel/MaxLevel — only bots within this level range are affected by SmartScale. +# Bots outside the range always use the full BotActiveAlone value. +# Force rules always win over SmartScale. # AiPlayerbot.botActiveAloneSmartScale = 1 AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor = 50 @@ -841,7 +966,6 @@ AiPlayerbot.botActiveAloneSmartScaleDiffLimitCeiling = 200 AiPlayerbot.botActiveAloneSmartScaleWhenMinLevel = 1 AiPlayerbot.botActiveAloneSmartScaleWhenMaxLevel = 80 -# # # #################################################################################################### @@ -849,22 +973,21 @@ AiPlayerbot.botActiveAloneSmartScaleWhenMaxLevel = 80 #################################################################################################### # QUESTS # -# -# Quest that will be completed and rewarded for all randombots +# Quests that will be completed and rewarded for all randombots AiPlayerbot.RandomBotQuestIds = "3802,5505,6502,7761,7848,10277,10285,11492,13188,13189,24499,24511,24710,24712" # Randombots will group with nearby randombots to do shared quests +# Note: Currently not functioning properly AiPlayerbot.RandomBotGroupNearby = 0 # Randombots will pick quests on their own and try to complete them # Default: 1 (enabled) AiPlayerbot.AutoDoQuests = 1 -# Quest items to keep in bots' inventories (do not destroy) +# Quest items to keep in bots' inventories (do not automatically destroy) AiPlayerbot.RandomBotQuestItems = "5175,5176,5177,5178,6948,11000,12382,13704,16309" -# # # #################################################################################################### @@ -872,11 +995,10 @@ AiPlayerbot.RandomBotQuestItems = "5175,5176,5177,5178,6948,11000,12382,13704,16 #################################################################################################### # SPELLS # -# # Randombots automatically learn class quest reward spells on levelup -# Default: 0 (disabled) -AiPlayerbot.AutoLearnQuestSpells = 0 +# Default: 1 (enabled) +AiPlayerbot.AutoLearnQuestSpells = 1 # Randombots automatically learn trainable spells on levelup # Default: 1 (enabled) @@ -886,13 +1008,13 @@ AiPlayerbot.AutoLearnTrainerSpells = 1 # Default: 1 (enabled) AiPlayerbot.AutoPickTalents = 1 -# Spells every randombot will learn automatically and every altbot will learn with maintenance (54197 - cold weather flying) +# Spells every randombot will learn automatically and every altbot will learn with maintenance +# Default: 54197 (Cold Weather Flying) AiPlayerbot.RandomBotSpellIds = "54197" # ID of spell to open lootable chests AiPlayerbot.OpenGoSpell = 6477 -# # # #################################################################################################### @@ -900,7 +1022,6 @@ AiPlayerbot.OpenGoSpell = 6477 #################################################################################################### # STRATEGIES # -# # Additional randombot strategies # Strategies added here are applied to all randombots, in addition (or subtraction) to spec/role-based default strategies. @@ -923,7 +1044,6 @@ AiPlayerbot.HealerDPSMapRestriction = 1 # Default: (Dungeon and Raid maps) "33,34,36,43,47,48,70,90,109,129,209,229,230,329,349,389,429,1001,1004,1007,269,540,542,543,545,546,547,552,553,554,555,556,557,558,560,585,574,575,576,578,595,599,600,601,602,604,608,619,632,650,658,668,409,469,509,531,532,534,544,548,550,564,565,580,249,533,603,615,616,624,631,649,724" AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,230,329,349,389,429,1001,1004,1007,269,540,542,543,545,546,547,552,553,554,555,556,557,558,560,585,574,575,576,578,595,599,600,601,602,604,608,619,632,650,658,668,409,469,509,531,532,534,544,548,550,564,565,580,249,533,603,615,616,624,631,649,724" -# # # #################################################################################################### @@ -931,7 +1051,6 @@ AiPlayerbot.RestrictedHealerDPSMaps = "33,34,36,43,47,48,70,90,109,129,209,229,2 #################################################################################################### # RPG STRATEGY # -# # Randombots will behave more like real players (experimental) # This option will override AiPlayerbot.AutoDoQuests, RandomBotTeleLowerLevel, and RandomBotTeleHigherLevel @@ -948,6 +1067,7 @@ AiPlayerbot.EnableNewRpgStrategy = 1 # DoQuest (Default: 60 Select quest from the quest log and head to the location to attempt completion​) # TravelFlight (Default: 15 Go to the nearest flightmaster and fly to a level-appropriate area) # Rest (Default: 5 Take a break for a while and do nothing) +# OutdoorPvp (Default: 10 Participate in outdoor PvP capture points if already in an outdoor PvP zone) AiPlayerbot.RpgStatusProbWeight.WanderRandom = 15 AiPlayerbot.RpgStatusProbWeight.WanderNpc = 20 AiPlayerbot.RpgStatusProbWeight.GoGrind = 15 @@ -955,6 +1075,7 @@ AiPlayerbot.RpgStatusProbWeight.GoCamp = 10 AiPlayerbot.RpgStatusProbWeight.DoQuest = 60 AiPlayerbot.RpgStatusProbWeight.TravelFlight = 15 AiPlayerbot.RpgStatusProbWeight.Rest = 5 +AiPlayerbot.RpgStatusProbWeight.OutdoorPvp = 10 # Bots' minimum and maximum level when teleporting in and out of a zone, according to the new RPG strategy # Format: AiPlayerbot.ZoneBracket.zoneID = minLevel,maxLevel @@ -1091,7 +1212,6 @@ AiPlayerbot.ZoneBracket.3537 = 68,75 AiPlayerbot.ZoneBracket.3711 = 75,80 AiPlayerbot.ZoneBracket.4197 = 79,80 -# # # #################################################################################################### @@ -1099,13 +1219,12 @@ AiPlayerbot.ZoneBracket.4197 = 79,80 #################################################################################################### # TELEPORTS # -# # Map IDs where bots can be teleported to # Defaults: 0 = Eastern Kingdoms, 1 = Kalimdor, 530 = Outland, 571 = Northrend AiPlayerbot.RandomBotMaps = 0,1,530,571 -# Probabilty bots teleport to banker (city) +# Probability bots teleport to banker (city) # Default: 0.25 AiPlayerbot.ProbTeleToBankers = 0.25 @@ -1123,7 +1242,7 @@ AiPlayerbot.TeleToSilvermoonCityWeight = 1 AiPlayerbot.TeleToShattrathCityWeight = 1 AiPlayerbot.TeleToDalaranWeight = 1 -# How far randombots are teleported after death +# How far randombots are teleported after death in yards AiPlayerbot.RandomBotTeleportDistance = 100 # How many levels below the lowest-level creature in a zone, can a bot be @@ -1140,7 +1259,6 @@ AiPlayerbot.RandomBotTeleHigherLevel = 3 # Default: 1 (enabled) AiPlayerbot.AutoTeleportForLevel = 1 -# # # #################################################################################################### @@ -1148,7 +1266,6 @@ AiPlayerbot.AutoTeleportForLevel = 1 #################################################################################################### # BATTLEGROUNDS & ARENAS & PVP # -# # Enable battlegrounds/arenas for randombots AiPlayerbot.RandomBotJoinBG = 1 @@ -1158,49 +1275,53 @@ AiPlayerbot.RandomBotAutoJoinBG = 0 # Required Configuration for RandomBotAutoJoinBG # -# Known issue: When enabling many brackats in combination with multiple instances, it can lead to more instances created by bots than intended (over-queuing). +# Known issue: When enabling many brackets in combination with multiple instances, it can lead to more +# instances created by bots than intended (over-queuing). # # This section controls the level brackets and automatic bot participation in battlegrounds and arenas. # # Brackets: -# - Specify the level ranges for bots to auto-join: -# - Warsong Gulch (WS): 0 = 10-19, 1 = 20-29, 2 = 30-39, ..., 7 = 80 (Default: 7) -# - Rated Arena: 0 = 10-14, 1 = 15-19, ..., 14 = 80-84 (Default: 14) -# - Multiple brackets can be specified as a comma-separated list (e.g., "0,2,5"). +# - Specify the level ranges for bots to auto-join (bracket IDs are per-BG and start at 0 from each BG's minimum level): +# - Warsong Gulch (WS): 0=10-19, 1=20-29, 2=30-39, 3=40-49, 4=50-59, 5=60-69, 6=70-79, 7=80 (Default: 7) +# - Arathi Basin (AB): 0=20-29, 1=30-39, 2=40-49, 3=50-59, 4=60-69, 5=70-79, 6=80 (Default: 6) +# - Alterac Valley (AV): 0=51-60, 1=61-70, 2=71-79, 3=80 (Default: 3) +# - Eye of the Storm (EY): 0=61-69, 1=70-79, 2=80 (Default: 2) +# - Isle of Conquest (IC): 0=71-79, 1=80 (Default: 1) +# - Multiple BG brackets can be specified as a comma-separated list (e.g., "0,2,5"). Not applicable for Rated Arena. +# - Rated Arena: 0=10-14, 1=15-19, 2=20-24, ..., 13=75-79, 14=80-84 (Default: 14) # # Counts: -# - Specify the number of battlegrounds to auto-fill per bracket. -# - For battlegrounds, 'Count" is the number of bots per bracket. For example: -# - Warsong Gulch Count = 1 adds 20 bots (10 per team). +# - Specify the number of battles to auto-fill per bracket. +# - For battlegrounds, 'Count" is the number of battles per bracket. For example: +# - RandomBotAutoJoinWSBrackets = 6,7 +# - RandomBotAutoJoinBGWSCount = 1 +# - These configs would create two battles, one for bracket 6 and one for bracket 7. # - Ensure there are enough eligible bots to meet the specified counts. -# -# Arena Considerations: -# - Rated arena brackets default to level 80-84 (bracket 14). -# - Custom code changes are required for lower-level arena brackets to function properly. -# + # Battleground bracket range possibilities: -# AiPlayerbot.RandomBotAutoJoinICBrackets = 0,1 -# AiPlayerbot.RandomBotAutoJoinEYBrackets = 0,1,2 -# AiPlayerbot.RandomBotAutoJoinAVBrackets = 0,1,2,3 -# AiPlayerbot.RandomBotAutoJoinABBrackets = 0,1,2,3,4,5,6 # AiPlayerbot.RandomBotAutoJoinWSBrackets = 0,1,2,3,4,5,6,7 - -AiPlayerbot.RandomBotAutoJoinICBrackets = 1 -AiPlayerbot.RandomBotAutoJoinEYBrackets = 2 -AiPlayerbot.RandomBotAutoJoinAVBrackets = 3 -AiPlayerbot.RandomBotAutoJoinABBrackets = 6 +# AiPlayerbot.RandomBotAutoJoinABBrackets = 0,1,2,3,4,5,6 +# AiPlayerbot.RandomBotAutoJoinAVBrackets = 0,1,2,3 +# AiPlayerbot.RandomBotAutoJoinEYBrackets = 0,1,2 +# AiPlayerbot.RandomBotAutoJoinICBrackets = 0,1 AiPlayerbot.RandomBotAutoJoinWSBrackets = 7 +AiPlayerbot.RandomBotAutoJoinABBrackets = 6 +AiPlayerbot.RandomBotAutoJoinAVBrackets = 3 +AiPlayerbot.RandomBotAutoJoinEYBrackets = 2 +AiPlayerbot.RandomBotAutoJoinICBrackets = 1 -# Battlegrounds count (per bracket!): -AiPlayerbot.RandomBotAutoJoinBGICCount = 0 -AiPlayerbot.RandomBotAutoJoinBGEYCount = 1 -AiPlayerbot.RandomBotAutoJoinBGAVCount = 0 -AiPlayerbot.RandomBotAutoJoinBGABCount = 1 +# Battlegrounds battles count (per bracket): AiPlayerbot.RandomBotAutoJoinBGWSCount = 1 +AiPlayerbot.RandomBotAutoJoinBGABCount = 1 +AiPlayerbot.RandomBotAutoJoinBGAVCount = 0 +AiPlayerbot.RandomBotAutoJoinBGEYCount = 1 +AiPlayerbot.RandomBotAutoJoinBGICCount = 0 -# Arena configuration: +# Arena bracket (single bracket only): +# Custom code changes are required for lower-level arena brackets to function properly. AiPlayerbot.RandomBotAutoJoinArenaBracket = 14 +# Arena battles count: AiPlayerbot.RandomBotAutoJoinBGRatedArena2v2Count = 0 AiPlayerbot.RandomBotAutoJoinBGRatedArena3v3Count = 0 AiPlayerbot.RandomBotAutoJoinBGRatedArena5v5Count = 0 @@ -1223,41 +1344,41 @@ AiPlayerbot.RandomBotArenaTeamMinRating = 1000 # Delete all randombot arena teams AiPlayerbot.DeleteRandomBotArenaTeams = 0 -# PvP Restricted Zones (bots don't pvp) +# PvP Restricted Zones (bots will not initiate PvP or defend themselves if attacked) 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" +# PvP Restricted Areas (bots will not initiate PvP or defend themselves if attacked) +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 -# # # #################################################################################################### #################################################################################################### -# RANDOM BOT TIMING AND BEHAVIOR -# +# RANDOMBOT TIMING AND BEHAVIOR # -# How often (in seconds) the random bot manager runs its main update loop +# How often (in seconds) the randombot manager runs its main update loop # Default: 20 AiPlayerbot.RandomBotUpdateInterval = 20 -# Minimum and maximum seconds before the manager re-evaluates and adjusts total random bot count +# Minimum and maximum seconds before the manager re-evaluates and adjusts total randombot count # Defaults: 1800 (min), 7200 (max) AiPlayerbot.RandomBotCountChangeMinInterval = 1800 AiPlayerbot.RandomBotCountChangeMaxInterval = 7200 -# Minimum and maximum seconds a random bot will stay online before logging out +# Minimum and maximum seconds a randombot will stay online before logging out # Defaults: 600 (min), 28800 (max) AiPlayerbot.MinRandomBotInWorldTime = 600 AiPlayerbot.MaxRandomBotInWorldTime = 28800 -# Minimum and maximum seconds before a bot is eligible for re-randomization (gear, class, talents, etc.) -# Defaults: 7200 (min), 1209600 (max) +# Some aspects of randombots are periodically randomized while they are logged in and idle, +# including their inventories and, if EquipAndSpecPersistence is disabled, their equipment and specs. +# This config sets the minimum and maximum seconds between such randomization events. +# Defaults: 7200 (min - 2 hours), 1209600 (max - 14 days) AiPlayerbot.MinRandomBotRandomizeTime = 7200 AiPlayerbot.MaxRandomBotRandomizeTime = 1209600 @@ -1279,7 +1400,6 @@ AiPlayerbot.MaxRandomBotTeleportInterval = 18000 # Default: 31104000 AiPlayerbot.PermanentlyInWorldTime = 31104000 -# # # #################################################################################################### @@ -1293,7 +1413,6 @@ AiPlayerbot.PermanentlyInWorldTime = 31104000 #################################################################################################### # INFORMATION # -# # AiPlayerbot.PremadeSpecName.. = #Name of the talent specialisation # AiPlayerbot.PremadeSpecLink... = #Wowhead style link the bot should work towards at given level. @@ -1301,7 +1420,6 @@ AiPlayerbot.PermanentlyInWorldTime = 31104000 # e.g., formulate the link on https://www.wowhead.com/wotlk/talent-calc/warrior/3022032123335100202012013031251-32505010002 # 0 <= specno < 20, 1 <= level <= 80 -# # # #################################################################################################### @@ -1309,18 +1427,17 @@ AiPlayerbot.PermanentlyInWorldTime = 31104000 #################################################################################################### # WARRIOR # -# AiPlayerbot.PremadeSpecName.1.0 = arms pve -AiPlayerbot.PremadeSpecGlyph.1.0 = 43418,43395,43423,43399,49084,43421 +AiPlayerbot.PremadeSpecGlyph.1.0 = 43418,43395,43423,43399,43397,43421 AiPlayerbot.PremadeSpecLink.1.0.60 = 3022032023335100002012211231241 AiPlayerbot.PremadeSpecLink.1.0.80 = 3022032023335100102012213231251-305-2033 AiPlayerbot.PremadeSpecName.1.1 = fury pve -AiPlayerbot.PremadeSpecGlyph.1.1 = 43418,43395,43414,43399,49084,43432 +AiPlayerbot.PremadeSpecGlyph.1.1 = 43418,43395,43414,43396,49084,43432 AiPlayerbot.PremadeSpecLink.1.1.60 = -305053000500310053120501351 AiPlayerbot.PremadeSpecLink.1.1.80 = 32002300233-305053000500310153120511351 AiPlayerbot.PremadeSpecName.1.2 = prot pve -AiPlayerbot.PremadeSpecGlyph.1.2 = 43424,43395,43425,43399,49084,45793 +AiPlayerbot.PremadeSpecGlyph.1.2 = 43429,43397,43425,43399,49084,45797 AiPlayerbot.PremadeSpecLink.1.2.60 = --053351225000210521030113321 AiPlayerbot.PremadeSpecLink.1.2.80 = 3500030023-301-053351225000210521030113321 AiPlayerbot.PremadeSpecName.1.3 = arms pvp @@ -1336,7 +1453,6 @@ AiPlayerbot.PremadeSpecGlyph.1.5 = 43425,43397,43415,43396,49084,45792 AiPlayerbot.PremadeSpecLink.1.5.60 = --250031220223012520332113321 AiPlayerbot.PremadeSpecLink.1.5.80 = 0502300123-3-250031220223012521332113321 -# # # #################################################################################################### @@ -1344,35 +1460,33 @@ AiPlayerbot.PremadeSpecLink.1.5.80 = 0502300123-3-250031220223012521332113321 #################################################################################################### # PALADIN # -# AiPlayerbot.PremadeSpecName.2.0 = holy pve -AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43369,43365,41109 +AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43368,43365,41109 AiPlayerbot.PremadeSpecLink.2.0.60 = 50350151020013053100515221 -AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312 +AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152100013053100515221-50320104203 AiPlayerbot.PremadeSpecName.2.1 = prot pve -AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43369,43365,45745 +AiPlayerbot.PremadeSpecGlyph.2.1 = 41100,43367,43869,43368,43369,45745 AiPlayerbot.PremadeSpecLink.2.1.60 = -05005135203102311333112321 AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003 AiPlayerbot.PremadeSpecName.2.2 = ret pve -AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43369,43365,43869 +AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43368,43340,43869 AiPlayerbot.PremadeSpecLink.2.2.60 = --05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331 AiPlayerbot.PremadeSpecName.2.3 = holy pvp -AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43366,43365,45747 +AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43369,43365,45747 AiPlayerbot.PremadeSpecLink.2.3.60 = 50332150300013050133215221 AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122 AiPlayerbot.PremadeSpecName.2.4 = prot pvp -AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43369,41101,43368,43365,45745 +AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43367,41101,43369,43365,45745 AiPlayerbot.PremadeSpecLink.2.4.60 = -15320130223122311323311321 AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502 AiPlayerbot.PremadeSpecName.2.5 = ret pvp -AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43369,41102,43368,43365,45747 +AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43367,41102,43369,43365,45747 AiPlayerbot.PremadeSpecLink.2.5.60 = --05230250203331222133201321 AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321 -# # # #################################################################################################### @@ -1380,13 +1494,12 @@ AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321 #################################################################################################### # HUNTER # -# AiPlayerbot.PremadeSpecName.3.0 = bm pve AiPlayerbot.PremadeSpecGlyph.3.0 = 42912,43350,42902,43351,43338,42914 AiPlayerbot.PremadeSpecLink.3.0.40 = 512002015051122301 AiPlayerbot.PremadeSpecLink.3.0.60 = 51200201505112233110531151 -AiPlayerbot.PremadeSpecLink.3.0.80 = 51200201505112243130531351-005305101 +AiPlayerbot.PremadeSpecLink.3.0.80 = 51200201505112233111531351-0323031-5 AiPlayerbot.PremadeSpecName.3.1 = mm pve AiPlayerbot.PremadeSpecGlyph.3.1 = 42912,43350,45625,43351,43338,42914 AiPlayerbot.PremadeSpecLink.3.1.60 = -035305101030013233115031151 @@ -1412,8 +1525,8 @@ AiPlayerbot.PremadeSpecLink.3.5.80 = -005305201-2300302510233330533135001031 # HUNTER PET # # Ferocity -AiPlayerbot.PremadeHunterPetLink.0.16 = 2100003030103010101 -AiPlayerbot.PremadeHunterPetLink.0.20 = 2100013030103010122 +AiPlayerbot.PremadeHunterPetLink.0.16 = 2100003130003010101 +AiPlayerbot.PremadeHunterPetLink.0.20 = 2100003130103010122 # Tenacity AiPlayerbot.PremadeHunterPetLink.1.16 = 21103000300120101001 AiPlayerbot.PremadeHunterPetLink.1.20 = 21303010300120101002 @@ -1421,7 +1534,6 @@ AiPlayerbot.PremadeHunterPetLink.1.20 = 21303010300120101002 AiPlayerbot.PremadeHunterPetLink.2.16 = 2100020330000211001 AiPlayerbot.PremadeHunterPetLink.2.20 = 21000203300002110221 -# # # #################################################################################################### @@ -1429,12 +1541,11 @@ AiPlayerbot.PremadeHunterPetLink.2.20 = 21000203300002110221 #################################################################################################### # ROGUE # -# AiPlayerbot.PremadeSpecName.4.0 = as pve AiPlayerbot.PremadeSpecGlyph.4.0 = 45768,43379,45761,43380,43378,45766 AiPlayerbot.PremadeSpecLink.4.0.60 = 005303104352100520103331051 -AiPlayerbot.PremadeSpecLink.4.0.80 = 005303104352100520103331051-005005005003-2 +AiPlayerbot.PremadeSpecLink.4.0.80 = 005303005352100520103331051-005005003-502 AiPlayerbot.PremadeSpecName.4.1 = combat pve AiPlayerbot.PremadeSpecGlyph.4.1 = 42962,43379,45762,43380,43378,42969 AiPlayerbot.PremadeSpecLink.4.1.60 = -0252051000035015223100501251 @@ -1456,7 +1567,6 @@ AiPlayerbot.PremadeSpecGlyph.4.5 = 42968,43376,45764,43380,43379,42971 AiPlayerbot.PremadeSpecLink.4.5.60 = --5120212030320121330133221251 AiPlayerbot.PremadeSpecLink.4.5.80 = 3023031-3-5120212030320121350135231251 -# # # #################################################################################################### @@ -1464,7 +1574,6 @@ AiPlayerbot.PremadeSpecLink.4.5.80 = 3023031-3-5120212030320121350135231251 #################################################################################################### # PRIEST # -# AiPlayerbot.PremadeSpecName.5.0 = disc pve AiPlayerbot.PremadeSpecGlyph.5.0 = 42408,43371,42400,43374,43342,45756 @@ -1475,7 +1584,7 @@ AiPlayerbot.PremadeSpecGlyph.5.1 = 42408,43371,42400,43374,43342,42396 AiPlayerbot.PremadeSpecLink.5.1.60 = -035050031301152530000331331 AiPlayerbot.PremadeSpecLink.5.1.80 = 05032031-235050032302152530000331351 AiPlayerbot.PremadeSpecName.5.2 = shadow pve -AiPlayerbot.PremadeSpecGlyph.5.2 = 42406,43371,42407,43374,43342,42415 +AiPlayerbot.PremadeSpecGlyph.5.2 = 45753,43371,42407,43374,43370,42415 AiPlayerbot.PremadeSpecLink.5.2.60 = --325003041203010323150301351 AiPlayerbot.PremadeSpecLink.5.2.80 = 0503203--325023051223010323152301351 AiPlayerbot.PremadeSpecName.5.3 = disc pvp @@ -1491,7 +1600,6 @@ AiPlayerbot.PremadeSpecGlyph.5.5 = 42407,43371,45753,43370,43374,42408 AiPlayerbot.PremadeSpecLink.5.5.60 = --005323241223112003102311351 AiPlayerbot.PremadeSpecLink.5.5.80 = 50332031003--005323241223112003102311351 -# # # #################################################################################################### @@ -1499,22 +1607,21 @@ AiPlayerbot.PremadeSpecLink.5.5.80 = 50332031003--005323241223112003102311351 #################################################################################################### # DEATH KNIGHT # -# AiPlayerbot.PremadeSpecName.6.0 = blood pve -AiPlayerbot.PremadeSpecGlyph.6.0 = 45805,43673,43827,43544,43672,43554 +AiPlayerbot.PremadeSpecGlyph.6.0 = 45805,43673,43538,43544,43672,43542 AiPlayerbot.PremadeSpecLink.6.0.60 = 035502150300331320102013111-005 -AiPlayerbot.PremadeSpecLink.6.0.80 = 0355021533003313201020131351-005-005032 +AiPlayerbot.PremadeSpecLink.6.0.80 = 0055021533303310201020131-305020510002-00522 AiPlayerbot.PremadeSpecName.6.1 = frost pve AiPlayerbot.PremadeSpecGlyph.6.1 = 45805,43673,43547,43544,43672,43543 AiPlayerbot.PremadeSpecLink.6.1.60 = -32003350332203012300023101351 AiPlayerbot.PremadeSpecLink.6.1.80 = -32002350352203012300033101351-230200305003 AiPlayerbot.PremadeSpecName.6.2 = unholy pve -AiPlayerbot.PremadeSpecGlyph.6.2 = 43542,43673,43546,43544,43672,43549 +AiPlayerbot.PremadeSpecGlyph.6.2 = 43542,43673,43546,43535,43672,43549 AiPlayerbot.PremadeSpecLink.6.2.60 = --2301303050032151000150013131151 -AiPlayerbot.PremadeSpecLink.6.2.80 = -320033500002-2301303050032151000150013133151 +AiPlayerbot.PremadeSpecLink.6.2.80 = 23050202--2302303350032152000150003133151 AiPlayerbot.PremadeSpecName.6.3 = double aura blood pve -AiPlayerbot.PremadeSpecGlyph.6.3 = 45805,43673,43827,43544,43672,43554 +AiPlayerbot.PremadeSpecGlyph.6.3 = 45805,43673,43538,43544,43672,43554 AiPlayerbot.PremadeSpecLink.6.3.60 = 005512153330030320102013-305 AiPlayerbot.PremadeSpecLink.6.3.80 = 005512153330030320102013-3050505002023001-002 AiPlayerbot.PremadeSpecName.6.4 = blood pvp @@ -1530,8 +1637,6 @@ AiPlayerbot.PremadeSpecGlyph.6.6 = 45804,43539,43549,43673,43672,45805 AiPlayerbot.PremadeSpecLink.6.6.60 = --2301323301002152230101203103151 AiPlayerbot.PremadeSpecLink.6.6.80 = -320050410002-2301323301002152230101203133151 - -# # # #################################################################################################### @@ -1539,18 +1644,17 @@ AiPlayerbot.PremadeSpecLink.6.6.80 = -320050410002-23013233010021522301012031331 #################################################################################################### # SHAMAN # -# AiPlayerbot.PremadeSpecName.7.0 = ele pve AiPlayerbot.PremadeSpecGlyph.7.0 = 41536,43385,41532,43386,44923,45776 AiPlayerbot.PremadeSpecLink.7.0.60 = 4530001520213351102301351 -AiPlayerbot.PremadeSpecLink.7.0.80 = 3530001523213351322301351-005050031 +AiPlayerbot.PremadeSpecLink.7.0.80 = 4530001523213351302301351-00525003 AiPlayerbot.PremadeSpecName.7.1 = enh pve -AiPlayerbot.PremadeSpecGlyph.7.1 = 41530,43385,41539,43386,44923,45771 +AiPlayerbot.PremadeSpecGlyph.7.1 = 41542,43385,41539,43386,43725,45771 AiPlayerbot.PremadeSpecLink.7.1.60 = -30305003105021333031121131051 AiPlayerbot.PremadeSpecLink.7.1.80 = 053030152-30305003105021333031131131051 AiPlayerbot.PremadeSpecName.7.2 = resto pve -AiPlayerbot.PremadeSpecGlyph.7.2 = 41527,43385,41517,43386,44923,45775 +AiPlayerbot.PremadeSpecGlyph.7.2 = 41527,43385,41517,43386,43725,45775 AiPlayerbot.PremadeSpecLink.7.2.60 = --50005301235310501102321251 AiPlayerbot.PremadeSpecLink.7.2.80 = -00502033-50005331335310501122331251 AiPlayerbot.PremadeSpecName.7.3 = ele pvp @@ -1566,8 +1670,6 @@ AiPlayerbot.PremadeSpecGlyph.7.5 = 45778,43388,45775,43725,43344,41535 AiPlayerbot.PremadeSpecLink.7.5.60 = --05032331331013501120321251 AiPlayerbot.PremadeSpecLink.7.5.80 = -023222301004-05032331331013501120331251 - -# # # #################################################################################################### @@ -1575,7 +1677,6 @@ AiPlayerbot.PremadeSpecLink.7.5.80 = -023222301004-05032331331013501120331251 #################################################################################################### # MAGE # -# AiPlayerbot.PremadeSpecName.8.0 = arcane pve AiPlayerbot.PremadeSpecGlyph.8.0 = 42735,43339,44955,43364,43361,42751 @@ -1584,7 +1685,7 @@ AiPlayerbot.PremadeSpecLink.8.0.80 = 230005231100330150323102505321-03-203303001 AiPlayerbot.PremadeSpecName.8.1 = fire pve AiPlayerbot.PremadeSpecGlyph.8.1 = 42739,43339,45737,43364,44920,42751 AiPlayerbot.PremadeSpecLink.8.1.60 = -0055030011302231053120321341 -AiPlayerbot.PremadeSpecLink.8.1.80 = 23000503110003-0055030011302331053120321351 +AiPlayerbot.PremadeSpecLink.8.1.80 = 23000503110003-0055032012303330053120300351 AiPlayerbot.PremadeSpecName.8.2 = frost pve AiPlayerbot.PremadeSpecGlyph.8.2 = 42742,43339,50045,43364,43361,42751 AiPlayerbot.PremadeSpecLink.8.2.60 = --0533030313203100030152231151 @@ -1606,8 +1707,6 @@ AiPlayerbot.PremadeSpecGlyph.8.6 = 42738,43364,45740,43357,43360,42752 AiPlayerbot.PremadeSpecLink.8.6.60 = --3533203210203100232102231151 AiPlayerbot.PremadeSpecLink.8.6.80 = 23032103010203--3533203210203100232102231151 - -# # # #################################################################################################### @@ -1615,7 +1714,6 @@ AiPlayerbot.PremadeSpecLink.8.6.80 = 23032103010203--353320321020310023210223115 #################################################################################################### # WARLOCK # -# AiPlayerbot.PremadeSpecName.9.0 = affli pve AiPlayerbot.PremadeSpecGlyph.9.0 = 45785,43390,50077,43394,43393,45779 @@ -1628,7 +1726,7 @@ AiPlayerbot.PremadeSpecLink.9.1.60 = -003203301135112530135201051 AiPlayerbot.PremadeSpecLink.9.1.70 = -003203301135112530135201051-55 AiPlayerbot.PremadeSpecLink.9.1.80 = -003203301135112530135221351-55000005 AiPlayerbot.PremadeSpecName.9.2 = destro pve -AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,45785 +AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,42453 AiPlayerbot.PremadeSpecLink.9.2.60 = --05203215200231051305031151 AiPlayerbot.PremadeSpecLink.9.2.80 = 23-0302-05203215220331051335231351 AiPlayerbot.PremadeSpecName.9.3 = affli pvp @@ -1644,8 +1742,6 @@ AiPlayerbot.PremadeSpecGlyph.9.5 = 42471,43392,42454,43390,43389,45783 AiPlayerbot.PremadeSpecLink.9.5.60 = --05230015220331351005031051 AiPlayerbot.PremadeSpecLink.9.5.80 = -2032003311302-05230015220331351005031051 - -# # # #################################################################################################### @@ -1653,24 +1749,23 @@ AiPlayerbot.PremadeSpecLink.9.5.80 = -2032003311302-05230015220331351005031051 #################################################################################################### # DRUID # -# AiPlayerbot.PremadeSpecName.11.0 = balance pve AiPlayerbot.PremadeSpecGlyph.11.0 = 40916,43331,40921,43335,44922,40919 -AiPlayerbot.PremadeSpecLink.11.0.60 = 5022203105331003213005301231 -AiPlayerbot.PremadeSpecLink.11.0.80 = 5032203105331303213305301231--205003012 +AiPlayerbot.PremadeSpecLink.11.0.60 = 5032003125031003213304301231 +AiPlayerbot.PremadeSpecLink.11.0.80 = 5032003125331303213305301231--205003012 AiPlayerbot.PremadeSpecName.11.1 = bear pve AiPlayerbot.PremadeSpecGlyph.11.1 = 40897,43331,46372,43335,43332,40899 -AiPlayerbot.PremadeSpecLink.11.1.60 = -500232130322110353100301310501 -AiPlayerbot.PremadeSpecLink.11.1.80 = -501232130322110353120303313511-20350001 +AiPlayerbot.PremadeSpecLink.11.1.60 = -503232132322010303120300013501 +AiPlayerbot.PremadeSpecLink.11.1.80 = -503232132322010353120303013511-20350001 AiPlayerbot.PremadeSpecName.11.2 = resto pve -AiPlayerbot.PremadeSpecGlyph.11.2 = 40913,43331,40906,43335,44922,45602 -AiPlayerbot.PremadeSpecLink.11.2.60 = --230033312031500531050113051 -AiPlayerbot.PremadeSpecLink.11.2.80 = 05320031--230033312031501531053313051 +AiPlayerbot.PremadeSpecGlyph.11.2 = 40906,43331,45602,43335,43674,45603 +AiPlayerbot.PremadeSpecLink.11.2.60 = --230033312031502331050313031 +AiPlayerbot.PremadeSpecLink.11.2.80 = 05320031--230033312031502431053313051 AiPlayerbot.PremadeSpecName.11.3 = cat pve -AiPlayerbot.PremadeSpecGlyph.11.3 = 40902,43331,40901,43335,44922,45604 +AiPlayerbot.PremadeSpecGlyph.11.3 = 40902,43331,40901,43674,43335,45604 AiPlayerbot.PremadeSpecLink.11.3.60 = -552202032322010053100030310501 -AiPlayerbot.PremadeSpecLink.11.3.80 = -553202032322010053100030310511-205503012 +AiPlayerbot.PremadeSpecLink.11.3.80 = -553202032322010053120030310511-203503012 AiPlayerbot.PremadeSpecName.11.4 = balance pvp AiPlayerbot.PremadeSpecGlyph.11.4 = 40921,43331,45622,43674,43335,45623 AiPlayerbot.PremadeSpecLink.11.4.60 = 5012203115331002213032311231 @@ -1684,8 +1779,6 @@ AiPlayerbot.PremadeSpecGlyph.11.6 = 40913,43331,40906,43335,43674,45623 AiPlayerbot.PremadeSpecLink.11.6.60 = --230033312031500511350013051 AiPlayerbot.PremadeSpecLink.11.6.80 = 05320021--230033312031500531353013251 - -# # # #################################################################################################### @@ -1699,18 +1792,16 @@ AiPlayerbot.PremadeSpecLink.11.6.80 = 05320021--230033312031500531353013251 #################################################################################################### # # -# # Applies automatically refreshing buffs to bots simulating effects of spells, flasks, food, runes, etc. # 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 -# # # #################################################################################################### @@ -1724,12 +1815,10 @@ AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIO #################################################################################################### # # -# # AiPlayerbot.RandomClassSpecProb.. # The probability to choose the spec # AiPlayerbot.RandomClassSpecIndex.. # The spec index in PremadeSpec -# # # #################################################################################################### @@ -1737,16 +1826,26 @@ AiPlayerbot.WorldBuffMatrix = # WARRIOR ARMS 1:0,1,0,80,80:53760,57358; # WARRIO #################################################################################################### # WARRIOR # -# +# arms pve AiPlayerbot.RandomClassSpecProb.1.0 = 20 AiPlayerbot.RandomClassSpecIndex.1.0 = 0 +# fury pve AiPlayerbot.RandomClassSpecProb.1.1 = 40 AiPlayerbot.RandomClassSpecIndex.1.1 = 1 +# prot pve AiPlayerbot.RandomClassSpecProb.1.2 = 40 AiPlayerbot.RandomClassSpecIndex.1.2 = 2 +# arms pvp +AiPlayerbot.RandomClassSpecProb.1.3 = 0 +AiPlayerbot.RandomClassSpecIndex.1.3 = 3 +# fury pvp +AiPlayerbot.RandomClassSpecProb.1.4 = 0 +AiPlayerbot.RandomClassSpecIndex.1.4 = 4 +# prot pvp +AiPlayerbot.RandomClassSpecProb.1.5 = 0 +AiPlayerbot.RandomClassSpecIndex.1.5 = 5 -# # # #################################################################################################### @@ -1754,16 +1853,26 @@ AiPlayerbot.RandomClassSpecIndex.1.2 = 2 #################################################################################################### # PALADIN # -# +# holy pve AiPlayerbot.RandomClassSpecProb.2.0 = 30 AiPlayerbot.RandomClassSpecIndex.2.0 = 0 +# prot pve AiPlayerbot.RandomClassSpecProb.2.1 = 40 AiPlayerbot.RandomClassSpecIndex.2.1 = 1 +# ret pve AiPlayerbot.RandomClassSpecProb.2.2 = 30 AiPlayerbot.RandomClassSpecIndex.2.2 = 2 +# holy pvp +AiPlayerbot.RandomClassSpecProb.2.3 = 0 +AiPlayerbot.RandomClassSpecIndex.2.3 = 3 +# prot pvp +AiPlayerbot.RandomClassSpecProb.2.4 = 0 +AiPlayerbot.RandomClassSpecIndex.2.4 = 4 +# ret pvp +AiPlayerbot.RandomClassSpecProb.2.5 = 0 +AiPlayerbot.RandomClassSpecIndex.2.5 = 5 -# # # #################################################################################################### @@ -1771,16 +1880,26 @@ AiPlayerbot.RandomClassSpecIndex.2.2 = 2 #################################################################################################### # HUNTER # -# +# bm pve AiPlayerbot.RandomClassSpecProb.3.0 = 33 AiPlayerbot.RandomClassSpecIndex.3.0 = 0 +# mm pve AiPlayerbot.RandomClassSpecProb.3.1 = 33 AiPlayerbot.RandomClassSpecIndex.3.1 = 1 +# surv pve AiPlayerbot.RandomClassSpecProb.3.2 = 33 AiPlayerbot.RandomClassSpecIndex.3.2 = 2 +# bm pvp +AiPlayerbot.RandomClassSpecProb.3.3 = 0 +AiPlayerbot.RandomClassSpecIndex.3.3 = 3 +# mm pvp +AiPlayerbot.RandomClassSpecProb.3.4 = 0 +AiPlayerbot.RandomClassSpecIndex.3.4 = 4 +# surv pvp +AiPlayerbot.RandomClassSpecProb.3.5 = 0 +AiPlayerbot.RandomClassSpecIndex.3.5 = 5 -# # # #################################################################################################### @@ -1788,16 +1907,26 @@ AiPlayerbot.RandomClassSpecIndex.3.2 = 2 #################################################################################################### # ROGUE # -# +# as pve AiPlayerbot.RandomClassSpecProb.4.0 = 45 AiPlayerbot.RandomClassSpecIndex.4.0 = 0 +# combat pve AiPlayerbot.RandomClassSpecProb.4.1 = 45 AiPlayerbot.RandomClassSpecIndex.4.1 = 1 +# subtlety pve AiPlayerbot.RandomClassSpecProb.4.2 = 10 AiPlayerbot.RandomClassSpecIndex.4.2 = 2 +# as pvp +AiPlayerbot.RandomClassSpecProb.4.3 = 0 +AiPlayerbot.RandomClassSpecIndex.4.3 = 3 +# combat pvp +AiPlayerbot.RandomClassSpecProb.4.4 = 0 +AiPlayerbot.RandomClassSpecIndex.4.4 = 4 +# subtlety pvp +AiPlayerbot.RandomClassSpecProb.4.5 = 0 +AiPlayerbot.RandomClassSpecIndex.4.5 = 5 -# # # #################################################################################################### @@ -1805,16 +1934,26 @@ AiPlayerbot.RandomClassSpecIndex.4.2 = 2 #################################################################################################### # PRIEST # -# +# disc pve AiPlayerbot.RandomClassSpecProb.5.0 = 40 AiPlayerbot.RandomClassSpecIndex.5.0 = 0 +# holy pve AiPlayerbot.RandomClassSpecProb.5.1 = 35 AiPlayerbot.RandomClassSpecIndex.5.1 = 1 +# shadow pve AiPlayerbot.RandomClassSpecProb.5.2 = 25 AiPlayerbot.RandomClassSpecIndex.5.2 = 2 +# disc pvp +AiPlayerbot.RandomClassSpecProb.5.3 = 0 +AiPlayerbot.RandomClassSpecIndex.5.3 = 3 +# holy pvp +AiPlayerbot.RandomClassSpecProb.5.4 = 0 +AiPlayerbot.RandomClassSpecIndex.5.4 = 4 +# shadow pvp +AiPlayerbot.RandomClassSpecProb.5.5 = 0 +AiPlayerbot.RandomClassSpecIndex.5.5 = 5 -# # # #################################################################################################### @@ -1822,16 +1961,29 @@ AiPlayerbot.RandomClassSpecIndex.5.2 = 2 #################################################################################################### # DEATH KNIGHT # -# +# blood pve AiPlayerbot.RandomClassSpecProb.6.0 = 30 AiPlayerbot.RandomClassSpecIndex.6.0 = 0 +# frost pve AiPlayerbot.RandomClassSpecProb.6.1 = 40 AiPlayerbot.RandomClassSpecIndex.6.1 = 1 +# unholy pve AiPlayerbot.RandomClassSpecProb.6.2 = 30 AiPlayerbot.RandomClassSpecIndex.6.2 = 2 +# double aura blood pve +AiPlayerbot.RandomClassSpecProb.6.3 = 0 +AiPlayerbot.RandomClassSpecIndex.6.3 = 3 +# blood pvp +AiPlayerbot.RandomClassSpecProb.6.4 = 0 +AiPlayerbot.RandomClassSpecIndex.6.4 = 4 +# frost pvp +AiPlayerbot.RandomClassSpecProb.6.5 = 0 +AiPlayerbot.RandomClassSpecIndex.6.5 = 5 +# unholy pvp +AiPlayerbot.RandomClassSpecProb.6.6 = 0 +AiPlayerbot.RandomClassSpecIndex.6.6 = 6 -# # # #################################################################################################### @@ -1839,16 +1991,26 @@ AiPlayerbot.RandomClassSpecIndex.6.2 = 2 #################################################################################################### # SHAMAN # -# +# ele pve AiPlayerbot.RandomClassSpecProb.7.0 = 33 AiPlayerbot.RandomClassSpecIndex.7.0 = 0 +# enh pve AiPlayerbot.RandomClassSpecProb.7.1 = 33 AiPlayerbot.RandomClassSpecIndex.7.1 = 1 +# resto pve AiPlayerbot.RandomClassSpecProb.7.2 = 33 AiPlayerbot.RandomClassSpecIndex.7.2 = 2 +# ele pvp +AiPlayerbot.RandomClassSpecProb.7.3 = 0 +AiPlayerbot.RandomClassSpecIndex.7.3 = 3 +# enh pvp +AiPlayerbot.RandomClassSpecProb.7.4 = 0 +AiPlayerbot.RandomClassSpecIndex.7.4 = 4 +# resto pvp +AiPlayerbot.RandomClassSpecProb.7.5 = 0 +AiPlayerbot.RandomClassSpecIndex.7.5 = 5 -# # # #################################################################################################### @@ -1856,16 +2018,29 @@ AiPlayerbot.RandomClassSpecIndex.7.2 = 2 #################################################################################################### # MAGE # -# +# arcane pve AiPlayerbot.RandomClassSpecProb.8.0 = 30 AiPlayerbot.RandomClassSpecIndex.8.0 = 0 +# fire pve AiPlayerbot.RandomClassSpecProb.8.1 = 30 AiPlayerbot.RandomClassSpecIndex.8.1 = 1 +# frost pve AiPlayerbot.RandomClassSpecProb.8.2 = 40 AiPlayerbot.RandomClassSpecIndex.8.2 = 2 +# frostfire pve +AiPlayerbot.RandomClassSpecProb.8.3 = 0 +AiPlayerbot.RandomClassSpecIndex.8.3 = 3 +# arcane pvp +AiPlayerbot.RandomClassSpecProb.8.4 = 0 +AiPlayerbot.RandomClassSpecIndex.8.4 = 4 +# fire pvp +AiPlayerbot.RandomClassSpecProb.8.5 = 0 +AiPlayerbot.RandomClassSpecIndex.8.5 = 5 +# frost pvp +AiPlayerbot.RandomClassSpecProb.8.6 = 0 +AiPlayerbot.RandomClassSpecIndex.8.6 = 6 -# # # #################################################################################################### @@ -1873,16 +2048,26 @@ AiPlayerbot.RandomClassSpecIndex.8.2 = 2 #################################################################################################### # WARLOCK # -# +# affli pve AiPlayerbot.RandomClassSpecProb.9.0 = 33 AiPlayerbot.RandomClassSpecIndex.9.0 = 0 +# demo pve AiPlayerbot.RandomClassSpecProb.9.1 = 34 AiPlayerbot.RandomClassSpecIndex.9.1 = 1 +# destro pve AiPlayerbot.RandomClassSpecProb.9.2 = 33 AiPlayerbot.RandomClassSpecIndex.9.2 = 2 +# affli pvp +AiPlayerbot.RandomClassSpecProb.9.3 = 0 +AiPlayerbot.RandomClassSpecIndex.9.3 = 3 +# demo pvp +AiPlayerbot.RandomClassSpecProb.9.4 = 0 +AiPlayerbot.RandomClassSpecIndex.9.4 = 4 +# destro pvp +AiPlayerbot.RandomClassSpecProb.9.5 = 0 +AiPlayerbot.RandomClassSpecIndex.9.5 = 5 -# # # #################################################################################################### @@ -1890,22 +2075,53 @@ AiPlayerbot.RandomClassSpecIndex.9.2 = 2 #################################################################################################### # DRUID # -# +# balance pve AiPlayerbot.RandomClassSpecProb.11.0 = 20 AiPlayerbot.RandomClassSpecIndex.11.0 = 0 +# bear pve AiPlayerbot.RandomClassSpecProb.11.1 = 25 AiPlayerbot.RandomClassSpecIndex.11.1 = 1 +# resto pve AiPlayerbot.RandomClassSpecProb.11.2 = 35 AiPlayerbot.RandomClassSpecIndex.11.2 = 2 +# cat pve AiPlayerbot.RandomClassSpecProb.11.3 = 20 AiPlayerbot.RandomClassSpecIndex.11.3 = 3 +# balance pvp +AiPlayerbot.RandomClassSpecProb.11.4 = 0 +AiPlayerbot.RandomClassSpecIndex.11.4 = 4 +# cat pvp +AiPlayerbot.RandomClassSpecProb.11.5 = 0 +AiPlayerbot.RandomClassSpecIndex.11.5 = 5 +# resto pvp +AiPlayerbot.RandomClassSpecProb.11.6 = 0 +AiPlayerbot.RandomClassSpecIndex.11.6 = 6 -# # # #################################################################################################### +################################### +# # +# RAIDS # +# # +################################### + +#################################################################################################### +# +# + +# Enable buffs in ICC to make Heroic easier and more casual. +# 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for +# non-tank bots, increased threat for tank bots. +# Buffs will be applied on LDW, PP, Sindragosa and Lich King. +# Default: 1 (enabled) +AiPlayerbot.EnableICCBuffs = 1 + +# +# +#################################################################################################### ################################### # # @@ -1916,8 +2132,6 @@ AiPlayerbot.RandomClassSpecIndex.11.3 = 3 #################################################################################################### # DATABASE & CONNECTIONS # -# - # PlayerbotsDatabaseInfo # Description: Database connection settings for the playerbots server. # Example: "hostname;port;username;password;database" @@ -1930,7 +2144,6 @@ AiPlayerbot.RandomClassSpecIndex.11.3 = 3 PlayerbotsDatabaseInfo = "127.0.0.1;3306;acore;acore;acore_playerbots" -# # PlayerbotsDatabase.WorkerThreads # Description: The amount of worker threads spawned to handle asynchronous (delayed) MySQL # statements. Each worker thread is mirrored with its own connection to the @@ -1939,7 +2152,6 @@ PlayerbotsDatabaseInfo = "127.0.0.1;3306;acore;acore;acore_playerbots" PlayerbotsDatabase.WorkerThreads = 1 -# # PlayerbotsDatabase.SynchThreads # Description: The amount of MySQL connections spawned to handle. # Default: 1 - (PlayerbotDatabase.WorkerThreads) @@ -1957,7 +2169,6 @@ Playerbots.Updates.EnableDatabases = 1 # Command server port, 0 - disabled AiPlayerbot.CommandServerPort = 8888 -# # # #################################################################################################### @@ -1965,7 +2176,6 @@ AiPlayerbot.CommandServerPort = 8888 #################################################################################################### # DEBUG SWITCHES # -# AiPlayerbot.SpellDump = 0 AiPlayerbot.LogInGroupOnly = 1 @@ -1979,7 +2189,6 @@ AiPlayerbot.TellWhenAvoidAoe = 0 # Enable/Disable performance monitor AiPlayerbot.PerfMonEnabled = 0 -# # # #################################################################################################### @@ -1987,7 +2196,6 @@ AiPlayerbot.PerfMonEnabled = 0 #################################################################################################### # CHAT SETTINGS # -# # Prefix for bot chat commands (e.g., follow, stay) AiPlayerbot.CommandPrefix = "" @@ -1995,6 +2203,14 @@ AiPlayerbot.CommandPrefix = "" # Separator for bot chat commands AiPlayerbot.CommandSeparator = "\\\\" +# Enable automatic item count/trade trigger when a chat message contains +# item-related keywords. When enabled (1), mentioning items in chat +# (e.g. "food", "potion", "ammo") will automatically show inventory and +# open a trade window with the bot. Explicit "c" and "t" commands still +# work regardless of this setting. +# Default: 1 (enabled) +AiPlayerbot.EnableAutoTradeOnItemMention = 1 + # Enable bots talking (say / yell / general chatting / lfg) AiPlayerbot.RandomBotTalk = 1 # Enable bots emoting @@ -2014,7 +2230,6 @@ AiPlayerbot.GuildRepliesRate = 100 # Bots without a master will say their lines AiPlayerbot.RandomBotSayWithoutMaster = 0 -# # # #################################################################################################### @@ -2022,17 +2237,15 @@ AiPlayerbot.RandomBotSayWithoutMaster = 0 #################################################################################################### # Broadcast rates # -# # Enable/disable broadcasts globally # Default: 1 (enabled) AiPlayerbot.EnableBroadcasts = 1 -# + # All broadcast chances should be in range 0-30000 # Value of 0 will disable this particular broadcast # Setting value to 30000 does not guarantee the broadcast, as there are some internal randoms as well -# -# Setting channel broadcast chance to 0, will re-route most broadcasts to other available channels +# Setting channel broadcast chance to 0 will re-route most broadcasts to other available channels # Setting all channel broadcasts to 0 will disable most broadcasts AiPlayerbot.BroadcastToGuildGlobalChance = 30000 AiPlayerbot.BroadcastToWorldGlobalChance = 30000 @@ -2042,7 +2255,7 @@ AiPlayerbot.BroadcastToLFGGlobalChance = 30000 AiPlayerbot.BroadcastToLocalDefenseGlobalChance = 30000 AiPlayerbot.BroadcastToWorldDefenseGlobalChance = 30000 AiPlayerbot.BroadcastToGuildRecruitmentGlobalChance = 30000 -# + # Individual settings # Setting one of these to 0 will disable the particular broadcast AiPlayerbot.BroadcastChanceLootingItemPoor = 30 @@ -2052,14 +2265,14 @@ AiPlayerbot.BroadcastChanceLootingItemRare = 20000 AiPlayerbot.BroadcastChanceLootingItemEpic = 30000 AiPlayerbot.BroadcastChanceLootingItemLegendary = 30000 AiPlayerbot.BroadcastChanceLootingItemArtifact = 30000 -# + AiPlayerbot.BroadcastChanceQuestAccepted = 6000 AiPlayerbot.BroadcastChanceQuestUpdateObjectiveCompleted = 300 AiPlayerbot.BroadcastChanceQuestUpdateObjectiveProgress = 300 AiPlayerbot.BroadcastChanceQuestUpdateFailedTimer = 300 AiPlayerbot.BroadcastChanceQuestUpdateComplete = 1000 AiPlayerbot.BroadcastChanceQuestTurnedIn = 10000 -# + AiPlayerbot.BroadcastChanceKillNormal = 30 AiPlayerbot.BroadcastChanceKillElite = 300 AiPlayerbot.BroadcastChanceKillRareelite = 3000 @@ -2068,39 +2281,40 @@ AiPlayerbot.BroadcastChanceKillRare = 10000 AiPlayerbot.BroadcastChanceKillUnknown = 100 AiPlayerbot.BroadcastChanceKillPet = 10 AiPlayerbot.BroadcastChanceKillPlayer = 30 -# + AiPlayerbot.BroadcastChanceLevelupGeneric = 20000 AiPlayerbot.BroadcastChanceLevelupTenX = 30000 AiPlayerbot.BroadcastChanceLevelupMaxLevel = 30000 -# + AiPlayerbot.BroadcastChanceSuggestInstance = 5000 AiPlayerbot.BroadcastChanceSuggestQuest = 10000 AiPlayerbot.BroadcastChanceSuggestGrindMaterials = 5000 AiPlayerbot.BroadcastChanceSuggestGrindReputation = 5000 AiPlayerbot.BroadcastChanceSuggestSell = 300 AiPlayerbot.BroadcastChanceSuggestSomething = 30000 -# + # Bots will say very rude things AiPlayerbot.BroadcastChanceSuggestSomethingToxic = 0 -# + # Specifically for " [item link]" AiPlayerbot.BroadcastChanceSuggestToxicLinks = 0 -# + # Prefix is used as a word in " [item link]" AiPlayerbot.ToxicLinksPrefix = gnomes -# + # Chance to suggest Thunderfury AiPlayerbot.BroadcastChanceSuggestThunderfury = 1 -# + # Does not depend on global chance AiPlayerbot.BroadcastChanceGuildManagement = 30000 + +# # #################################################################################################### #################################################################################################### # LOGS # -# # Custom config to allow logfiles to be created. # Example: AiPlayerbot.AllowedLogFiles = travelNodes.csv,travelPaths.csv,TravelNodeStore.h,bot_movement.csv,bot_location.csv @@ -2108,38 +2322,15 @@ AiPlayerbot.AllowedLogFiles = "" # # -# -#################################################################################################### - -#################################################################################################### -# A list of gameObject GUID's that are not allowed for bots to interact with. -# -AiPlayerbot.DisallowedGameObjects = 176213,17155,2656,74448,19020,3719,3658,3705,3706,105579,75293,2857,179490,141596,160836,160845,179516,176224,181085,176112,128308,128403,165739,165738,175245,175970,176325,176327,123329,2560 -# -# List of GUID's: -# QuestItems: -# 176213 = Blood of Heroes, 17155 = Defias Gunpowder, 2656 = Waterlogged Envelope, 123329 = Baelogs Chest, 2560 = Half-Buried Bottle -# Chests: -# Large Solid Chest = 74448, Box of Assorted Parts = 19020, Food Crate = 3719, Water Barrel = 3658, Barrel of Milk = 3705, Barrel of sweet Nectar = 3706, Tattered Chest = 105579, Large bettered Chest = 75293, Solid Chest = 2857, Battered Foodlocker = 179490, Witch Doctor's Chest = 141596, Relic Coffer = 160836, Dark Coffer = 160845, Fengus's Chest = 179516, Supply Crate = 176224/181085, Malor's Strongbox = 176112 -# Other: -# Shallow Grave (Zul'Farrak) = 128308/128403, Grim Guzzler Boar (Blackrock Depths) = 165739, Dark Iron Ale Mug (Blackrock Depths) = 165738, Father Flame (Blackrock Spire) = 175245, Unforged Runic Breastplate (Blackrock Spire) = 175970, Blacksmithing Plans (Stratholme) = 176325/176327 -# Feel free to edit and help to complete. -# #################################################################################################### ############################################## # Deprecated Settings (yet still in use) # ############################################## -# Log on all randombots on start -AiPlayerbot.RandomBotLoginAtStartup = 1 - # Guild Task system AiPlayerbot.EnableGuildTasks = 0 -# Enable dungeon suggestions in lower case randomly -AiPlayerbot.SuggestDungeonsInLowerCaseRandomly = 0 - # Chance bot chooses RPG (Teleport to random camp for their level) instead of grinding AiPlayerbot.RandomBotRpgChance = 0.20 @@ -2149,10 +2340,6 @@ AiPlayerbot.RandombotsWalkingRPG = 0 # Set randombots movement speed to walking only inside buildings AiPlayerbot.RandombotsWalkingRPG.InDoors = 0 -# Premade spell to avoid (undetected spells) -# spellid-radius, ... -AiPlayerbot.PremadeAvoidAoe = 62234-4 - AiPlayerbot.MinRandomBotsPriceChangeInterval = 7200 AiPlayerbot.MaxRandomBotsPriceChangeInterval = 172800 AiPlayerbot.MinRandomBotChangeStrategyTime = 180 @@ -2181,9 +2368,3 @@ AiPlayerbot.TargetPosRecalcDistance = 0.1 # Allow bots to be summoned near innkeepers AiPlayerbot.SummonAtInnkeepersEnabled = 1 - -# Enable buffs in ICC to make Heroic easier and more casual. -# 30% more damage, 40% damage reduction (tank bots), increased all resistances, reduced threat for non tank bots, increased threat for tank bots. -# Buffs will be applied on PP, Sindragosa and Lich King - -AiPlayerbot.EnableICCBuffs = 1 diff --git a/modules/mod-playerbots/data/sql/playerbots/base/ai_playerbot_texts.sql b/modules/mod-playerbots/data/sql/playerbots/base/ai_playerbot_texts.sql index 844881874..61f4680e3 100644 --- a/modules/mod-playerbots/data/sql/playerbots/base/ai_playerbot_texts.sql +++ b/modules/mod-playerbots/data/sql/playerbots/base/ai_playerbot_texts.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS `ai_playerbot_texts`; + CREATE TABLE IF NOT EXISTS `ai_playerbot_texts` ( `id` smallint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL COMMENT 'name - used in strategies/code as filter', @@ -16,2002 +17,1743 @@ CREATE TABLE IF NOT EXISTS `ai_playerbot_texts` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; -INSERT INTO `ai_playerbot_texts` (`name`, `text`, `say_type`, `reply_type`, `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) VALUES - --- Updated Chatter Text over 900 lines added by https://github.com/AzoraNova on 12 jan 2025 --- Translated Chatter Text to German by https://github.com/Raz0r1337 on 10 feb 2025 --- Translated Chatter Text to Chinese by https://github.com/sd3420625 on 17 may 2025 - --- strings -('string_unknown_area', 'the middle of nowhere', 0, 0, '', 'au milieu de nulle part', 'Mitten im Nirgendwo', '荒无人烟之地', '', '', '', ''), -('string_unknown_area', 'an undisclosed location', 0, 0, '', 'un lieu non divulgué', 'Ein geheimer Ort', '某个不为人知的地方', '', '', '', ''), -('string_unknown_area', 'somewhere', 0, 0, '', 'Quelque part.', 'Irgendwo', '某个地方', '', '', '', ''), -('string_empty_link', 'something', 0, 0, '', 'quelque chose', 'Irgendwas', '某样东西', '', '', '', ''), - - --- looting item events --- usable placeholders: --- %item_link --- %zone_name --- %area_name --- %my_race --- %my_class --- %my_level -('broadcast_looting_item_normal', 'wonder what %item_link tastes like', 0, 0, '', 'Je me demande à quoi %item_link peut bien goûter.', 'Ich frag mich, wie %item_link wohl schmeckt.', '想知道这%item_link吃起来啥味儿', '', '', '', ''), -('broadcast_looting_item_poor', 'nooo, I got %item_link', 0, 0, '', 'Oh non, j’ai reçu %item_link', 'Neeein, ich hab %item_link bekommen.', '不!我捡了个%item_link', '', '', '', ''), -('broadcast_looting_item_poor', 'aww not this trash again %item_link', 0, 0, '', 'Encore cette poubelle %item_link.', 'Och nö, schon wieder dieser Schrott %item_link.', '哎,又是这破烂%item_link', '', '', '', ''), -('broadcast_looting_item_poor', 'seem to be looting trash %item_link', 0, 0, '', 'Il semble que je fouille dans les ordures %item_link.', 'Scheint so, als würde ich nur Müll looten %item_link.', '看来我捡的都是垃圾%item_link', '', '', '', ''), -('broadcast_looting_item_poor', 'well, it’s better than nothing I guess %item_link', 0, 0, '', 'Eh bien, c’est toujours mieux que rien, je suppose %item_link', 'Naja, besser als nix, denk ich %item_link.', '好吧,有%item_link总比没有强', '', '', '', ''), -('broadcast_looting_item_poor', 'not sure what to do with %item_link', 0, 0, '', 'Je ne suis pas certain de ce qu’il faut faire avec %item_link.', 'Keine Ahnung, was ich mit %item_link anfangen soll.', '不知道这%item_link能干啥', '', '', '', ''), -('broadcast_looting_item_normal', 'well, I can keep looting %item_link all day', 0, 0, '', 'Eh bien, je peux passer la journée à looter %item_link!', 'Super, ich könnte den ganzen Tag %item_link looten.', '好吧,我能捡%item_link捡一天', '', '', '', ''), -('broadcast_looting_item_normal', 'another day, another %item_link', 0, 0, '', 'Encore une journée, encore un %item_link', 'Neuer Tag, neues %item_link.', '又是新的一天,又捡了个%item_link', '', '', '', ''), -('broadcast_looting_item_normal', 'looted some %item_link', 0, 0, '', 'J’ai pillé quelques %item_link.', 'Hab ein paar %item_link gelootet.', '捡了点%item_link', '', '', '', ''), -('broadcast_looting_item_normal', 'some %item_link is alright', 0, 0, '', 'Certains %item_link passent bien', 'Ein paar %item_link sind ganz okay.', '有点%item_link还凑合', '', '', '', ''), -('broadcast_looting_item_uncommon', 'not bad, I just got %item_link', 0, 0, '', 'Pas mal du tout, je viens juste de recevoir %item_link', 'Nicht schlecht, hab grad %item_link bekommen.', '不错,刚捡到个%item_link', '', '', '', ''), -('broadcast_looting_item_uncommon', 'just looted %item_link in %zone_name', 0, 0, '', 'Je viens de piller %item_link dans %zone_name.', 'Hab gerade %item_link in %zone_name gelootet.', '刚在%zone_name捡到个%item_link', '', '', '', ''), -('broadcast_looting_item_uncommon', 'I could put that to good use %item_link', 0, 0, '', 'Je pourrais bien utiliser ça %item_link', 'Könnte %item_link gut gebrauchen.', '这%item_link我能派上用场', '', '', '', ''), -('broadcast_looting_item_uncommon', 'money money money %item_link', 0, 0, '', 'L’argent, l’argent, l’argent! %item_link', 'Geld, Geld, Geld! %item_link', '发财了发财了!%item_link', '', '', '', ''), -('broadcast_looting_item_uncommon', 'got %item_link', 0, 0, '', 'J’ai eu %item_link.', 'Hab %item_link bekommen.', '拿到了%item_link', '', '', '', ''), -('broadcast_looting_item_rare', '%item_link is hunter bis', 0, 0, '', 'Ah, %item_link c’est le meilleur équipement pour les chasseurs.', '%item_link ist perfekt für Jäger.', '%item_link简直是猎人的极品装备', '', '', '', ''), -('broadcast_looting_item_rare', '%item_link is %my_class bis', 0, 0, '', '%item_link est %my_class bis.', '%item_link ist ideal für %my_class.', '%item_link对%my_class来说是极品', '', '', '', ''), -('broadcast_looting_item_rare', 'RNG is not bad today %item_link', 0, 0, '', 'La RNG n’est pas mauvaise aujourd’hui %item_link.', 'Das RNG ist heute gar nicht mal so schlecht %item_link.', '今天人品不错,捡到个%item_link', '', '', '', ''), -('broadcast_looting_item_rare', 'sweet %item_link, freshly looted', 0, 0, '', 'Doux %item_link, fraîchement looté', 'Schönes %item_link, frisch gelootet.', '太棒了,新鲜出炉的%item_link', '', '', '', ''), -('broadcast_looting_item_rare', 'wow, I just got %item_link', 0, 0, '', 'wow, je viens de recevoir %item_link', 'Wow, hab grad %item_link bekommen!', '哇,我刚捡到个%item_link', '', '', '', ''), -('broadcast_looting_item_rare', '%item_link is hunter bis', 0, 0, '', 'Ah, %item_link, c’est le meilleur équipement pour les chasseurs.', '%item_link ist perfekt für Jäger.', '%item_link是猎人的顶级装备', '', '', '', ''), -('broadcast_looting_item_rare', '%item_link is %my_class bis', 0, 0, '', '%item_link est %my_class bis.', '%item_link ist ideal für %my_class.', '%item_link对%my_class来说是顶级货', '', '', '', ''), -('broadcast_looting_item_rare', 'RNG is not bad today %item_link', 0, 0, '', 'Aujourd’hui, la RNG n’est pas mauvaise %item_link.', 'Das RNG ist heute gar nicht mal so schlecht %item_link.', '今天运气真好,捡到%item_link', '', '', '', ''), -('broadcast_looting_item_rare', 'sweet %item_link, freshly looted', 0, 0, '', 'Oh, %item_link, fraîchement pillé', 'Schönes %item_link, frisch gelootet.', '太好了,刚捡到个%item_link', '', '', '', ''), -('broadcast_looting_item_epic', 'OMG, look at what I just got %item_link!!!', 0, 0, '', 'OMG, regardez ce que je viens d’obtenir %item_link !!!', 'OMG, schaut euch an, was ich grad bekommen habe %item_link!!!', '天啊,快看我刚捡到的%item_link!!!', '', '', '', ''), -('broadcast_looting_item_legendary', 'No @#$@% way! This can not be, I got %item_link, this is insane', 0, 0, '', 'Pas moyen! Ça ne peut pas être vrai, j’ai obtenu %item_link, c’est fou!', 'Nicht dein Ernst! Das kann nicht sein, ich hab %item_link bekommen, das ist irre!', '不是吧!这不可能,我竟然捡到%item_link,太疯狂了!', '', '', '', ''), -('broadcast_looting_item_artifact', 'No @#$@% way! This can not be, I got %item_link, this is insane', 0, 0, '', 'Pas possible! Ça ne peut pas être vrai, j’ai eu %item_link, c’est fou', 'Unfassbar! Das kann nicht sein, ich hab %item_link bekommen, das ist verrückt!', '天哪!这不可能,我拿到了%item_link,简直不敢相信!', '', '', '', ''), -('broadcast_looting_item_poor', 'Seriously? Another %item_link? My luck is broken.', 0, 0, '', 'Sérieusement? Encore un %item_link? La poisse me suit, c’est pas possible.', 'Ernsthaft? Noch ein %item_link? Ich habe kein Glück.', '不是吧?又是个%item_link?我的运气真是烂透了。', '', '', '', ''), -('broadcast_looting_item_poor', 'This %item_link is not even worth the pixels it’s printed on.', 0, 0, '', 'Cet %item_link ne vaut même pas les pixels sur lesquels il est imprimé.', 'Dieser %item_link ist nicht mal die Pixel wert, auf denen er angezeigt wird.', '这%item_link连显示它的像素都不值。', '', '', '', ''), -('broadcast_looting_item_poor', 'I got %item_link... Ugh, here we go again.', 0, 0, '', 'J’ai de nouveau %item_link... Oh là là, c’est reparti.', 'Ich hab %item_link... Uff, schon wieder.', '我又捡到个%item_link...哎,又来了。', '', '', '', ''), -('broadcast_looting_item_poor', 'I don’t even know why I’m picking up %item_link, it’s useless.', 0, 0, '', 'Je me demande pourquoi je prends %item_link, c’est inutile.', 'Ich weiß nicht mal, warum ich %item_link aufhebe, ist eh nutzlos.', '我都不知道为啥要捡%item_link,完全没用。', '', '', '', ''), -('broadcast_looting_item_poor', 'Look at this shiny %item_link... too bad it’s garbage.', 0, 0, '', 'Regarde ce magnifique %item_link... dommage que ce soit de la camelote.', 'Schau dir dieses glänzende %item_link an... schade, dass es Müll ist.', '看看这闪亮的%item_link...可惜就是个垃圾。', '', '', '', ''), -('broadcast_looting_item_poor', 'This %item_link isn’t even worth the paper it’s written on.', 0, 0, '', '%item_link ne vaut même pas le papier sur lequel il est écrit.', 'Dieser %item_link ist nicht mal das Papier wert, auf dem er steht.', '这%item_link连写它的羊皮纸都不值。', '', '', '', ''), -('broadcast_looting_item_poor', 'I guess %item_link is better than nothing? Barely.', 0, 0, '', 'Je suppose que %item_link vaut mieux que rien ? À peine.', 'Ich schätze, %item_link ist besser als nichts? Gerade noch.', '我想%item_link总比没有强吧?勉强。', '', '', '', ''), -('broadcast_looting_item_poor', 'Not sure what to do with %item_link, maybe I’ll just sell it for 1 silver.', 0, 0, '', 'Je ne suis pas sûr de ce que faire avec %item_link, peut-être que je vais juste le vendre pour 1 pièce d’argent.', 'Keine Ahnung, was ich mit %item_link machen soll, vielleicht verkauf ich’s für 1 Silber.', '不知道%item_link能干嘛,或许卖1个银币得了。', '', '', '', ''), -('broadcast_looting_item_poor', 'Another %item_link... well, at least I’m getting something.', 0, 0, '', 'Encore un %item_link... eh bien, au moins je reçois quelque chose.', 'Noch ein %item_link... naja, immerhin bekomme ich was.', '又是个%item_link...好吧,至少有点收获。', '', '', '', ''), -('broadcast_looting_item_poor', 'I hope %item_link is actually worth something because it looks awful.', 0, 0, '', 'J’espère que %item_link vaut en fait quelque chose parce que ça a l’air affreux.', 'Hoffentlich ist %item_link wenigstens was wert, sieht nämlich furchtbar aus.', '希望%item_link能值点钱,因为它看起来真垃圾。', '', '', '', ''), -('broadcast_looting_item_poor', 'Is %item_link a joke? Someone’s laughing at me.', 0, 0, '', '%item_link c’est une blague ? Quelqu’un se moque de moi.', 'Ist %item_link ein Witz? Irgendwer lacht mich aus.', '这%item_link是开玩笑吗?谁在嘲笑我啊!', '', '', '', ''), -('broadcast_looting_item_poor', 'Why do I keep getting %item_link... did I anger RNG?', 0, 0, '', 'Pourquoi est-ce que je continue à obtenir %item_link... est-ce que j’ai fâché le RNG?', 'Warum krieg ich immer wieder %item_link... hab ich die RNG-Götter verärgert?', '为啥我老是拿到%item_link……我是不是得罪了随机之神?', '', '', '', ''), -('broadcast_looting_item_poor', 'It’s like %item_link is following me around. I don’t want it!', 0, 0, '', 'C’est comme si %item_link me suivait partout. Je ne veux pas de ça !', 'Es ist, als würde %item_link mir nachlaufen. Ich will das nicht!', '这%item_link就像缠着我一样,我才不要呢!', '', '', '', ''), -('broadcast_looting_item_poor', 'There’s %item_link, again. What a surprise.', 0, 0, '', 'Encore %item_link. Quelle surprise.', 'Schon wieder %item_link. Was für eine Überraschung.', '又是%item_link,真是“惊喜”啊。', '', '', '', ''), -('broadcast_looting_item_poor', 'I guess %item_link is good for vendor trash?', 0, 0, '', 'I présume qu’ %item_link est bon pour vendre à un marchand ?', 'Ich schätze, %item_link taugt immerhin als Müll für den Händler?', '看来%item_link只能当垃圾卖给商人了?', '', '', '', ''), -('broadcast_looting_item_poor', 'Noooo, not %item_link again! RNG, please!', 0, 0, '', 'Oh non, pas encore %item_link! RNG s’il te plaît!', 'Neeein, nicht schon wieder %item_link! RNG, bitte!', '不!别又是%item_link!随机之神,求放过!', '', '', '', ''), -('broadcast_looting_item_poor', 'Well, at least %item_link isn’t completely useless, right?', 0, 0, '', 'Eh bien, au moins %item_link n’est pas complètement inutile, n’est-ce pas?', 'Naja, wenigstens ist %item_link nicht komplett nutzlos, oder?', '好吧,至少%item_link还不算完全没用,对吧?', '', '', '', ''), -('broadcast_looting_item_poor', 'Another %item_link? RNG, do you even care?', 0, 0, '', 'Encore %item_link ? La RNG est-ce que ça te dit quelque chose au moins?', 'Noch ein %item_link? RNG, interessiert dich das überhaupt?', '又一个%item_link?随机之神,你在乎过我的感受吗?', '', '', '', ''), -('broadcast_looting_item_poor', 'Maybe %item_link will be worth something in 10 years.', 0, 0, '', 'Peut-être que %item_link vaudra quelque chose dans 10 ans.', 'Vielleicht ist %item_link in 10 Jahren mal was wert.', '或许%item_link十年后能值点钱吧。', '', '', '', ''), -('broadcast_looting_item_poor', 'I guess I’ll just admire how %item_link looks... and throw it away.', 0, 0, '', 'Je suppose que je vais juste admirer %item_link ... et le jeter.', 'Ich schätze, ich bewundere einfach, wie %item_link aussieht... und werfe es weg.', '我就欣赏一下%item_link的样子……然后扔了吧。', '', '', '', ''), -('broadcast_looting_item_poor', 'Who needs %item_link? Not me, that’s for sure.', 0, 0, '', 'Qui a besoin de %item_link ? Pas moi en tout cas, ça c’est sûr.', 'Wer braucht %item_link? Ich auf jeden Fall nicht.', '谁需要%item_link啊?反正我肯定不要。', '', '', '', ''), -('broadcast_looting_item_poor', 'Maybe %item_link will be useful... just kidding, it’s trash.', 0, 0, '', 'Peut-être que %item_link sera utile... non, je plaisante, c’est nul.', 'Vielleicht ist %item_link nützlich... nur Spaß, ist Müll.', '也许%item_link有用……开玩笑的,就是个垃圾。', '', '', '', ''), -('broadcast_looting_item_poor', 'The universe has given me %item_link. Great.', 0, 0, '', 'L’univers m’a donné %item_link. Génial.', 'Das Universum hat mir %item_link geschenkt. Toll.', '宇宙赐予了我%item_link,棒极了。', '', '', '', ''), -('broadcast_looting_item_poor', 'Well, I have %item_link. Can’t say I’m thrilled.', 0, 0, '', 'Eh bien, j’ai %item_link. Franchement, je ne suis pas super emballé.', 'Naja, ich hab %item_link. Begeistert bin ich nicht.', '好吧,我拿到了%item_link,可一点也高兴不起来。', '', '', '', ''), -('broadcast_looting_item_poor', 'What am I even going to do with %item_link? Can’t even sell it.', 0, 0, '', 'Qu’est-ce que je vais bien pouvoir faire avec %item_link ? Je ne peux même pas le vendre.', 'Was soll ich bloß mit %item_link anfangen? Kann es nicht mal verkaufen.', '我拿%item_link能干啥?连卖都卖不掉。', '', '', '', ''), -('broadcast_looting_item_poor', 'Let’s add %item_link to my growing pile of disappointment.', 0, 0, '', 'Ajoutons %item_link à ma pile croissante de déceptions.', 'Packen wir %item_link zu meinem wachsenden Berg der Enttäuschungen.', '把%item_link扔进我那堆失望的垃圾里吧。', '', '', '', ''), -('broadcast_looting_item_poor', 'Thanks for %item_link... it’s exactly what I didn’t need.', 0, 0, '', 'Merci pour %item_link... c’est exactement ce qu’il me fallait pas.', 'Danke für %item_link... genau das, was ich nicht gebraucht habe.', '谢谢给了我%item_link……这正是我不需要的东西。', '', '', '', ''), -('broadcast_looting_item_poor', 'Another %item_link... RNG, we need to talk.', 0, 0, '', 'Encore un %item_link... RNG, on doit avoir une discussion.', 'Noch ein %item_link... RNG, wir müssen reden.', '又一个%item_link……随机之神,咱们得谈谈。', '', '', '', ''), -('broadcast_looting_item_poor', 'I’ve looted so many %item_link, I could open a store with them.', 0, 0, '', 'J’ai récupéré tellement de %item_link, je pourrais ouvrir un magasin avec.', 'Ich hab so viele %item_link gelootet, ich könnte einen Laden eröffnen.', '我捡了这么多%item_link,够开个杂货铺了。', '', '', '', ''), -('broadcast_looting_item_poor', 'A shiny %item_link... maybe for the trash pile.', 0, 0, '', 'Un brillant %item_link... peut-être destiné à la poubelle.', 'Ein glänzendes %item_link... perfekt für den Müll.', '一个闪亮的%item_link……大概只能扔垃圾堆了。', '', '', '', ''), -('broadcast_looting_item_poor', 'It’s %item_link, everybody. Don’t get too excited.', 0, 0, '', 'C’est %item_link, tout le monde. Pas de quoi s’affoler.', 'Es ist %item_link, Leute. Bloß nicht zu sehr freuen.', '各位,这是%item_link,别太激动哦。', '', '', '', ''), -('broadcast_looting_item_poor', 'Well, at least it’s %item_link... could be worse.', 0, 0, '', 'Eh bien, au moins c’est %item_link... ça pourrait être pire.', 'Naja, immerhin ist es %item_link... könnte schlimmer sein.', '好吧,至少是%item_link……还能更糟呢。', '', '', '', ''), -('broadcast_looting_item_poor', 'I’ll take %item_link over nothing.', 0, 0, '', 'Je préfère %item_link à rien du tout.', 'Lieber %item_link als gar nichts.', '有%item_link总比啥也没有强。', '', '', '', ''), -('broadcast_looting_item_poor', 'Another %item_link. Not the worst.', 0, 0, '', 'Encore un %item_link. Pas le pire.', 'Noch ein %item_link. Geht schlimmer.', '又一个%item_link,还不算最差。', '', '', '', ''), -('broadcast_looting_item_poor', 'Hey, %item_link... it’s okay, I suppose.', 0, 0, '', 'Salut, %item_link... c’est correct, je suppose.', 'Hey, %item_link... naja, ist okay, denk ich.', '嘿,%item_link……还行吧,我觉得。', '', '', '', ''), -('broadcast_looting_item_normal', 'Hey, %item_link. Not bad, not great, but I’ll take it.', 0, 0, '', 'Salut, %item_link. Pas mal, pas super, mais je vais le prendre.', 'Hey, %item_link. Nicht schlecht, nicht überragend, aber ich nehm’s.', '嘿,%item_link。不算好也不算差,拿了再说。', '', '', '', ''), -('broadcast_looting_item_normal', 'Could be worse... I just looted %item_link.', 0, 0, '', 'Ça aurait pu être pire... Je viens juste de mettre la main sur %item_link.', 'Könnte schlimmer sein... hab grad %item_link gelootet.', '还行吧……刚捡到个%item_link。', '', '', '', ''), -('broadcast_looting_item_normal', 'I guess %item_link will come in handy for something.', 0, 0, '', 'Je suppose que %item_link va être utile pour quelque chose.', 'Ich schätze, %item_link wird irgendwann mal nützlich sein.', '我觉得%item_link总能派上点用场。', '', '', '', ''), -('broadcast_looting_item_normal', 'Another day, another %item_link. Can’t complain.', 0, 0, '', 'Encore une journée, encore un %item_link. Je ne vais pas me plaindre.', 'Neuer Tag, neues %item_link. Kann mich nicht beschweren.', '又是新的一天,又捡到个%item_link,挺好。', '', '', '', ''), -('broadcast_looting_item_normal', 'Well, %item_link is alright, I guess.', 0, 0, '', 'Hmm, %item_link, c’est pas mal, je suppose.', 'Joah, %item_link geht klar, denk ich.', '嗯,%item_link还行吧,我觉得。', '', '', '', ''), -('broadcast_looting_item_normal', 'Just got %item_link. It’s not the best, but it’s something.', 0, 0, '', 'Je viens de récupérer %item_link. Ce n’est pas le meilleur, mais c’est mieux que rien.', 'Hab grad %item_link bekommen. Nicht das Beste, aber besser als nix.', '刚拿到%item_link,不是最好的,但也算有点收获。', '', '', '', ''), -('broadcast_looting_item_normal', 'Could be worse, at least it’s %item_link.', 0, 0, '', 'Ça pourrait être pire, au moins c’est %item_link.', 'Könnte schlimmer sein, immerhin ist es %item_link.', '还能更糟呢,至少是%item_link。', '', '', '', ''), -('broadcast_looting_item_normal', '%item_link is good enough, I’m not mad.', 0, 0, '', '%item_link est assez bon, je ne suis pas fâché.', '%item_link ist gut genug, ich kann mich nicht beklagen.', '%item_link还不错,我没啥好抱怨的。', '', '', '', ''), -('broadcast_looting_item_normal', 'It’s no legendary, but %item_link will do.', 0, 0, '', 'Ce n’est pas légendaire, mais %item_link fera l’affaire.', 'Ist kein Legendäres, aber %item_link reicht.', '虽然不是传说装备,但%item_link也凑合了。', '', '', '', ''), -('broadcast_looting_item_normal', 'Not a bad drop... %item_link is alright.', 0, 0, '', 'Pas mal du tout cette trouvaille... %item_link est pas mal du tout.', 'Kein schlechter Drop... %item_link geht in Ordnung.', '掉落还行……%item_link不错。', '', '', '', ''), -('broadcast_looting_item_normal', 'I’ll take %item_link over nothing.', 0, 0, '', 'Je préfère %item_link à rien du tout.', 'Lieber %item_link als gar nichts.', '有%item_link总比啥也没有强。', '', '', '', ''), -('broadcast_looting_item_normal', 'Not the best, but %item_link is a nice addition.', 0, 0, '', 'Ce n’est pas le meilleur, mais %item_link est une belle addition.', 'Nicht das Beste, aber %item_link ist eine nette Ergänzung.', '不是最好的,但%item_link也是个不错的收获。', '', '', '', ''), -('broadcast_looting_item_normal', 'Getting %item_link again. I guess it’s better than nothing.', 0, 0, '', 'Encore en train de récupérer %item_link. Je suppose que c’est mieux que rien.', 'Schon wieder %item_link. Naja, besser als nix.', '又拿了个%item_link,总比啥也没有好。', '', '', '', ''), -('broadcast_looting_item_normal', '%item_link again, huh? I’m not thrilled, but I’ll live.', 0, 0, '', 'Encore %item_link, hein? Je ne suis pas super enthousiaste, mais bon, je vais m’en sortir.', 'Schon wieder %item_link, hm? Bin nicht begeistert, aber naja.', '又是%item_link?不太激动,不过还能接受。', '', '', '', ''), -('broadcast_looting_item_normal', 'Another %item_link? I’m okay with that.', 0, 0, '', 'Encore un %item_link? Ça me va.', 'Noch ein %item_link? Passt schon.', '又一个%item_link?我觉得还行。', '', '', '', ''), -('broadcast_looting_item_normal', 'Could’ve gotten worse, but %item_link isn’t so bad.', 0, 0, '', 'Ça aurait pu être pire, mais %item_link n’est pas si mal.', 'Hätte schlimmer kommen können, %item_link ist okay.', '本来可能更糟,但%item_link还算可以。', '', '', '', ''), -('broadcast_looting_item_normal', 'It’s %item_link... but at least it’s something.', 0, 0, '', 'C’est %item_link... mais au moins c’est quelque chose.', 'Es ist %item_link... aber immerhin etwas.', '是%item_link……但至少是个东西。', '', '', '', ''), -('broadcast_looting_item_normal', 'I’ll take %item_link, could be worse.', 0, 0, '', 'Je vais prendre %item_link, ça aurait pu être pire.', 'Ich nehm %item_link, hätte schlimmer sein können.', '我就拿%item_link吧,总比更糟的好。', '', '', '', ''), -('broadcast_looting_item_normal', 'Looted %item_link, nothing special but okay.', 0, 0, '', 'J’ai pillé %item_link, rien de spécial mais bon.', 'Hab %item_link gelootet, nichts Besonderes, aber okay.', '捡了个%item_link,没啥特别的,但还行。', '', '', '', ''), -('broadcast_looting_item_normal', 'Not what I wanted, but %item_link will do for now.', 0, 0, '', 'Ce n’est pas exactement ce que je voulais, mais %item_link fera l’affaire pour l’instant.', 'Nicht das, was ich wollte, aber %item_link reicht erstmal.', '不是我想要的,但%item_link先凑合用吧。', '', '', '', ''), -('broadcast_looting_item_normal', 'Another %item_link. Not amazing, but it’ll do.', 0, 0, '', 'Encore un %item_link. Pas extraordinaire, mais ça fera l’affaire.', 'Noch ein %item_link. Nicht der Hammer, aber geht.', '又一个%item_link,不算惊艳,但能用。', '', '', '', ''), -('broadcast_looting_item_normal', 'Looted %item_link, could be worse.', 0, 0, '', 'J’ai pillé %item_link, ça aurait pu être pire.', 'Hab %item_link gelootet, hätte schlimmer sein können.', '捡了个%item_link,还不算太糟。', '', '', '', ''), -('broadcast_looting_item_normal', 'Well, %item_link is better than a poke in the eye.', 0, 0, '', 'Eh bien, %item_link vaut mieux que de se faire poke dans l’œil.', 'Naja, %item_link ist besser, als ins Auge gestochen zu werden.', '好吧,%item_link总比被戳眼强。', '', '', '', ''), -('broadcast_looting_item_normal', 'I’ll take %item_link and move on.', 0, 0, '', 'Je vais prendre %item_link et continuer.', 'Ich nehm %item_link und mach weiter.', '拿上%item_link,继续前行吧。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Nice! A solid %item_link for the collection.', 0, 0, '', 'Super! Un bel %item_link pour la collection.', 'Nice! Ein solides %item_link für die Sammlung.', '不错!一个靠谱的%item_link,收好了!', '', '', '', ''), -('broadcast_looting_item_uncommon', 'I’ll take that %item_link any day.', 0, 0, '', 'Je prendrais bien ce lien de %item_link n’importe quel jour.', 'Den %item_link nehm ich jederzeit.', '这%item_link我随时都乐意拿!', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Not bad, %item_link might come in handy.', 0, 0, '', 'Pas mal, %item_link pourrait s’avérer utile.', 'Nicht schlecht, %item_link könnte nützlich sein.', '还不错,%item_link说不定能派上用场。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Got myself a %item_link, now we’re talking.', 0, 0, '', 'Je me suis procuré %item_link, ça va être la folie maintenant !', 'Hab mir ein %item_link geholt, jetzt geht’s ab.', '搞到个%item_link,这下有戏了!', '', '', '', ''), -('broadcast_looting_item_uncommon', 'That %item_link is better than most, I’ll take it.', 0, 0, '', 'Ce %item_link est meilleur que la plupart, je vais le prendre.', 'Dieses %item_link ist besser als die meisten, nehm ich.', '这%item_link比大多数都好,我收下了!', '', '', '', ''), -('broadcast_looting_item_uncommon', 'I could use %item_link for something good.', 0, 0, '', 'Je pourrais utiliser %item_link pour quelque chose de bien.', 'Ich könnte %item_link für etwas Nützliches gebrauchen.', '我能用%item_link干点好事。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Not bad, %item_link will work just fine.', 0, 0, '', 'Pas mal, %item_link va parfaitement convenir.', 'Nicht schlecht, %item_link passt schon.', '不错,%item_link用起来正好。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Another %item_link, at least it’s not useless.', 0, 0, '', 'Encore un %item_link, au moins ce n’est pas inutile.', 'Noch ein %item_link, immerhin nicht nutzlos.', '又一个%item_link,至少不是没用的。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'That %item_link might make a decent profit.', 0, 0, '', 'Ce %item_link pourrait me rapporter un joli profit.', 'Das %item_link könnte einen netten Gewinn bringen.', '这%item_link说不定能卖个好价钱。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'A solid %item_link, now the day is looking better.', 0, 0, '', 'Un bon %item_link, ça rend la journée plus belle maintenant.', 'Ein solides %item_link, jetzt sieht der Tag besser aus.', '一个靠谱的%item_link,今天心情都变好了。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Well, I didn’t expect %item_link, but I’ll take it.', 0, 0, '', 'Eh bien, je ne m’attendais pas à %item_link, mais je vais le prendre.', 'Nun, %item_link hab ich nicht erwartet, aber ich nehm’s.', '嗯,没想到会拿到%item_link,但我收下了。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Not sure what to do with %item_link, but I’m happy to have it.', 0, 0, '', 'Je ne suis pas sûr de ce qu’il faut faire avec %item_link, mais je suis content de l’avoir.', 'Keine Ahnung, was ich mit %item_link machen soll, aber ich freu mich drüber.', '不太确定%item_link干啥用,但我很开心能拿到。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Not my first choice, but I’ll happily accept %item_link.', 0, 0, '', 'Ce n’était pas mon premier choix, mais je vais volontiers accepter %item_link.', 'Nicht meine erste Wahl, aber ich nehm %item_link gerne.', '不是我的首选,但我很乐意收下%item_link。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'That %item_link is exactly what I needed today.', 0, 0, '', 'Ce %item_link est exactement ce dont j’avais besoin aujourd’hui.', '%item_link ist genau das, was ich heute gebraucht habe.', '这%item_link正是我今天需要的!', '', '', '', ''), -('broadcast_looting_item_uncommon', 'I’ll never say no to a nice %item_link.', 0, 0, '', 'Je ne dis jamais non à une belle %item_link.', 'Zu einem schönen %item_link sag ich nie nein.', '对一个不错的%item_link,我从不说不。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'Another %item_link. I’ll take it, no complaints here.', 0, 0, '', 'Encore un %item_link. Je le prends, pas de soucis ici.', 'Noch ein %item_link. Ich nehm’s, keine Beschwerden.', '又一个%item_link,我收下,没啥好抱怨的。', '', '', '', ''), -('broadcast_looting_item_uncommon', 'At least %item_link has some use. Not the worst loot.', 0, 0, '', 'Au moins %item_link a une certaine utilité. Ce n’est pas le pire butin.', 'Zumindest hat %item_link eine Verwendung. Nicht der schlechteste Loot.', '至少%item_link有点用,不是最差的战利品。', '', '', '', ''), -('broadcast_looting_item_uncommon', '%item_link might just be the one thing I need for the day.', 0, 0, '', 'Hmm, le lien %item_link pourrait bien être exactement ce dont j’ai besoin pour la journée.', 'Vielleicht ist %item_link genau das, was ich heute brauche.', '或许%item_link正是我今天需要的东西。', '', '', '', ''), - - - - --- quest events --- usable placeholders: --- %quest_link --- %zone_name --- %area_name --- %my_race --- %my_class --- %my_level -('broadcast_quest_accepted_generic', 'I have just taken the %quest_link', 0, 0, '', 'Je viens de commencer %quest_link.', 'Ich habe gerade %quest_link angenommen.', '我刚接了%quest_link任务。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'just accepted %quest_link', 0, 0, '', 'Je viens d’accepter %quest_link', 'Hab gerade %quest_link angenommen.', '刚接受了%quest_link任务。', '', '', '', ''), -('broadcast_quest_accepted_generic', '%quest_link gonna try to complete this one', 0, 0, '', '%quest_link Je vais essayer de terminer celle-ci.', '%quest_link – Ich versuch mal, die abzuschließen.', '%quest_link,这个任务我得试着完成。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'took %quest_link in %zone_name', 0, 0, '', 'J’ai pris %quest_link dans %zone_name.', 'Hab %quest_link in %zone_name angenommen.', '在%zone_name接下了%quest_link任务。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I just picked up %quest_link, time to get to work!', 0, 0, '', 'Je viens de prendre %quest_link, c’est parti pour se mettre au travail !', 'Hab gerade %quest_link angenommen, ran an die Arbeit!', '刚接到%quest_link,是时候开干了!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Got %quest_link, let’s see what this one is about.', 0, 0, '', 'J’ai obtenu %quest_link, voyons de quoi il s’agit.', 'Hab %quest_link bekommen, mal sehen, worum’s geht.', '拿到了%quest_link,看看这是个啥任务。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Just grabbed %quest_link, let’s do this!', 0, 0, '', 'Je viens de prendre %quest_link, allons-y!', 'Hab gerade %quest_link geschnappt, los geht’s!', '刚抢到%quest_link,走起!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Another quest, another %quest_link. Let’s go!', 0, 0, '', 'Encore une autre quête, encore %quest_link. Allons-y!', 'Noch eine Quest, noch ein %quest_link. Auf geht’s!', '又一个任务,又是%quest_link,走吧!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Looks like it’s %quest_link time, wish me luck.', 0, 0, '', 'Il semblerait que c’est l’heure de faire %quest_link, souhaitez-moi bonne chance.', 'Sieht aus, als wäre %quest_link dran. Drückt mir die Daumen!', '看来是时候做%quest_link了,祝我好运吧。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Accepted %quest_link, let’s see how tough this one is.', 0, 0, '', 'Accepté %quest_link, eh bien, voyons à quel point celle-ci est difficile.', 'Hab %quest_link angenommen, mal sehen, wie schwer sie ist.', '接受了%quest_link,看看这任务有多难。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Picked up %quest_link, time to go make some progress.', 0, 0, '', 'J’ai récupéré %quest_link, il est temps d’avancer et de progresser.', 'Hab %quest_link aufgenommen, Zeit, Fortschritt zu machen.', '拿下%quest_link,该去干点活儿了。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Just took %quest_link. Let’s see how hard this one is.', 0, 0, '', 'Je viens de commencer %quest_link. Voyons à quel point elle est difficile.', 'Hab gerade %quest_link angenommen. Mal sehen, wie hart sie ist.', '刚接了%quest_link,看看这任务有多棘手。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Accepted %quest_link, let’s hope there’s no surprise boss at the end.', 0, 0, '', 'Accepté %quest_link, espérons qu’il n’y ait pas de boss surprise à la fin.', 'Hab %quest_link angenommen, hoffentlich gibt’s am Ende keinen Überraschungsboss.', '接了%quest_link,希望最后别蹦出个惊喜首领。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'On my way to complete %quest_link. Here we go!', 0, 0, '', 'En route pour terminer ma quête %quest_link! C’est parti!', 'Unterwegs, um %quest_link abzuschließen. Auf geht’s!', '正在赶去完成%quest_link,走起!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I guess I’m doing %quest_link now. Let’s hope it’s worth it.', 0, 0, '', 'Je suppose que je me lance dans %quest_link maintenant. Espérons que cela en vaille la peine.', 'Sieht so aus, als würde ich jetzt %quest_link machen. Hoffentlich lohnt es sich.', '看来现在得做%quest_link,希望值回票价。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Picked up %quest_link. Let’s get moving!', 0, 0, '', 'J’ai récupéré %quest_link. Allez, on se bouge !', 'Hab %quest_link bekommen. Los geht’s!', '拿到了%quest_link,赶紧行动起来!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I’ve just taken %quest_link. This should be a piece of cake.', 0, 0, '', 'Je viens juste de prendre %quest_link. Ça devrait être facile comme tout.', 'Hab gerade %quest_link angenommen. Das sollte ein Klacks sein.', '刚接了%quest_link,这应该很简单。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I accepted %quest_link, now to knock it out of the park.', 0, 0, '', 'J’ai accepté %quest_link, maintenant il va falloir assurer comme jamais.', 'Hab %quest_link angenommen, jetzt bring ich’s zu Ende.', '我接了%quest_link,现在得干得漂亮。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Took %quest_link in %zone_name. Let’s do this!', 0, 0, '', 'J’ai pris %quest_link dans %zone_name. Allons-y!', 'Hab %quest_link in %zone_name angenommen. Los geht’s!', '在%zone_name接了%quest_link,干吧!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'On my way to finish %quest_link. Let’s make it quick!', 0, 0, '', 'En route pour terminer %quest_link. Allez, on se dépêche !', 'Unterwegs, um %quest_link zu beenden. Machen wir’s schnell!', '正在去完成%quest_link,赶紧搞定!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Accepted %quest_link. Wonder what awaits me in %zone_name.', 0, 0, '', 'Quête %quest_link acceptée. Je me demande ce qui m’attend dans la zone %zone_name.', '%quest_link angenommen. Mal sehen, was mich in %zone_name erwartet.', '接了%quest_link,不知道%zone_name会有啥等着我。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Got %quest_link, hope it doesn’t take forever.', 0, 0, '', 'J’ai attrapé %quest_link, j’espère que ça ne prendra pas une éternité.', 'Hab %quest_link bekommen, hoffentlich dauert das nicht ewig.', '拿到了%quest_link,希望别拖太久。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Just grabbed %quest_link. Here we go again.', 0, 0, '', 'Je viens de récupérer %quest_link. C’est reparti pour une nouvelle aventure.', 'Hab %quest_link geschnappt. Und wieder geht’s los.', '刚抢到%quest_link,又开始了。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Looks like %quest_link is the next one on my list.', 0, 0, '', 'Il semblerait que %quest_link soit le prochain sur ma liste.', 'Sieht aus, als wäre %quest_link die nächste auf meiner Liste.', '看来%quest_link是我列表上的下一个任务。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Off to complete %quest_link. I hope this one is fun.', 0, 0, '', 'En route pour accomplir %quest_link. J’espère que celle-ci est amusante.', 'Auf geht’s, %quest_link abschließen. Hoffentlich macht die Spaß.', '去完成%quest_link,希望这任务有点意思。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I’m diving into %quest_link. This will be an adventure!', 0, 0, '', 'Je plonge dans %quest_link. Ça s’annonce comme une aventure passionnante !', 'Ich stürze mich in %quest_link. Das wird ein Abenteuer!', '我一头扎进%quest_link,这会是一场冒险!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Accepted %quest_link in %zone_name. Let’s see what this brings!', 0, 0, '', '%Quête acceptée dans %zone_name. Voyons ce que cela nous réserve !', '%quest_link in %zone_name angenommen. Mal sehen, was das bringt!', '在%zone_name接了%quest_link,看看会发生啥!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Just took %quest_link. Time to see what I’m up against.', 0, 0, '', 'Je viens de prendre %quest_link. C’est l’heure de voir à quoi je dois faire face.', 'Hab gerade %quest_link angenommen. Mal sehen, was mich erwartet.', '刚接了%quest_link,是时候看看我要面对啥了。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Another %quest_link. Let’s get this done quickly!', 0, 0, '', 'Encore une %quest_link. On va régler ça rapidement!', 'Noch eine %quest_link. Machen wir’s schnell!', '又一个%quest_link,赶紧搞定吧!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I just took %quest_link. This will be a fun one!', 0, 0, '', 'Je viens de prendre %quest_link. Ça promet d’être amusant !', 'Hab gerade %quest_link angenommen. Das wird bestimmt lustig!', '刚接了%quest_link,这任务应该挺有趣!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Taking on %quest_link in %zone_name. Let’s go!', 0, 0, '', 'Je me lance dans %quest_link dans la région de %zone_name. C’est parti !', 'Starte %quest_link in %zone_name. Los geht’s!', '在%zone_name挑战%quest_link,走起!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Grabbed %quest_link. Can’t wait to see what happens next.', 0, 0, '', 'Attrapé %quest_link. Hâte de voir ce qui se passe ensuite.', 'Hab %quest_link geschnappt. Bin gespannt, was als Nächstes passiert.', '抢到%quest_link,迫不及待想知道接下来会发生啥。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Took %quest_link. Time to dive in and get it done.', 0, 0, '', 'J’ai pris %quest_link. Il est temps de s’y mettre et de le réaliser.', 'Hab %quest_link angenommen. Zeit, loszulegen.', '接了%quest_link,是时候埋头苦干了。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Alright, %quest_link is on. Let’s get to work!', 0, 0, '', 'D’accord, %quest_link est en marche. Au travail !', 'Alles klar, %quest_link läuft. An die Arbeit!', '好吧,%quest_link开始了,干活去!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'On to %quest_link. Let’s hope it’s not too hard.', 0, 0, '', 'En route vers %quest_link. Espérons que ce ne soit pas trop difficile.', 'Auf zu %quest_link. Hoffentlich ist die nicht zu schwer.', '前往做%quest_link,希望别太难。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Just grabbed %quest_link. Should be an easy one!', 0, 0, '', 'Je viens de saisir %quest_link. Ça devrait être une mission facile !', 'Hab gerade %quest_link geschnappt. Sollte einfach sein!', '刚抢到%quest_link,应该挺简单的!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'I’ve accepted %quest_link. Let’s make it quick!', 0, 0, '', 'J’ai accepté %quest_link. Allons-y rapidement!', 'Hab %quest_link angenommen. Machen wir’s schnell!', '我接了%quest_link,赶紧完成吧!', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Took %quest_link, now to see what it’s all about.', 0, 0, '', 'J’ai pris %quest_link, maintenant je vais voir de quoi il s’agit.', 'Hab %quest_link angenommen, mal sehen, worum’s geht.', '接了%quest_link,现在看看这是个啥任务。', '', '', '', ''), -('broadcast_quest_accepted_generic', 'Just accepted %quest_link, let’s get moving!', 0, 0, '', 'Je viens d’accepter la %quest_link, allons-y !', 'Gerade %quest_link angenommen, los geht’s!', '刚接受了%quest_link,动起来吧!', '', '', '', ''), - - --- usable placeholders: --- %quest_link --- %zone_name --- %area_name --- %quest_obj_available --- %quest_obj_required --- %quest_obj_missing --- %quest_obj_name --- %quest_obj_full_formatted --- %my_race --- %my_class --- %my_level -('broadcast_quest_update_add_kill_objective_completed', 'Finally done with the %quest_obj_name for %quest_link', 0, 0, '', 'Enfin terminé avec la quête %quest_obj_name pour %quest_link.', 'Endlich fertig mit %quest_obj_name für %quest_link.', '终于完成了%quest_link的%quest_obj_name任务目标!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'finally got %quest_obj_available/%quest_obj_required of %quest_obj_name for the %quest_link', 0, 0, '', 'J’ai enfin obtenu %quest_obj_available/%quest_obj_required de %quest_obj_name pour %quest_link.', 'Endlich %quest_obj_available/%quest_obj_required von %quest_obj_name für %quest_link bekommen.', '终于在%quest_link任务中拿到了%quest_obj_available/%quest_obj_required个%quest_obj_name!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', '%quest_obj_full_formatted for the %quest_link, at last', 0, 0, '', 'Enfin, j’ai terminé la %quest_obj_full_formatted pour %quest_link.', '%quest_obj_full_formatted für %quest_link, endlich fertig.', '终于搞定了%quest_link的%quest_obj_full_formatted!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Oof, got %quest_obj_available/%quest_obj_required %quest_obj_name for %quest_link', 0, 0, '', 'Oof, j’ai obtenu %quest_obj_available/%quest_obj_required %quest_obj_name pour %quest_link.', 'Uff, %quest_obj_available/%quest_obj_required %quest_obj_name für %quest_link gesammelt.', '呼,完成了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'still need %quest_obj_missing more of %quest_obj_name for %quest_link', 0, 0, '', 'Il me manque encore %quest_obj_missing de %quest_obj_name pour %quest_link.', 'Mir fehlen noch %quest_obj_missing von %quest_obj_name für %quest_link.', '还差%quest_obj_missing个%quest_obj_name才能完成%quest_link。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', '%quest_obj_full_formatted, still working on %quest_link', 0, 0, '', 'Je suis toujours en train de travailler sur %quest_obj_full_formatted, ça prend un peu de temps mais je vais y arriver.', '%quest_obj_full_formatted erledigt, arbeite noch an %quest_link.', '%quest_obj_full_formatted完成了,还在努力做%quest_link。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', '%quest_obj_name complete for %quest_link, what a grind!', 0, 0, '', '%quest_obj_name complété pour %quest_link, quelle corvée !', '%quest_obj_name für %quest_link erledigt, was für ein Grind!', '%quest_obj_name完成了%quest_link,真是累死人!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Getting close to completing %quest_obj_name for %quest_link, just need %quest_obj_missing more.', 0, 0, '', 'Je suis sur le point de terminer %quest_obj_name pour %quest_link, il me manque juste %quest_obj_missing.', 'Fast fertig mit %quest_obj_name für %quest_link, brauche nur noch %quest_obj_missing.', '快完成%quest_link的%quest_obj_name了,只差%quest_obj_missing个。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', '%quest_obj_name is almost done, need just %quest_obj_missing more for %quest_link.', 0, 0, '', 'Il ne manque presque rien à %quest_obj_name, j’ai juste besoin de %quest_obj_missing de plus pour %quest_link.', '%quest_obj_name fast geschafft, brauche nur noch %quest_obj_missing für %quest_link.', '%quest_obj_name快完成了,%quest_link还差%quest_obj_missing个。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'So far, %quest_obj_available/%quest_obj_required %quest_obj_name completed for %quest_link.', 0, 0, '', 'Jusqu’à présent, %quest_obj_available/%quest_obj_required %quest_obj_name ont été complétés pour %quest_link.', 'Bisher %quest_obj_available/%quest_obj_required %quest_obj_name für %quest_link erledigt.', '目前完成了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'Finally finished %quest_obj_name for %quest_link! That took a while!', 0, 0, '', 'J’ai enfin terminé %quest_obj_name pour %quest_link! Ça m’a pris un moment!', 'Endlich %quest_obj_name für %quest_link abgeschlossen! Das hat gedauert!', '终于完成了%quest_link的%quest_obj_name,花了好长时间!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Still going strong, got %quest_obj_available/%quest_obj_required of %quest_obj_name for %quest_link.', 0, 0, '', 'Toujours en pleine forme, j’ai réussi à obtenir %quest_obj_available/%quest_obj_required de %quest_obj_name pour %quest_link.', 'Läuft gut, %quest_obj_available/%quest_obj_required von %quest_obj_name für %quest_link bekommen.', '进展顺利,拿到了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'Yes! Completed %quest_obj_name for %quest_link. On to the next one!', 0, 0, '', 'Oui! J’ai fini %quest_obj_name pour %quest_link. Passons à la prochaine!', 'Ja! %quest_obj_name für %quest_link abgeschlossen. Weiter geht’s!', '好耶!完成了%quest_link的%quest_obj_name,下一目标!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Made progress on %quest_obj_name, %quest_obj_available/%quest_obj_required completed for %quest_link.', 0, 0, '', 'J’ai fait des progrès sur l’objectif de quête, j’ai terminé %quest_obj_available/%quest_obj_required pour %quest_link.', 'Fortschritt bei %quest_obj_name gemacht, %quest_obj_available/%quest_obj_required für %quest_link abgeschlossen.', '在%quest_obj_name上有了进展,完成了%quest_link的%quest_obj_available/%quest_obj_required。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', '%quest_obj_full_formatted... almost there for %quest_link!', 0, 0, '', '%quest_obj_full_formatted... presque arrivé pour %quest_link!', '%quest_obj_full_formatted... fast fertig mit %quest_link!', '%quest_obj_full_formatted…快搞定%quest_link了!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Just need %quest_obj_missing more %quest_obj_name for %quest_link, almost done!', 0, 0, '', 'J’ai juste besoin de quelques %quest_obj_missing supplémentaires de %quest_obj_name pour %quest_link, presque terminé !', 'Brauche nur noch %quest_obj_missing mehr von %quest_obj_name für %quest_link, fast geschafft!', '只差%quest_obj_missing个%quest_obj_name就能完成%quest_link,快好了!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'Finally done with %quest_obj_name for %quest_link, what a relief!', 0, 0, '', 'Enfin terminé avec %quest_obj_name pour %quest_link, quel soulagement !', 'Endlich fertig mit %quest_obj_name für %quest_link, was für eine Erleichterung!', '终于搞定了%quest_link的%quest_obj_name,真是松了一口气!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Got %quest_obj_available/%quest_obj_required of %quest_obj_name for %quest_link, making progress!', 0, 0, '', 'J’ai obtenu %quest_obj_available/%quest_obj_required de %quest_obj_name pour %quest_link, je progresse !', '%quest_obj_available/%quest_obj_required von %quest_obj_name für %quest_link bekommen, Fortschritt gemacht!', '拿到了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name,进展不错!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'The %quest_obj_name for %quest_link is complete! Now for the next task.', 0, 0, '', 'La %quest_obj_name pour %quest_link est terminée ! Maintenant, passons à la tâche suivante.', '%quest_obj_name für %quest_link ist erledigt! Jetzt zur nächsten Aufgabe.', '%quest_link的%quest_obj_name完成了!接下来是下一个目标。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', '100% of %quest_obj_name done for %quest_link, now onto the rewards!', 0, 0, '', 'J’ai terminé 100% de %quest_obj_name pour %quest_link, passons maintenant aux récompenses!', '100% von %quest_obj_name für %quest_link erledigt, jetzt zu den Belohnungen!', '完成了 %quest_obj_name 的 100% 进度,为 %quest_link,接下来就是领取奖励了!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Still need %quest_obj_missing more of %quest_obj_name for %quest_link, almost done!', 0, 0, '', 'J’ai encore besoin de %quest_obj_missing de %quest_obj_name pour %quest_link, presque terminé !', 'Brauche noch %quest_obj_missing mehr von %quest_obj_name für %quest_link, fast fertig!', '还差 %quest_obj_missing 个 %quest_obj_name 就能完成 %quest_link,快搞定了!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Working on %quest_obj_name for %quest_link, currently %quest_obj_available/%quest_obj_required done.', 0, 0, '', 'Je travaille sur la quête %quest_obj_name pour %quest_link, j’ai actuellement accompli %quest_obj_available sur %quest_obj_required étapes.', 'Arbeite an %quest_obj_name für %quest_link, bisher %quest_obj_available/%quest_obj_required erledigt.', '正在为 %quest_link 搞定 %quest_obj_name,目前完成了 %quest_obj_available/%quest_obj_required。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', '%quest_obj_full_formatted for %quest_link. That was a long one!', 0, 0, '', '%quest_obj_full_formatted pour %quest_link. Quelle aventure épique!', '%quest_obj_full_formatted für %quest_link. Das war eine lange Aufgabe!', '完成了 %quest_obj_full_formatted,为 %quest_link。这任务真够长的!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Just need a few more of %quest_obj_name for %quest_link, and I’m done!', 0, 0, '', 'J’ai juste besoin de quelques %quest_obj_name de plus pour %quest_link, et j’ai fini!', 'Brauche nur noch ein paar %quest_obj_name für %quest_link, dann bin ich fertig!', '就差几个 %quest_obj_name 就能完成 %quest_link,马上就搞定啦!', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'Another objective completed! %quest_obj_full_formatted for %quest_link', 0, 0, '', 'Encore un objectif accompli! %quest_obj_full_formatted pour %quest_link', 'Noch ein Ziel erreicht! %quest_obj_full_formatted für %quest_link', '又一个目标完成!%quest_obj_full_formatted,为 %quest_link。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_progress', 'Still grinding on %quest_obj_name for %quest_link, almost there with %quest_obj_available/%quest_obj_required.', 0, 0, '', 'Je continue de travailler sur %quest_obj_name pour %quest_link, j’y suis presque avec %quest_obj_available/%quest_obj_required.', 'Noch am Farmen für %quest_obj_name für %quest_link, fast fertig mit %quest_obj_available/%quest_obj_required.', '还在为 %quest_link 刷 %quest_obj_name,快到了,目前 %quest_obj_available/%quest_obj_required。', '', '', '', ''), -('broadcast_quest_update_add_kill_objective_completed', 'Objective done! %quest_obj_full_formatted for %quest_link. On to the next one!', 0, 0, '', 'Objectif accompli ! J’ai rempli l’objectif pour la quête « %quest_link ». Passons à la suivante !', 'Ziel erreicht! %quest_obj_full_formatted für %quest_link erledigt. Weiter geht’s!', '目标完成!%quest_obj_full_formatted,为 %quest_link。下一个!', '', '', '', ''), - - - --- usable placeholders: --- %quest_link --- %zone_name --- %area_name --- %quest_obj_available --- %quest_obj_required --- %quest_obj_missing --- %item_link --- %quest_obj_full_formatted --- %my_race --- %my_class --- %my_level -('broadcast_quest_update_add_item_objective_completed', 'Finally done with the %item_link for %quest_link', 0, 0, '', 'Enfin fini avec l’%item_link pour %quest_link.', 'Endlich fertig mit %item_link für %quest_link.', '终于搞定了 %item_link,为 %quest_link!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'finally got %quest_obj_available/%quest_obj_required of %item_link for the %quest_link', 0, 0, '', 'J’ai enfin obtenu %quest_obj_available/%quest_obj_required de %item_link pour la %quest_link.', 'Endlich %quest_obj_available/%quest_obj_required von %item_link für %quest_link bekommen.', '终于收集到 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', '%quest_obj_full_formatted for the %quest_link, at last', 0, 0, '', 'La quête est enfin terminée pour %quest_link.', '%quest_obj_full_formatted für %quest_link, endlich geschafft.', '终于完成了 %quest_obj_full_formatted,为 %quest_link!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Oof, got %quest_obj_available/%quest_obj_required %item_link for %quest_link', 0, 0, '', 'Ouf, j’ai obtenu %quest_obj_available sur %quest_obj_required %item_link pour la quête %quest_link.', 'Uff, %quest_obj_available/%quest_obj_required %item_link für %quest_link gesammelt.', '哎呀,搞到 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'still need %quest_obj_missing more of %item_link for %quest_link', 0, 0, '', 'Il me faut %quest_obj_missing encore plus de %item_link pour la quête %quest_link.', 'Brauche noch %quest_obj_missing mehr von %item_link für %quest_link.', '还差 %quest_obj_missing 个 %item_link 就能完成 %quest_link。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', '%quest_obj_full_formatted, still working on %quest_link', 0, 0, '', 'Désolé, je suis toujours en train de travailler sur %quest_link.', '%quest_obj_full_formatted erledigt, arbeite noch an %quest_link.', '完成了 %quest_obj_full_formatted,还在为 %quest_link 努力。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Completed the %item_link objective for %quest_link. Finally!', 0, 0, '', 'J’ai terminé l’objectif %item_link de la quête %quest_link. Enfin!', 'Das Ziel mit %item_link für %quest_link abgeschlossen. Endlich!', '完成了 %item_link 目标,为 %quest_link。终于搞定!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Making progress! %quest_obj_available/%quest_obj_required %item_link for %quest_link.', 0, 0, '', 'Je progresse! J’ai actuellement %quest_obj_available sur les %quest_obj_required %item_link requis pour la quête %quest_link.', 'Fortschritt gemacht! %quest_obj_available/%quest_obj_required %item_link für %quest_link.', '有进展!目前有 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', '%quest_obj_name is halfway done, need %quest_obj_missing more %item_link for %quest_link.', 0, 0, '', '%quest_obj_name est à moitié terminée, il me manque %quest_obj_missing %item_link pour %quest_link.', '%quest_obj_name ist zur Hälfte erledigt, brauche noch %quest_obj_missing %item_link für %quest_link.', '%quest_obj_name 完成了一半,还差 %quest_obj_missing 个 %item_link 为 %quest_link。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Still need %quest_obj_missing of %item_link for %quest_link, but getting closer!', 0, 0, '', 'Il me manque encore %quest_obj_missing de %item_link pour %quest_link, mais je me rapproche !', 'Brauche noch %quest_obj_missing von %item_link für %quest_link, aber ich komme näher!', '还差 %quest_obj_missing 个 %item_link 为 %quest_link,不过越来越近了!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Finally finished %item_link for %quest_link, now time for the next task!', 0, 0, '', 'J’ai enfin terminé %item_link pour la quête %quest_link, il est temps de passer à la prochaine tâche !', 'Endlich %item_link für %quest_link abgeschlossen, jetzt auf zur nächsten Aufgabe!', '终于完成了 %item_link 为 %quest_link,接下来是下一个任务!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Almost there! Got %quest_obj_available/%quest_obj_required %item_link for %quest_link.', 0, 0, '', 'On y est presque! J’ai obtenu %quest_obj_available/%quest_obj_required %item_link pour %quest_link.', 'Fast geschafft! Habe %quest_obj_available/%quest_obj_required %item_link für %quest_link.', '快搞定了!已经拿到 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Yes! Completed %item_link for %quest_link. Now moving on to the next objective.', 0, 0, '', 'Oui ! J’ai terminé %item_link pour %quest_link. Je passe maintenant à l’objectif suivant.', 'Ja! %item_link für %quest_link abgeschlossen. Weiter zum nächsten Ziel.', '好嘞!完成了 %item_link 为 %quest_link。下一个目标走起!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Still need %quest_obj_missing more %item_link for %quest_link, but progress is being made!', 0, 0, '', 'Il me manque encore %quest_obj_missing %item_link pour la quête %quest_link, mais je progresse!', 'Brauche noch %quest_obj_missing %item_link für %quest_link, aber es geht voran!', '还差 %quest_obj_missing 个 %item_link 为 %quest_link,不过已经在进步了!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Made good progress on %item_link for %quest_link, need %quest_obj_missing more.', 0, 0, '', 'J’ai bien avancé sur %item_link pour %quest_link, mais il me manque encore %quest_obj_missing.', 'Guten Fortschritt mit %item_link für %quest_link gemacht, brauche noch %quest_obj_missing.', '在 %item_link 上有不错进展,为 %quest_link,还差 %quest_obj_missing 个。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', '100% of %item_link done for %quest_link! Time to move forward.', 0, 0, '', 'J’ai complété 100% de %item_link pour %quest_link! Il est temps de passer à la suite.', '100% von %item_link für %quest_link erledigt! Zeit weiterzumachen.', '100% 完成 %item_link 为 %quest_link!该继续前进了。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Got %quest_obj_available/%quest_obj_required %item_link for %quest_link. I can feel the finish line!', 0, 0, '', 'J’ai obtenu %quest_obj_available/%quest_obj_required %item_link pour %quest_link. Je sens que je touche au but!', 'Habe %quest_obj_available/%quest_obj_required %item_link für %quest_link. Ziel fast erreicht!', '拿到 %quest_obj_available/%quest_obj_required 个 %item_link 为 %quest_link,终点就在眼前!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'That’s it, %item_link complete for %quest_link. What a relief!', 0, 0, '', 'C’est bon, %item_link complet pour %quest_link. Quel soulagement !', 'Geschafft, %item_link für %quest_link erledigt. Was für eine Erleichterung!', '搞定,完成了 %item_link 为 %quest_link。松了一口气!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Working hard on %item_link for %quest_link, still need %quest_obj_missing more.', 0, 0, '', 'Je travaille dur sur %item_link pour %quest_link, mais j’ai encore besoin de %quest_obj_missing de plus.', 'Arbeite hart an %item_link für %quest_link, brauche noch %quest_obj_missing mehr.', '正在努力搞 %item_link 为 %quest_link,还差 %quest_obj_missing 个。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Almost finished with %item_link for %quest_link, just %quest_obj_missing more left.', 0, 0, '', 'Presque terminé avec %item_link pour %quest_link, il ne reste plus que %quest_obj_missing.', 'Fast fertig mit %item_link für %quest_link, nur noch %quest_obj_missing übrig.', '快搞定 %item_link 为 %quest_link了,只剩 %quest_obj_missing 个。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Completed %item_link for %quest_link, the grind is real!', 0, 0, '', 'J’ai terminé %item_link pour %quest_link, la galère!', '%item_link für %quest_link abgeschlossen, das war ein harter Grind!', '完成了 %item_link 为 %quest_link,刷得真辛苦!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Still chipping away at %item_link for %quest_link, %quest_obj_available/%quest_obj_required done so far.', 0, 0, '', 'Je suis toujours en train de travailler sur %item_link pour %quest_link, j’ai accompli %quest_obj_available/%quest_obj_required jusqu’à présent.', 'Immer noch dabei, %item_link für %quest_link zu sammeln, bisher %quest_obj_available/%quest_obj_required erledigt.', '还在慢慢搞 %item_link 为 %quest_link,目前完成了 %quest_obj_available/%quest_obj_required。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', '%item_link for %quest_link is getting closer to completion, %quest_obj_available/%quest_obj_required done.', 0, 0, '', '%item_link pour la quête %quest_link approche de la fin, j’ai accompli %quest_obj_available/%quest_obj_required objectifs.', '%item_link für %quest_link fast fertig, %quest_obj_available/%quest_obj_required erledigt.', '%item_link 为 %quest_link 快完成了,目前完成 %quest_obj_available/%quest_obj_required。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Yes! %item_link finished for %quest_link! That one took a bit longer.', 0, 0, '', 'Oui! %item_link terminé pour %quest_link! Celui-là a pris un peu plus de temps.', 'Ja! %item_link für %quest_link abgeschlossen! Das hat etwas länger gedauert.', '好嘞!%item_link 为 %quest_link 完成!这个花了点时间。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'I have %quest_obj_available/%quest_obj_required of %item_link for %quest_link, still going strong!', 0, 0, '', 'J’ai %quest_obj_available/%quest_obj_required de %item_link pour %quest_link, et je continue sur ma lancée!', 'Habe %quest_obj_available/%quest_obj_required von %item_link für %quest_link, läuft weiter gut!', '我有 %quest_obj_available/%quest_obj_required 个 %item_link 为 %quest_link,继续努力!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Done with %item_link for %quest_link. On to the next one!', 0, 0, '', 'Terminé avec %item_link pour %quest_link. Passons à la suite !', 'Fertig mit %item_link für %quest_link. Weiter zur nächsten!', '搞定 %item_link 为 %quest_link,下一个任务走起!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_completed', 'Another one down! %item_link is complete for %quest_link!', 0, 0, '', 'Encore un de fait! %item_link est complet pour %quest_link!', 'Wieder eine erledigt! %item_link für %quest_link abgeschlossen!', '又搞定一个!%item_link 为 %quest_link 完成!', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Still grinding on %item_link for %quest_link, got %quest_obj_available/%quest_obj_required done.', 0, 0, '', 'Je continue à travailler sur %item_link pour la quête %quest_link, j’ai accompli %quest_obj_available/%quest_obj_required objectifs jusqu’à présent.', 'Noch am Farmen für %item_link für %quest_link, bisher %quest_obj_available/%quest_obj_required erledigt.', '还在刷 %item_link 为 %quest_link,目前完成 %quest_obj_available/%quest_obj_required。', '', '', '', ''), -('broadcast_quest_update_add_item_objective_progress', 'Getting there! Need %quest_obj_missing more %item_link for %quest_link.', 0, 0, '', 'On y est presque! Il me manque %quest_obj_missing %item_link pour %quest_link.', 'Fast geschafft! Noch %quest_obj_missing %item_link für %quest_link nötig.', '快到了!还差 %quest_obj_missing 个 %item_link 为 %quest_link。', '', '', '', ''), - - --- usable placeholders: --- %quest_link --- %zone_name --- %area_name --- %my_race --- %my_class --- %my_level -('broadcast_quest_update_failed_timer', 'Failed to finish %quest_link in time...', 0, 0, '', 'J’ai échoué à terminer %quest_link à temps...', 'Hab es nicht geschafft, %quest_link rechtzeitig abzuschließen...', '没能在时间内完成 %quest_link……', '', '', '', ''), -('broadcast_quest_update_failed_timer', 'Ran out of time for %quest_link :(', 0, 0, '', 'J’ai manqué de temps pour %quest_link :(', 'Keine Zeit mehr für %quest_link :(', '时间不够,没完成 %quest_link :(', '', '', '', ''), - - --- usable placeholders: --- %quest_link --- %zone_name --- %area_name --- %my_race --- %my_class --- %my_level -('broadcast_quest_update_complete', 'I have completed all objectives for %quest_link', 0, 0, '', 'J’ai terminé tous les objectifs pour %quest_link.', 'Alle Ziele für %quest_link abgeschlossen.', '我已经完成了 %quest_link 的所有目标!', '', '', '', ''), -('broadcast_quest_update_complete', 'Completed all objectives for %quest_link', 0, 0, '', 'J’ai rempli tous les objectifs pour %quest_link.', 'Alle Aufgaben für %quest_link erledigt.', '完成了 %quest_link 的所有目标!', '', '', '', ''), -('broadcast_quest_update_complete', 'Gonna turn in the %quest_link soon, just finished all objectives', 0, 0, '', 'Je vais bientôt remettre la %quest_link, je viens de finir tous les objectifs.', 'Werde %quest_link bald abgeben, hab gerade alle Ziele erledigt.', '马上要去交 %quest_link 了,刚刚完成所有目标!', '', '', '', ''), --- usable placeholders: --- %quest_link --- %zone_name --- %area_name --- %my_race --- %my_class --- %my_level -('broadcast_quest_turned_in', 'Yess, I have finally turned in the %quest_link', 0, 0, '', 'Oh ouiii, j’ai enfin remis la %quest_link', 'Ja, endlich %quest_link abgegeben!', '耶,终于完成了%quest_link任务,交掉了!', '', '', '', ''), -('broadcast_quest_turned_in', 'turned in the %quest_link', 0, 0, '', 'J’ai soumis %quest_link.', '%quest_link abgegeben.', '交掉了%quest_link任务!', '', '', '', ''), -('broadcast_quest_turned_in', 'managed to finish %quest_link, just turned in', 0, 0, '', 'J’ai réussi à terminer %quest_link, je viens tout juste de le remettre.', '%quest_link abgeschlossen und gerade abgegeben.', '终于搞定%quest_link任务,刚交了!', '', '', '', ''), -('broadcast_quest_turned_in', 'just turned in %quest_link', 0, 0, '', 'Je viens de remettre %quest_link.', 'Gerade %quest_link abgegeben.', '刚交了%quest_link任务!', '', '', '', ''), -('broadcast_quest_turned_in', 'just turned in %quest_link in %zone_name', 0, 0, '', 'Je viens de terminer la quête %quest_link dans la zone %zone_name.', 'Gerade %quest_link in %zone_name abgeschlossen.', '刚在%zone_name交了%quest_link任务!', '', '', '', ''), -('broadcast_quest_turned_in', 'Another quest completed! %quest_link turned in', 0, 0, '', 'Encore une quête terminée! %quest_link remise', 'Noch eine Quest erledigt! %quest_link abgegeben.', '又完成一个任务!%quest_link交掉了!', '', '', '', ''), -('broadcast_quest_turned_in', 'Mission accomplished! %quest_link is turned in', 0, 0, '', 'Mission accomplie ! J’ai remis %quest_link.', 'Mission erfüllt! %quest_link abgegeben.', '任务完成!%quest_link交掉了!', '', '', '', ''), -('broadcast_quest_turned_in', 'Finally, %quest_link turned in! That was a journey!', 0, 0, '', 'Enfin, j’ai remis la %quest_link ! Quelle aventure !', 'Endlich %quest_link abgegeben! Was für eine Reise!', '终于交了%quest_link任务!真是一场冒险!', '', '', '', ''), -('broadcast_quest_turned_in', 'Just completed %quest_link and turned it in, feeling good!', 0, 0, '', 'Je viens de terminer %quest_link et de le rendre, je me sens bien !', 'Gerade %quest_link abgeschlossen und abgegeben, fühlt sich gut an!', '刚完成%quest_link任务并交了,感觉真不错!', '', '', '', ''), -('broadcast_quest_turned_in', 'Turned in %quest_link in %zone_name. Now, on to the next challenge!', 0, 0, '', 'J’ai remis la %quest_link dans %zone_name. Maintenant, je passe au défi suivant !', '%quest_link in %zone_name abgegeben. Jetzt weiter zur nächsten Herausforderung!', '在%zone_name交了%quest_link任务,接下来迎接新的挑战!', '', '', '', ''), -('broadcast_quest_turned_in', 'I have turned in %quest_link. One more down!', 0, 0, '', 'J’ai remis %quest_link. Un de moins à faire !', '%quest_link abgegeben. Wieder eine weniger!', '我交了%quest_link任务,又搞定一个!', '', '', '', ''), -('broadcast_quest_turned_in', 'Successfully turned in %quest_link, that took a while!', 0, 0, '', 'J’ai enfin réussi à remettre la %quest_link, ça m’a pris du temps !', '%quest_link endlich abgegeben, das hat gedauert!', '成功交了%quest_link任务,耗了不少时间!', '', '', '', ''), -('broadcast_quest_turned_in', 'Completed and turned in %quest_link in %zone_name. Feels great!', 0, 0, '', 'J’ai terminé et remis la %quest_link dans la zone %zone_name. Je me sens génial!', '%quest_link in %zone_name abgeschlossen und abgegeben. Fühlt sich super an!', '在%zone_name完成了%quest_link任务并交了,感觉棒极了!', '', '', '', ''), -('broadcast_quest_turned_in', 'Quest complete! %quest_link turned in and ready for the next one!', 0, 0, '', 'Quête terminée! J’ai remis %quest_link et je suis prêt pour la prochaine!', 'Quest abgeschlossen! %quest_link abgegeben und bereit für die nächste!', '任务完成!%quest_link交掉了,准备接下一个!', '', '', '', ''), -('broadcast_quest_turned_in', 'Finally turned in %quest_link in %zone_name. That one was tough!', 0, 0, '', 'Je viens enfin de rendre la %quest_link dans la zone de %zone_name. Celle-là était vraiment coriace !', 'Endlich %quest_link in %zone_name abgegeben. Die war echt hart!', '终于在%zone_name交了%quest_link任务,这任务真难!', '', '', '', ''), -('broadcast_quest_turned_in', 'That’s it, %quest_link turned in! Now to collect the reward!', 0, 0, '', 'C’est bon, la %quest_link est terminée ! Maintenant, je vais chercher ma récompense !', 'Geschafft, %quest_link abgegeben! Jetzt die Belohnung abholen!', '搞定了,%quest_link交掉了!现在去领赏!', '', '', '', ''), -('broadcast_quest_turned_in', 'Finished %quest_link! Now time to turn it in and move on.', 0, 0, '', 'J’ai fini la %quest_link ! Maintenant, il est temps de la remettre et de passer à la suite.', '%quest_link abgeschlossen! Jetzt abgeben und weiter geht’s.', '完成了%quest_link任务!现在交掉,继续前行。', '', '', '', ''), -('broadcast_quest_turned_in', 'Just turned in %quest_link, feeling accomplished!', 0, 0, '', 'Je viens juste de terminer %quest_link, je me sens tellement accompli !', 'Gerade %quest_link abgegeben, fühlt sich gut an!', '刚交了%quest_link任务,感觉很有成就感!', '', '', '', ''), -('broadcast_quest_turned_in', 'I’ve turned in %quest_link. Let’s see what’s next!', 0, 0, '', 'J’ai remis %quest_link. Voyons ce qui nous attend ensuite!', 'Hab %quest_link abgegeben. Mal sehen, was als Nächstes kommt!', '我交了%quest_link任务,看看接下来有什么!', '', '', '', ''), -('broadcast_quest_turned_in', 'Just wrapped up %quest_link and turned it in. On to the next one!', 0, 0, '', 'Je viens de terminer %quest_link et de la rendre. En route pour la suite !', '%quest_link abgeschlossen und abgegeben. Weiter geht’s!', '刚搞定%quest_link任务并交了,继续下一个!', '', '', '', ''), -('broadcast_quest_turned_in', 'Another one in the books, %quest_link turned in', 0, 0, '', 'Encore une de faite, %quest_link remise.', 'Wieder eine erledigt, %quest_link abgegeben.', '又记上一笔,%quest_link任务交掉了!', '', '', '', ''), -('broadcast_quest_turned_in', 'Turned in %quest_link! Time to collect my rewards.', 0, 0, '', 'J’ai rendu la %quest_link! Il est temps de récupérer mes récompenses.', '%quest_link abgegeben! Zeit, die Belohnung einzusacken.', '交了%quest_link任务!该去领奖励了。', '', '', '', ''), -('broadcast_quest_turned_in', 'Completed and turned in %quest_link, what a relief!', 0, 0, '', 'J’ai terminé et remis %quest_link, quel soulagement !', '%quest_link abgeschlossen und abgegeben, was für eine Erleichterung!', '完成了%quest_link任务并交了,真是松了一口气!', '', '', '', ''), -('broadcast_quest_turned_in', 'That was quick! %quest_link turned in already!', 0, 0, '', 'Déjà?! %quest_link a été rendu si rapidement!', 'Das ging schnell! %quest_link schon abgegeben!', '这么快就搞定了!%quest_link任务已经交了!', '', '', '', ''), -('broadcast_quest_turned_in', 'Just turned in %quest_link in %zone_name. Ready for the next adventure!', 0, 0, '', 'Je viens de rendre %quest_link dans %zone_name. Je suis prêt pour la prochaine aventure !', 'Gerade %quest_link in %zone_name abgegeben. Bereit für das nächste Abenteuer!', '刚在%zone_name交了%quest_link任务,准备好迎接下一次冒险了!', '', '', '', ''), -('broadcast_quest_turned_in', 'Successfully turned in %quest_link. Feels like progress!', 0, 0, '', 'J’ai réussi à rendre %quest_link. On dirait que j’avance!', '%quest_link erfolgreich abgegeben. Fühlt sich nach Fortschritt an!', '成功交了%quest_link任务,感觉自己在进步!', '', '', '', ''), -('broadcast_quest_turned_in', 'Mission complete! %quest_link is turned in!', 0, 0, '', 'Mission accomplie! %quest_link a été rendue!', 'Mission erfüllt! %quest_link abgegeben!', '任务完成!%quest_link交掉了!', '', '', '', ''), -('broadcast_quest_turned_in', 'I just finished %quest_link and turned it in, on to the next one!', 0, 0, '', 'Je viens de terminer %quest_link et de le remettre, passons à la suite!', 'Gerade %quest_link abgeschlossen und abgegeben, weiter zur nächsten!', '刚完成%quest_link任务并交了,接下来继续!', '', '', '', ''), - - - --- kill mob events --- usable placeholders: --- %victim_name --- %zone_name --- %area_name --- %victim_level --- %my_race --- %my_class --- %my_level -('broadcast_killed_normal', 'another %victim_name down', 0, 0, '', 'Encore une %victim_name tombée.', 'Noch ein %victim_name erledigt.', '又干掉了一个%victim_name!', '', '', '', ''), -('broadcast_killed_normal', 'I keep killing %victim_name, nothing to talk about', 0, 0, '', 'Je continue de tuer %victim_name, il n’y a rien à dire à ce sujet.', 'Ich töte weiter %victim_name, nichts Besonderes.', '老是杀%victim_name,没啥好说的。', '', '', '', ''), -('broadcast_killed_normal', 'another %victim_name bites the dust', 0, 0, '', 'Encore une fois, %victim_name mord la poussière.', 'Noch ein %victim_name beißt ins Gras.', '又一个%victim_name倒下了!', '', '', '', ''), -('broadcast_killed_normal', 'one less %victim_name in %zone_name', 0, 0, '', 'Une victime en moins dans la zone.', 'Ein %victim_name weniger in %zone_name.', '%zone_name少了一个%victim_name!', '', '', '', ''), -('broadcast_killed_elite', 'Took down this elite bastard %victim_name!', 0, 0, '', 'J’ai descendu cet enfoiré d’élite %victim_name !', 'Diesen Elite-Bastard %victim_name erledigt!', '干掉了这个精英混蛋%victim_name!', '', '', '', ''), -('broadcast_killed_player', 'killed elite %victim_name in %zone_name', 0, 0, '', 'J’ai tué l’élite %victim_name dans la zone %zone_name', 'Elite %victim_name in %zone_name erledigt.', '在%zone_name干掉了精英%victim_name!', '', '', '', ''), -('broadcast_killed_rareelite', 'Fooof, managed to take down %victim_name!', 0, 0, '', 'Oh là là, j’ai réussi à abattre %victim_name !', 'Puh, hab %victim_name endlich erledigt!', '呼,终于干掉了%victim_name!', '', '', '', ''), -('broadcast_killed_worldboss', 'That was sick! Just killed %victim_name! Can tell tales now', 0, 0, '', 'C’était fou! Je viens juste de tuer %victim_name! Je pourrais raconter des histoires maintenant.', 'Das war heftig! Gerade %victim_name getötet! Jetzt gibt’s was zu erzählen.', '太爽了!刚干掉%victim_name!这下有故事可以吹了!', '', '', '', ''), -('broadcast_killed_rare', 'Yoo, I just killed %victim_name!', 0, 0, '', 'Eh, je viens juste de tuer %victim_name!', 'Yo, hab gerade %victim_name erledigt!', '嘿,刚干掉了%victim_name!', '', '', '', ''), -('broadcast_killed_player', 'killed rare %victim_name in %zone_name', 0, 0, '', 'J’ai tué un rare %victim_name dans %zone_name', 'Seltenen %victim_name in %zone_name erledigt.', '在%zone_name干掉了稀有怪%victim_name!', '', '', '', ''), -('broadcast_killed_unknown', 'WTF did I just kill? %victim_name', 0, 0, '', 'Qu’est-ce que je viens de tuer ? %victim_name', 'WTF hab ich da gerade getötet? %victim_name', '我刚杀了啥?%victim_name?', '', '', '', ''), -('broadcast_killed_pet', 'Just killed that pet %victim_name', 0, 0, '', 'Je viens de tuer l’animal de compagnie, %victim_name.', 'Hab gerade das Pet %victim_name erledigt.', '刚干掉了那个宠物%victim_name。', '', '', '', ''), -('broadcast_killed_player', 'Oh yeah, I just killed %victim_name', 0, 0, '', 'Oh ouais, je viens juste de tuer %victim_name', 'Oh yeah, gerade %victim_name erledigt!', '哦耶,刚干掉了%victim_name!', '', '', '', ''), -('broadcast_killed_player', 'killed %victim_name in %zone_name', 0, 0, '', 'J’ai tué %victim_name dans %zone_name.', '%victim_name in %zone_name getötet.', '在%zone_name干掉了%victim_name!', '', '', '', ''), -('broadcast_killed_normal', 'another %victim_name down', 0, 0, '', 'Encore une %victim_name de moins.', 'Noch ein %victim_name erledigt.', '又干掉了一个%victim_name!', '', '', '', ''), -('broadcast_killed_normal', 'I keep killing %victim_name, nothing to talk about', 0, 0, '', 'Je continue de tuer %victim_name, rien à dire à ce sujet.', 'Ich töte weiter %victim_name, nichts Besonderes.', '一直在杀%victim_name,没啥好聊的。', '', '', '', ''), -('broadcast_killed_normal', 'another %victim_name bites the dust', 0, 0, '', 'Encore une victime %victim_name qui mord la poussière.', 'Noch ein %victim_name beißt ins Gras.', '又一个%victim_name倒下了!', '', '', '', ''), -('broadcast_killed_normal', 'one less %victim_name in %zone_name', 0, 0, '', 'Il y a une victime %victim_name de moins dans la zone.', 'Ein %victim_name weniger in %zone_name.', '%zone_name少了一个%victim_name!', '', '', '', ''), -('broadcast_killed_normal', 'Just took down %victim_name like it’s nothing', 0, 0, '', 'Je viens de neutraliser %victim_name comme si c’était rien.', 'Hab %victim_name erledigt, als wär’s nichts.', '刚干掉%victim_name,轻松得跟玩儿似的!', '', '', '', ''), -('broadcast_killed_normal', '%victim_name didn’t stand a chance', 0, 0, '', '%victim_name n’avait aucune chance.', '%victim_name hatte keine Chance.', '%victim_name完全不是对手!', '', '', '', ''), -('broadcast_killed_normal', 'One more %victim_name out of the way', 0, 0, '', 'Encore une victime %victim_name de moins sur notre chemin', 'Noch ein %victim_name aus dem Weg geräumt.', '又一个%victim_name被我清理掉了!', '', '', '', ''), -('broadcast_killed_normal', '%victim_name was too easy, on to the next one', 0, 0, '', '%victim_name était trop facile, passons au suivant.', '%victim_name war zu einfach, weiter geht’s.', '%victim_name太简单了,下一只!', '', '', '', ''), -('broadcast_killed_normal', 'Another %victim_name defeated. Easy peasy', 0, 0, '', 'Encore une autre victime vaincue %victim_name. C’était du gâteau.', 'Noch ein %victim_name besiegt. Kinderleicht.', '又干掉了一个%victim_name,小菜一碟!', '', '', '', ''), -('broadcast_killed_normal', '%victim_name wasn’t tough at all', 0, 0, '', '%victim_name n’était vraiment pas du tout résistant.', '%victim_name war überhaupt nicht schwer.', '%victim_name一点都不难对付!', '', '', '', ''), -('broadcast_killed_normal', 'Down goes %victim_name, nothing special', 0, 0, '', '%victim_name tombe, rien de spécial', '%victim_name erledigt, nichts Besonderes.', '%victim_name倒下了,没啥特别的。', '', '', '', ''), -('broadcast_killed_normal', 'That was too easy, %victim_name down', 0, 0, '', 'C’était trop facile, %victim_name est hors jeu.', 'Das war zu einfach, %victim_name erledigt.', '太简单了,%victim_name倒下了!', '', '', '', ''), -('broadcast_killed_normal', '%victim_name dropped so fast, blink and it’s over', 0, 0, '', '%victim_name a chuté si vite, cligne des yeux et c’est déjà terminé.', '%victim_name fiel so schnell, ein Blinzeln und vorbei.', '%victim_name 倒得太快了,眨眼就完事!', '', '', '', ''), -('broadcast_killed_elite', 'Took down this elite %victim_name!', 0, 0, '', 'J’ai vaincu cet élite %victim_name !', 'Elite %victim_name besiegt!', '干掉了精英 %victim_name!', '', '', '', ''), -('broadcast_killed_elite', 'Elite %victim_name is no more', 0, 0, '', 'Elite %victim_name n’est plus.', 'Elite %victim_name gibt’s nicht mehr.', '精英 %victim_name 已被消灭!', '', '', '', ''), -('broadcast_killed_elite', 'Took down the elite %victim_name like a champ', 0, 0, '', 'J’ai descendu l’élite %victim_name comme un champion.', 'Hab die Elite %victim_name locker besiegt.', '像个冠军一样干掉了精英 %victim_name!', '', '', '', ''), -('broadcast_killed_elite', 'Elite monster %victim_name, no match for me!', 0, 0, '', 'Monstre d’élite %victim_name, pas de taille pour moi !', 'Elite-Monster %victim_name, keine Konkurrenz für mich!', '精英怪 %victim_name,根本不是我的对手!', '', '', '', ''), -('broadcast_killed_elite', 'Finally took down the mighty %victim_name', 0, 0, '', 'J’ai enfin vaincu le redoutable %victim_name.', 'Endlich den mächtigen %victim_name besiegt.', '终于干掉了强大的 %victim_name!', '', '', '', ''), -('broadcast_killed_player', 'killed elite %victim_name in %zone_name', 0, 0, '', 'J’ai tué l’élite %victim_name dans la zone %zone_name.', 'Elite %victim_name in %zone_name besiegt.', '在 %zone_name 干掉了精英 %victim_name!', '', '', '', ''), -('broadcast_killed_player', 'Just got the elite %victim_name in %zone_name', 0, 0, '', 'Je viens tout juste d’obtenir l’élite %victim_name dans la région de %zone_name.', 'Gerade die Elite %victim_name in %zone_name besiegt.', '刚在 %zone_name 搞定了精英 %victim_name!', '', '', '', ''), -('broadcast_killed_player', 'Killed the elite %victim_name. Too easy', 0, 0, '', 'J’ai tué l’élite %victim_name. Trop facile.', 'Die Elite %victim_name erledigt. Viel zu einfach.', '干掉了精英 %victim_name,太简单了!', '', '', '', ''), -('broadcast_killed_player', 'Took down %victim_name in %zone_name, what a fight', 0, 0, '', 'J’ai abattu %victim_name dans %zone_name, quel combat!', '%victim_name in %zone_name erledigt, was für ein Kampf!', '在 %zone_name 干掉了 %victim_name,真是一场激战!', '', '', '', ''), -('broadcast_killed_player', 'Killed elite %victim_name. Guess I’m unstoppable', 0, 0, '', 'J’ai tué l’élite %victim_name. Je suis imbattable, je crois.', 'Elite %victim_name besiegt. Unaufhaltbar!', '干掉了精英 %victim_name,我简直无人能挡!', '', '', '', ''), -('broadcast_killed_rareelite', 'Fooof, managed to take down %victim_name!', 0, 0, '', 'Whouaa, j’ai réussi à éliminer %victim_name !', 'Fooof, es ist gelungen, %victim_name auszuschalten!', '呼,成功干掉了 %victim_name!', '', '', '', ''), -('broadcast_killed_rareelite', 'Rare elite %victim_name has fallen', 0, 0, '', 'Un rare élite nommé %victim_name est tombé', 'Seltener Elite %victim_name ist gefallen.', '稀有精英 %victim_name 倒下了!', '', '', '', ''), -('broadcast_killed_rareelite', '%victim_name is no more. Another rare elite slain', 0, 0, '', '%victim_name n’est plus. Encore un rare élite abattu.', '%victim_name ist Geschichte. Ein weiterer seltener Elite besiegt.', '%victim_name 完了,又一个稀有精英被我干掉!', '', '', '', ''), -('broadcast_killed_rareelite', 'Rare elite %victim_name down. I’m on fire!', 0, 0, '', 'Rare elite %victim_name abattu. Je suis en feu!', 'Seltener Elite %victim_name erledigt. Läuft bei mir!', '稀有精英 %victim_name 被我干掉了,我现在状态火热!', '', '', '', ''), -('broadcast_killed_rareelite', 'I just killed the rare elite %victim_name', 0, 0, '', 'Je viens de vaincre l’élite rare %%victim_name.', 'Gerade den seltenen Elite %victim_name erledigt.', '我刚干掉了稀有精英 %victim_name!', '', '', '', ''), -('broadcast_killed_worldboss', 'That was awesome! Just killed %victim_name! What a battle', 0, 0, '', 'C’était incroyable! Je viens juste de tuer %victim_name! Quelle bataille!', 'Das war der Hammer! Gerade %victim_name besiegt! Was für ein Kampf.', '太棒了!刚干掉了 %victim_name!真是场精彩的战斗!', '', '', '', ''), -('broadcast_killed_worldboss', 'World boss %victim_name is down! That was epic', 0, 0, '', 'Le boss mondial %victim_name est tombé ! C’était épique.', 'Weltboss %victim_name ist gefallen! Das war episch.', '世界首领 %victim_name 倒下了!这场战斗太史诗了!', '', '', '', ''), -('broadcast_killed_worldboss', 'I’ve slain the world boss %victim_name. Let’s celebrate!', 0, 0, '', 'J’ai abattu le boss mondial %victim_name. On va fêter ça !', 'Weltboss %victim_name erledigt! Das muss gefeiert werden!', '我干掉了世界首领 %victim_name,来庆祝一下吧!', '', '', '', ''), -('broadcast_killed_worldboss', 'Killed %victim_name! That world boss was no joke!', 0, 0, '', 'J’ai tué %victim_name! Ce world boss était vraiment costaud!', '%victim_name besiegt! Dieser Weltboss hatte es echt in sich!', '干掉了 %victim_name!这个世界首领可不是开玩笑的!', '', '', '', ''), -('broadcast_killed_worldboss', 'Just killed %victim_name, that was legendary', 0, 0, '', 'Je viens juste de tuer %victim_name, c’était légendaire!', 'Gerade %victim_name erledigt, das war legendär!', '刚干掉了 %victim_name,简直是传奇一战!', '', '', '', ''), -('broadcast_killed_rare', 'Yoo, I just killed %victim_name!', 0, 0, '', 'Eh, je viens juste de tuer %victim_name!', 'Yo, hab gerade %victim_name erledigt!', '嘿,我刚干掉了 %victim_name!', '', '', '', ''), -('broadcast_killed_rare', 'Rare %victim_name down. That was a fun fight', 0, 0, '', 'Rare %victim_name down. C’était un combat amusant.', 'Seltener %victim_name besiegt. Das hat Spaß gemacht!', '稀有怪 %victim_name 倒下了,这场战斗真有趣!', '', '', '', ''), -('broadcast_killed_rare', 'I just killed the rare %victim_name', 0, 0, '', 'Je viens de tuer le rare %victim_name.', 'Gerade den seltenen %victim_name erledigt.', '我刚干掉了稀有怪 %victim_name!', '', '', '', ''), -('broadcast_killed_rare', 'Rare %victim_name defeated in %zone_name', 0, 0, '', 'Rare %victim_name vaincu dans %zone_name', 'Seltener %victim_name in %zone_name besiegt.', '在 %zone_name 击败了稀有怪 %victim_name!', '', '', '', ''), -('broadcast_killed_rare', 'Just killed rare %victim_name in %zone_name. That was quick!', 0, 0, '', 'Je viens de tuer le rare %victim_name dans %zone_name. Ça a été rapide!', 'Seltenen %victim_name in %zone_name erledigt. Das ging fix!', '刚在 %zone_name 干掉了稀有怪 %victim_name,速度真快!', '', '', '', ''), -('broadcast_killed_player', 'killed rare %victim_name in %zone_name', 0, 0, '', 'J’ai tué un %victim_name rare dans la zone de %zone_name.', 'Seltenen %victim_name in %zone_name besiegt.', '在 %zone_name 干掉了稀有怪 %victim_name!', '', '', '', ''), -('broadcast_killed_player', 'Got the rare %victim_name in %zone_name', 0, 0, '', 'J’ai trouvé le rare %victim_name dans %zone_name', 'Den seltenen %victim_name in %zone_name erledigt.', '在 %zone_name 搞定了稀有怪 %victim_name!', '', '', '', ''), -('broadcast_killed_player', 'I killed %victim_name in %zone_name. That felt good', 0, 0, '', 'J’ai tué %victim_name dans %zone_name. Ça m’a fait du bien.', 'Hab %victim_name in %zone_name besiegt. Das hat gut getan.', '在 %zone_name 干掉了 %victim_name,感觉真爽!', '', '', '', ''), -('broadcast_killed_player', 'Took down %victim_name in %zone_name. They didn’t stand a chance', 0, 0, '', 'J’ai vaincu %victim_name dans %zone_name. Ils n’avaient aucune chance.', '%victim_name in %zone_name besiegt. Hatte keine Chance.', '在 %zone_name 干掉了 %victim_name,他们毫无还手之力!', '', '', '', ''), -('broadcast_killed_player', 'Just killed %victim_name in %zone_name, easy', 0, 0, '', 'Je viens de tuer %victim_name à %zone_name, c’était facile', 'Gerade %victim_name in %zone_name erledigt, war easy.', '刚在 %zone_name 干掉了 %victim_name,太轻松了!', '', '', '', ''), -('broadcast_killed_unknown', 'What did I just kill? %victim_name', 0, 0, '', 'Qui est-ce que je viens de tuer? %victim_name', 'Was hab ich da gerade getötet? %victim_name', '我刚杀了啥?%victim_name?', '', '', '', ''), -('broadcast_killed_unknown', 'What kind of %victim_name is that? Never seen anything like it', 0, 0, '', 'Quel genre de %victim_name est-ce ça ? Je n’ai jamais rien vu de tel auparavant.', 'Was für ein %victim_name ist das? Noch nie sowas gesehen.', '这 %victim_name 是啥玩意儿?从没见过这样的!', '', '', '', ''), -('broadcast_killed_unknown', 'Just killed something strange, %victim_name', 0, 0, '', 'Je viens de tuer quelque chose d’étrange, %victim_name', 'Hab gerade was Seltsames getötet, %victim_name.', '刚干掉了个奇怪的东西,%victim_name!', '', '', '', ''), -('broadcast_killed_unknown', '%victim_name was weird, but down they go', 0, 0, '', '%victim_name était bizarre, mais au sol ils vont', '%victim_name war seltsam, aber erledigt ist erledigt.', '%victim_name 怪怪的,不过还是被我干掉了!', '', '', '', ''), -('broadcast_killed_pet', 'Just defeated %victim_name', 0, 0, '', 'Je viens juste de vaincre %victim_name.', 'Gerade %victim_name besiegt.', '刚击败了 %victim_name!', '', '', '', ''), -('broadcast_killed_pet', 'Goodbye, %victim_name. You were just a pet', 0, 0, '', 'Au revoir, %victim_name. Tu étais qu’un animal de compagnie.', 'Tschüss, %victim_name. Warst halt nur ein Pet.', '再见了,%victim_name,你不过是个宠物而已。', '', '', '', ''), -('broadcast_killed_pet', 'Just took down the pet %victim_name. That was unexpected', 0, 0, '', 'Je viens de m’occuper de l’animal de compagnie %victim_name. Ça c’était inattendu.', 'Gerade das Pet %victim_name erledigt. Das kam unerwartet.', '刚干掉了宠物 %victim_name,真没想到!', '', '', '', ''), -('broadcast_killed_pet', 'Down goes %victim_name. Better luck next time, pet', 0, 0, '', '%victim_name mord la poussière. Meilleure chance la prochaine fois, mon pote.', '%victim_name erledigt. Nächstes Mal mehr Glück, Pet.', '%victim_name 倒下了,下次好运吧,小宠物!', '', '', '', ''), - --- levelup events --- usable placeholders: --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('broadcast_levelup_generic', '%my_level Ding!', 0, 0, '', '%my_level Ding !', '%my_level Ding!', '%my_level 级,升级啦!', '', '', '', ''), -('broadcast_levelup_generic', 'Yess, I am level %my_level!', 0, 0, '', 'Oui, je suis au niveau %my_level !', 'Ja, ich bin Level %my_level!', '耶,我到 %my_level 级了!', '', '', '', ''), -('broadcast_levelup_generic', 'I just leveled up', 0, 0, '', 'Je viens de monter de niveau', 'Hab gerade ein Level-Up bekommen!', '我刚升级了!', '', '', '', ''), -('broadcast_levelup_10x', 'I am level %my_level!!!', 0, 0, '', 'Je suis au niveau %my_level !!!', 'Ich bin Level %my_level!!!', '我到 %my_level 级了!!!', '', '', '', ''), -('broadcast_levelup_10x', 'getting stronger, already level %my_level!!!', 0, 0, '', 'Je deviens de plus en plus fort, je suis déjà au niveau %my_level !!!', 'Werde stärker, schon Level %my_level!!!', '越来越强了,已经到 %my_level 级!!!', '', '', '', ''), -('broadcast_levelup_10x', 'Just reached level %my_level!!!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level !!!', 'Gerade Level %my_level erreicht!!!', '刚达到 %my_level 级!!!', '', '', '', ''), -('broadcast_levelup_max_level', 'OMG, finally level %my_level!!!', 0, 0, '', 'OMG, enfin le niveau %my_level!!!', 'OMG, endlich Level %my_level!!!', '天啊,终于到 %my_level 级了!!!', '', '', '', ''), -('broadcast_levelup_max_level', '%my_level!!! can do endgame content now', 0, 0, '', '%my_level!!! je peux me permettre de faire du contenu de fin de jeu maintenant.', '%my_level!!! Endgame, ich komme!', '%my_level 级!!!现在可以挑战终极内容了!', '', '', '', ''), -('broadcast_levelup_max_level', 'fresh new level %my_level %my_class!!!', 0, 0, '', 'Nouveau niveau %my_level %my_class!!!', 'Frisch auf Level %my_level als %my_class!!!', '全新等级 %my_level 的 %my_class!!!', '', '', '', ''), -('broadcast_levelup_max_level', 'one more level %my_level %my_race %my_class!', 0, 0, '', 'Encore un niveau de plus %my_level pour ma race %my_race et ma classe %my_class!', 'Noch ein Level %my_level für meinen %my_race %my_class!', '又升了一级,%my_level 级的 %my_race %my_class!', '', '', '', ''), -('broadcast_levelup_generic', 'Ding! Another level up!', 0, 0, '', 'Ding! Encore un niveau de gagné!', 'Ding! Wieder ein Level-Up!', '叮!又升了一级!', '', '', '', ''), -('broadcast_levelup_generic', 'I’m officially level %my_level now!', 0, 0, '', 'Je suis maintenant officiellement au niveau %my_level !', 'Ich bin jetzt offiziell Level %my_level!', '我现在正式达到 %my_level 级了!', '', '', '', ''), -('broadcast_levelup_generic', 'Level %my_level, feeling stronger already!', 0, 0, '', 'Niveau %my_level, je me sens déjà plus fort!', 'Level %my_level, fühle mich jetzt schon stärker!', '%my_level 级,感觉已经更强了!', '', '', '', ''), -('broadcast_levelup_generic', 'Up I go! Level %my_level reached', 0, 0, '', 'Je monte ! Niveau %my_level atteint', 'Auf geht’s! Level %my_level erreicht.', '我升级了!达到 %my_level 级!', '', '', '', ''), -('broadcast_levelup_generic', 'That was quick, level %my_level already!', 0, 0, '', 'Déjà niveau %my_level ! Ça se passe bien !', 'Das ging schnell, schon Level %my_level!', '这么快,已经到 %my_level 级了!', '', '', '', ''), -('broadcast_levelup_generic', 'Just hit level %my_level, let’s do this!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level, allons-y !', 'Gerade Level %my_level erreicht, los geht’s!', '刚达到 %my_level 级,来吧,继续干!', '', '', '', ''), -('broadcast_levelup_generic', 'Level %my_level, on to bigger things!', 0, 0, '', 'Niveau %my_level, passons à des choses plus grandes !', 'Level %my_level, jetzt kommt das große Zeug!', '%my_level 级,接下来是更大的挑战!', '', '', '', ''), -('broadcast_levelup_generic', 'Level %my_level, I’m on fire now!', 0, 0, '', 'Niveau %my_level, je suis en feu maintenant !', 'Level %my_level, jetzt geht’s ab!', '%my_level 级,现在我状态火热!', '', '', '', ''), -('broadcast_levelup_generic', 'Level %my_level complete, onward!', 0, 0, '', 'Niveau %my_level terminé, en avant !', 'Level %my_level abgeschlossen, weiter geht’s!', '%my_level 级完成,继续前进!', '', '', '', ''), -('broadcast_levelup_generic', 'I’m now level %my_level, what’s next?', 0, 0, '', 'Je suis maintenant niveau %my_level, qu’est-ce qui suit?', 'Jetzt bin ich Level %my_level, was kommt als Nächstes?', '我现在是 %my_level 级,接下来干啥?', '', '', '', ''), -('broadcast_levelup_10x', 'Level %my_level!!! It’s happening!', 0, 0, '', 'Niveau %my_level!!! C’est en train de se passer!', 'Level %my_level!!! Es passiert wirklich!', '%my_level 级!!!这就发生了!', '', '', '', ''), -('broadcast_levelup_10x', 'Just hit level %my_level, this feels amazing!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level, c’est incroyable !', 'Gerade Level %my_level erreicht, fühlt sich großartig an!', '刚达到 %my_level 级,感觉太棒了!', '', '', '', ''), -('broadcast_levelup_10x', 'Level %my_level and I’m only getting started!', 0, 0, '', 'Niveau %my_level et je ne fais que commencer!', 'Level %my_level und ich fange gerade erst an!', '%my_level 级,我才刚开始呢!', '', '', '', ''), -('broadcast_levelup_10x', 'Wow, level %my_level already, I’m unstoppable!', 0, 0, '', 'Wow, niveau %my_level déjà, je suis imbattable!', 'Wow, schon Level %my_level, ich bin unaufhaltsam!', '哇,已经到 %my_level 级了,我无人能挡!', '', '', '', ''), -('broadcast_levelup_10x', 'Reaching new heights at level %my_level!!!', 0, 0, '', 'Atteignant de nouveaux sommets au niveau %my_level !!!', 'Neue Höhen erreicht auf Level %my_level!!!', '在 %my_level 级达到了新的高度!!!', '', '', '', ''), -('broadcast_levelup_10x', 'Level %my_level and still pushing forward!', 0, 0, '', 'Niveau %my_level et je continue d’avancer à fond !', 'Level %my_level und immer noch auf dem Vormarsch!', '%my_level 级,我还在继续前进!', '', '', '', ''), -('broadcast_levelup_10x', 'Level %my_level, I’m too strong now!', 0, 0, '', 'Niveau %my_level, je suis trop fort maintenant!', 'Level %my_level, jetzt bin ich zu stark!', '达到%my_level级,我现在强得不行了!', '', '', '', ''), -('broadcast_levelup_10x', 'Hitting %my_level, it feels so good!', 0, 0, '', 'Atteindre %my_level, ça fait tellement du bien !', '%my_level erreicht, fühlt sich großartig an!', '升到%my_level级,感觉真是太爽了!', '', '', '', ''), -('broadcast_levelup_10x', 'I’m level %my_level now! Let’s go!', 0, 0, '', 'Je suis au niveau %my_level maintenant ! Allez, c’est parti !', 'Ich bin jetzt Level %my_level! Auf geht’s!', '我现在是%my_level级了!咱们走起!', '', '', '', ''), -('broadcast_levelup_10x', 'Level %my_level, still a lot to go!', 0, 0, '', 'Niveau %my_level, il me reste encore beaucoup de chemin à parcourir !', 'Level %my_level, noch viel zu tun!', '升到%my_level级,还有很长的路要走啊!', '', '', '', ''), -('broadcast_levelup_max_level', 'Finally hit level %my_level, I can do anything!', 0, 0, '', 'J’ai enfin atteint le niveau %my_level, je peux tout faire !', 'Endlich Level %my_level erreicht, jetzt geht alles!', '终于达到%my_level级了,我无所不能!', '', '', '', ''), -('broadcast_levelup_max_level', 'Max level %my_level, endgame here I come!', 0, 0, '', 'Niveau maximum %my_level atteint, je me rapproche de la fin du jeu !', 'Max-Level %my_level erreicht, Endgame ich komme!', '满级%my_level,终极挑战我来了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level! Ready for the endgame!', 0, 0, '', 'Niveau %my_level ! Prêt pour la phase finale !', 'Level %my_level! Bereit fürs Endgame!', '%my_level级达成!准备好迎接终极内容了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Endgame unlocked at level %my_level, bring it on!', 0, 0, '', 'Fin du jeu débloquée au niveau %my_level, ça va déménager !', 'Endgame freigeschaltet auf Level %my_level, jetzt geht’s los!', '%my_level级解锁终极内容,来吧,挑战我!', '', '', '', ''), -('broadcast_levelup_max_level', 'I’ve reached level %my_level, time to shine!', 0, 0, '', 'J’ai atteint le niveau %my_level, c’est le moment de briller!', 'Level %my_level erreicht, Zeit zu glänzen!', '我达到%my_level级了,是时候大放异彩了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, let’s do some endgame raiding!', 0, 0, '', 'Niveau %my_level, allons faire un raid de fin de jeu!', 'Level %my_level, Zeit für Endgame-Raids!', '%my_level级了,咱去刷终极团队副本吧!', '', '', '', ''), -('broadcast_levelup_max_level', 'At level %my_level, nothing can stop me now!', 0, 0, '', 'Au niveau %my_level, rien ne peut m’arrêter maintenant !', 'Auf Level %my_level kann mich nichts mehr aufhalten!', '在%my_level级,谁也挡不住我了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, all systems go for high-level content!', 0, 0, '', 'Niveau %my_level, toutes les systèmes sont prêts pour du contenu de haut niveau !', 'Level %my_level, alle Systeme bereit für High-Level-Content!', '%my_level级,一切就绪,准备迎接高难度内容!', '', '', '', ''), -('broadcast_levelup_max_level', 'Yes! Level %my_level, %my_class ready for everything!', 0, 0, '', 'Oui! Niveau %my_level, %my_class prêt pour tout!', 'Ja! Level %my_level, %my_class bereit für alles!', '太棒了!%my_level级,我的%my_class准备好面对一切了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, it’s time to take on the world!', 0, 0, '', 'Niveau %my_level, c’est le moment de conquérir le monde!', 'Level %my_level, jetzt wird die Welt erobert!', '%my_level级,是时候征服这个世界了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Reached level %my_level, let’s take on those elite bosses!', 0, 0, '', 'J’ai atteint le niveau %my_level, allons affronter ces boss d’élite!', 'Level %my_level erreicht, Zeit, die Elitebosse anzugehen!', '达到%my_level级了,咱们去挑战那些精英首领吧!', '', '', '', ''), -('broadcast_levelup_max_level', 'Now I’m %my_level, the real adventure begins!', 0, 0, '', 'Maintenant que je suis %my_level, la vraie aventure commence!', 'Jetzt bin ich %my_level, das wahre Abenteuer beginnt!', '现在我是%my_level级,真正的冒险才刚刚开始!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level! Time for some serious action!', 0, 0, '', 'Niveau %my_level ! Il est temps d’entrer en action sérieusement !', 'Level %my_level! Jetzt wird’s ernst!', '%my_level级!是时候来点硬核行动了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level and still growing stronger!', 0, 0, '', 'Niveau %my_level et je continue de devenir de plus en plus fort !', 'Level %my_level und werde immer stärker!', '%my_level级了,而且我还会变得更强!', '', '', '', ''), -('broadcast_levelup_max_level', 'Finally at level %my_level! Let’s show off what I can do!', 0, 0, '', 'Enfin, je suis au niveau %my_level ! Il est temps de montrer de quoi je suis capable !', 'Endlich Level %my_level! Zeit, zu zeigen, was ich draufhabe!', '终于到%my_level级了!让大家看看我的厉害!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level is here! I’m ready for the big leagues!', 0, 0, '', 'Le niveau %my_level est arrivé! Je suis prêt pour les grands matchs!', 'Level %my_level erreicht! Bereit für die großen Herausforderungen!', '%my_level级来了!我准备好迎接大挑战了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Just hit %my_level, get ready for the real challenges!', 0, 0, '', 'Je viens d’atteindre mon niveau %my_level, je suis prêt pour affronter de vrais défis !', 'Gerade %my_level erreicht, jetzt kommen die echten Herausforderungen!', '刚到%my_level级,准备好迎接真正的挑战吧!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level complete, it’s time for the epic content!', 0, 0, '', 'Niveau %my_level complet, il est temps pour le contenu épique !', 'Level %my_level abgeschlossen, jetzt kommt das epische Zeug!', '%my_level级达成,是时候体验史诗内容了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Fresh out of %my_level! Ready for endgame dungeons!', 0, 0, '', 'Je viens enfin d’atteindre %my_level ! Prêt pour les donjons de fin de jeu !', 'Frisch Level %my_level erreicht! Bereit für Endgame-Dungeons!', '刚到%my_level级!准备好挑战终极地下城了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Now that I’m level %my_level, nothing is beyond my reach!', 0, 0, '', 'Maintenant que je suis au niveau %my_level, rien ne peut m’arrêter !', 'Jetzt, wo ich Level %my_level bin, ist nichts mehr unmöglich!', '现在我是%my_level级,没有什么是我做不到的!', '', '', '', ''), -('broadcast_levelup_max_level', 'It’s official! Level %my_level, time to dominate!', 0, 0, '', 'C’est officiel ! Niveau %my_level, c’est le moment de tout déchirer !', 'Es ist offiziell! Level %my_level, Zeit zu dominieren!', '正式宣布!%my_level级,是时候称霸一方了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Made it to level %my_level! Time to make my mark on the world!', 0, 0, '', 'J’ai atteint le niveau %my_level! Le moment de laisser ma trace dans le monde est enfin arrivé!', 'Level %my_level erreicht! Zeit, meinen Fußabdruck in der Welt zu hinterlassen!', '达到%my_level级了!是时候在这个世界上留下我的印记了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level unlocked! The real fun begins now!', 0, 0, '', 'Niveau %my_level débloqué! Maintenant, les choses sérieuses commencent!', 'Level %my_level freigeschaltet! Jetzt geht der Spaß erst richtig los!', '%my_level级解锁!真正的乐趣现在才开始!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, and the world is my playground!', 0, 0, '', 'Niveau %my_level, et le monde est mon terrain de jeu!', 'Level %my_level, die Welt ist mein Spielplatz!', '%my_level级,这个世界就是我的游乐场!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, ready to show the world my power!', 0, 0, '', 'Niveau %my_level, prêt à montrer au monde ma puissance !', 'Level %my_level, bereit, der Welt meine Macht zu zeigen!', '%my_level级,准备好向世界展示我的力量了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Just hit %my_level, I’m going all in now!', 0, 0, '', 'Je viens d’atteindre %my_level, je me lance à fond maintenant !', 'Gerade %my_level erreicht, jetzt gebe ich alles!', '刚到%my_level级,现在我要全力以赴了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, I can finally take on everything!', 0, 0, '', 'Niveau %my_level, je peux enfin tout affronter !', 'Level %my_level, endlich kann ich alles in Angriff nehmen!', '%my_level级,我终于能挑战一切了!', '', '', '', ''), -('broadcast_levelup_max_level', 'Level %my_level, I’m feeling unstoppable!', 0, 0, '', 'Niveau %my_level, je me sens imbattable!', 'Level %my_level, fühle mich unaufhaltsam!', '%my_level级,我感觉自己势不可挡!', '', '', '', ''), -('broadcast_levelup_max_level', 'Just reached level %my_level, can’t wait to dive into the endgame!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level, j’ai hâte de me plonger dans le contenu de fin de jeu!', 'Gerade Level %my_level erreicht, kann es kaum erwarten, ins Endgame einzutauchen!', '刚达到%my_level级,迫不及待要投入终极内容了!', '', '', '', ''), - --- guild --- usable placeholders: --- %other_name --- %other_class --- %other_race --- %other_level -('broadcast_guild_promotion', 'Good job %other_name. You deserved this.', 0, 0, '', 'Bien joué %other_name. Tu l’as bien mérité.', 'Gut gemacht %other_name. Das hast du dir verdient.', '干得好,%other_name,你当之无愧!', '', '', '', ''), -('broadcast_guild_demotion', 'That was awful %other_name. I hate to do this but...', 0, 0, '', 'Désolé %other_name, mais ça a été horrible. Je déteste devoir faire ça, mais...', 'Das war echt schlecht %other_name. Ich hasse es, das zu tun, aber...', '太糟糕了,%other_name,我也不想这么做,但是...', '', '', '', ''), -('broadcast_guild_promotion', 'Congrats %other_name, you’ve earned this promotion!', 0, 0, '', 'Félicitations %other_name, tu as mérité cette promotion !', 'Glückwunsch %other_name, du hast dir diese Beförderung verdient!', '恭喜你,%other_name,这晋升是你应得的!', '', '', '', ''), -('broadcast_guild_promotion', 'Well deserved %other_name, welcome to the next level!', 0, 0, '', 'Bien mérité %other_name, bienvenue au niveau suivant !', 'Verdient %other_name, willkommen auf der nächsten Stufe!', '名副其实,%other_name,欢迎进入下一阶段!', '', '', '', ''), -('broadcast_guild_promotion', '%other_name is leveling up! Congratulations on the promotion!', 0, 0, '', '%other_name progresse dans sa carrière ! Félicitations pour la promotion !', '%other_name steigt auf! Glückwunsch zur Beförderung!', '%other_name升职了!恭喜晋升!', '', '', '', ''), -('broadcast_guild_promotion', 'Promotion time! %other_name, you’ve earned this one!', 0, 0, '', 'Promotion time! %other_name, tu l’as bien mérité !', 'Beförderungszeit! %other_name, das hast du dir verdient!', '晋升时刻!%other_name,你当之无愧!', '', '', '', ''), -('broadcast_guild_promotion', 'A new role for %other_name! Well done, keep it up!', 0, 0, '', 'Une nouvelle fonction pour %other_name ! Bravo, continue comme ça !', 'Eine neue Rolle für %other_name! Gut gemacht, weiter so!', '%other_name获得了新角色!干得漂亮,继续加油!', '', '', '', ''), -('broadcast_guild_promotion', 'Huge congrats to %other_name, you’ve earned a well-deserved promotion!', 0, 0, '', '%other_name, félicitations pour ta super promotion, tu la mérites amplement!', 'Große Glückwünsche an %other_name, du hast dir diese Beförderung verdient!', '热烈祝贺%other_name,你实至名归地获得了晋升!', '', '', '', ''), -('broadcast_guild_promotion', 'Let’s all give a round of applause to %other_name, for the promotion!', 0, 0, '', 'Donnons tous un grand round d’applaudissements à %other_name pour sa promotion!', 'Lasst uns %other_name für die Beförderung applaudieren!', '让我们为%other_name的晋升鼓掌庆祝吧!', '', '', '', ''), -('broadcast_guild_promotion', 'Cheers to %other_name on the promotion! Keep shining!', 0, 0, '', 'Salut à %other_name pour sa promotion! Continue de briller!', 'Prost auf %other_name zur Beförderung! Bleib am Ball!', '为%other_name的晋升干杯!继续发光发热!', '', '', '', ''), -('broadcast_guild_promotion', 'Congrats %other_name, you’re moving up! Well done!', 0, 0, '', 'Félicitations %other_name, tu progresses dans ton parcours ! Bravo !', 'Glückwunsch %other_name, du steigst auf! Gut gemacht!', '恭喜%other_name,你更进一步了!干得好!', '', '', '', ''), -('broadcast_guild_promotion', 'Another step up for %other_name! Congratulations!', 0, 0, '', 'Encore une promotion pour %other_name ! Félicitations !', 'Noch ein Aufstieg für %other_name! Glückwunsch!', '%other_name又迈上新台阶!恭喜!', '', '', '', ''), -('broadcast_guild_demotion', 'Tough decision, but %other_name, you’ve been demoted. Time to step up!', 0, 0, '', 'Décision difficile, mais %other_name, tu as été rétrogradé. C’est le moment de te ressaisir!', 'Schwere Entscheidung, aber %other_name, du wurdest degradiert. Zeit, dich zu beweisen!', '艰难的决定,%other_name,你被降职了。加把劲吧!', '', '', '', ''), -('broadcast_guild_demotion', 'Sorry %other_name, but it’s time for a demotion. Hope you come back stronger!', 0, 0, '', 'Désolé %other_name, mais c’est l’heure d’une rétrogradation. J’espère que tu reviendras plus fort !', 'Tut mir leid %other_name, aber es ist Zeit für eine Degradierung. Hoffe, du kommst stärker zurück!', '抱歉,%other_name,现在得降职了。希望你能更强地回来!', '', '', '', ''), -('broadcast_guild_demotion', 'Unfortunately, %other_name, it’s a demotion this time. You’ll get ''em next time!', 0, 0, '', '', 'Leider, %other_name, handelt es sich diesmal um eine Degradierung. Das nächste Mal kriegst du sie!', '遗憾的是,%other_name,这次是降职。下次再努力吧!', '', '', '', ''), -('broadcast_guild_demotion', 'A hard decision, but %other_name, you’ve been demoted. Time to grind back up!', 0, 0, '', 'Une décision difficile, mais %other_name, tu as été rétrogradé. Il est temps de remonter la pente !', 'Schwere Entscheidung, aber %other_name, du wurdest degradiert. Zeit, dich wieder hochzuarbeiten!', '艰难的决定,%other_name,你被降职了。是时候努力爬回去了!', '', '', '', ''), -('broadcast_guild_demotion', 'We hate to see it, but %other_name, it’s a demotion for now.', 0, 0, '', 'C’est toujours difficile à accepter, %other_name, mais pour le moment, c’est une rétrogradation.', 'Es ist nie schön, aber %other_name, du wurdest degradiert – vorerst.', '我们也不想看到,%other_name,但现在是降职。', '', '', '', ''), -('broadcast_guild_demotion', 'Sorry %other_name, your rank has been lowered. Let’s work harder together!', 0, 0, '', 'Désolé %other_name, ton rang a été abaissé. On va devoir redoubler d’efforts ensemble !', 'Tut mir leid %other_name, dein Rang wurde gesenkt. Lass uns gemeinsam härter arbeiten!', '对不起,%other_name,你的等级被降低了。咱们一起更努力吧!', '', '', '', ''), -('broadcast_guild_demotion', 'A demotion for %other_name... Time to focus and rise back up!', 0, 0, '', 'Une rétrogradation pour %other_name... Il est temps de se concentrer et de remonter la pente!', 'Eine Degradierung für %other_name... Zeit, sich zu fokussieren und wieder aufzusteigen!', '%other_name被降职了...是时候集中精力再爬上去了!', '', '', '', ''), -('broadcast_guild_demotion', 'Not what we wanted, but %other_name, you’ve been demoted. Learn and come back stronger!', 0, 0, '', 'Pas ce que nous voulions, mais %other_name, tu as été rétrogradé. Apprends et reviens plus fort !', 'Nicht das, was wir wollten, aber %other_name, du wurdest degradiert. Lern daraus und komm stärker zurück!', '这不是我们想要的,%other_name,你被降职了。吸取教训,变得更强吧!', '', '', '', ''), -('broadcast_guild_demotion', 'A tough moment for %other_name, but we know you can bounce back from this demotion!', 0, 0, '', 'Un moment difficile pour %other_name, mais on sait que tu peux rebondir après cette rétrogradation!', 'Ein harter Moment für %other_name, aber wir wissen, dass du dich von dieser Degradierung erholen kannst!', '对%other_name来说是艰难的一刻,但我们相信你能从这次降职中恢复!', '', '', '', ''), -('broadcast_guild_demotion', 'Sorry %other_name, but a demotion has been applied. It’s time for improvement!', 0, 0, '', 'Désolé %other_name, mais une rétrogradation a été appliquée. Il est temps de s’améliorer !', 'Tut mir leid %other_name, aber du wurdest degradiert. Jetzt ist es Zeit, sich zu verbessern!', '抱歉,%other_name,你被降职了。是时候改进自己了!', '', '', '', ''), -('broadcast_guild_demotion', 'We didn’t want this for %other_name, but a demotion has occurred. Time to get better!', 0, 0, '', 'On n’a pas voulu cela pour %other_name, mais une rétrogradation a eu lieu. Il est temps de s’améliorer !', 'Das wollten wir nicht für %other_name, aber es ist zu einer Degradierung gekommen. Zeit, besser zu werden!', '我们不希望这样,%other_name,但降职已发生。是时候变得更好了!', '', '', '', ''), -('broadcast_guild_recruitment', 'Looking for fresh blood! Join the guild, be part of something great!', 0, 0, '', 'À la recherche de sang neuf ! Rejoins la guilde, fais partie de quelque chose de grandiose !', 'Auf der Suche nach frischem Blut! Tritt der Gilde bei und sei Teil von etwas Großem!', '寻找新鲜血液!加入我们的公会,一起成就伟业!', '', '', '', ''), -('broadcast_guild_recruitment', 'Recruiting members! If you’re ready to join us, the guild door is open!', 0, 0, '', 'Recrutement de membres ! Si tu es prêt à nous rejoindre, la porte de la guilde est ouverte!', 'Wir rekrutieren! Wenn du bereit bist, uns beizutreten, steht die Tür der Gilde offen!', '招募成员!如果你准备好了,公会的大门随时为你敞开!', '', '', '', ''), -('broadcast_guild_recruitment', 'The guild is looking for skilled adventurers. Join us and thrive!', 0, 0, '', 'La guilde recherche des aventuriers compétents. Rejoins-nous et prospère!', 'Die Gilde sucht erfahrene Abenteurer. Tritt bei und wachse mit uns!', '公会正在寻找有实力的冒险者。加入我们,一起成长!', '', '', '', ''), -('broadcast_guild_recruitment', 'Want to join a great guild? We’re looking for new members! Come on in!', 0, 0, '', 'Envie de rejoindre une super guilde ? Nous cherchons de nouveaux membres ! Viens nous rejoindre !', 'Willst du einer großartigen Gilde beitreten? Wir suchen neue Mitglieder! Komm rein!', '想加入一个厉害的公会吗?我们在招新成员!快来加入吧!', '', '', '', ''), -('broadcast_guild_recruitment', 'The guild is growing! Join us for exciting adventures and rewards!', 0, 0, '', 'La guilde grandit! Rejoignez-nous pour vivre des aventures palpitantes et obtenir des récompenses!', 'Die Gilde wächst! Schließ dich uns an für spannende Abenteuer und Belohnungen!', '公会正在壮大!加入我们,体验刺激的冒险和丰厚的奖励!', '', '', '', ''), - - - --- random instance --- usable placeholders: --- %my_role --- %instance_name --- %my_class --- %my_race --- %my_level -('suggest_instance', 'Anyone wants %instance_name?', 0, 0, '', 'Qui veut %instance_name?', 'Wer will %instance_name?', '有人想去%instance_name吗?', '', '¿Alguien quiere ir a %instance_name?', '', ''), -('suggest_instance', 'Any groups for %instance_name?', 0, 0, '', 'Des groupes pour %instance_name?', 'Gibt es Gruppen für %instance_name?', '有组队去%instance_name的吗?', '', '¿Algún grupo para %instance_name?', '', ''), -('suggest_instance', 'Need help for %instance_name?', 0, 0, '', 'Besoin d’aide pour %instance_name ?', 'Braucht jemand Hilfe für %instance_name?', '有人需要帮忙打%instance_name吗?', '', '¿Alguien necesita ayuda con %instance_name?', '', ''), -('suggest_instance', 'LFD: %instance_name.', 0, 0, '', 'LFG: %instance_name.', 'LFG: %instance_name.', '寻找队伍:%instance_name。', '', 'Busco grupo para %instance_name.', '', ''), -('suggest_instance', 'Anyone needs %my_role for %instance_name?', 0, 0, '', 'Besoin de %my_role pour %instance_name?', 'Braucht jemand einen %my_role für %instance_name?', '有人需要%my_role去%instance_name吗?', '', '¿Alguien necesita un %my_role para %instance_name?', '', ''), -('suggest_instance', 'Missing %my_role for %instance_name?', 0, 0, '', 'Il manque %my_role pour %instance_name ?', 'Fehlt ein %my_role für %instance_name?', '缺一个%my_role去%instance_name,有人来吗?', '', '¿Buscando un %my_role para %instance_name?', '', ''), -('suggest_instance', 'Can be a %my_role for %instance_name.', 0, 0, '', 'Je peux être %my_role pour %instance_name.', 'Ich kann als %my_role für %instance_name gehen.', '我可以当%my_role去%instance_name。', '', 'Puedo ser un %my_role para %instance_name.', '', ''), -('suggest_instance', 'Need help with %instance_name?', 0, 0, '', 'Besoin d’aide avec %instance_name?', 'Braucht jemand Hilfe bei %instance_name?', '有人需要帮忙过%instance_name吗?', '', '¿Alguien necesita ayuda con %instance_name?', '', ''), -('suggest_instance', 'Need %my_role help with %instance_name?', 0, 0, '', 'Besoin d’aide avec %instance_name? Je peux t’aider avec ça?', 'Braucht jemand die Hilfe eines %my_role für %instance_name?', '需要一个%my_role帮忙打%instance_name吗?', '', '¿Necesitas ayuda de %my_role con %instance_name?', '', ''), -('suggest_instance', 'Anyone needs gear from %instance_name?', 0, 0, '', 'Besoin d’équipement de %instance_name?', 'Braucht jemand Ausrüstung aus %instance_name?', '有人需要%instance_name的装备吗?', '', '¿Alguien necesita equipo de %instance_name?', '', ''), -('suggest_instance', 'A little grind in %instance_name?', 0, 0, '', 'Un petit farming dans %instance_name ?', 'Eine kleine Farm-Runde in %instance_name?', '去%instance_name刷一波怎么样?', '', '¿Un poco de farmeo en %instance_name?', '', ''), -('suggest_instance', 'WTR %instance_name', 0, 0, '', 'WTR %instance_name', 'WTR %instance_name', '想跑%instance_name', '', 'WTR %instance_name', '', ''), -('suggest_instance', 'Need help for %instance_name.', 0, 0, '', 'Besoin d’aide pour %instance_name.', 'Brauche Hilfe für %instance_name.', '需要帮忙打%instance_name。', '', 'Necesito ayuda para %instance_name.', '', ''), -('suggest_instance', 'Wanna run %instance_name.', 0, 0, '', 'Tu veux lancer %instance_name.', 'Lust auf %instance_name?', '想去跑一趟%instance_name。', '', 'Quiero hacer %instance_name.', '', ''), -('suggest_instance', '%my_role looks for %instance_name.', 0, 0, '', 'Je recherche %instance_name dans le rôle de %my_role.', '%my_role sucht nach einer Gruppe für %instance_name.', '%my_role寻找%instance_name的队伍。', '', '%my_role busca %instance_name.', '', ''), -('suggest_instance', 'What about %instance_name?', 0, 0, '', 'Et %instance_name, qu’en est-il?', 'Wie sieht’s mit %instance_name aus?', '%instance_name怎么样,去不去?', '', '¿Qué pasa con %instance_name?', '', ''), -('suggest_instance', 'Who wants to farm %instance_name?', 0, 0, '', 'Qui veut farm %instance_name ?', 'Wer will %instance_name farmen?', '有谁想去刷%instance_name?', '', '¿Quién quiere farmear %instance_name?', '', ''), -('suggest_instance', 'Go in %instance_name?', 0, 0, '', 'On va dans %instance_name ?', 'Gehen wir nach %instance_name?', '去%instance_name怎么样?', '', '¿Vamos a %instance_name?', '', ''), -('suggest_instance', 'Looking for %instance_name.', 0, 0, '', 'Je recherche %instance_name.', 'Suche Leute für %instance_name.', '寻找%instance_name的队伍。', '', 'Buscando gente para %instance_name.', '', ''), -('suggest_instance', 'Need help with %instance_name quests?', 0, 0, '', 'Besoin d’aide avec les quêtes de %instance_name ?', 'Braucht jemand Hilfe bei Quests in %instance_name?', '有人需要帮忙做%instance_name的任务吗?', '', '¿Necesitas ayuda con las misiones en %instance_name?', '', ''), -('suggest_instance', 'Wanna quest in %instance_name.', 0, 0, '', 'Tu veux partir en quête dans %instance_name?', 'Lust auf Quests in %instance_name?', '想去%instance_name做任务。', '', 'Quiero hacer una vuelta rapida en %instance_name.', '', ''), -('suggest_instance', 'Anyone with quests in %instance_name?', 0, 0, '', 'Quelqu’un a des quêtes dans %instance_name?', 'Hat jemand Quests in %instance_name?', '有人有%instance_name的任务一起做吗?', '', '¿Alguien con misiones en %instance_name?', '', ''), -('suggest_instance', 'Could help with quests in %instance_name.', 0, 0, '', '%instance_name, vous pouvez m’aider avec les quêtes ?', 'Kann bei Quests in %instance_name helfen.', '我可以帮忙做%instance_name的任务。', '', 'Podría ayudar con las misiones en %instance_name.', '', ''), -('suggest_instance', '%my_role: any place in group for %instance_name?', 0, 0, '', '%my_role: Est-ce qu’il y a une place dans le groupe pour %instance_name?', '%my_role: Gibt es einen Platz in einer Gruppe für %instance_name?', '%my_role:%instance_name的队伍有位置吗?', '', '%my_role: ¿algún lugar en el grupo para %instance_name?', '', ''), -('suggest_instance', 'Does anybody still run %instance_name this days?', 0, 0, '', 'Est-ce que quelqu’un utilise encore %instance_name de nos jours?', 'Läuft noch jemand %instance_name heutzutage?', '现在还有人跑%instance_name吗?', '', '¿Alguien todavía hace %instance_name estos días?', '', ''), -('suggest_instance', '%instance_name: anyone wants to take a %my_role?', 0, 0, '', '%instance_name: Est-ce que quelqu’un veut prendre un %my_role?', '%instance_name: Braucht jemand einen %my_role?', '%instance_name:有人需要一个%my_role吗?', '', '¿Alguien quiere un %my_role para %instance_name?', '', ''), -('suggest_instance', 'Is there any point being %my_role in %instance_name?', 0, 0, '', 'Est-ce que ça vaut la peine d’être %my_role dans %instance_name?', 'Macht es überhaupt Sinn, %my_role in %instance_name zu sein?', '在%instance_name当%my_role有意义吗?', '', '¿Tiene algún sentido ser %my_role en %instance_name?', '', ''), -('suggest_instance', 'It is really worth to go to %instance_name?', 0, 0, '', 'Est-ce que ça vaut vraiment la peine d’aller à %instance_name?', 'Lohnt es sich wirklich, nach %instance_name zu gehen?', '去%instance_name真的值得吗?', '', '¿Realmente vale la pena ir a %instance_name?', '', ''), -('suggest_instance', 'Anybody needs more people for %instance_name?', 0, 0, '', 'Est-ce que quelqu’un a besoin de plus de personnes pour %instance_name?', 'Braucht jemand noch Leute für %instance_name?', '有人需要更多人一起打%instance_name吗?', '', '¿Alguien necesita más personas para %instance_name?', '', ''), -('suggest_instance', '%instance_name bosses drop good gear. Wanna run?', 0, 0, '', '%instance_name Les boss laissent tomber du bon équipement. Tu veux qu’on y aille?', '%instance_name Bosse droppen gutes Gear. Lust auf einen Run?', '%instance_name的首领掉好装备,想去跑一趟吗?', '', 'Los jefes en %instance_name sueltan buen equipo. ¿Quieres ir?', '', ''), -('suggest_instance', 'What about %instance_name?', 0, 0, '', 'Que dire de %instance_name ?', 'Wie sieht’s mit %instance_name aus?', '%instance_name咋样,去不?', '', '¿Qué pasa con %instance_name?', '', ''), -('suggest_instance', 'Anybody needs %my_role?', 0, 0, '', 'Est-ce que quelqu’un a besoin de %my_role?', 'Braucht jemand einen %my_role?', '有人需要一个%my_role吗?', '', '¿Alguien necesita un %my_role?', '', ''), -('suggest_instance', 'Anyone needs %my_role?', 0, 0, '', 'Besoin de %my_role?', 'Braucht jemand einen %my_role?', '有人缺一个%my_role吗?', '', '¿Alguien hacer un grupo con un %my_role?', '', ''), -('suggest_instance', 'Who wants %instance_name?', 0, 0, '', 'Qui veut %instance_name ?', 'Wer will %instance_name?', '谁想去%instance_name?', '', '¿Quién quiere %instance_name?', '', ''), -('suggest_instance', 'Can anybody summon me at %instance_name?', 0, 0, '', 'Est-ce que quelqu’un peut m’invoquer à %instance_name?', 'Kann mich jemand nach %instance_name herbeirufen?', '有人能在%instance_name召唤我吗?', '', '¿Alguien puede invocarme en %instance_name?', '', ''), -('suggest_instance', 'Meet me in %instance_name', 0, 0, '', 'Rencontre-moi dans %instance_name', 'Triff mich in %instance_name.', '在%instance_name见我吧。', '', 'Encuéntrame en %instance_name', '', ''), -('suggest_instance', 'Wanna quick %instance_name run', 0, 0, '', 'Tu veux lancer rapidement %instance_name ?', 'Lust auf einen schnellen %instance_name Run?', '想快速跑一趟%instance_name吗?', '', 'Quiero ir a %instance_name rápido', '', ''), -('suggest_instance', 'Wanna full %instance_name run', 0, 0, '', 'Bien sûr, je veux que l’instance %instance_name fonctionne.', 'Lust auf einen kompletten %instance_name Run?', '想完整跑一遍%instance_name吗?', '', 'Quiero hacer %instance_name completa', '', ''), -('suggest_instance', 'How many times were you in %instance_name?', 0, 0, '', 'Combien de fois ai-je été dans %instance_name?', 'Wie oft warst du schon in %instance_name?', '你去过%instance_name几次了?', '', '¿Cuántas veces estuviste en %instance_name?', '', ''), -('suggest_instance', 'Another %instance_name run?', 0, 0, '', 'Une autre éxpédition dans %instance_name ?', 'Noch ein Run in %instance_name?', '再跑一趟%instance_name怎么样?', '', '¿Otra vuelta en %instance_name ?', '', ''), -('suggest_instance', 'Wiped in %instance_name? Take me instead!', 0, 0, '', 'Wipe dans %instance_name ? Prends-moi à la place !', 'Wipe in %instance_name? Nehmt mich stattdessen!', '在%instance_name团灭了?带我吧!', '', '¿Borrado en %instance_name? ¡Tómame a mí en su lugar!', '', ''), -('suggest_instance', 'Take me in %instance_name please.', 0, 0, '', 'Prenez-moi dans %instance_name s’il vous plaît.', 'Nehmt mich mit in %instance_name, bitte.', '请带我去%instance_name。', '', 'Tómame en %instance_name por favor.', '', ''), -('suggest_instance', 'Quick %instance_name run?', 0, 0, '', 'Rapidement, %instance_name, on y va ?', 'Schneller %instance_name Run?', '快速跑一趟%instance_name?', '', '¿Partida rápida de %instance_name?', '', ''), -('suggest_instance', 'Full %instance_name run?', 0, 0, '', 'Complet %instance_name ?', 'Kompletter %instance_name Run?', '完整跑一遍%instance_name?', '', '¿Partida completa de %instance_name?', '', ''), -('suggest_instance', 'Who can take %my_role to %instance_name?', 0, 0, '', 'Qui peut prendre %my_role à %instance_name?', 'Wer kann als %my_role nach %instance_name gehen?', '谁能带一个%my_role去%instance_name?', '', '¿Quién puede ir de %my_role para %instance_name?', '', ''), -('suggest_instance', 'LFG %instance_name, I am %my_role', 0, 0, '', 'LFG %instance_name, je suis %my_role.', 'LFG %instance_name, ich bin %my_role.', '找队%instance_name,我是%my_role', '', '', '', ''), -('suggest_instance', '%my_role LFG %instance_name', 0, 0, '', '%my_role LFG %instance_name.', '%my_role LFG %instance_name.', '%my_role找队%instance_name', '', '', '', ''), -('suggest_instance', 'Anyone looking for a group for %instance_name?', 0, 0, '', 'Quelqu’un cherche un groupe pour %instance_name?', 'Sucht jemand eine Gruppe für %instance_name?', '有人在找%instance_name的队伍吗?', '', '¿Alguien busca grupo para %instance_name?', '', ''), -('suggest_instance', 'Need a DPS for %instance_name', 0, 0, '', 'Besoin d’un DPS pour %instance_name.', 'Brauchen noch einen DPS für %instance_name.', '需要一个DPS去%instance_name', '', 'Necesito un DPS para %instance_name', '', ''), -('suggest_instance', 'Looking for a healer for %instance_name', 0, 0, '', 'Je suis à la recherche d’un guérisseur pour %instance_name.', 'Suche einen Heiler für %instance_name.', '找一个治疗去%instance_name', '', 'Busco un sanador para %instance_name', '', ''), -('suggest_instance', 'Anyone need a tank for %instance_name?', 0, 0, '', 'Besoin d’un tank pour %instance_name ?', 'Braucht jemand einen Tank für %instance_name?', '有人需要一个坦克去%instance_name吗?', '', '¿Alguien necesita un tanque para %instance_name?', '', ''), -('suggest_instance', 'Trying to do %instance_name. Need a group!', 0, 0, '', 'J’essaie de faire %instance_name. Il me faut un groupe !', 'Ich versuche %instance_name zu machen. Brauche eine Gruppe!', '想打%instance_name,缺个队伍!', '', 'Intentando hacer %instance_name. ¡Necesito un grupo!', '', ''), -('suggest_instance', 'Anyone down for %instance_name?', 0, 0, '', 'Qui est partant pour %instance_name?', 'Wer hat Lust auf %instance_name?', '有人想一起去%instance_name吗?', '', '¿Alguien se apunta para %instance_name?', '', ''), -('suggest_instance', 'Looking for 1 more for %instance_name', 0, 0, '', 'Il nous manque 1 personne pour %instance_name.', 'Uns fehlt noch 1 Person für %instance_name.', '还差1人去%instance_name', '', 'Busco 1 más para %instance_name', '', ''), -('suggest_instance', 'Anyone wanna join for %instance_name?', 0, 0, '', 'Quelqu’un veut se joindre pour %instance_name?', 'Möchte jemand für %instance_name mitkommen?', '有人想加入%instance_name的队伍吗?', '', '¿Alguien quiere unirse para %instance_name?', '', ''), -('suggest_instance', 'Anyone interested in %instance_name?', 0, 0, '', 'Quelqu’un intéressé par %instance_name ?', 'Hat jemand Interesse an %instance_name?', '有人对%instance_name感兴趣吗?', '', '¿Alguien interesado en %instance_name?', '', ''), -('suggest_instance', 'Looking for a quick run through %instance_name', 0, 0, '', 'À la recherche d’un rapide survol de %instance_name', 'Suche einen schnellen Run durch %instance_name.', '想快速通关%instance_name', '', 'Buscando una corrida rápida en %instance_name', '', ''), -('suggest_instance', 'Who’s up for %instance_name?', 0, 0, '', 'Qui est partant pour %instance_name?', 'Wer ist bereit für %instance_name?', '谁愿意去%instance_name?', '', '¿Quién está listo para %instance_name?', '', ''), -('suggest_instance', 'Need help to clear %instance_name', 0, 0, '', 'Besoin d’aide pour vider %instance_name', 'Brauche Hilfe, um %instance_name zu clearen', '需要帮忙清%instance_name', '', 'Necesito ayuda para limpiar %instance_name', '', ''), -('suggest_instance', 'Want to farm %instance_name?', 0, 0, '', 'Envie de farmer %instance_name?', 'Lust, %instance_name zu farmen?', '想去刷%instance_name吗?', '', '¿Quieres farmear %instance_name?', '', ''), -('suggest_instance', 'Looking to complete %instance_name', 0, 0, '', 'Je cherche à terminer %instance_name.', 'Suche Gruppe, um %instance_name abzuschließen.', '想完成%instance_name', '', 'Buscando completar %instance_name', '', ''), -('suggest_instance', 'Anyone needs %instance_name items?', 0, 0, '', 'Besoin d’objets %instance_name ?', 'Braucht jemand Items aus %instance_name?', '有人需要%instance_name的道具吗?', '', '¿Alguien necesita objetos de %instance_name?', '', ''), -('suggest_instance', 'Looking for a carry through %instance_name', 0, 0, '', 'Je recherche un portage à travers %instance_name.', 'Suche Carry für %instance_name.', '找人带我过%instance_name', '', 'Buscando un carry para %instance_name', '', ''), -('suggest_instance', 'Anyone want to run %instance_name?', 0, 0, '', 'Quelqu’un veut faire tourner %instance_name?', 'Will jemand %instance_name laufen?', '有人想跑%instance_name吗?', '', '¿Alguien quiere hacer %instance_name?', '', ''), -('suggest_instance', 'Running %instance_name, looking for more', 0, 0, '', 'En cours d’exécution de %instance_name, je suis à la recherche de plus d’informations.', 'Laufe %instance_name, suche noch Mitspieler.', '正在跑%instance_name,还缺人', '', 'Haciendo %instance_name, buscando más personas', '', ''), -('suggest_instance', 'Anyone available for %instance_name?', 0, 0, '', 'Quelqu’un disponible pour %instance_name ?', 'Hat jemand Zeit für %instance_name?', '有人有空去%instance_name吗?', '', '¿Alguien disponible para %instance_name?', '', ''), -('suggest_instance', 'Looking for someone to help with %instance_name', 0, 0, '', 'Besoin d’aide pour %instance_name', 'Suche Hilfe für %instance_name.', '找人帮忙打%instance_name', '', 'Buscando a alguien para ayudar con %instance_name', '', ''), -('suggest_instance', 'Need %my_role for %instance_name!', 0, 0, '', 'J’ai besoin de %my_role pour %nom_de_l’instance!', 'Brauche einen %my_role für %instance_name!', '需要一个%my_role去%instance_name!', '', '¡Necesito un %my_role para %instance_name!', '', ''), -('suggest_instance', 'Want to do a %instance_name run?', 0, 0, '', 'Tu veux faire une séance de %instance_name ?', 'Lust auf einen %instance_name Run?', '有人想一起刷%instance_name吗?', '', '¿Quieres hacer una corrida de %instance_name?', '', ''), -('suggest_instance', 'Need %my_role for %instance_name run', 0, 0, '', 'J’ai besoin de %my_role pour exécuter %instance_name.', 'Brauche %my_role für einen %instance_name Run.', '刷%instance_name需要一个%my_role!', '', 'Necesito %my_role para la corrida de %instance_name', '', ''), -('suggest_instance', 'Anyone want to farm %instance_name gear?', 0, 0, '', 'Ça vous dit de farmer les équipements de %instance_name ?', 'Hat jemand Lust, %instance_name Gear zu farmen?', '有人想一起刷%instance_name的装备吗?', '', '¿Alguien quiere farmear equipo de %instance_name?', '', ''), -('suggest_instance', 'Looking for a group to speedrun %instance_name', 0, 0, '', 'Je recherche un groupe pour faire une speedrun de %instance_name.', 'Suche Gruppe für einen Speedrun in %instance_name.', '找个队伍一起速刷%instance_name!', '', 'Buscando un grupo para speedrun de %instance_name', '', ''), -('suggest_instance', 'Need a group to clear %instance_name', 0, 0, '', 'Besoin d’un groupe pour nettoyer %instance_name', 'Brauche eine Gruppe, um %instance_name zu clearen.', '需要一个队伍清理%instance_name!', '', 'Necesito un grupo para limpiar %instance_name', '', ''), -('suggest_instance', 'Anyone interested in farming %instance_name?', 0, 0, '', 'Quelqu’un intéressé par cultiver %instance_name?', 'Hat jemand Interesse daran, %instance_name zu farmen?', '有人对刷%instance_name感兴趣吗?', '', '¿Alguien interesado en farmear %instance_name?', '', ''), -('suggest_instance', 'Looking for some gear from %instance_name', 0, 0, '', 'Je cherche des équipements de %instance_name.', 'Suche nach Ausrüstung aus %instance_name.', '想从%instance_name弄点装备,有一起的吗?', '', 'Buscando equipo de %instance_name', '', ''), -('suggest_instance', 'Anyone for a quick %instance_name run?', 0, 0, '', 'Qui est partant pour une petite instance rapide de %instance_name ?', 'Hat jemand Lust auf einen schnellen %instance_name Run?', '有人想速刷一波%instance_name吗?', '', '¿Alguien para una corrida rápida de %instance_name?', '', ''), -('suggest_instance', 'Anyone want to try %instance_name together?', 0, 0, '', 'Qui veut essayer %instance_name ensemble ?', 'Wer möchte %instance_name gemeinsam ausprobieren?', '有人想一起挑战%instance_name吗?', '', '¿Alguien quiere intentar %instance_name juntos?', '', ''), -('suggest_instance', 'Looking for people to complete %instance_name', 0, 0, '', 'Je recherche des personnes pour compléter %instance_name.', 'Suche Leute, um %instance_name abzuschließen.', '找人一起完成%instance_name!', '', 'Buscando personas para completar %instance_name', '', ''), -('suggest_instance', 'Can we get a group for %instance_name?', 0, 0, '', 'Peut-on former un groupe pour %instance_name?', 'Können wir eine Gruppe für %instance_name zusammenstellen?', '能组个队去%instance_name吗?', '', '¿Podemos formar un grupo para %instance_name?', '', ''), -('suggest_instance', 'Need a quick run for %instance_name', 0, 0, '', 'Besoin d’une exécution rapide pour %instance_name', 'Brauche einen schnellen Run für %instance_name.', '需要快速刷一波%instance_name!', '', 'Necesito una corrida rápida para %instance_name', '', ''), -('suggest_instance', 'Anyone down to farm %instance_name together?', 0, 0, '', 'Qui est partant pour farmer %instance_name ensemble ?', 'Hat jemand Lust, %instance_name gemeinsam zu farmen?', '有人想一起刷%instance_name吗?', '', '¿Alguien quiere farmear %instance_name juntos?', '', ''), -('suggest_instance', 'Looking for a few more for %instance_name', 0, 0, '', 'Je cherche encore quelques personnes pour %instance_name.', 'Suche noch ein paar Leute für %instance_name.', '还差几个人一起去%instance_name!', '', 'Buscando algunos más para %instance_name', '', ''), -('suggest_instance', 'Anyone need help with %instance_name mechanics?', 0, 0, '', 'Besoin d’aide avec les mécanismes de %instance_name ?', 'Braucht jemand Hilfe bei den Mechaniken von %instance_name?', '有人需要%instance_name的机制讲解吗?', '', '¿Alguien necesita ayuda con las mecánicas de %instance_name?', '', ''), -('suggest_instance', 'Need a group for %instance_name fast', 0, 0, '', 'J’ai besoin d’un groupe pour %instance_name rapidement', 'Brauche schnell eine Gruppe für %instance_name.', '急需一个队伍去%instance_name!', '', 'Necesito un grupo rápido para %instance_name', '', ''), -('suggest_instance', 'Anyone want to speedrun %instance_name?', 0, 0, '', 'Quelqu’un veut speedrunner %instance_name?', 'Will jemand %instance_name im Speedrun machen?', '有人想速刷%instance_name吗?', '', '¿Alguien quiere hacer %instance_name en speedrun?', '', ''), -('suggest_instance', 'Looking for a quick %instance_name completion', 0, 0, '', 'Je cherche une %instance_name complétion rapide.', 'Suche nach einer schnellen %instance_name Abschließung.', '找人快速完成%instance_name!', '', 'Buscando completar %instance_name rápido', '', ''), -('suggest_instance', 'Looking for anyone to join %instance_name', 0, 0, '', 'Je recherche des personnes pour rejoindre %instance_name.', 'Suche Leute, die sich %instance_name anschließen möchten.', '找人一起加入%instance_name!', '', 'Buscando a alguien para unirse a %instance_name', '', ''), -('suggest_instance', 'Anyone down to clear %instance_name?', 0, 0, '', 'Quelqu’un est partant pour nettoyer %instance_name?', 'Hat jemand Lust, %instance_name zu clearen?', '有人想一起清%instance_name吗?', '', '¿Alguien para limpiar %instance_name?', '', ''), -('suggest_instance', 'Need help with the final boss of %instance_name', 0, 0, '', 'Besoin d’aide avec le boss final de %instance_name?', 'Brauche Hilfe beim Endboss von %instance_name.', '需要帮忙打%instance_name的最终BOSS!', '', 'Necesito ayuda con el último jefe de %instance_name', '', ''), -('suggest_instance', 'Anyone interested in running %instance_name today?', 0, 0, '', 'Quelqu’un intéressé pour lancer %instance_name aujourd’hui?', 'Hat jemand Interesse, heute %instance_name zu machen?', '今天有人想刷%instance_name吗?', '', '¿Alguien interesado en hacer %instance_name hoy?', '', ''), -('suggest_instance', 'Looking for a quick run of %instance_name to farm some gear', 0, 0, '', 'Je cherche à faire rapidement %instance_name pour récupérer un peu d’équipement.', 'Suche nach einem schnellen Run in %instance_name, um etwas Gear zu farmen.', '想速刷%instance_name搞点装备,有人一起吗?', '', 'Buscando una corrida rápida de %instance_name para farmear equipo', '', ''), -('suggest_instance', 'Anyone looking to farm materials from %instance_name?', 0, 0, '', 'Qui veut farmer des matériaux dans %instance_name ?', 'Will jemand Materialien aus %instance_name farmen?', '有人想刷%instance_name的材料吗?', '', '¿Alguien quiere farmear materiales de %instance_name?', '', ''), -('suggest_instance', 'Let’s get a group for %instance_name', 0, 0, '', 'Formons un groupe pour %instance_name.', 'Lasst uns eine Gruppe für %instance_name zusammenstellen.', '咱们组个队去%instance_name吧!', '', 'Formemos un grupo para %instance_name', '', ''), - --- random quest --- usable placeholders: --- %my_role --- %quest_link --- %quest_level --- %my_class --- %my_race --- %my_level -('suggest_quest', 'Need help with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Brauche Hilfe bei %quest_link?', '有人能帮我做%quest_link吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), -('suggest_quest', 'Anyone wants to share %quest_link?', 0, 0, '', 'Est-ce que quelqu’un veut partager %quest_link ?', 'Möchte jemand %quest_link teilen?', '有人想一起分享%quest_link吗?', '', '¿Alguien quiere hacer %quest_link?', '', ''), -('suggest_quest', 'Anyone doing %quest_link?', 0, 0, '', 'Quelqu’un fait la %quest_link?', 'Macht gerade jemand %quest_link?', '有人在做%quest_link吗?', '', '¿Alguien está haciendo %quest_link?', '', ''), -('suggest_quest', 'Wanna do %quest_link.', 0, 0, '', 'Tu veux faire la %quest_link ?', 'Lust auf %quest_link?', '想做%quest_link,有人一起吗?', '', '¿Una ayudita con %quest_link?', '', ''), -('suggest_quest', 'Looking to group for %quest_link.', 0, 0, '', 'À la recherche de personnes pour %quest_link.', 'Suche Gruppe für %quest_link.', '找人组队做%quest_link!', '', 'Buscando un grupo para %quest_link.', '', ''), -('suggest_quest', 'Who wants to help with %quest_link?', 0, 0, '', 'Qui veut aider avec %quest_link?', 'Wer möchte bei %quest_link helfen?', '谁想帮我做%quest_link?', '', '¿Quién quiere ayudar con %quest_link?', '', ''), -('suggest_quest', 'Need assistance with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Brauche Unterstützung bei %quest_link?', '需要帮忙做%quest_link吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), -('suggest_quest', 'Anyone interested in %quest_link?', 0, 0, '', 'Quelqu’un intéressé par %quest_link?', 'Hat jemand Interesse an %quest_link?', '有人对%quest_link感兴趣吗?', '', '¿Alguien interesado en %quest_link?', '', ''), -('suggest_quest', 'Anyone wants to finish %quest_link?', 0, 0, '', 'Qui veut terminer la %quest_link ?', 'Wer will %quest_link abschließen?', '有人想完成%quest_link吗?', '', '¿Alguien quiere terminar %quest_link?', '', ''), -('suggest_quest', 'Need a team for %quest_link?', 0, 0, '', 'Besoin d’une équipe pour %quest_link?', 'Brauche ein Team für %quest_link?', '需要一个队伍做%quest_link吗?', '', '¿Necesito un equipo para %quest_link?', '', ''), -('suggest_quest', 'Any takers for %quest_link?', 0, 0, '', 'Des volontaires pour %quest_link ?', 'Freiwillige für %quest_link?', '有志愿者一起做%quest_link吗?', '', '¿Alguien se apunta a %quest_link?', '', ''), -('suggest_quest', 'Let’s do %quest_link together.', 0, 0, '', 'Faisons cette %quest_link ensemble.', 'Lass uns %quest_link zusammen machen.', '咱们一起做%quest_link吧!', '', 'Vamos a hacer %quest_link juntos.', '', ''), -('suggest_quest', 'Need someone for %quest_link.', 0, 0, '', 'Besoin de quelqu’un pour %quest_link.', 'Brauche jemanden für %quest_link.', '需要一个人帮忙做%quest_link。', '', '¿Necesito a alguien para %quest_link?', '', ''), -('suggest_quest', 'Anyone needs help with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Braucht jemand Hilfe bei %quest_link?', '有人需要帮忙做%quest_link吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), -('suggest_quest', 'Need help with %quest_link? I got you.', 0, 0, '', 'Besoin d’aide avec %quest_link? Pas de souci, je suis là pour t’épauler.', 'Brauchst du Hilfe bei %quest_link? Ich helfe dir.', '需要帮忙做%quest_link吗?我来帮你!', '', '¿Necesitas ayuda con %quest_link? Yo te ayudo.', '', ''), -('suggest_quest', 'Looking to do %quest_link. Anyone in?', 0, 0, '', 'Je veux faire %quest_link. Qui veut se joindre à moi ?', 'Ich will %quest_link machen. Wer kommt mit?', '想做%quest_link,有人加入吗?', '', 'Busco hacer %quest_link. ¿Alguien se une?', '', ''), -('suggest_quest', 'Help wanted for %quest_link.', 0, 0, '', 'Besoin d’aide pour %quest_link.', 'Hilfe gesucht für %quest_link.', '需要帮忙做%quest_link!', '', 'Se busca ayuda para %quest_link.', '', ''), -('suggest_quest', 'Who is up for %quest_link?', 0, 0, '', 'Qui est partant pour %quest_link ?', 'Wer ist dabei für %quest_link?', '谁想一起做%quest_link?', '', '¿Quién se apunta a %quest_link?', '', ''), -('suggest_quest', 'Who wants to join for %quest_link?', 0, 0, '', 'Qui veut se joindre à moi pour %quest_link ?', 'Wer möchte sich für %quest_link anschließen?', '谁想加入做%quest_link?', '', '¿Quién quiere unirse a %quest_link?', '', ''), -('suggest_quest', 'Anyone up for %quest_link?', 0, 0, '', 'Qui est partant pour %quest_link ?', 'Hat jemand Lust auf %quest_link?', '有人想做%quest_link吗?', '', '¿Alguien quiere hacer %quest_link?', '', ''), -('suggest_quest', 'Need some help with %quest_link here.', 0, 0, '', 'Besoin d’aide avec %quest_link ici.', 'Brauche hier Hilfe mit %quest_link.', '这里需要帮忙做%quest_link。', '', 'Necesito ayuda con %quest_link.', '', ''), -('suggest_quest', 'Anyone for %quest_link?', 0, 0, '', 'Quelqu’un pour %quest_link ?', 'Jemand für %quest_link?', '有人一起做%quest_link吗?', '', '¿Alguien para %quest_link?', '', ''), -('suggest_quest', 'Who is ready to do %quest_link?', 0, 0, '', 'Qui est prêt à faire %quest_link ?', 'Wer ist bereit für %quest_link?', '谁准备好做%quest_link了?', '', '¿Quién está listo para %quest_link?', '', ''), -('suggest_quest', 'Looking for some %quest_link help.', 0, 0, '', 'Je cherche de l’aide pour %quest_link.', 'Suche Hilfe für %quest_link.', '找人帮忙做%quest_link。', '', 'Buscando ayuda para %quest_link.', '', ''), -('suggest_quest', 'Anyone need %quest_link help?', 0, 0, '', 'Besoin d’aide pour la quête %quest_link?', 'Braucht jemand Hilfe für die Quest %quest_link?', '有人需要%quest_link的帮助吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), -('suggest_quest', 'Who is down to complete %quest_link?', 0, 0, '', 'Qui est partant pour compléter la %quest_link?', 'Wer möchte %quest_link abschließen?', '谁想一起完成%quest_link?', '', '¿Quién se apunta a completar %quest_link?', '', ''), -('suggest_quest', 'Anyone need assistance for %quest_link?', 0, 0, '', 'Besoin d’aide pour %quest_link ?', 'Braucht jemand Unterstützung für %quest_link?', '有人需要%quest_link的协助吗?', '', '¿Alguien necesita asistencia con %quest_link?', '', ''), -('suggest_quest', 'Who can help with %quest_link?', 0, 0, '', 'Qui peut m’aider avec %quest_link?', 'Wer kann mit %quest_link helfen?', '谁能帮忙做%quest_link?', '', '¿Quién puede ayudar con %quest_link?', '', ''), -('suggest_quest', 'Looking for a hand with %quest_link.', 0, 0, '', 'Recherchde l aide pour %quest_link.', 'Suche Unterstützung für %quest_link.', '找人帮一把做%quest_link。', '', 'Buscando ayuda con %quest_link.', '', ''), -('suggest_quest', 'Anyone ready for %quest_link?', 0, 0, '', 'Qui est prêt pour %quest_link?', 'Wer ist bereit für %quest_link?', '有人准备好做%quest_link了吗?', '', '¿Alguien listo para %quest_link?', '', ''), -('suggest_quest', 'Can anyone join me for %quest_link?', 0, 0, '', 'Est-ce que quelqu’un peut se joindre à moi pour %quest_link?', 'Kann sich jemand für %quest_link anschließen?', '有人能和我一起做%quest_link吗?', '', '¿Alguien puede unirse para %quest_link?', '', ''), -('suggest_quest', 'Looking for some partners for %quest_link.', 0, 0, '', 'Je recherche des partenaires pour %quest_link.', 'Suche Mitstreiter für %quest_link.', '找几个伙伴一起做%quest_link。', '', 'Buscando compañeros para %quest_link.', '', ''), -('suggest_quest', 'Anyone want to team up for %quest_link?', 0, 0, '', 'Quelqu’un veut faire équipe pour %quest_link?', 'Will jemand ein Team für %quest_link bilden?', '有人想组队做%quest_link吗?', '', '¿Alguien quiere formar un equipo para %quest_link?', '', ''), -('suggest_quest', 'Who is willing to do %quest_link?', 0, 0, '', 'Qui est prêt à faire %quest_link ?', 'Wer ist bereit, %quest_link zu machen?', '谁愿意做%quest_link?', '', '¿Quién está dispuesto a hacer %quest_link?', '', ''), -('suggest_quest', 'Who needs %quest_link to be completed?', 0, 0, '', 'Qui a besoin que %quest_link soit complété ?', 'Wer muss %quest_link abschließen?', '谁需要完成%quest_link?', '', '¿Quién necesita completar %quest_link?', '', ''), -('suggest_quest', 'Looking to complete %quest_link.', 0, 0, '', 'Je cherche à accomplir la %quest_link.', 'Suche Gruppe, um %quest_link abzuschließen.', '想完成%quest_link,有人一起吗?', '', 'Buscando completar %quest_link.', '', ''), -('suggest_quest', 'Who can join me for %quest_link?', 0, 0, '', 'Qui peut se joindre à moi pour %quest_link ?', 'Wer kann sich mir für %quest_link anschließen?', '谁能和我一起做%quest_link?', '', '¿Quién puede unirse a %quest_link?', '', ''), -('suggest_quest', 'Anyone interested in completing %quest_link?', 0, 0, '', 'Qui est intéressé pour compléter la %quest_link ?', 'Hat jemand Interesse, %quest_link abzuschließen?', '有人想完成%quest_link吗?', '', '¿Alguien interesado en completar %quest_link?', '', ''), -('suggest_quest', 'Wanna help with %quest_link?', 0, 0, '', 'Tu veux de l’aide avec %quest_link?', 'Willst du bei %quest_link helfen?', '想帮我做%quest_link吗?', '', '¿Quieres ayudar con %quest_link?', '', ''), -('suggest_quest', 'Anyone willing to help with %quest_link?', 0, 0, '', '%quest_link ? Oh, vous avez besoin d’aide pour cette quête ? Je suis là pour vous aider !', 'Braucht jemand Hilfe bei %quest_link? Ich bin dabei!', '有人愿意帮忙做%quest_link吗?', '', '¿Alguien dispuesto a ayudar con %quest_link?', '', ''), -('suggest_quest', 'Need someone to finish %quest_link.', 0, 0, '', 'Besoin que quelqu’un termine %quest_link.', 'Brauche jemanden, der %quest_link abschließt.', '需要有人一起完成%quest_link。', '', 'Necesito a alguien para terminar %quest_link.', '', ''), -('suggest_quest', 'Anyone want to finish %quest_link?', 0, 0, '', 'Quelqu’un veut finir %quest_link ?', 'Möchte jemand %quest_link beenden?', '有人想一起完成%quest_link吗?', '', '¿Alguien quiere terminar %quest_link?', '', ''), -('suggest_quest', 'Any help with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Braucht jemand Hilfe bei %quest_link?', '有谁能帮忙做%quest_link?', '', '¿Necesito ayuda con %quest_link?', '', ''), -('suggest_quest', 'Anyone up for %quest_link?', 0, 0, '', 'Qui est partant pour %quest_link ?', 'Wer hat Lust auf %quest_link?', '有人想去做%quest_link吗?', '', '¿Alguien se apunta a %quest_link?', '', ''), -('suggest_quest', 'Looking for %quest_link completion partner.', 0, 0, '', 'Je recherche un partenaire pour compléter %quest_link.', 'Suche einen Partner, um %quest_link abzuschließen.', '找个伙伴一起完成%quest_link。', '', 'Buscando compañero para completar %quest_link.', '', ''), -('suggest_quest', 'Who is down to finish %quest_link?', 0, 0, '', 'Qui est partant pour finir la %quest_link?', 'Wer möchte %quest_link fertigstellen?', '谁愿意一起完成%quest_link?', '', '¿Quién se apunta a terminar %quest_link?', '', ''), -('suggest_quest', 'Help wanted for %quest_link.', 0, 0, '', 'Besoin d’aide pour %quest_link.', 'Hilfe gesucht für %quest_link.', '求助,需要帮忙做%quest_link。', '', 'Se busca ayuda para %quest_link.', '', ''), -('suggest_quest', 'Anyone interested in %quest_link today?', 0, 0, '', 'Quelqu’un intéressé par %quest_link aujourd’hui?', 'Hat jemand heute Interesse an %quest_link?', '今天有人对%quest_link感兴趣吗?', '', '¿Alguien interesado en %quest_link hoy?', '', ''), -('suggest_quest', 'Anyone doing %quest_link now?', 0, 0, '', 'Est-ce que quelqu’un est en train de faire %quest_link maintenant?', 'Macht gerade jemand %quest_link?', '现在有人在做%quest_link吗?', '', '¿Alguien haciendo %quest_link ahora?', '', ''), -('suggest_quest', 'Any takers for %quest_link now?', 0, 0, '', 'Des preneurs pour %quest_link maintenant?', 'Freiwillige für %quest_link jetzt?', '现在有人想做%quest_link吗?', '', '¿Alguien se apunta a %quest_link ahora?', '', ''), -('suggest_quest', 'Anyone wants to start %quest_link?', 0, 0, '', 'Qui veut commencer %quest_link?', 'Wer will %quest_link starten?', '有人想开始做%quest_link吗?', '', '¿Alguien quiere empezar %quest_link?', '', ''), -('suggest_quest', 'Need help with %quest_link please.', 0, 0, '', 'Besoin d’aide avec %quest_link s’il vous plaît.', 'Brauche Hilfe bei %quest_link, bitte.', '求帮忙做%quest_link,拜托了。', '', 'Necesito ayuda con %quest_link, por favor.', '', ''), -('suggest_quest', 'Who can help me with %quest_link?', 0, 0, '', 'Qui peut m’aider avec %quest_link?', 'Wer kann mir mit %quest_link helfen?', '谁能帮我做%quest_link?', '', '¿Quién puede ayudarme con %quest_link?', '', ''), -('suggest_quest', 'Looking to group for %quest_link.', 0, 0, '', 'Je cherche des compagnons pour %quest_link.', 'Suche eine Gruppe für %quest_link.', '找队伍一起做%quest_link。', '', 'Buscando un grupo para %quest_link.', '', ''), - --- random trade? --- usable placeholders: --- %my_role --- %category --- %my_class --- %my_race --- %my_level -('suggest_trade', 'Anyone to farm %category?', 0, 0, '', 'Quelqu’un pour farmer %category?', 'Jemand zum Farmen von %category?', '有人一起刷%category吗?', '', '¿Alguien para farmear %category?', '', ''), -('suggest_trade', 'Looking for help farming %category.', 0, 0, '', 'Je cherche de l’aide pour farm %category.', 'Suche Hilfe beim Farmen von %category.', '找人帮忙一起刷%category。', '', 'Buscando ayuda para farmear %category.', '', ''), -('suggest_trade', 'Damn %category are so expensive!', 0, 0, '', 'Ah mince, les %category sont tellement chers !', 'Verdammt, %category sind so teuer!', '靠,%category也太贵了吧!', '', '¡Malditas %category son tan caras!', '', ''), -('suggest_trade', 'Wanna %category.', 0, 0, '', 'Envie de %category.', 'Lust auf %category.', '想要%category。', '', 'Quiero %category.', '', ''), -('suggest_trade', 'Need help with %category.', 0, 0, '', 'Besoin d’aide avec %category.', 'Brauche Hilfe mit %category.', '需要帮忙搞%category。', '', 'Necesito ayuda con %category.', '', ''), -('suggest_trade', 'WTB %category.', 0, 0, '', 'Je cherche à acheter %category.', 'Kaufe %category.', '求购%category。', '', 'Quiero comprar %category.', '', ''), -('suggest_trade', 'Anyone interested in %category?', 0, 0, '', 'Quelqu’un intéressé par %category ?', 'Hat jemand Interesse an %category?', '有人对%category感兴趣吗?', '', '¿Alguien interesado en %category?', '', ''), -('suggest_trade', 'WTS %category.', 0, 0, '', 'Je vends %category.', 'Verkaufe %category.', '出售%category。', '', 'Quiero vender %category.', '', ''), -('suggest_trade', 'I am selling %category cheaper than AH.', 0, 0, '', 'Je vends %category moins cher que chez AH.', 'Verkaufe %category günstiger als im AH.', '我卖的%category比拍卖行便宜。', '', 'Estoy vendiendo %category más barato que en las subastas.', '', ''), -('suggest_trade', 'Who wants to farm %category?', 0, 0, '', 'Qui veut farm %category?', 'Wer will %category farmen?', '谁想一起刷%category?', '', '¿Quién quiere farmear %category?', '', ''), -('suggest_trade', 'Wanna farm %category.', 0, 0, '', 'Envie de farmer %category.', 'Lust auf %category Farmen.', '想去刷%category。', '', 'Quiero farmear %category.', '', ''), -('suggest_trade', 'Looking for party after %category.', 0, 0, '', 'Je cherche un group pour farm %category.', 'Suche Gruppe nach %category.', '找队伍去搞%category。', '', 'Buscando grupo después de %category.', '', ''), -('suggest_trade', 'Any %category are appreciated.', 0, 0, '', 'Toute %category est appréciée.', 'Jede %category ist willkommen.', '任何%category都欢迎。', '', 'Cualquier %category es apreciada.', '', ''), -('suggest_trade', 'Buying anything of %category.', 0, 0, '', 'Acheter toute votre %category.', 'Kaufe jegliche %category.', '收购所有%category。', '', 'Comprando algo de %category.', '', ''), -('suggest_trade', 'Wow, anybody is farming %category!', 0, 0, '', 'Oh là là, tout le monde farm le %category !', 'Wow, jeder farmt gerade %category!', '哇,有人正在刷%category!', '', '¡Guau, alguien está farmeando %category!', '', ''), -('suggest_trade', '%category are selling mad in the AH.', 0, 0, '', 'Les %category se vendent comme des petits pains à l’HDV.', '%category gehen im AH weg wie warme Semmeln.', '%category在拍卖行卖疯了。', '', '%category están vendiendo locos en las subastas.', '', ''), -('suggest_trade', 'AH is hot for %category.', 0, 0, '', 'AH c’est fan de %category.', 'AH läuft heiß für %category.', '拍卖行对%category很火爆。', '', 'Las subastas estan caliente para %category.', '', ''), -('suggest_trade', '%category are on the market.', 0, 0, '', 'Des %category sont disponibles sur le marché.', '%category sind auf dem Markt.', '%category市场上有了。', '', '%category están en el mercado.', '', ''), -('suggest_trade', 'Wanna trade some %category.', 0, 0, '', 'Tu veux échanger des %category ?', 'Will jemand %category tauschen?', '想换点%category吗?', '', 'Quiero intercambiar alguna %category.', '', ''), -('suggest_trade', 'Need more %category.', 0, 0, '', 'J’ai besoin de plus de %category.', 'Brauche mehr %category.', '需要更多的%category。', '', 'Necesito más %category.', '', ''), -('suggest_trade', 'Anybody can spare some %category?', 0, 0, '', 'Est-ce que quelqu’un pourrait me passer un peu de %category?', 'Kann jemand etwas %category entbehren?', '有人能匀点%category给我吗?', '', '¿Alguien puede ahorrar algo de %category?', '', ''), -('suggest_trade', 'Who wants %category?', 0, 0, '', 'Qui veut la %category ?', 'Wer will %category?', '谁想要%category?', '', '¿Quién quiere %category?', '', ''), -('suggest_trade', 'Some %category please?', 0, 0, '', 'Une %category s’il vous plaît ?', '%category bitte?', '能给点%category吗?', '', '¿Alguna %category, por favor?', '', ''), -('suggest_trade', 'I should have got skill for %category.', 0, 0, '', 'J’aurais dû avoir des compétences pour cette %category.', 'Ich hätte eine Fähigkeit für %category lernen sollen.', '我真该学点关于%category的技能。', '', 'Debería haber adquirido habilidad para %category.', '', ''), -('suggest_trade', 'I am dying for %category.', 0, 0, '', 'Je meurs d’envie de %category.', 'Ich sterbe für %category.', '我超级想要%category。', '', 'Me muero por %category.', '', ''), -('suggest_trade', 'People are killing for %category.', 0, 0, '', 'Les gens se tuent pour cette %category.', 'Die Leute bringen sich um für %category.', '大家都在抢着要%category。', '', 'La gente está matando por %category.', '', ''), -('suggest_trade', '%category is a great bargain!', 0, 0, '', 'La catégorie est une super affaire !', '%category ist ein echtes Schnäppchen!', '%category真是大便宜!', '', '%category es una gran ganga!', '', ''), -('suggest_trade', 'Everybody is mad for %category!', 0, 0, '', 'Tout le monde est fou de %category !', 'Alle sind verrückt nach %category!', '大家都为%category疯狂!', '', '¡Todo el mundo está loco por %category!', '', ''), -('suggest_trade', 'Where is the best place to farm for %category?', 0, 0, '', 'Quel est le meilleur endroit pour farm des %category?', 'Wo ist der beste Ort, um %category zu farmen?', '刷%category最好的地方是哪儿?', '', '¿Dónde está el mejor lugar para farmear para %category?', '', ''), -('suggest_trade', 'I am all set for %category.', 0, 0, '', 'Je suis prêt(e) pour la %category.', 'Ich bin bereit für %category.', '我已经准备好搞%category了。', '', 'Estoy listo para %category.', '', ''), -('suggest_trade', 'Is it good to sell %category?', 0, 0, '', 'Est-ce une bonne idée de vendre %category?', 'Lohnt es sich, %category zu verkaufen?', '现在卖%category合适吗?', '', '¿Es bueno vender %category?', '', ''), -('suggest_trade', 'I’d probably keep all my %category with me.', 0, 0, '', 'Je garderais probablement toutes mes %category avec moi.', 'Ich würde wahrscheinlich alle meine %category behalten.', '我大概会把所有%category留着。', '', 'Probablemente mantendría toda mi %category conmigo', '', ''), -('suggest_trade', 'Need group? Maybe to farm some %category?', 0, 0, '', 'Besoin d’un groupe ? Peut-être pour farmer un peu de %category ?', 'Brauche eine Gruppe? Vielleicht um %category zu farmen?', '需要组队吗?一起刷点%category怎么样?', '', '¿Necesitas un grupo? ¿Tal vez para farmear alguna %category?', '', ''), -('suggest_trade', 'I am still thinking about %category.', 0, 0, '', 'Je suis toujours en train de réfléchir à farm %category.', 'Ich denke immer noch über %category nach.', '我还在考虑%category的事。', '', 'Todavía estoy pensando en %category.', '', ''), -('suggest_trade', 'I heard about %category already, but my pockets are empty.', 0, 0, '', 'J’ai déjà entendu parler de %category, mais mes poches sont vides.', 'Ich habe schon von %category gehört, aber meine Taschen sind leer.', '我早就听说过%category,可惜没钱。', '', 'Ya escuché sobre %category, pero mis bolsillos están vacíos.', '', ''), -('suggest_trade', 'LFG for %category', 0, 0, '', 'Je cherche un groupe pour %category.', 'Suche Gruppe für %category.', '找队伍搞%category。', '', 'Busco grupo para %category', '', ''), -('suggest_trade', 'Would selling %category make me rich?', 0, 0, '', 'Est-ce que vendre %category me rendrait riche?', 'Würde der Verkauf von %category mich reich machen?', '卖%category能让我发财吗?', '', '¿Vender %category me haría rico?', '', ''), -('suggest_trade', 'OK. I an farming %category tomorrow.', 0, 0, '', 'D’accord. Demain, je vais grind %category.', 'OK. Morgen farme ich %category.', '好吧,明天我去刷%category。', '', 'OK. Tengo una sesion de farmeo de %category mañana.', '', ''), -('suggest_trade', 'Everyone is talking about %category.', 0, 0, '', 'Tout le monde parle de %category.', 'Alle reden über %category.', '大家都在讨论%category。', '', 'Todos hablan de %category', '', ''), -('suggest_trade', 'I saw at least ten people farming for %category.', 0, 0, '', 'J’ai vu au moins dix personnes qui farm %category.', 'Ich habe mindestens zehn Leute gesehen, die %category farmen.', '我看到至少十个人在刷%category。', '', 'Vi al menos diez personas farmeando para %category.', '', ''), -('suggest_trade', 'I sold all my %category yesterday. I am completely broke!', 0, 0, '', 'J’ai vendu toutes mes %category hier. Je suis complètement fauché !', 'Ich habe gestern all meine %category verkauft. Jetzt bin ich pleite!', '昨天我把所有%category都卖了,现在彻底破产了!', '', 'Ayer vendí todo mi %category. ¡Estoy completamente arruinado!', '', ''), -('suggest_trade', 'Wanna join a guild farming for %category.', 0, 0, '', 'Tu veux rejoindre une guilde qui farme %category.', 'Willst du einer Gilde beitreten, die %category farmt?', '想加入一个刷%category的公会。', '', 'Quiero unirme a una hermandad de farmeo para %category.', '', ''), -('suggest_trade', 'I am selling %category cheaper than AH.', 0, 0, '', 'Je vends %category moins cher qu’à l’AH.', 'Verkaufe %category günstiger als im AH.', '我卖的%category比拍卖行便宜。', '', 'Estoy vendiendo %category más barato que en las subastas.', '', ''), -('suggest_trade', 'Who wants to farm %category?', 0, 0, '', 'Qui veut faire du farming pour %category?', 'Wer will %category farmen?', '谁想一起刷%category?', '', '¿Quién quiere farmear %category?', '', ''), -('suggest_trade', 'Selling %category, anyone interested?', 0, 0, '', 'Vente %category, des intéressés ?', 'Verkaufe %category, jemand interessiert?', '卖%category,有人感兴趣吗?', '', 'Vendo %category, ¿alguien interesado?', '', ''), -('suggest_trade', 'Anyone needs %category?', 0, 0, '', 'Besoin de %category?', 'Braucht jemand %category?', '有人需要%category吗?', '', '¿Alguien necesita %category?', '', ''), -('suggest_trade', 'Selling %category, whisper me.', 0, 0, '', 'Vente de %category, wisp me.', 'Verkaufe %category, flüstert mich an.', '卖%category,私聊我。', '', 'Vendo %category, susúrrame.', '', ''), -('suggest_trade', 'Looking to trade %category for %category.', 0, 0, '', 'Je cherche à échanger %category contre %category.', 'Suche Tausch %category gegen %category.', '想用%category换%category。', '', 'Busco intercambiar %category por %category.', '', ''), -('suggest_trade', 'Need %category, anyone selling?', 0, 0, '', 'Besoin de %category, est-ce que quelqu’un en vend?', 'Brauche %category, verkauft jemand?', '需要%category,有人卖吗?', '', 'Necesito %category, ¿alguien vende?', '', ''), -('suggest_trade', 'Got %category for sale, hit me up!', 0, 0, '', 'J’ai %category à vendre, fais-moi signe!', 'Habe %category zu verkaufen, meldet euch!', '有%category卖,联系我!', '', '¡Tengo %category en venta, contáctame!', '', ''), -('suggest_trade', 'Anyone willing to farm %category?', 0, 0, '', '"Quelqu’un prêt à faire du farming %category?"', 'Ist jemand bereit, %category zu farmen?', '有人愿意一起刷%category吗?', '', '¿Alguien dispuesto a farmear %category?', '', ''), -('suggest_trade', 'Want to trade %category for %category.', 0, 0, '', 'Je veux échanger %category contre %category.', 'Will %category gegen %category tauschen.', '想用 %category 换 %category,有人感兴趣吗?', '', 'Quiero intercambiar %category por %category.', '', ''), -('suggest_trade', 'Selling %category, best price in the market.', 0, 0, '', 'Je vends %category, au meilleur prix du marché.', 'Verkaufe %category zum besten Preis am Markt.', '出售 %category,市场最低价!', '', 'Vendo %category, el mejor precio del mercado.', '', ''), -('suggest_trade', 'Looking for %category in exchange for %category.', 0, 0, '', 'Je recherche %category en échange de %category.', 'Suche %category im Austausch gegen %category.', '想找 %category,愿意用 %category 交换。', '', 'Busco %category a cambio de %category.', '', ''), -('suggest_trade', 'Anyone got %category to trade?', 0, 0, '', 'Quelqu’un a %category à échanger ?', 'Hat jemand %category zum Tauschen?', '有人有 %category 能换吗?', '', '¿Alguien tiene %category para intercambiar?', '', ''), -('suggest_trade', 'Who needs %category? I got some for sale!', 0, 0, '', 'Qui a besoin de %category? J’en ai à vendre!', 'Wer braucht %category? Ich habe etwas zu verkaufen!', '谁需要 %category?我有些要卖!', '', '¿Quién necesita %category? ¡Tengo algo para vender!', '', ''), -('suggest_trade', 'I have %category for sale, whisper for price.', 0, 0, '', 'J’ai %category à vendre, chuchotez-moi pour le prix.', 'Ich habe %category zu verkaufen, flüstert mich für den Preis an.', '我有 %category 要卖,私聊问价。', '', 'Tengo %category en venta, susúrrame para el precio.', '', ''), -('suggest_trade', 'Looking to sell %category, whisper for details.', 0, 0, '', 'Je cherche à vendre des %category, envoyez-moi un message pour plus de détails.', 'Suche Käufer für %category, flüstert mich für Details an.', '想卖 %category,私聊了解详情。', '', 'Busco vender %category, susúrrame para detalles.', '', ''), -('suggest_trade', 'WTB %category. Anyone selling?', 0, 0, '', 'Je recherche %category. Est-ce que quelqu’un en vend?', 'Kaufe %category. Verkauft jemand?', '求购 %category,有人卖吗?', '', 'Busco comprar %category. ¿Alguien vende?', '', ''), -('suggest_trade', 'I am looking for %category, please contact me.', 0, 0, '', 'Je recherche %category, n’hésitez pas à me contacter.', 'Suche %category, bitte kontaktiert mich.', '我在找 %category,有货请联系我。', '', 'Estoy buscando %category, por favor contáctame.', '', ''), -('suggest_trade', 'Any takers for %category?', 0, 0, '', 'Des intéressés pour %category ?', 'Interessiert sich jemand für %category?', '有人想要 %category 吗?', '', '¿Alguien interesado en %category?', '', ''), -('suggest_trade', 'Can anyone help farm %category?', 0, 0, '', 'Est-ce que quelqu’un peut aider à farmer %category?', 'Kann jemand helfen, %category zu farmen?', '有人能帮我刷 %category 吗?', '', '¿Alguien puede ayudar a farmear %category?', '', ''), -('suggest_trade', 'Anyone have %category to sell?', 0, 0, '', 'Est-ce que quelqu’un a %category à vendre?', 'Hat jemand %category zu verkaufen?', '有人有 %category 卖吗?', '', '¿Alguien tiene %category para vender?', '', ''), -('suggest_trade', 'Looking to buy %category, please message me.', 0, 0, '', 'Je cherche à acheter %category, n’hésitez pas à me contacter.', 'Suche %category zu kaufen, bitte Nachricht senden.', '想买 %category,有货请私我。', '', 'Busco comprar %category, por favor mándame un mensaje.', '', ''), -('suggest_trade', 'I am looking for %category. Who has it?', 0, 0, '', 'Je cherche %category. Qui l’a en stock?', 'Ich suche %category. Wer hat es auf Lager?', '我在找 %category,谁有货?', '', 'Estoy buscando %category. ¿Quién lo tiene?', '', ''), -('suggest_trade', 'WTB %category at a fair price.', 0, 0, '', 'Je recherche %category à un prix raisonnable.', 'Kaufe %category zu einem fairen Preis.', '求购 %category,价格合理就行。', '', 'Busco comprar %category a un precio justo.', '', ''), -('suggest_trade', 'Need %category urgently, anyone got it?', 0, 0, '', 'Besoin de %category de toute urgence, est-ce que quelqu’un en a ?', 'Brauche dringend %category, hat es jemand?', '急需 %category,有人有吗?', '', 'Necesito %category urgente, ¿alguien lo tiene?', '', ''), -('suggest_trade', 'Got %category, selling at a low price.', 0, 0, '', 'J’ai trouvé %category, vendu à petit prix.', 'Habe %category, verkaufe es günstig.', '有 %category,低价出售!', '', 'Tengo %category, vendo a un precio bajo.', '', ''), -('suggest_trade', 'Selling %category for a good deal.', 0, 0, '', 'Je vends des %category à bon prix.', 'Verkaufe %category zu einem guten Preis.', '出售 %category,价格很划算。', '', 'Vendo %category a buen precio.', '', ''), -('suggest_trade', 'I’m looking to buy %category, anyone selling cheap?', 0, 0, '', 'Je suis à la recherche de %category, est-ce que quelqu’un en vendrait à bon prix ?', 'Ich suche %category, verkauft es jemand günstig?', '想买 %category,有人便宜卖吗?', '', 'Busco comprar %category, ¿alguien lo vende barato?', '', ''), -('suggest_trade', 'Anyone selling %category? I’m interested.', 0, 0, '', 'Quelqu’un vend de %category ? Ça m’intéresse.', 'Verkauft jemand %category? Bin interessiert.', '有人卖 %category 吗?我感兴趣。', '', '¿Alguien vende %category? Estoy interesado.', '', ''), -('suggest_trade', 'Need %category, please whisper if selling.', 0, 0, '', 'Besoin de la %category, s’il vous plaît wisp me si vous vendez.', 'Brauche %category, bitte flüstert mich an, wenn ihr es verkauft.', '需要 %category,有卖的请私聊我。', '', 'Necesito %category, susúrrame si lo vendes.', '', ''), -('suggest_trade', 'Anyone got extra %category for trade?', 0, 0, '', 'Quelqu’un aurait un %category supplémentaire à échanger?', 'Hat jemand extra %category zum Tauschen?', '有人有多余的 %category 能换吗?', '', '¿Alguien tiene %category extra para intercambiar?', '', ''), -('suggest_trade', 'Looking for %category. Anyone selling?', 0, 0, '', 'À la recherche de %category. Quelqu’un vend-il ?', 'Suche %category. Verkauft jemand?', '找 %category,有人卖吗?', '', 'Busco %category. ¿Alguien lo vende?', '', ''), -('suggest_trade', 'Anyone interested in trading %category?', 0, 0, '', '%category trading, ça vous tente?', 'Hat jemand Interesse, %category zu tauschen?', '有人想换 %category 吗?', '', '¿Alguien interesado en intercambiar %category?', '', ''), -('suggest_trade', 'Selling %category, hit me up for a trade.', 0, 0, '', 'Je vends des %category, n’hésitez pas à me contacter pour faire un échange.', 'Verkaufe %category, meldet euch für einen Tausch.', '卖 %category,想换的找我。', '', 'Vendo %category, contáctame para un intercambio.', '', ''), -('suggest_trade', 'WTB %category, looking for good deals.', 0, 0, '', 'Je recherche des %category à acheter, je souhaite trouver de bonnes affaires.', 'Kaufe %category, suche nach guten Angeboten.', '求购 %category,找个好价格。', '', 'Busco comprar %category, buscando buenas ofertas.', '', ''), -('suggest_trade', 'Who needs %category? I have extras.', 0, 0, '', 'Qui a besoin de %category? J’ai des extras.', 'Wer braucht %category? Habe ein paar übrig.', '谁需要 %category?我有多余的。', '', '¿Quién necesita %category? Tengo extras.', '', ''), -('suggest_trade', 'Anyone wants %category? Selling for cheap.', 0, 0, '', 'Qui veut de la %category? Je vends à bas prix.', 'Wer will %category? Verkaufe günstig.', '有人要 %category 吗?便宜卖。', '', '¿Alguien quiere %category? Lo vendo barato.', '', ''), -('suggest_trade', 'Looking for %category, anyone selling?', 0, 0, '', 'Je recherche des %category, est-ce que quelqu’un en vend?', 'Suche %category, verkauft jemand?', '找 %category,有人卖吗?', '', 'Busco %category, ¿alguien lo vende?', '', ''), -('suggest_trade', 'Anyone need %category? I have some for trade.', 0, 0, '', 'Besoin de %category? J’en ai quelques-uns à échanger.', 'Braucht jemand %category? Habe einige zum Tauschen.', '有人需要 %category 吗?我有些可以换。', '', '¿Alguien necesita %category? Tengo algunos para intercambiar.', '', ''), -('suggest_trade', 'WTB %category. Anyone selling cheaper than AH?', 0, 0, '', 'Je recherche %category. Est-ce que quelqu’un vend moins cher que à l’HV?', 'Kaufe %category. Verkauft jemand günstiger als im AH?', '求购 %category,有人比拍卖行便宜卖吗?', '', 'Busco comprar %category. ¿Alguien lo vende más barato que en la subasta?', '', ''), -('suggest_trade', 'Selling %category cheap, whisper for details.', 0, 0, '', 'Vente de %category à petit prix, chuchotez pour plus de détails.', 'Verkaufe %category günstig, flüstert mich für Details an.', '低价卖 %category,私聊了解详情。', '', 'Vendo %category barato, susúrrame para detalles.', '', ''), -('suggest_trade', 'Anyone needs %category? I have some for sale.', 0, 0, '', 'Besoin de %category ? J’en ai en vente.', 'Braucht jemand %category? Habe einige zum Verkauf.', '有人需要 %category 吗?我有些要卖。', '', '¿Alguien necesita %category? Tengo algunos para vender.', '', ''), -('suggest_trade', 'Looking to trade %category for something else.', 0, 0, '', 'Je cherche à échanger %category contre autre chose.', 'Möchte %category gegen etwas anderes tauschen.', '想用 %category 换点别的东西。', '', 'Busco intercambiar %category por algo más.', '', ''), -('suggest_trade', 'Got %category, willing to trade it for %category.', 0, 0, '', 'J’ai %category, je serais prêt à l’échanger contre %category.', 'Habe %category, tausche es gegen %category.', '有 %category,愿意换成 %category。', '', 'Tengo %category, dispuesto a intercambiarlo por %category.', '', ''), -('suggest_trade', 'WTB %category, anyone has extras?', 0, 0, '', 'Je recherche %category, est-ce que quelqu’un en a en plus ?', 'Suche %category, hat jemand welche übrig?', '求购 %category,有人有多余的吗?', '', 'Busco comprar %category, ¿alguien tiene extras?', '', ''), - --- random faction rep grind --- usable placeholders: --- %my_role --- %rep_level --- %rndK --- %faction --- %my_class --- %my_race --- %my_level -('suggest_faction', 'Anyone farming %faction rep?', 0, 0, '', 'Qui ici farm la réputation de la faction %faction ?', 'Farmt jemand Ruf für %faction?', '有人在刷 %faction 的声望吗?', '', '¿Alguien farmea reputación con %faction?', '', ''), -('suggest_faction', 'Anyone help with %faction?', 0, 0, '', 'Besoin d’aide avec la réputation %faction?', 'Kann jemand bei %faction helfen?', '有人能帮我搞定 %faction 的声望吗?', '', '¿Alguien me ayuda con %faction?', '', ''), -('suggest_faction', 'Wanna quest for %faction.', 0, 0, '', 'Tu veux partir en quête pour la faction %faction?', 'Lust, Quests für %faction zu machen?', '想一起做 %faction 的任务吗?', '', 'Quiero buscar a %faction.', '', ''), -('suggest_faction', '%faction is the best.', 0, 0, '', '%faction est la meilleure.', '%faction ist die beste.', '%faction 真是最棒的!', '', '%faction es el mejor.', '', ''), -('suggest_faction', 'Need just a bit to be %rep_level with %faction.', 0, 0, '', 'J’ai juste besoin d’un peu pour être %rep_level avec %faction.', 'Brauche nur noch ein wenig für %rep_level bei %faction.', '还差一点就能跟 %faction 达到 %rep_level 了。', '', 'Solo necesito un poco para estar %rep_level con %faction.', '', ''), -('suggest_faction', 'Anyone got %rep_level with %faction?', 0, 0, '', 'Est-ce que quelqu’un a le niveau de réputation %rep_level avec %faction ?', 'Hat jemand schon %rep_level bei %faction?', '有人跟 %faction 达到 %rep_level 了吗?', '', '¿Alguien tiene %rep_level con %faction?', '', ''), -('suggest_faction', 'Who wants to be %rep_level with %faction?', 0, 0, '', 'Qui veut être %rep_level avec %faction?', 'Wer will %rep_level bei %faction erreichen?', '谁想跟 %faction 达到 %rep_level?', '', '¿Quién quiere estar en %rep_level con %faction?', '', ''), -('suggest_faction', 'I’ll never be %rep_level with %faction.', 0, 0, '', 'Je ne serai jamais %rep_level souhaité avec %faction.', 'Ich werde niemals %rep_level bei %faction erreichen.', '我恐怕永远也跟 %faction 达不到 %rep_level。', '', 'Nunca estaré %rep_level con %faction.', '', ''), -('suggest_faction', 'Someone missing %faction rep?', 0, 0, '', 'Quelqu’un manque de réputation auprès de %faction ?', 'Fehlt jemandem noch Ruf bei %faction?', '有人缺 %faction 的声望吗?', '', '¿Alguien falta reputación con %faction?', '', ''), -('suggest_faction', 'Could help farming %faction rep.', 0, 0, '', 'Je peux t’aider à augmenter ta réputation avec %faction.', 'Ich kann beim Farmen von %faction-Ruf helfen.', '我能帮忙刷 %faction 的声望。', '', 'Podría ayudar a farmear %faction rep.', '', ''), -('suggest_faction', 'The more rep the better. Especially with %faction.', 0, 0, '', 'Plus il y a de réput, mieux c’est. Surtout avec %faction', 'Je mehr Ruf, desto besser. Besonders bei %faction.', '声望越高越好,特别是跟 %faction 的。', '', 'Cuantas más repeticiones, mejor. Especialmente con %faction.', '', ''), -('suggest_faction', '%faction: need %rndK for %rep_level.', 0, 0, '', '%faction: J’ai besoin de %rndK pour %rep_level.', '%faction: Brauche %rndK für %rep_level.', '%faction:还差 %rndK 就能到 %rep_level。', '', '%faction: necesita %rndK para %rep_level.', '', ''), -('suggest_faction', 'Who can share %faction quests?', 0, 0, '', 'Qui peut partager les quêtes de %faction?', 'Wer kann %faction-Quests teilen?', '谁能分享一下 %faction 的任务?', '', '¿Quién puede compartir misiones de %faction?', '', ''), -('suggest_faction', 'Any dungeons for %faction?', 0, 0, '', 'Des donjons pour %faction ?', 'Gibt es Dungeons für %faction?', '有给 %faction 刷声望的副本吗?', '', '¿Alguna mazmorra para %faction?', '', ''), -('suggest_faction', 'Wanna do %faction rep grind.', 0, 0, '', 'Je veux monter ma réputation avec la %faction.', 'Ich will Ruf farmen für %faction.', '想一起刷 %faction 的声望吗?', '', 'Quiero hacer %faction rep grind.', '', ''), -('suggest_faction', 'Let’s farm %faction rep!', 0, 0, '', 'On va monter la réputation de la %faction !', 'Lass uns %faction-Ruf farmen!', '咱们一起刷 %faction 声望吧!', '', '¡Vamos a farmear reputación con %faction!', '', ''), -('suggest_faction', 'Farming for %faction rep.', 0, 0, '', 'Je fais du farm pour gagner de la réputation auprès de %faction.', 'Ich farme Ruf für %faction.', '正在刷 %faction 的声望。', '', 'Farming for %faction rep.', '', ''), -('suggest_faction', 'Wanna farm for %faction.', 0, 0, '', 'Envie de farmer pour %faction ?', 'Lust, %faction zu farmen?', '想刷 %faction 的声望吗?', '', 'Quiero farmear para %faction.', '', ''), -('suggest_faction', 'Need help with %faction.', 0, 0, '', 'Besoin d’aide avec %faction.', 'Brauche Hilfe mit %faction.', '需要帮忙搞定 %faction 的声望。', '', 'Necesito ayuda con %faction.', '', ''), -('suggest_faction', 'Is %faction sells something useful?', 0, 0, '', 'Est-ce que %faction vend quelque chose d’utile ?', 'Verkauft %faction etwas Nützliches?', '%faction 有卖什么有用的东西吗?', '', '¿%faction vende algo útil?', '', ''), -('suggest_faction', 'Are there %faction vendors?', 0, 0, '', 'Y a-t-il des vendeurs de %faction ?', 'Gibt es Händler für %faction?', '有 %faction 的商人吗?', '', '¿Hay %proveedores de facciones?', '', ''), -('suggest_faction', 'Who farms %faction?', 0, 0, '', 'Ah, qui farm %faction?', 'Wer farmt %faction?', '谁在刷 %faction 的声望?', '', '¿Quién farmea %faction?', '', ''), -('suggest_faction', 'Which is the best way to farm %faction?', 0, 0, '', 'Quelle est la meilleurs façon de farmer %faction ?', 'Was ist die beste Methode, um %faction zu farmen?', '刷 %faction 声望最好的方法是什么?', '', '¿Cuál es la mejor manera de farmear %faction?', '', ''), -('suggest_faction', 'I hate %faction rep grind.', 0, 0, '', 'Je déteste ce fichu grind de réputation pour %faction.', 'Ich hasse das Ruf-Farmen für %faction.', '我真是讨厌刷 %faction 的声望。', '', 'Odio %repetir de facción', '', ''), -('suggest_faction', 'I am so tired of %faction.', 0, 0, '', 'Je suis tellement fatigué de cette %faction.', 'Ich bin so müde von %faction.', '我真是受够了 %faction。', '', 'Estoy tan cansado de %faction.', '', ''), -('suggest_faction', 'Go for %faction?', 0, 0, '', 'Allez pour la %faction!', 'Gehen wir für %faction?', '去刷 %faction 吗?', '', '¿Quereis ir a por %faction?', '', ''), -('suggest_faction', 'Seems everyone is %rep_level with %faction. Only me is late as usually.', 0, 0, '', 'Il semble que tout le monde soit au niveau %rep_level avec la faction %faction. Et bien sûr, je suis en retard comme d’habitude.', 'Sieht so aus, als wären alle %rep_level bei %faction. Nur ich bin wie immer zu spät.', '好像大家都跟 %faction 达到 %rep_level 了,就我一如既往地落后。', '', 'Parece que todo el mundo está %rep_level con %faction. Solo yo llego tarde como siempre.', '', ''), -('suggest_faction', 'LFG for %faction rep grind?', 0, 0, '', 'Je recherche un groupe pour %faction rep grind !', 'Suche Gruppe für %faction Ruf-Farm!', '找人一起刷 %faction 的声望吗?', '', 'Busco grupo para subir reputación con %faction', '', ''), -('suggest_faction', 'Can anobody suggest a good spot for %faction rep grind?', 0, 0, '', 'Est-ce que quelqu’un peut me recommander un bon endroit pour gagner de la réputation avec %faction?', 'Kann jemand einen guten Spot für %faction Ruf-Farm empfehlen?', '有谁能推荐个刷 %faction 声望的好地方?', '', '¿Alguien puede sugerir un buen lugar farmear reputacion con %faction?', '', ''), -('suggest_faction', 'Would %faction rep benefit me?', 0, 0, '', 'Est-ce que la réputation de %faction me sera bénéfique ?', 'Bringt mir der Ruf bei %faction etwas?', '刷 %faction 的声望对我有好处吗?', '', '¿Me beneficiaría subir reputación con %faction?', '', ''), -('suggest_faction', 'Who would’ve thought that %faction rep will be useful after all...', 0, 0, '', 'Qui aurait pensé que cette réputation %faction serait finalement utile...', 'Wer hätte gedacht, dass Ruf bei %faction doch nützlich ist...', '谁能想到 %faction 的声望竟然还真有用……', '', 'Quién hubiera pensado que subir reputacion con %faction sería útil después de todo...', '', ''), -('suggest_faction', 'I wanna be exalted with all factions, starting with %faction.', 0, 0, '', 'Je veux être honoré par toutes les factions, commençant par %faction.', 'Ich will bei allen Fraktionen ehrfürchtig sein, angefangen mit %faction.', '我想跟所有阵营都达到崇拜,先从 %faction 开始!', '', 'Quiero ser exaltado con todas las facciones, comenzando con %faction.', '', ''), -('suggest_faction', 'Is there any point to improve my rep with %faction?', 0, 0, '', 'Est-ce que ça vaut la peine d’améliorer ma réputation avec la faction %faction ?', 'Lohnt es sich, meinen Ruf bei %faction zu verbessern?', '提高 %faction 的声望有意义吗?', '', '¿Hay algún punto para mejorar mi reputación con %faction?', '', ''), -('suggest_faction', 'What is better for %faction? Quests or mob grinding?', 0, 0, '', 'Pour %faction, il vaut mieux grind des mobs ou faire des quêtes ?', 'Was ist besser für %faction? Quests oder Mob-Farming?', '刷 %faction 声望,做任务好还是刷怪好?', '', '¿Qué es mejor para %faction? ¿Misiones o mafiosos?', '', ''), -('suggest_faction', 'Will grind %faction rep for you. Just give me some gold.', 0, 0, '', 'Je vais augmenter la réputation de %faction pour toi. Il me suffit de me donner un peu d’or.', 'Ich farme %faction-Ruf für dich. Gib mir einfach etwas Gold.', '我可以帮你刷 %faction 声望,给点金币就行。', '', 'Ganare reputacion con %faction por ti. Sólo dame un poco de oro.', '', ''), -('suggest_faction', 'I think grinding rep with %faction would take forever.', 0, 0, '', 'Je pense que monter ma réputation avec %faction prendrait une éternité.', 'Ich glaube, Ruf farmen für %faction dauert ewig.', '我觉得刷 %faction 声望得花一辈子时间。', '', 'Creo que subir la reputación con %faction tomaría una eternidad.', '', ''), -('suggest_faction', 'I am killing for %faction every day now but still far from %rep_level.', 0, 0, '', 'Je fais des missions pour la %faction tous les jours, mais je suis encore loin du niveau de réputation %rep_level.', 'Ich töte für %faction jeden Tag, aber bin noch weit entfernt von %rep_level.', '我现在每天都在为 %faction 杀怪,但离 %rep_level 还差得远。', '', 'Estoy matando por %faction todos los días ahora, pero aún estoy lejos del %rep_level.', '', ''), -('suggest_faction', 'At %my_level AH deposits will decrease, right?', 0, 0, '', 'À mon niveau %my_level, les dépôts AH vont diminuer, n’est-ce pas ?', 'Auf %my_level werden die Auktionshausgebühren sinken, oder?', '到 %my_level 拍卖行的手续费会降低,对吧?', '', 'Al %rep_level, los depósitos subastas disminuirán, ¿verdad?', '', ''), -('suggest_faction', 'How many exalted reps do you have?', 0, 0, '', 'Combien de représentants exaltés avez-vous?', 'Wie viele ehrfürchtige Rufe hast du?', '你有多少个崇拜的声望?', '', '¿Cuántas reputaciones exaltadas tienes?', '', ''), -('suggest_faction', 'Who wants to be %my_level with %faction?', 0, 0, '', 'Qui veut être %my_level avec %faction?', 'Wer will %my_level bei %faction erreichen?', '有谁想跟我一起把 %faction 刷到 %my_level?', '', '¿Quién quiere estar en %rep_level con %faction?', '', ''), -('suggest_faction', 'Damn. My guild did a good %faction grind yesterday without me.', 0, 0, '', 'Mince. Mon groupe a bien grindé la réputation %faction hier sans moi.', 'Verdammt. Meine Gilde hat gestern ordentlich %faction gefarmt ohne mich.', '靠!我的公会昨天没带我去刷 %faction 声望,刷得还挺好。', '', 'Maldición. Mi hermandad hizo un buen farmeo de %faction ayer sin mí.', '', ''), -('suggest_faction', 'Nobody wants to help me because I am %rep_level with %faction.', 0, 0, '', 'Personne ne veut m’aider parce que je suis %rep_level avec %faction.', 'Niemand will mir helfen, weil ich %rep_level bei %faction bin.', '没人愿意帮我,就因为我跟 %faction 才 %rep_level。', '', 'Nadie quiere ayudarme porque estoy %rep_level con %faction.', '', ''), -('suggest_faction', 'Please stay away from %faction.', 0, 0, '', 'S’il te plaît, garde tes distances avec %faction.', 'Bitte halte dich von %faction fern.', '请离 %faction 远点。', '', 'Por favor manténgase alejado de %faction.', '', ''), -('suggest_faction', 'Anyone farming %faction rep today?', 0, 0, '', 'Vous êtes en train de farmer la réputation de %faction aujourd’hui ?', 'Farmt heute jemand %faction-Ruf?', '今天有谁在刷 %faction 声望吗?', '', '¿Alguien farmea reputación de %faction hoy?', '', ''), -('suggest_faction', 'Help me grind %faction rep!', 0, 0, '', 'Aide-moi à augmenter ma réputation avec la faction %faction!', 'Hilf mir, %faction-Ruf zu farmen!', '帮我刷 %faction 声望吧!', '', '¡Ayúdame a subir mi reputación con %faction!', '', ''), -('suggest_faction', 'I need %rep_level with %faction so badly!', 0, 0, '', 'J’ai tellement besoin d’atteindre le niveau %rep_level avec la faction %faction !', 'Ich brauche unbedingt %rep_level bei %faction!', '我急需把 %faction 刷到 %rep_level!', '', '¡Necesito %rep_level con %faction con urgencia!', '', ''), -('suggest_faction', 'Is %faction grind worth it?', 0, 0, '', 'Est-ce que le farm de %faction vaut vraiment la peine?', 'Lohnt sich das Farmen für %faction?', '刷 %faction 声望值得吗?', '', '¿Vale la pena el farmeo de %faction?', '', ''), -('suggest_faction', 'Anyone know %faction rep farm spots?', 0, 0, '', 'Je connais des endroits pour %faction pour augmenter ma réputation.', 'Kennt jemand gute Spots zum %faction-Ruf farmen?', '有谁知道刷 %faction 声望的好地方吗?', '', '¿Alguien conoce lugares para farmear reputación con %faction?', '', ''), -('suggest_faction', 'I love grinding %faction rep!', 0, 0, '', 'J’adore augmenter ma réputation auprès de la faction %!', 'Ich liebe es, %faction-Ruf zu farmen!', '我超爱刷 %faction 声望!', '', '¡Me encanta hacer grind de reputación con %faction!', '', ''), -('suggest_faction', 'Need help to finish %faction rep grind!', 0, 0, '', 'Besoin d’aide pour finir ma montée de réputation avec %faction!', 'Brauche Hilfe, um meinen %faction-Ruf-Farm abzuschließen!', '需要帮忙把 %faction 声望刷完!', '', '¡Necesito ayuda para terminar el grind de reputación de %faction!', '', ''), -('suggest_faction', 'Only a few more %rep_level with %faction to go!', 0, 0, '', 'Il ne me reste que quelques niveaux de réputation à faire avec %faction !', 'Nur noch ein paar %rep_level bei %faction zu erreichen!', '只差一点就能把 %faction 刷到 %rep_level 了!', '', '¡Solo me faltan unos %rep_level para llegar a %faction!', '', ''), -('suggest_faction', 'Anyone up for %faction grind today?', 0, 0, '', 'Quelqu’un est motivé pour farmer la réputation de la %faction aujourd’hui?', 'Hat heute jemand Lust auf %faction-Ruf farmen?', '今天有人想一起刷 %faction 声望吗?', '', '¿Alguien se anima al farmeo de %faction hoy?', '', ''), -('suggest_faction', 'Looking for %faction rep grind buddies!', 0, 0, '', 'À la recherche de compagnons de réputation de %faction! Qui est partant pour monter les réputations ensemble?', 'Suche Mitstreiter für %faction-Ruf farmen!', '找几个一起刷 %faction 声望的小伙伴!', '', 'Busco compañeros para farmear %faction.', '', ''), -('suggest_faction', 'I’m tired of grinding %faction rep!', 0, 0, '', 'J’en ai marre de faire monter ma réputation auprès de la faction %faction !', 'Ich habe genug vom %faction-Ruf farmen!', '我刷 %faction 声望刷到累死了!', '', '¡Estoy cansado de hacer grind de %faction!', '', ''), -('suggest_faction', 'Anyone on %faction rep grind too?', 0, 0, '', 'Oh, quelqu’un d’autre qui est en train de monter sa réputation avec %faction ?', 'Ist noch jemand dabei, %faction-Ruf zu farmen?', '还有人在刷 %faction 声望吗?', '', '¿Alguien más está haciendo el grind de %faction?', '', ''), -('suggest_faction', 'Time to hit %faction rep grind hard!', 0, 0, '', 'C’est le moment de faire le grind %faction sérieusement !', 'Zeit, den %faction-Ruf richtig zu farmen!', '是时候认真刷 %faction 声望了!', '', '¡Es hora de darle duro al grind de %faction!', '', ''), -('suggest_faction', 'Help me finish %faction rep!', 0, 0, '', 'Aide-moi à terminer ma réputation de faction !', 'Hilf mir, den %faction-Ruf abzuschließen!', '帮我把 %faction 声望刷完吧!', '', '¡Ayúdame a completar la reputación de %faction!', '', ''), -('suggest_faction', 'Need just %rndK more for %faction rep!', 0, 0, '', 'Il ne me manque plus que %rndK pour augmenter ma réputation auprès de la faction!', 'Brauche nur noch %rndK für den %faction-Ruf!', '只差 %rndK 就能提升 %faction 声望了!', '', '¡Solo me faltan %rndK para la reputación de %faction!', '', ''), -('suggest_faction', 'Grinding %faction rep all day!', 0, 0, '', 'À farmer la réputation de la faction toute la journée !', 'Den ganzen Tag %faction-Ruf farmen!', '今天一整天都在刷 %faction 声望!', '', '¡Farmearé la reputación de %faction todo el día!', '', ''), -('suggest_faction', 'Almost at %rep_level with %faction!', 0, 0, '', 'Je suis presque au niveau %rep_level avec la faction %faction !', 'Fast %rep_level bei %faction erreicht!', '快到 %faction 的 %rep_level 了!', '', '¡Casi al %rep_level con %faction!', '', ''), -('suggest_faction', '%faction rep grind never ends!', 0, 0, '', 'La réputation de faction, c’est jamais fini !', 'Das %faction-Ruf farmen nimmt kein Ende!', '%faction 声望刷到天荒地老也刷不完!', '', '¡El grind de reputación de %faction nunca termina!', '', ''), -('suggest_faction', 'Help with %faction daily quests?', 0, 0, '', 'Besoin d’aide avec les quêtes journalières de %faction ?', 'Brauche Hilfe mit den täglichen Quests für %faction!', '有谁能帮我做 %faction 的日常任务?', '', '¿Ayuda con las misiones diarias de %faction?', '', ''), -('suggest_faction', 'Who’s on %faction grind right now?', 0, 0, '', 'Qui est en train de faire du %faction en ce moment ?', 'Wer farmt gerade %faction-Ruf?', '谁现在正在刷 %faction 声望?', '', '¿Quién está farmeando %faction ahora?', '', ''), -('suggest_faction', 'Let’s do %faction quests together!', 0, 0, '', 'Faisons des quêtes de faction ensemble!', 'Lasst uns zusammen %faction-Quests machen!', '一起去做 %faction 的任务吧!', '', '¡Hagamos las misiones de %faction juntos!', '', ''), -('suggest_faction', 'Anyone farming %faction rep today?', 0, 0, '', 'Quelqu’un farme la réputation de la faction %faction aujourd’hui?', 'Farmt heute jemand %faction-Ruf?', '今天有人刷 %faction 声望吗?', '', '¿Alguien está farmeando reputación con %faction hoy?', '', ''), -('suggest_faction', 'Need %faction rep help for %rep_level!', 0, 0, '', 'Besoin d’aide pour la réputation de %faction %rep_level %rep_level !', 'Brauche Hilfe für %faction-Ruf bis %rep_level!', '需要帮我把 %faction 声望刷到 %rep_level!', '', '¡Necesito ayuda con la reputación de %faction para %rep_level!', '', ''), -('suggest_faction', 'Who wants to grind %faction rep with me?', 0, 0, '', 'Qui veut faire monter notre réputation auprès de %faction avec moi?', 'Wer will mit mir %faction-Ruf farmen?', '有谁想跟我一起刷 %faction 声望?', '', '¿Quién quiere farmeo de %faction conmigo?', '', ''), -('suggest_faction', 'Come join me for %faction rep grind!', 0, 0, '', 'Viens me rejoindre pour %faction rep grind!', 'Komm, schließ dich mir beim %faction-Ruf farmen an!', '快来跟我一起刷 %faction 声望!', '', '¡Ven a unirte a mí para el grind de reputación de %faction!', '', ''), -('suggest_faction', 'On a roll with %faction rep grind!', 0, 0, '', 'En plein dans la montée de réputation de la %faction !', 'Bin voll dabei beim %faction-Ruf farmen!', '刷 %faction 声望刷得正起劲!', '', '¡Estoy avanzando mucho con el grind de %faction!', '', ''), -('suggest_faction', 'Anyone want %faction rep grind boosts?', 0, 0, '', 'Qui est intéressé par des boosts pour monter sa réputation auprès de la faction ?', 'Wer braucht einen Boost für %faction-Ruf?', '有谁需要我帮忙加速刷 %faction 声望?', '', '¿Alguien quiere ayudas para el grind de %faction?', '', ''), -('suggest_faction', 'Just hit %rep_level with %faction!', 0, 0, '', 'Je viens d’atteindre le niveau de réputation %rep_level avec la faction %faction !', 'Gerade %rep_level bei %faction erreicht!', '刚把 %faction 刷到 %rep_level!', '', '¡Acabo de alcanzar %rep_level con %faction!', '', ''), -('suggest_faction', '%faction is so close to %rep_level, help me out!', 0, 0, '', 'La faction est tellement proche du niveau de réputation, aidez-moi !', '%faction ist so nah an %rep_level, helft mir!', '%faction 声望离 %rep_level 就差一点,帮帮我!', '', '¡%faction está tan cerca de %rep_level, ayúdame!', '', ''), -('suggest_faction', 'Wanna help me grind %faction rep?', 0, 0, '', 'Tu veux m’aider à monter ma réputation %faction ?', 'Willst du mir helfen, %faction-Ruf zu farmen?', '想帮我刷 %faction 声望吗?', '', '¿Quieres ayudarme a farmear reputación de %faction?', '', ''), -('suggest_faction', 'Farming %faction rep again!', 0, 0, '', 'Encore en train de gagner de la réputation avec %faction grâce à l’agriculture !', 'Schon wieder %faction-Ruf farmen!', '又在刷 %faction 声望了!', '', '¡Farmear reputación de %faction otra vez!', '', ''), -('suggest_faction', 'Anyone know a good spot for %faction rep?', 0, 0, '', 'Est-ce que quelqu’un sait un bon endroit pour augmenter ma réputation avec %faction?', 'Kennt jemand einen guten Spot für %faction-Ruf?', '有谁知道刷 %faction 声望的好地方?', '', '¿Alguien conoce un buen lugar para farmear %faction?', '', ''), -('suggest_faction', 'How much more for %faction rep grind?', 0, 0, '', 'Combien de plus pour monter ma réputation avec la faction %faction ?', 'Wie viel fehlt noch für den %faction-Ruf?', '刷 %faction 声望还差多少?', '', '¿Cuánto más para terminar el grind de %faction?', '', ''), -('suggest_faction', 'Anyone doing %faction quests with me?', 0, 0, '', '%Quelqu’un pour faire des quêtes de faction avec moi ?', 'Wer macht %faction-Quests mit mir?', '有谁跟我一起做 %faction 的任务?', '', '¿Alguien haciendo misiones de %faction conmigo?', '', ''), -('suggest_faction', 'Looking for %rep_level %faction rep grind!', 0, 0, '', 'Je recherche un moyen d’augmenter ma réputation avec la faction %faction de niveau %rep_level !', 'Suche eine Möglichkeit, %rep_level bei %faction zu farmen!', '找人一起刷 %faction 声望到 %rep_level!', '', 'Busco %rep_level para el grind de %faction!', '', ''), -('suggest_faction', 'Only need %rndK more %faction rep!', 0, 0, '', 'J’ai seulement besoin de %rndK de plus de réputation avec %faction!', 'Brauche nur noch %rndK für %faction-Ruf!', '只差 %rndK 就能提升 %faction 声望了!', '', '¡Solo necesito %rndK más de reputación con %faction!', '', ''), -('suggest_faction', '%faction grind is real!', 0, 0, '', 'La course à la réputation est intense!', 'Der %faction-Ruf grind ist real!', '刷 %faction 声望真是累死人!', '', '¡El grind de %faction es real!', '', ''), -('suggest_faction', 'Anyone farming %faction rep for %rep_level?', 0, 0, '', '%Est-ce que quelqu’un fait de l’agriculture de réputation de faction pour atteindre le niveau de réputation %rep_level?', 'Farmt jemand %faction-Ruf für %rep_level?', '有谁在刷 %faction 声望到 %rep_level?', '', '¿Alguien farmeando %faction para %rep_level?', '', ''), -('suggest_faction', 'Let’s do %faction rep grind together!', 0, 0, '', 'Hey, on va augmenter notre réputation avec la %faction ensemble !', 'Lasst uns gemeinsam %faction-Ruf farmen!', '一起刷 %faction 声望吧!', '', '¡Hagamos juntos el grind de reputación de %faction!', '', ''), -('suggest_faction', 'More %faction rep, more rewards!', 0, 0, '', 'Plus de réputation auprès de la faction, plus de récompenses !', 'Mehr %faction-Ruf, mehr Belohnungen!', '刷 %faction 声望越多,奖励越多!', '', '¡Más reputación de %faction, más recompensas!', '', ''), -('suggest_faction', 'Farming %faction rep with %my_class', 0, 0, '', 'Je monte ma réputation avec la faction agricole en jouant avec ma classe.', 'Farme %faction-Ruf mit %my_class.', '用我的 %my_class 刷 %faction 声望!', '', 'Farmeando reputación con %faction con %my_class', '', ''), -('suggest_faction', 'Anyone else stuck at %rep_level with %faction?', 0, 0, '', 'Je suis coincé au niveau %rep_level avec la faction %faction. Vous aussi?', 'Hängt noch jemand bei %rep_level mit %faction fest?', '还有人卡在 %faction 的 %rep_level 吗?', '', '¿Alguien más atascado en %rep_level con %faction?', '', ''), -('suggest_faction', 'Doing %faction rep grind all night long!', 0, 0, '', 'À enchaîner les quêtes pour ma réputation auprès de la faction toute la nuit !', 'Die ganze Nacht %faction-Ruf farmen!', '通宵刷 %faction 声望!', '', '¡Voy a farmear reputación de %faction toda la noche!', '', ''), -('suggest_faction', 'Who wants to finish %faction rep grind with me?', 0, 0, '', 'Qui veut finir la réputation de %faction avec moi ?', 'Wer will den %faction-Ruf grind mit mir abschließen?', '有谁想跟我一起把 %faction 声望刷完?', '', '¿Quién quiere terminar el grind de %faction conmigo?', '', ''), -('suggest_faction', 'I can help with %faction rep grind!', 0, 0, '', 'Je peux t’aider avec la montée de réputation de %faction!', 'Ich kann beim %faction-Ruf grind helfen!', '我可以帮你刷 %faction 声望!', '', '¡Puedo ayudar con el grind de reputación de %faction!', '', ''), -('suggest_faction', 'I’m ready for more %faction rep grind!', 0, 0, '', 'Je suis prêt à gagner davantage de réputation auprès de la %faction !', 'Bin bereit für mehr %faction-Ruf farmen!', '我准备好继续刷 %faction 声望了!', '', '¡Estoy listo para más grind de reputación de %faction!', '', ''), -('suggest_faction', 'Wanna help with %faction rep? I need it bad!', 0, 0, '', 'Tu veux de l’aide pour %faction rep? J’en ai vraiment besoin!', 'Willst du mir bei %faction-Ruf helfen? Ich brauche es dringend!', '能帮我刷 %faction 声望吗?我真的很需要!', '', '¿Quieres ayudarme con %faction rep? ¡Lo necesito mucho!', '', ''), -('suggest_faction', 'I need %rndK more for %faction rep!', 0, 0, '', 'Il me manque %rndK de plus pour augmenter ma réputation avec %faction !', 'Ich brauche noch %rndK für %faction-Ruf!', '我还差 %rndK 就能提升 %faction 声望!', '', '¡Me faltan %rndK para la reputación de %faction!', '', ''), -('suggest_faction', 'Farm with me for %faction rep!', 0, 0, '', 'Viens faire de l’agriculture avec moi pour gagner de la réputation auprès de ma faction!', 'Farmt mit mir für %faction-Ruf!', '跟我一起刷 %faction 声望吧!', '', '¡Farmea conmigo para reputación de %faction!', '', ''), -('suggest_faction', 'I just need %rndK more %faction rep!', 0, 0, '', 'Il ne me manque plus que %rndK de réputation avec %faction!', 'Ich brauche nur noch %rndK für %faction-Ruf!', '我只差 %rndK 就能提升 %faction 声望了!', '', '¡Solo necesito %rndK más de %faction!', '', ''), -('suggest_faction', 'Doing %faction rep grind is never-ending...', 0, 0, '', 'Faire le farming de réputation pour la %faction n’en finit jamais...', '%faction-Ruf farmen hört einfach nie auf...', '刷 %faction 声望简直没完没了……', '', 'El grind de %faction nunca termina...', '', ''), -('suggest_faction', 'Anyone got %faction rep boost items?', 0, 0, '', 'Est-ce que quelqu’un a des objets de boost de réputation de %faction ?', 'Hat jemand Ruf-Boost-Gegenstände für %faction?', '有谁有加速 %faction 声望的道具?', '', '¿Alguien tiene objetos para boostear reputación de %faction?', '', ''), -('suggest_faction', 'I need %rep_level %faction to unlock cool rewards!', 0, 0, '', 'J’ai besoin d’atteindre le niveau %rep_level auprès de la faction %faction pour débloquer des récompenses géniales!', 'Ich brauche %rep_level bei %faction, um coole Belohnungen freizuschalten!', '我需要把 %faction 刷到 %rep_level 才能解锁超酷的奖励!', '', '¡Necesito %rep_level de %faction para desbloquear buenas recompensas!', '', ''), -('suggest_faction', 'Anyone doing %faction rep for mounts?', 0, 0, '', 'Qui est en train de monter sa réputation avec %faction pour les montures?', 'Wer farmt %faction-Ruf für Mounts?', '有谁在刷 %faction 声望是为了坐骑?', '', '¿Alguien farmeando reputación de %faction para monturas?', '', ''), -('suggest_faction', 'Any chance to farm %faction rep with me?', 0, 0, '', 'Tu veux farmer ta réputation avec la faction %faction avec moi?', 'Lust, mit mir %faction-Ruf zu farmen?', '有机会跟我一起刷 %faction 声望吗?', '', '¿Algún chance de farmear reputación de %faction conmigo?', '', ''), -('suggest_faction', 'I am so close to %rep_level with %faction!', 0, 0, '', 'Je suis tellement proche du niveau %rep_level avec la faction %faction !', 'Ich bin so nah an %rep_level bei %faction!', '我离 %faction 的 %rep_level 就差一点了!', '', '¡Estoy tan cerca de %rep_level con %faction!', '', ''), -('suggest_faction', 'Help me get %rep_level with %faction!', 0, 0, '', 'Aide-moi à atteindre le niveau de réputation %rep_level avec %faction !', 'Hilf mir, %rep_level bei %faction zu erreichen!', '帮我把 %faction 声望刷到 %rep_level!', '', '¡Ayúdame a conseguir %rep_level con %faction!', '', ''), -('suggest_faction', 'Getting %faction rep grind done!', 0, 0, '', 'Je suis en train de grinder ma réputation avec la faction %faction !', 'Ich bin dabei, den %faction-Ruf grind abzuschließen!', '正在努力刷完 %faction 声望!', '', '¡Estoy terminando el grind de reputación de %faction!', '', ''), -('suggest_faction', 'I’m in need of %faction rep grinding help!', 0, 0, '', 'J’ai besoin d’aide pour augmenter ma réputation avec %faction !', 'Ich brauche Hilfe beim %faction-Ruf farmen!', '我需要帮手刷 %faction 声望!', '', '¡Necesito ayuda con el grind de %faction!', '', ''), -('suggest_faction', 'Need %rep_level %faction for the next rewards!', 0, 0, '', 'J’ai besoin d’atteindre le niveau %rep_level pour %faction afin de débloquer les prochaines récompenses !', 'Brauche %rep_level bei %faction für die nächsten Belohnungen!', '我得把 %faction 的声望刷到 %rep_level 才能拿到下个奖励!', '', '¡Necesito %rep_level de %faction para las siguientes recompensas!', '', ''), -('suggest_faction', 'Who can farm %faction rep with me?', 0, 0, '', 'Qui peut farmer la réputation de %faction avec moi ?', 'Wer kann mit mir %faction-Ruf farmen?', '有谁能跟我一起刷 %faction 的声望啊?', '', '¿Quién puede farmear la reputación de %faction conmigo?', '', ''), -('suggest_faction', 'Grinding %faction rep non-stop!', 0, 0, '', 'En train de farmer la réputation de %faction sans arrêt !', 'Ununterbrochen %faction-Ruf farmen!', '一直在拼命刷 %faction 声望,停不下来!', '', '¡Grind de %faction sin parar!', '', ''), -('suggest_faction', 'Who else is grinding %faction rep right now?', 0, 0, '', 'Je suis en train de bosser le rang de réputation de %faction en ce moment. Qui d’autre est dans le même cas ?', 'Wer farmt gerade auch %faction-Ruf?', '现在还有谁在刷 %faction 的声望?', '', '¿Quién más está farmeando %faction ahora?', '', ''), -('suggest_faction', 'Anyone want to farm %faction rep for fun?', 0, 0, '', 'Qui est partant pour monter la réputation de la faction %faction pour le plaisir ?', 'Wer hat Lust, zum Spaß %faction-Ruf zu farmen?', '有没人想一起刷 %faction 声望,纯属娱乐?', '', '¿Alguien quiere farmear %faction por diversión?', '', ''), -('suggest_faction', 'Need %faction rep grind to unlock new quests!', 0, 0, '', 'Il me faut améliorer ma réputation auprès de %faction pour débloquer de nouvelles quêtes!', 'Muss %faction-Ruf farmen, um neue Quests freizuschalten!', '得刷 %faction 声望才能解锁新任务!', '', '¡Necesito hacer grind de %faction para desbloquear nuevas misiones!', '', ''), -('suggest_faction', 'Wanna grind %faction rep for rewards!', 0, 0, '', 'Je veux faire monter ma réputation auprès de %faction pour obtenir des récompenses !', 'Will %faction-Ruf farmen für Belohnungen!', '想刷 %faction 声望拿奖励,有人一起吗?', '', '¡Vamos a grindear reputación de %faction por recompensas!', '', ''), -('suggest_faction', 'Grinding %faction rep, one day at a time!', 0, 0, '', 'À faire ma réputation avec %faction, un jour à la fois !', 'Einen Tag nach dem anderen %faction-Ruf farmen!', '每天刷一点 %faction 声望,慢慢来!', '', '¡Farmeando reputación de %faction, un día a la vez!', '', ''), -('suggest_faction', 'Anyone want to help with %faction rep grind?', 0, 0, '', 'Est-ce que quelqu’un veut m’aider à augmenter ma réputation auprès de %faction ?', 'Kann mir jemand beim %faction-Ruf farmen helfen?', '有人愿意帮我刷 %faction 声望吗?', '', '¿Alguien quiere ayudar con el grind de %faction?', '', ''), -('suggest_faction', 'Help me unlock rewards with %faction rep!', 0, 0, '', 'Aide-moi à débloquer des récompenses avec ma réputation auprès de la faction %faction !', 'Hilf mir, Belohnungen mit %faction-Ruf freizuschalten!', '帮我刷 %faction 声望,解锁奖励!', '', '¡Ayúdame a desbloquear recompensas con la reputación de %faction!', '', ''), -('suggest_faction', 'I’m grinding %faction rep for the mounts!', 0, 0, '', 'Je suis en train de farmer la réputation de %faction pour les montures !', 'Farme %faction-Ruf für die Mounts!', '我在刷 %faction 声望,为了那些坐骑!', '', '¡Estoy farmeando %faction para las monturas!', '', ''), -('suggest_faction', 'Come farm %faction rep with me!', 0, 0, '', 'Viens faire du farm de réputation %faction avec moi!', 'Komm, farm mit mir %faction-Ruf!', '来跟我一起刷 %faction 声望吧!', '', '¡Ven a farmear %faction conmigo!', '', ''), -('suggest_faction', 'I’m almost there with %faction rep!', 0, 0, '', 'Je suis presque arrivé à la réputation de %faction !', 'Ich bin fast fertig mit dem %faction-Ruf!', '我快把 %faction 声望刷到目标了!', '', '¡Casi llego con %faction!', '', ''), -('suggest_faction', 'Grinding %faction rep with my friends!', 0, 0, '', 'En train de monter la réputation de %faction avec mes potes !', 'Farmen %faction-Ruf mit meinen Freunden!', '和朋友们一起刷 %faction 声望!', '', '¡Estoy farmeando %faction con mis amigos!', '', ''), - --- random generic --- usable placeholders: --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('suggest_something', 'Wanna party in %zone_name.', 0, 0, '', 'Envie de faire un groupe dans %zone_name.', 'Lust auf eine Party in %zone_name.', '想在 %zone_name 组队吗?', '', '¡Vamos a perrear a %zone_name!', '', 'Ищу группу в %zone_name.'), -('suggest_something', 'Anyone is looking for %my_role?', 0, 0, '', 'Est-ce que quelqu’un recherche %my_role ?', 'Sucht jemand einen %my_role?', '有人需要 %my_role 吗?', '', '¿Alguien está buscando %my_role?', '', 'Кто-нибудь ищет %my_role?'), -('suggest_something', '%my_role is looking for guild.', 0, 0, '', '%my_role Je suis à la recherche d’une guilde.', '%my_role sucht eine Gilde.', '我是 %my_role,求加入一个公会。', '', '%my_role está buscando hermandad.', '', '%my_role ищу гильдию.'), -('suggest_something', 'Looking for gold.', 0, 0, '', 'À la recherche d’or.', 'Suche Gold.', '缺金币,求支援!', '', 'Buscando oro.', '', 'Дайте голды'), -('suggest_something', '%my_role wants to join a good guild.', 0, 0, '', 'Je veux rejoindre une bonne guilde.', '%my_role möchte einer guten Gilde beitreten.', '我是 %my_role,想加入一个靠谱的公会。', '', '%my_role quiere unirse a una buen hermandad.', '', '%my_role хочу в хорошую гильдию.'), -('suggest_something', 'Need a friend.', 0, 0, '', 'Besoin d’un ami.', 'Brauche einen Freund.', '想找个朋友一起玩。', '', 'Necesito un amigo...', '', 'Ищу друга.'), -('suggest_something', 'Anyone feels alone?', 0, 0, '', 'Vous vous sentez seul ?', 'Fühlt sich jemand allein?', '有人觉得孤单吗?', '', '¿Alguien se siente solo?', '', 'Кому-нибудь одиноко?'), -('suggest_something', 'Boring...', 0, 0, '', 'Je m’ennui', 'Langweilig...', '好无聊啊……', '', 'Aburrido...', '', 'Скучно...'), -('suggest_something', 'Who wants some?', 0, 0, '', 'Qui en veut?', 'Wer will was?', '谁想来点刺激?', '', '¿Quién quiere hacer grupo para levear?', '', 'Кому навалять?'), -('suggest_something', 'Go get me!', 0, 0, '', 'Viens me chercher!', 'Hol mich!', '来抓我啊!', '', '¡Ven a buscarme!', '', 'Поймайте меня!'), -('suggest_something', 'Maybe a duel in %zone_name?', 0, 0, '', 'Peut-être un duel dans %zone_name?', 'Vielleicht ein Duell in %zone_name?', '在 %zone_name 来场决斗如何?', '', '¿Quizás un duelo en %zone_name?', '', 'Кто на дуэль в %zone_name?'), -('suggest_something', 'Anybody doing something?', 0, 0, '', 'Est-ce que quelqu’un fait quelque chose?', 'Macht jemand gerade etwas?', '有人在干啥吗?', '', '¿Alguien está haciendo algo?', '', 'Кто чем занят?'), -('suggest_something', '%zone_name: is anybody here?', 0, 0, '', '%zone_name: Est-ce qu’il y a quelqu’un ici ?', '%zone_name: Ist hier jemand?', '%zone_name:有人在吗?', '', '%zone_name: ¿hay alguien aquí?', '', '%zone_name: есть кто нибудь?'), -('suggest_something', '%zone_name: where is everyone?', 0, 0, '', '%zone_name: Où est tout le monde ?', '%zone_name: Wo sind die Leute?', '%zone_name:人都去哪了?', '', '¿Hay alguien en %zone_name?', '', '%zone_name: где все?'), -('suggest_something', 'Looks like I am alone in %zone_name.', 0, 0, '', 'Il semble que je sois seul dans %zone_name.', 'Sieht so aus, als wäre ich allein in %zone_name.', '看起来我在 %zone_name 就一个人。', '', 'Parece que estoy solo en %zone_name.', '', 'Похоже я один в %zone_name.'), -('suggest_something', 'Meet me in %zone_name.', 0, 0, '', 'Retrouve-moi dans %zone_name.', 'Triff mich in %zone_name.', '来 %zone_name 找我吧。', '', 'Encuéntrame en %zone_name.', '', 'Встретимся в %zone_name.'), -('suggest_something', 'Let’s quest in %zone_name!', 0, 0, '', 'Partons à l’aventure dans %zone_name!', 'Lasst uns in %zone_name questen!', '去 %zone_name 做任务吧!', '', '¡Vamos a hacer un grupo para levear en %zone_name!', '', 'Давайте квестить в %zone_name!'), -('suggest_something', '%zone_name is the best place to be!', 0, 0, '', '%zone_name est l’endroit idéal où se trouver !', '%zone_name ist der beste Ort zum Sein!', '%zone_name 是最棒的地方!', '', '%zone_name es el mejor lugar para estar!', '', '%zone_name лучшее место!'), -('suggest_something', 'Wanna go to %zone_name. Anybody with me?', 0, 0, '', 'Qui veut aller à la %zone_name? Qui est partant avec moi?', 'Will jemand nach %zone_name? Wer kommt mit?', '想去 %zone_name,有人一起吗?', '', 'Quiero ir a %zone_name. ¿Alguien se apunta?', '', 'Собираюсь в %zone_name. Кто со мной7'), -('suggest_something', 'Who wants going to %zone_name?', 0, 0, '', 'Qui veut aller à %zone_name?', 'Wer will nach %zone_name?', '谁想去 %zone_name?', '', '¿Quién quiere ir a %zone_name?', '', 'Кто собирается в %zone_name?'), -('suggest_something', 'I don’t like %zone_name. Where to go?', 0, 0, '', 'Je n’aime pas %zone_name. Je devrais aller où ?', 'Ich mag %zone_name nicht. Wohin soll ich gehen?', '我不喜欢 %zone_name,换个地方吧?', '', 'No me gusta %zone_name. ¿Dónde podria ir?', '', 'Не нравится %zone_name. Куда идти?'), -('suggest_something', 'Are there a good quests in %zone_name?', 0, 0, '', 'Y a-t-il de bonnes quêtes dans %zone_name ?', 'Gibt es gute Quests in %zone_name?', '%zone_name 有啥好任务吗?', '', '¿Hay buenas misiones en %zone_name?', '', 'А в %zone_name есть хорошие квесты?'), -('suggest_something', 'Where to go after %zone_name?', 0, 0, '', 'Où devrais-je aller après %zone_name ?', 'Wohin soll ich nach %zone_name gehen?', '从 %zone_name 走后去哪?', '', '¿Adónde puedo ir después de %zone_name?', '', 'Куда идти после %zone_name?'), -('suggest_something', 'Who is in %zone_name?', 0, 0, '', 'Qui est dans %zone_name ?', 'Wer ist in %zone_name?', '谁在 %zone_name?', '', '¿Quién está en %zone_name?', '', 'Кто в %zone_name?'), -('suggest_something', 'LFG in %zone_name.', 0, 0, '', 'LFG dans %zone_name.', 'LFG in %zone_name.', '在 %zone_name 找队伍。', '', 'Busco grupo en %zone_name.', '', 'ЛФГ %zone_name.'), -('suggest_something', '%zone_name is the worst place to be.', 0, 0, '', '%zone_name est le pire endroit où se trouver.', '%zone_name ist der schlimmste Ort an dem man grade sein kann.', '%zone_name 是最烂的地方。', '', '%zone_name es el peor lugar para estar', '', '%zone_name это худшее место.'), -('suggest_something', 'Catch me in %zone_name!', 0, 0, '', 'Retrouve-moi dans %zone_name !', 'Triff mich in %zone_name!', '在 %zone_name 抓我啊!', '', '¡Atrápame en %zone_name!', '', 'Поймай меня в %zone_name!'), -('suggest_something', 'Go for %zone_name!', 0, 0, '', 'Allons-y pour %zone_name!', 'Auf nach %zone_name!', '冲向 %zone_name!', '', 'Me voy a %zone_name!', '', 'Гоу в %zone_name!'), -('suggest_something', 'Wanna quest in %zone_name', 0, 0, '', 'Tu veux partir en quête dans %zone_name?', 'Lust auf Questen in %zone_name?', '想在 %zone_name 做任务吗?', '', 'Quiero hacer una búsqueda en %zone_name', '', 'Хочешь поделать квесты в %zone_name'), -('suggest_something', 'Anyone has quests in %zone_name?', 0, 0, '', 'Est-ce que quelqu’un a des quêtes dans %zone_name?', 'Hat jemand Quests in %zone_name?', '有人在 %zone_name 有任务吗?', '', '¿Alguien tiene misiones en %zone_name?', '', 'Есть у кого квесты в %zone_name?'), -('suggest_something', 'Come here to %zone_name!', 0, 0, '', 'Viens ici dans %zone_name!', 'Komm nach %zone_name!', '来 %zone_name 这儿!', '', '¡Ven aquí a %zone_name!', '', 'Приходите сюда в %zone_name!'), -('suggest_something', 'Seems there is no Horde in %zone_name', 0, 0, '', 'Il semblerait qu’il n’y ait pas de Hordeux dans %zone_name.', 'Sieht so aus, als gäbe es keine Horde in %zone_name.', '看来 %zone_name 没有部落的人。', '', 'Parece que no hay Horda en %zone_name', '', '%zone_name Орды вроде нет.'), -('suggest_something', 'Seems there is no Alliance in %zone_name', 0, 0, '', 'Il semble qu’il n’y ait pas d’Allianceux dans %zone_name.', 'Sieht so aus, als gäbe es keine Allianz in %zone_name.', '看来 %zone_name 没有联盟的人。', '', 'Parece que no hay Alianza en %zone_name', '', '%zone_name Альянса вроде нет'), -('suggest_something', 'I am really tired of %zone_name. Maybe go somewhere else?', 0, 0, '', 'Je suis vraiment fatigué de %zone_name. Pourquoi n’irions-nous pas ailleurs ?', 'Ich bin echt müde von %zone_name. Vielleicht sollte ich woanders hingehen?', '我真受够了 %zone_name,要不要换个地方?', '', 'Estoy realmente cansado de %zone_name. Quizás ire a otro lugar', '', 'В %zone устал уже, может в другое место пойдем?'), -('suggest_something', 'GL', 0, 0, '', 'Bonne chance', 'Viel Glück', '祝好运!', '', '', '', ''), -('suggest_something', 'I want to go home, and then edge', 0, 0, '', 'Je veux rentrer chez moi, puis me détendre.', 'Ich will nach Hause und dann entspannen.', '我想回家,然后放松一下。', '', '', '', ''), -('suggest_something', 'does anyone know what you need to dual wield?', 0, 0, '', 'Est-ce que quelqu’un peux m’aider avec la double spé ?', 'Weiß jemand, was man zum beidhändigen Kämpfen braucht?', '有人知道双持武器需要啥条件吗?', '', '', '', ''), -('suggest_something', 'hi everyone!', 0, 0, '', 'Salut tout le monde!', 'Hallo zusammen!', '大家好!', '', '', '', ''), -('suggest_something', '%zone_name is comfy', 0, 0, '', '%zone_name est vraiment agréable.', '%zone_name ist gemütlich.', '%zone_name 感觉还挺舒服的。', '', '', '', ''), -('suggest_something', 'i feel great', 0, 0, '', 'Je me sens super bien.', 'Ich fühle mich großartig.', '我感觉棒极了!', '', '', '', ''), -('suggest_something', 'i don’t ignore people i troll them until they ignore me', 0, 0, '', 'Je n’ignore pas les gens, je les trolle jusqu’à ce qu’ils m’ignorent.', 'Ich ignoriere Leute nicht, ich trolle sie, bis sie mich ignorieren.', '我不无视别人,我会逗他们直到他们无视我。', '', '', '', ''), -('suggest_something', 'What do you guys think about my build? %my_role', 0, 0, '', 'Qu’est-ce que vous pensez de mon build? %mon_rolôle', 'Was haltet ihr von meinem Build? %my_role', '大家觉得我的装备咋样?我是 %my_role', '', '', '', ''), -('suggest_something', 'good to see chat still remembers', 0, 0, '', 'Content de voir que le chat se souvient toujours', 'Schön zu sehen, dass der Chat sich noch erinnert.', '很高兴聊天频道里还有人记得我。', '', '', '', ''), -('suggest_something', 'like all weapons its hunter BIS', 0, 0, '', 'Comme toutes les armes, c’est le meilleur équipement pour les chasseurs.', 'Wie alle Waffen ist es natürlich das beste für Hunter.', '跟所有武器一样,这对猎人来说是极品。', '', '', '', ''), -('suggest_something', 'whole point to the game for me is soloing and finding new ways to solo stuff', 0, 0, '', 'Pour moi, le cœur du jeu réside dans le fait d’évoluer en solo et de découvrir de nouvelles façons de vaincre mes adversaires en solo.', 'Für mich dreht sich das Spiel darum, solo zu spielen und neue Wege zu finden, Dinge alleine zu erledigen.', '对我来说,游戏的乐趣就是单刷,找到新的单刷方法。', '', '', '', ''), -('suggest_something', 'i’ve NEVER ripped off anyone', 0, 0, '', 'Je n’ai JAMAIS arnaqué qui que ce soit.', 'Ich habe NIEMALS jemanden abgezockt.', '我从没坑过任何人!', '', '', '', ''), -('suggest_something', 'ah yes the world of warcraft, where i come for life advice', 0, 0, '', 'Ah yes, le monde de Warcraft, là où je viens chercher des conseils de vie.', 'Ah ja, die Welt von Warcraft, wo ich Ratschläge fürs Lebensrat herbekomme.', '啊对,魔兽世界,我来这里寻求人生建议的地方。', '', '', '', ''), -('suggest_something', 'HELLO?', 0, 0, '', 'Salut! Comment ça va?', 'HALLO?', '喂,有人吗?', '', '', '', ''), -('suggest_something', 'Time to fight my way into %zone_name', 0, 0, '', 'Il est temps que je me batte pour rentrer dans %zone_name.', 'Zeit, mich nach %zone_name durchzukämpfen.', '是时候杀进 %zone_name 了!', '', '', '', ''), -('suggest_something', '%zone_name', 0, 0, '', '%zone_name', '%zone_name', '%zone_name', '', '', '', ''), -('suggest_something', 'gotta poop', 0, 0, '', 'je dois aller aux toilettes.', 'Muss mal eben aufs Klo.', '得去解决一下个人问题,等等我!', '', '', '', ''), -('suggest_something', 'if you don’t loot your skinnable kills your pp loses 1mm permanently', 0, 0, '', 'Si vous ne pillez pas les ennemis que je peux dépecer, ton zgegue perd 1mm de façon permanente.', 'Wenn du deine häutbaren Kills nicht lootest, schrumpft dein PP um 1mm dauerhaft.', '不拾取能剥皮的战利品,你的小兄弟可要永久缩水1毫米啊!', '', '', '', ''), -('suggest_something', 'NOOOOOOOOOOOOO', 0, 0, '', 'Oh nonnnnnnnnnn!', 'NEEEEEEEEEEEIN!', '不啊啊啊啊啊啊啊!', '', '', '', ''), -('suggest_something', 'I LIKE POTATO', 0, 0, '', 'J’AIME LA POMME DE TERRE.', 'ICH MAG ZÜGE.', '我超爱土豆!', '', '', '', ''), -('suggest_something', 'w chat', 0, 0, '', 'w chat', 'w Chat', '聊天真棒!', '', '', '', ''), -('suggest_something', 'hi how are you guys', 0, 0, '', 'Salut, comment vous allez ?', 'Hi, wie geht’s euch?', '嗨,大家好吗?', '', '', '', ''), -('suggest_something', 'i just logged out and logged back in', 0, 0, '', 'Je viens de me déconnecter et de me reconnecter.', 'Gerade ausgeloggt und wieder eingeloggt.', '我刚掉线又重新登录了。', '', '', '', ''), -('suggest_something', 'can you guys keep it down a bit, im lost in %zone_name', 0, 0, '', 'Est-ce que vous pouvez baisser un peu le volume, je suis perdu dans %zone_name.', 'Könnt ihr mal leiser sein? Ich bin in %zone_name verloren.', '能小声点吗?我在%zone_name迷路了!', '', '', '', ''), -('suggest_something', 'anyone want to have a drink with me at %zone_name ...hic!', 0, 0, '', 'Quelqu’un veut boire un verre avec moi à %zone_name ...hic!', 'Will jemand mit mir in %zone_name einen trinken? ...Hicks!', '有谁想在%zone_name和我喝一杯吗……嗝!', '', '', '', ''), -('suggest_something', 'hahahahaheeeeeeee dirin diring ingggggg hahahahaheeeeeeeeeeeeee', 0, 0, '', 'hahahahaheeeeeeeeee dirin diring ingggggg hahahahaheeeeeeeeeeeeee', 'hahahahaheeeeeeee dirin diring ingggggg hahahahaheeeeeeeeeeeeee', '哈哈哈哈嘿嘿嘿叮叮叮哈哈哈哈嘿嘿嘿!', '', '', '', ''), -('suggest_something', 'bait used to be believeable', 0, 0, '', 'Le leurre était autrefois crédible.', 'Früher war das Bait glaubwürdig.', '以前的诱饵还能让人信服。', '', '', '', ''), -('suggest_something', 'maybe you just lost your innocence', 0, 0, '', 'Peut-être que tu viens juste de perdre ton innocence.', 'Vielleicht hast du einfach deine Unschuld verloren.', '可能你只是丢掉了天真。', '', '', '', ''), -('suggest_something', 'any guilds willing to carry %my_role?', 0, 0, '', 'Y a-t-il des guildes prêtes à me prendre %my_role?', 'Gibt es Gilden, die %my_role mitziehen wollen?', '有公会愿意带带我这个%my_role吗?', '', '', '', ''), -('suggest_something', 'once you start getting higher, gold is so easy to get', 0, 0, '', 'Une fois que tu commences à monter en niveau, l’or devient si facile à obtenir.', 'Sobald man höher im Level ist, bekommt man Gold so einfach.', '等级高了之后,金币真是手到擒来。', '', '', '', ''), -('suggest_something', 'morning', 0, 0, '', 'Bonjour, comment ça va ce matin ?', 'Morgen!', '早上好啊!', '', '', '', ''), -('suggest_something', 'why does my ass hurt?', 0, 0, '', 'Pourquoi j’ai mal aux fesses?', 'Warum tut mein Hintern weh?', '为啥我的屁股这么疼?', '', '', '', ''), -('suggest_something', 'I feel like spirit is bis for leveling', 0, 0, '', 'Je pense que l’esprit est bis pour monter de niveau.', 'Ich denke, Willenskraft ist das Beste zum Leveln.', '我觉得精神属性对升级来说是极品。', '', '', '', ''), -('suggest_something', 'Even more for troll', 0, 0, '', 'Encore plus pour les trolls', 'Noch mehr für Trolle.', '对巨魔来说更是如此。', '', '', '', ''), -('suggest_something', 'CAN SOMEONE INVITE ME', 0, 0, '', 'ON PEUT M INVITER', 'KANN MICH JEMAND EINLADEN?', '谁能邀请我一下啊!', '', '', '', ''), -('suggest_something', 'need a lot of drinks', 0, 0, '', 'J’ai besoin de beaucoup de boissons.', 'Brauche viele Drinks.', '需要一大堆饮料!', '', '', '', ''), -('suggest_something', 'damn gnomes', 0, 0, '', 'Ces satanés gnomes!', 'Verdammte Gnome!', '该死的侏儒!', '', '', '', ''), -('suggest_something', 'no one likes gnomes', 0, 0, '', 'Personne n’aime les gnomes.', 'Niemand mag Gnome.', '没人喜欢侏儒。', '', '', '', ''), -('suggest_something', 'gnomes are only good for one thing', 0, 0, '', 'Les gnomes ne sont bons que pour une seule chose', 'Gnome taugen nur für eine Sache.', '侏儒就只有一件事干得不错。', '', '', '', ''), -('suggest_something', 'Well', 0, 0, '', 'Bien', 'Nun', '好吧', '', '', '', ''), -('suggest_something', 'mushrooms', 0, 0, '', 'champignons', 'Pilze', '蘑菇', '', '', '', ''), -('suggest_something', 'automatic thoughts are a scary thing', 0, 0, '', 'Les pensées automatiques sont effrayantes, elles surgissent sans prévenir et peuvent vraiment perturber ma tranquillité d’esprit.', 'Automatische Gedanken sind eine beängstigende Sache.', '自动冒出来的想法真挺吓人的。', '', '', '', ''), -('suggest_something', 'the mind is more pliable than we like to believe', 0, 0, '', '', 'Der Geist ist formbarer, als wir glauben.', '思想比我们想象中更容易被塑造。', '', '', '', ''), -('suggest_something', 'any leveling guilds out there?', 0, 0, '', 'Des guildes de leveling par ici ?', 'Gibt es hier irgendwelche Leveling-Gilden?', '有升级公会吗?', '', '', '', ''), -('suggest_something', 'brb', 0, 0, '', 'brb.', 'bin gleich zurück.', '马上回来!', '', '', '', ''), -('suggest_something', 'Why is snow white but Ice is clear ? made of the same thing', 0, 0, '', 'Oh, la neige est blanche parce que ses cristaux reflètent la lumière du soleil, ce qui donne cette couleur. Tandis que la glace est claire car elle compacte les cristaux de glace, ce qui la rend transparente. C’est fou de penser que ce sont tous les deux faits de la même chose, n’est-ce pas ?', 'Schnee ist weiß, weil die Eiskristalle das Licht streuen, während Eis klar ist, weil es eine kompaktere Struktur hat.', '为啥雪是白的而冰是透明的?明明是同一种东西啊!', '', '', '', ''), -('suggest_something', 'why is whipped cream fluffy and regular not', 0, 0, '', 'Oh, whipped cream is all light and airy because we are beating some heavy cream until it traps air in. Regular cream just sits there all heavy and dense because it hasn’t been whipped yet. ', 'Schlagsahne ist luftig, weil beim Schlagen Luft eingeschlossen wird. Normale Sahne ist dicht und schwer, weil sie nicht aufgeschlagen wurde.', '为啥打发的奶油这么蓬松,而普通的就不行呢?', '', '', '', ''), -('suggest_something', 'why do feet smell if they have no nose', 0, 0, '', 'Pourquoi les pieds sentent alors qu’ils n’ont pas de nez !?', 'Warum stinken Füße, wenn sie keine Nase haben?', '脚上又没长鼻子,咋还能臭呢?', '', '', '', ''), -('suggest_something', 'seems a can of newbies arrived', 0, 0, '', 'Il semble qu’une brochette de noob soit arrivée.', 'Scheint, als wäre eine Fuhre Neulinge angekommen.', '看来来了一群菜鸟啊。', '', '', '', ''), -('suggest_something', 'stop trolling new players with BS answers', 0, 0, '', 'Arrête de troll les nouveaux joueurs avec des réponses bidon.', 'Hört auf, neue Spieler mit falschen Antworten zu trollen.', '别再用瞎话忽悠新手了!', '', '', '', ''), -('suggest_something', 'Is there PvP on this server?', 0, 0, '', 'Il y’a du pvp sur ce serveur ?', 'Gibt es PvP auf diesem Server?', '这个服务器有PVP吗?', '', '', '', ''), -('suggest_something', 'duh', 0, 0, '', 'duh', 'tja', '废话!', '', '', '', ''), -('suggest_something', 'phew... :)', 0, 0, '', 'Phew... :)', 'Puh... :)', '呼……还好 :)', '', '', '', ''), -('suggest_something', 'did you guys know that', 0, 0, '', 'Savez-vous que...', 'Wusstet ihr, dass...', '你们知道吗?', '', '', '', ''), -('suggest_something', 'i don’t try to imagine what other creatures feel like', 0, 0, '', 'Je ne cherche pas à imaginer ce que d’autres créatures peuvent ressentir.', 'Ich versuche nicht, mir vorzustellen, was andere Kreaturen fühlen.', '我可不去猜其他生物心里咋想的。', '', '', '', ''), -('suggest_something', 'oh whoops, wrong chat', 0, 0, '', 'Oh mince, mauvais chat', 'Oh hoppla, falscher Chat.', '哎呀,发错频道了!', '', '', '', ''), -('suggest_something', 'bruh you guys are wildin out today', 0, 0, '', 'Oh là là, vous les gars, vous déconnez aujourd’hui!', 'Junge, ihr seid heute echt wild drauf!', '兄弟们,今天你们也太嗨了吧!', '', '', '', ''), -('suggest_something', 'just let it be known that my text was here', 0, 0, '', 'Je vais juste faire en sorte que mon texte soit là.', 'Lasst es einfach bekannt sein, dass mein Text hier war.', '就让大家知道,我的话在这儿呢!', '', '', '', ''), -('suggest_something', 'grrr angyy', 0, 0, '', 'Grrrrrr colère.', 'Grrrr wütend.', '咕噜咕噜,气炸了!', '', '', '', ''), -('suggest_something', 'the grind is fun', 0, 0, '', 'Le grind est amusant', 'Das Grinden macht Spaß.', '刷怪升级真有意思!', '', '', '', ''), -('suggest_something', 'Wow keeps me sharp', 0, 0, '', 'Wow me garde alerte', 'WoW hält mich wachsam.', '魔兽让我保持敏锐!', '', '', '', ''), -('suggest_something', 'hey have a question where can i take the roll for more xp? im in %zone_name', 0, 0, '', 'Hey, j’ai une question. Où est-ce que je peux trouver le rouleau pour gagner plus d’XP? Je suis dans %zone_name.', 'Hey, ich habe eine Frage. Wo bekomme ich die Rolle für mehr XP? Ich bin in %zone_name.', '嘿,有个问题,哪里能接任务多拿经验?我在%zone_name。', '', '', '', ''), -('suggest_something', 'do you guys like sausages?', 0, 0, '', 'Hmm, est-ce que vous aimez les saucisses?', 'Mögt ihr Würstchen?', '你们喜欢香肠吗?', '', '', '', ''), -('suggest_something', 'Invite me. I’ll help', 0, 0, '', 'Invite-moi. Je vais t’aider.', 'Ladet mich ein. Ich helfe.', '组我吧,我能帮忙!', '', '', '', ''), -('suggest_something', 'which class do you think is better for pvp?', 0, 0, '', 'Quelle classe est la meilleurs pour PvP !?', 'Welche Klasse ist besser für PvP?', '你们觉得哪个职业在PVP里更强?', '', '', '', ''), -('suggest_something', 'where the fuck is the cooking trainer in %zone_name', 0, 0, '', '%zone_name? Ah, putain, c’est toujours la galère pour trouver les PNJ dans cette zone-là. Attends, laisse-moi voir sur ma carte... Ah, je crois que le formateur en cuisine est près de la taverne, à droite de l’aubergiste. Tu devrais le trouver là-bas!', 'Wo zum Teufel ist der Kochlehrer in %zone_name?', '靠,%zone_name的烹饪训练师到底在哪?', '', '', '', ''), -('suggest_something', 'you know what happens in %zone_name?', 0, 0, '', 'Tu sais ce qu’il se passe dans la %zone_name ?', 'Weißt du, was in %zone_name passiert?', '你知道%zone_name发生了啥事吗?', '', '', '', ''), -('suggest_something', 'I need to craft something', 0, 0, '', 'Je dois fabriquer quelque chose.', 'Ich muss etwas herstellen.', '我得做点东西。', '', '', '', ''), -('suggest_something', 'Anyone up for some nonsense in %zone_name?', 0, 0, '', 'Quelqu’un pour un peu de folie dans la %zone_name?', 'Hat jemand Lust auf etwas Unsinn in %zone_name?', '有谁想在%zone_name搞点乱子吗?', '', '¿Alguien para tonterías en %zone_name?', '', 'Кто-нибудь для глупости в %zone_name?'), -('suggest_something', 'Let’s make %zone_name our playground.', 0, 0, '', 'Faisons de %zone_name notre terrain de jeu.', 'Lasst uns %zone_name zu unserem Spielplatz machen.', '咱们把%zone_name变成我们的游乐场吧!', '', 'Hagamos de %zone_name nuestro patio de recreo.', '', 'Давайте сделаем %zone_name нашей игровой площадкой.'), -('suggest_something', 'I’m gonna get lost in %zone_name again.', 0, 0, '', 'Je vais me perdre dans %zone_name encore une fois.', 'Ich werde mich in %zone_name wieder verlaufen.', '我又要在%zone_name迷路了。', '', 'Voy a perderme otra vez en %zone_name.', '', 'Снова потеряюсь в %zone_name.'), -('suggest_something', 'Can’t believe I’m still here in %zone_name.', 0, 0, '', 'Je n’arrive pas à croire que je suis toujours dans %zone_name.', 'Ich kann nicht glauben, dass ich immer noch in %zone_name bin.', '真不敢相信我还在%zone_name。', '', 'No puedo creer que todavía esté aquí en %zone_name.', '', 'Не верю, что я до сих пор здесь в %zone_name.'), -('suggest_something', 'Wanna team up for %zone_name madness?', 0, 0, '', 'Tu veux faire équipe pour une folie dans la zone %zone_name?', 'Willst du dich für Chaos in %zone_name zusammentun?', '想组队在%zone_name大闹一场吗?', '', '¿Quieres unirte para la locura en %zone_name?', '', 'Хочешь составить команду для сумасшествия в %zone_name?'), -('suggest_something', 'Let’s raid the mailbox in %zone_name!', 0, 0, '', 'Allons piller la boîte aux lettres dans %zone_name!', 'Lasst uns den Briefkasten in %zone_name plündern!', '咱们去%zone_name抢邮箱吧!', '', '¡Vamos a asaltar el buzón en %zone_name!', '', 'Давайте захватим почтовый ящик в %zone_name!'), -('suggest_something', 'I swear, %zone_name is full of trolls.', 0, 0, '', 'Je te jure, %zone_name est rempli de trolls.', 'Ich schwöre, %zone_name ist voller Trolle.', '我发誓,%zone_name全是巨魔。', '', 'Juro que %zone_name está lleno de trolls.', '', 'Клянусь, %zone_name полно троллей.'), -('suggest_something', 'Let’s create chaos in %zone_name together.', 0, 0, '', 'Créons le chaos dans %zone_name ensemble.', 'Lasst uns zusammen Chaos in %zone_name anrichten.', '咱们一起在%zone_name制造混乱吧!', '', 'Creamos caos juntos en %zone_name.', '', 'Давайте устроим хаос в %zone_name вместе.'), -('suggest_something', 'I feel like doing absolutely nothing in %zone_name.', 0, 0, '', 'J’ai juste envie de ne rien faire du tout dans %zone_name.', 'Ich habe einfach Lust, in %zone_name absolut nichts zu tun.', '我只想在%zone_name啥也不干。', '', 'Tengo ganas de no hacer nada en %zone_name.', '', 'Хочу просто ничего не делать в %zone_name.'), -('suggest_something', 'No one ever listens in %zone_name.', 0, 0, '', 'Personne n’écoute jamais dans %zone_name.', 'Niemand hört jemals in %zone_name zu.', '在%zone_name没人听我说话。', '', 'Nadie escucha nunca en %zone_name.', '', 'В %zone_name никто никогда не слушает.'), -('suggest_something', 'Is %zone_name even real?', 0, 0, '', '%zone_name est vraiment réel?', 'Ist %zone_name überhaupt real?', '%zone_name到底是不是真的?', '', '¿Es %zone_name siquiera real?', '', 'Существует ли вообще %zone_name?'), -('suggest_something', 'Someone send help to %zone_name! It’s wild!', 0, 0, '', 'Envoyez de l’aide dans %zone_name ! C’est la folie !', 'Schickt Hilfe nach %zone_name! Es ist verrückt dort!', '谁来%zone_name救救我!这里乱成一团!', '', '¡Alguien envíe ayuda a %zone_name! ¡Es una locura!', '', 'Кто-нибудь, отправьте помощь в %zone_name! Тут творится безумие!'), -('suggest_something', 'I am sure %zone_name is haunted.', 0, 0, '', 'Je suis persuadé que %zone_name est hanté.', 'Ich bin mir sicher, dass %zone_name verflucht ist.', '我敢肯定%zone_name闹鬼。', '', 'Estoy seguro de que %zone_name está embrujado.', '', 'Уверен, что в %zone_name есть призраки.'), -('suggest_something', 'If %zone_name could talk, it would probably scream.', 0, 0, '', 'Si %zone_name pouvait parler, je pense qu’il crierait à s’en briser la voix.', 'Wenn %zone_name sprechen könnte, würde es wahrscheinlich schreien.', '如果%zone_name会说话,估计得尖叫起来。', '', 'Si %zone_name pudiera hablar, probablemente gritaría.', '', 'Если бы %zone_name мог говорить, он, наверное, кричал бы.'), -('suggest_something', 'There’s something fishy about %zone_name.', 0, 0, '', 'Il y a quelque chose de louche avec %zone_name.', 'Irgendwas ist faul an %zone_name.', '%zone_name有点不对劲。', '', 'Hay algo sospechoso acerca de %zone_name.', '', 'В %zone_name что-то подозрительное.'), -('suggest_something', 'I’m about to break %zone_name with my awesomeness.', 0, 0, '', 'Je m’apprête à conquérir %zone_name avec ma grandeur.', 'Ich werde %zone_name mit meiner Großartigkeit zerstören.', '我要用我的牛逼把%zone_name给炸了!', '', 'Estoy a punto de romper %zone_name con mi grandeza.', '', 'Я собираюсь разрушить %zone_name своей крутостью.'), -('suggest_something', 'Someone tell %zone_name that I am here.', 0, 0, '', 'Quelqu’un dit à %zone_name que je suis ici.', 'Jemand soll %zone_name sagen, dass ich hier bin.', '谁告诉%zone_name,我来了!', '', 'Alguien dígale a %zone_name que estoy aquí.', '', 'Кто-нибудь скажите %zone_name, что я здесь.'), -('suggest_something', 'I’m lost in %zone_name, send help.', 0, 0, '', 'Je suis perdu dans %zone_name, envoyez de l’aide.', 'Ich habe mich in %zone_name verlaufen, schickt Hilfe.', '我在%zone_name迷路了,快来救我!', '', 'Estoy perdido en %zone_name, envíen ayuda.', '', 'Я заблудился в %zone_name, отправьте помощь.'), -('suggest_something', 'I’m starting a revolution in %zone_name.', 0, 0, '', 'Je lance une révolution dans %zone_name.', 'Ich starte eine Revolution in %zone_name.', '我在%zone_name发起一场革命!', '', 'Estoy comenzando una revolución en %zone_name.', '', 'Я начинаю революцию в %zone_name.'), -('suggest_something', 'Anyone else find %zone_name suspiciously quiet?', 0, 0, '', 'Est-ce que vous trouvez que %zone_name est étrangement calme, non?', 'Findet noch jemand %zone_name verdächtig ruhig?', '还有人觉得%zone_name安静得诡异吗?', '', '¿Alguien más encuentra %zone_name sospechosamente tranquila?', '', 'Кто-нибудь еще находит %zone_name подозрительно тихой?'), -('suggest_something', 'I bet %zone_name has secrets we don’t know about.', 0, 0, '', 'Je parie que %zone_name a des secrets que nous ne connaissons pas.', 'Ich wette, %zone_name hat Geheimnisse, von denen wir nichts wissen.', '我打赌%zone_name藏着我们不知道的秘密。', '', 'Apuesto a que %zone_name tiene secretos que no conocemos.', '', 'Держу пари, что в %zone_name есть секреты, о которых мы не знаем.'), -('suggest_something', 'You can never have enough %my_race in %zone_name.', 0, 0, '', 'On ne peut jamais avoir assez de %my_race dans %zone_name.', 'Man kann nie genug %my_race in %zone_name haben.', '在%zone_name,%my_race永远不够多。', '', 'Nunca se tiene suficiente %my_race en %zone_name.', '', 'Никогда не бывает слишком много %my_race в %zone_name.'), -('suggest_something', 'I’m beginning to think %zone_name is just a myth.', 0, 0, '', 'Je commence à penser que %zone_name n’est qu’un mythe.', 'Ich fange an zu glauben, dass %zone_name nur ein Mythos ist.', '我开始怀疑%zone_name是不是只是个传说。', '', 'Empiezo a pensar que %zone_name es solo un mito.', '', 'Я начинаю думать, что %zone_name — это миф.'), -('suggest_something', 'Who knew %zone_name would be this crazy?', 0, 0, '', 'Qui aurait pensé que la %zone_name serait aussi folle ?', 'Wer hätte gedacht, dass %zone_name so verrückt wäre?', '谁能想到%zone_name会这么疯狂啊?', '', '¿Quién sabía que %zone_name sería tan loco?', '', 'Кто знал, что %zone_name будет таким безумным?'), -('suggest_something', 'It’s like the game broke in %zone_name.', 0, 0, '', 'C’est comme si le jeu avait buggé dans %zone_name.', 'Es ist, als wäre das Spiel in %zone_name kaputtgegangen.', '感觉在%zone_name游戏都崩了!', '', 'Es como si el juego se rompiera en %zone_name.', '', 'Похоже, игра сломалась в %zone_name.'), -('suggest_something', 'Is %zone_name actually a desert or am I just lost?', 0, 0, '', 'Est-ce que %zone_name est vraiment un désert ou suis-je simplement perdu(e) ?', 'Ist %zone_name wirklich eine Wüste oder bin ich einfach nur verloren?', '%zone_name到底是沙漠,还是我迷路了?', '', '¿%zone_name es realmente un desierto o simplemente estoy perdido?', '', '%zone_name реально пустыня или я просто заблудился?'), -('suggest_something', 'This place is starting to feel like %zone_name in a blender.', 0, 0, '', 'Cet endroit commence à ressembler à une véritable tornade de %zone_name.', 'Dieser Ort fühlt sich langsam an wie %zone_name in einem Mixer.', '这地方感觉就像把%zone_name扔进搅拌机里!', '', 'Este lugar empieza a sentirse como %zone_name en una licuadora.', '', 'Это место начинает напоминать %zone_name в блендере.'), -('suggest_something', 'Anyone got a map for %zone_name?', 0, 0, '', 'Est-ce que quelqu’un a une carte pour %zone_name?', 'Hat jemand eine Karte für %zone_name?', '谁有%zone_name的地图啊?', '', '¿Alguien tiene un mapa para %zone_name?', '', 'Кто-нибудь имеет карту для %zone_name?'), -('suggest_something', 'Who else thinks %zone_name is trying to kill us?', 0, 0, '', 'Qui d’autre pense que %zone_name essaie de nous tuer ?', 'Wer sonst denkt, dass %zone_name versucht, uns zu töten?', '还有谁觉得%zone_name是想弄死咱们?', '', '¿Quién más cree que %zone_name está tratando de matarnos?', '', 'Кто еще думает, что %zone_name пытается нас убить?'), -('suggest_something', 'I’m on a wild ride through %zone_name!', 0, 0, '', 'Je suis en train de vivre une aventure folle à travers %zone_name !', 'Ich bin auf einer wilden Fahrt durch %zone_name!', '我在%zone_name经历了一场狂野冒险!', '', '¡Estoy en una aventura salvaje a través de %zone_name!', '', 'Я на дикой поездке через %zone_name!'), -('suggest_something', 'Is it just me or is %zone_name getting weirder?', 0, 0, '', 'Est-ce que c’est moi ou %zone_name devient de plus en plus étrange ?', 'Liegt es nur an mir, oder wird %zone_name immer seltsamer?', '是我一个人觉得%zone_name越来越诡异了吗?', '', '¿Soy solo yo o %zone_name se está volviendo más raro?', '', 'Только мне кажется, что %zone_name становится все страннее?'), -('suggest_something', 'I’m convinced %zone_name is secretly an amusement park.', 0, 0, '', 'Je suis convaincu que %zone_name est secrètement un parc d’attractions.', 'Ich bin überzeugt, dass %zone_name heimlich ein Freizeitpark ist.', '我敢肯定%zone_name其实是个隐秘的游乐园!', '', 'Estoy convencido de que %zone_name es secretamente un parque de atracciones.', '', 'Я убежден, что %zone_name — это тайный парк аттракционов.'), - --- random generic toxic phrases --- usable placeholders: --- %random_inventory_item_link --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('suggest_something_toxic', 'why is my keyboard not working, is it because I don’t have enough friends', 0, 0, '', 'Pourquoi mon clavier ne fonctionne pas ? Ce doit être parceque je n’ai pas d’amis ....', 'Warum funktioniert meine Tastatur nicht? Liegt es daran, dass ich nicht genug Freunde habe?', '我的键盘怎么不好使了,是因为我朋友不够多吗?', '', '', '', ''), -('suggest_something_toxic', 'do you think the trees in this game have feelings', 0, 0, '', 'Vous pensez que les arbres ont des sentiments ?', 'Glaubt ihr, die Bäume in diesem Spiel haben Gefühle?', '你们觉得游戏里的树有感情吗?', '', '', '', ''), -('suggest_something_toxic', 'I keep pressing the same button, but i’m still not winning', 0, 0, '', 'Je continue d’appuyer sur le même bouton, mais je ne gagne toujours pas.', 'Ich drücke immer wieder denselben Knopf, aber ich gewinne trotzdem nicht.', '我一直在按同一个键,但怎么还是赢不了?', '', '', '', ''), -('suggest_something_toxic', 'I bet i could win a raid if i just yelled loot', 0, 0, '', 'Je parie que je pourrais gagner un raid si je hurlais LOOT !', 'Ich wette, ich könnte einen Raid gewinnen, wenn ich einfach LOOT schreien würde!', '我敢打赌,只要我大喊“战利品”,就能赢下团队副本!', '', '', '', ''), -('suggest_something_toxic', 'I’ve been trying to mount my mount for an hour, nothing’s happening', 0, 0, '', 'Ça fait une heure que j’essaie de monterma monture, mais rien ne se passe.', 'Ich versuche seit einer Stunde, mein Reittier zu besteigen, aber nichts passiert.', '我试了一个小时想上坐骑,啥反应都没有!', '', '', '', ''), -('suggest_something_toxic', 'Is there a way to become invincible without actually playing the game', 0, 0, '', 'Il y’a un moyen de devenir invincible sans même jouer au jeu ?', 'Gibt es eine Möglichkeit, unbesiegbar zu werden, ohne das Spiel tatsächlich zu spielen?', '有没有办法不玩游戏也能变无敌啊?', '', '', '', ''), -('suggest_something_toxic', 'can i just press random keys and still win, asking for a friend', 0, 0, '', 'Est-ce que je peux juste appuyer sur des touches au hasard et quand même gagner, c’est pour un ami.', 'Kann ich einfach zufällige Tasten drücken und trotzdem gewinnen? Frage für einen Freund.', '我随便按键也能赢吗?替朋友问的。', '', '', '', ''), -('suggest_something_toxic', 'If i stand still, does that mean i’m a better player than you', 0, 0, '', 'Si je reste immobile, est-ce que cela signifie que je suis un meilleur joueur que toi ?', 'Wenn ich stillstehe, bedeutet das, dass ich ein besserer Spieler bin als du?', '如果我站着不动,是不是就比你厉害了?', '', '', '', ''), -('suggest_something_toxic', 'why doesn’t my pet heal me when i stand in fire', 0, 0, '', 'Pourquoi mon pet ne me soigne pas quand je reste dans le feu ?', 'Warum heilt mein Begleiter mich nicht, wenn ich im Feuer stehe?', '为什么我站在火里,宠物不给我加血?', '', '', '', ''), -('suggest_something_toxic', 'so wait, do i need armor to be a true hero or is it optional', 0, 0, '', 'Alors, attends, est-ce que j’ai besoin d’une armure pour être un vrai héros ou est-ce facultatif ?', 'Warte mal, brauche ich eine Rüstung, um ein echter Held zu sein, oder ist das optional?', '等等,成为真正的英雄得穿铠甲吗,还是可选的?', '', '', '', ''), -('suggest_something_toxic', 'I need to collect 10000 Lucky Charm’s to level up, right?', 0, 0, '', 'Pour monter de niveau, j’ai besoin de collecter 10000 Lucky Charm’s, c’est ça ?', 'Ich muss 10000 Glücksbringer sammeln, um aufzusteigen, richtig?', '我得收集10000个幸运符才能升级,对吧?', '', '', '', ''), -('suggest_something_toxic', 'my weapon is broken, but can I still just slap enemies with my huge Di.. err fist', 0, 0, '', 'Oh non, mon arme est cassée, mais je peux toujours juste gifler les ennemis avec mon énorme Bi.. euh poing, non?', 'Oh nein, meine Waffe ist kaputt, aber kann ich Feinde trotzdem mit meiner riesigen Bi.. äh Faust schlagen?', '我的武器坏了,但还能用我巨大的……呃,拳头打敌人吗?', '', '', '', ''), -('suggest_something_toxic', 'Can we just rename this game to “World of DieCraft a Player keeps killing me”', 0, 0, '', 'Pouvons-nous simplement renommer le jeu en “World of DieCraft, un Joueur me farm la gueule“ ?', 'Können wir dieses Spiel einfach in "World of DieCraft, ein Spieler tötet mich ständig" umbenennen?', '能不能把这游戏改名叫“死亡世界”,有个玩家老是杀我!', '', '', '', ''), -('suggest_something_toxic', 'why doesn’t my horse fly? can’t it just go up', 0, 0, '', 'Pourquoi mon cheval ne vol pas ? Pourquoi il ne peut juste pas aller vers le ciel ?', 'Warum fliegt mein Pferd nicht? Kann es nicht einfach hochgehen?', '为什么我的马不会飞?就不能直接飞起来吗?', '', '', '', ''), -('suggest_something_toxic', 'Is there an option to turn off the graphics so it looks like my old calculator', 0, 0, '', 'Il n’y a pas une option pour désactiver les graphismes calculette ? Le jeu est vraiment moche.', 'Gibt es eine Option, die Grafik auszuschalten, damit es aussieht wie mein alter Taschenrechner?', '有没选项能关掉画面,让它看起来像我老计算器一样?', '', '', '', ''), -('suggest_something_toxic', 'I was supposed to kill something but i fell asleep, is that normal', 0, 0, '', 'Je devais tuer un truc mais je me suis endormi, c’est normal ?', 'Ich sollte etwas töten, aber ich bin eingeschlafen, ist das normal?', '我本来要去杀怪,结果睡着了,这正常吗?', '', '', '', ''), -('suggest_something_toxic', 'does anyone else just kill random NPCs for fun or is it just me', 0, 0, '', 'Est-ce que je suis le seul à tuer des PNJ au hasard juste pour le plaisir, ou est-ce qu’il y a d’autres dans le coin?', 'Bin ich der Einzige, der zufällig NPCs aus Spaß tötet, oder gibt es noch andere hier?', '还有谁跟我一样随便杀NPC玩吗?', '', '', '', ''), -('suggest_something_toxic', 'I’m %my_level and i already know all the secrets of the game', 0, 0, '', 'Je suis %my_level et je connais déjà tous les secrets du jeu.', 'Ich bin %my_level und kenne bereits alle Geheimnisse des Spiels.', '我才%my_level就知道游戏里所有秘密了!', '', '', '', ''), -('suggest_something_toxic', 'can i just talk to the NPCs until they give me all the loot?', 0, 0, '', 'Je peux juste parler aux PNJ jusqu’à ce qu’ils me donnent tout le butin?', 'Kann ich einfach mit den NPCs reden, bis sie mir die ganze Beute geben?', '我能一直跟NPC聊天,直到他们把所有装备给我吗?', '', '', '', ''), -('suggest_something_toxic', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', 0, 0, '', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', '', '', '', ''), -('suggest_something_toxic', 'why am i losing, i’ve been playing for 15 minutes', 0, 0, '', 'Pourquoi est-ce que je perds, je joue depuis 15 minutes.', 'Warum verliere ich? Ich spiele doch erst seit 15 Minuten.', '我玩了15分钟了,怎么还在输啊?', '', '', '', ''), -('suggest_something_toxic', 'I don’t need a strategy, i just need to spam this ability and hope for the best', 0, 0, '', 'Je n’ai pas besoin d’une stratégie, je vais juste spammer cette compétence et espérer le meilleur.', 'Ich brauche keine Strategie, ich werde einfach diese Fähigkeit spammen und auf das Beste hoffen.', '我不需要啥策略,就狂按这个技能,祈祷好运吧!', '', '', '', ''), -('suggest_something_toxic', 'If i equip a bunch of Lucky Charm’s, does that make me get more loot?', 0, 0, '', 'Équiper plusieurs Lucky Charm’s ne me donne pas un avantage supplémentaire pour obtenir plus de butin. Ces amulettes n’affectent pas directement la quantité de butin que je reçois.', 'Bringt es mir mehr Beute, wenn ich ganz viele Glücksbringer anlege?', '如果我装备一堆幸运符,能多爆装备吗?', '', '', '', ''), -('suggest_something_toxic', 'Is the secret to the game just screaming at the screen', 0, 0, '', 'Le secret du jeu est juste d’hurler sur l’écran', 'Ist das Geheimnis des Spiels einfach, den Bildschirm anzuschreien?', '游戏的秘诀是不是就是对着屏幕大喊大叫?', '', '', '', ''), -('suggest_something_toxic', 'can i just put on every item i find and hope for the best', 0, 0, '', 'Est-ce que je peux juste equipper n’importe quel loot et yolo ?', 'Kann ich einfach alles anziehen, was ich finde, und auf das Beste hoffen?', '我能把找到的所有东西都穿上,然后祈祷好运吗?', '', '', '', ''), -('suggest_something_toxic', 'do i really need to do quests or can i just explore and yell at people', 0, 0, '', 'Ah, est-ce que je dois vraiment faire des quêtes ou est-ce que je peux juste explorer et crier sur les gens?', 'Muss ich wirklich Quests machen, oder kann ich einfach herumlaufen und Leute anschreien?', '我真得做任务吗,还是能随便逛逛然后对人乱吼?', '', '', '', ''), -('suggest_something_toxic', 'I’m at work playing this game, they say I should be working >.> but it’s WOW!', 0, 0, '', 'Je suis au travail en train de jouer à ce jeu, ils disent que je devrais travailler >.> mais c’est WOW!', 'Ich bin bei der Arbeit und spiele WoW, meine Kollegen sagen, ich sollte arbeiten >.> aber es ist WOW!', '我在上班玩游戏,他们说我该工作>.>但这是魔兽啊!', '', '', '', ''), -('suggest_something_toxic', 'If i eat all the food in the game, will it make me invincible', 0, 0, '', 'Si je mange toute la nourriture dans le jeu, est-ce que cela va me rendre invincible ?', 'Wenn ich das ganze Essen im Spiel esse, werde ich dann unbesiegbar?', '如果我把游戏里所有食物都吃了,会不会变无敌?', '', '', '', ''), -('suggest_something_toxic', 'so what if i just throw my pet into every boss fight and need all time, it’s hunters rules', 0, 0, '', 'Donc je balance mon pet sur tout les boss du jeu et je clique sur need, c’est la règle de base des chasseurs."', 'Also, ich werfe mein Pet einfach in jeden Bosskampf und drücke immer auf Gier, das sind die Hunter-Regeln.', '要是每次打BOSS我都把宠物扔上去,然后一直点需求,这是猎人的规矩吧?', '', '', '', ''), -('suggest_something_toxic', 'can you die if you’re already dead, asking for a ghost', 0, 0, '', 'On peut mourir si on est déjà mort ? Je demande pour un fantome...', 'Kann man sterben, wenn man schon tot ist? Frage für einen Geist...', '如果你已经死了,还能再死一次吗?替一个幽灵问的。', '', '', '', ''), -('suggest_something_toxic', 'does anyone know where the legendary sword is, i’ve only looked for 5 minutes', 0, 0, '', 'Est-ce que quelqu’un sait où se trouve l’épée légendaire? Je n’ai cherché que pendant 5 minutes.', 'Weiß jemand, wo das legendäre Schwert ist? Ich habe erst 5 Minuten gesucht.', '有人知道传说之剑在哪吗?我才找了5分钟。', '', '', '', ''), -('suggest_something_toxic', 'I was told if I come here I get 100000 Gold where do I go?', 0, 0, '', 'On m’a dit que si je viens ici, je reçois 100 000 pièces d’or. Où dois-je aller?', 'Mir wurde gesagt, wenn ich hierher komme, bekomme ich 100000 Gold. Wo muss ich hin?', '听说来这里能拿10万金币,我该去哪?', '', '', '', ''), -('suggest_something_toxic', 'I play this game like I had your mum lastnight, in a screaming glory!', 0, 0, '', 'Je joue à ce jeu comme si j’avais ta mère la nuit dernière, dans un vacarme triomphant!', 'Ich spiele dieses Spiel, als hätte ich letzte Nacht deine Mutter gehabt – in triumphierendem Geschrei!', '我玩这游戏就像昨晚跟你妈一样,喊得震天响!', '', '', '', ''), -('suggest_something_toxic', 'so when does the game start getting fun, or is it all just grinding forever', 0, 0, '', 'Il devient fun quand le jeu ? Ou c’est juste du grind pour toujours ?', 'Wann wird das Spiel eigentlich spaßig, oder ist es einfach nur endloses Grinden?', '这游戏啥时候开始好玩啊,还是就一直刷刷刷?', '', '', '', ''), -('suggest_something_toxic', 'I keep asking for directions, but they keep sending me to random places, is this normal', 0, 0, '', 'J’arrête pas de demander mon chemin mais ils m’envoient constament à des endroits aléatoires, c’est normal ?', 'Ich frage ständig nach dem Weg, aber sie schicken mich immer irgendwohin. Ist das normal?', '我一直问路,但他们老把我指到乱七八糟的地方,这正常吗?', '', '', '', ''), -('suggest_something_toxic', 'I found out if your press ALF+f4 you get 100000 Gold, trick is you need to attact a red NPC 1st', 0, 0, '', 'WOOOW Si tu presses ALF + F4, tu te fais 100,000 Gold. Le truc c’est que tu dois attaquer un mob rouge juste avant !!!', 'WOOOW Wenn du ALT + F4 drückst, bekommst du 100.000 Gold! Der Trick ist, dass du vorher einen roten NPC angreifen musst!', '我发现按ALT+F4能得10万金币,窍门是你得先攻击一个红名NPC!', '', '', '', ''), -('suggest_something_toxic', 'why does my character always look so confused, is that my fault', 0, 0, '', 'Pourquoi mon personnage a l’air complètement con ? C’est de ma faute ?', 'Warum sieht mein Charakter immer so verwirrt aus? Ist das meine Schuld?', '为什么我的角色老是一脸懵逼,是我的错吗?', '', '', '', ''), -('suggest_something_toxic', 'Hey your mum said goto bed!', 0, 0, '', 'Hey, ta mère a dit d’aller te coucher!', 'Hey, deine Mutter hat gesagt, du sollst ins Bett gehen!', '嘿,你妈说该上床睡觉了!', '', '', '', ''), -('suggest_something_toxic', 'Warcraft is a grind just like your mum was other day, she even has monthly subscription!', 0, 0, '', 'Warcraft, c’est une vraie corvée, tout comme ta mère l’était l’autre jour, elle a même un abonnement mensuel!', 'Warcraft ist ein Grind, genau wie deine Mutter neulich – sie hat sogar ein monatliches Abo!', '魔兽就是个刷刷刷的游戏,就像前几天你妈一样,她还有月卡呢!', '', '', '', ''), -('suggest_something_toxic', 'Is it true that Chuck Norris once leveled up without even pressing a button', 0, 0, '', 'Oh là là! Il parait que Chuck Norris a déjà monté de niveau sans même appuyer sur un bouton! C’est ça la légende de Chuck Norris, incroyable non?', 'Stimmt es, dass Chuck Norris einmal gelevelt hat, ohne eine Taste zu drücken?', '听说查克·诺里斯不用按键就能升级,这是真的吗?', '', '', '', ''), -('suggest_something_toxic', 'Chuck Norris doesn’t need a mount, the ground just moves under him', 0, 0, '', 'Chuck Norris n’a pas besoin d’une monture, le sol se déplace juste sous lui', 'Chuck Norris braucht kein Reittier, der Boden bewegt sich einfach unter ihm.', '查克·诺里斯不需要坐骑,地面会自己在他脚下移动。', '', '', '', ''), -('suggest_something_toxic', 'when Chuck Norris enters a raid, the boss just surrenders', 0, 0, '', 'Quand Chuck Norris entre dans un raid, le boss se rend simplement', 'Wenn Chuck Norris einen Raid betritt, gibt der Boss einfach auf.', '查克·诺里斯一进团队副本,BOSS直接投降。', '', '', '', ''), -('suggest_something_toxic', 'Chuck Norris doesn’t use abilities, he just looks at the enemies and they die', 0, 0, '', 'Chuck Norris ne se sert pas de ses capacités, il regarde juste les ennemis et ils meurent.', 'Chuck Norris benutzt keine Fähigkeiten, er schaut die Gegner einfach an und sie sterben.', '查克·诺里斯不用技能,瞪一眼敌人就死了。', '', '', '', ''), -('suggest_something_toxic', 'Chuck Norris doesn’t need healing, his enemies just heal him out of respect', 0, 0, '', 'Chuck Norris n’a pas besoin de guérison, ses ennemis le soignent juste par respect.', 'Chuck Norris braucht keine Heilung, seine Feinde heilen ihn aus Respekt.', '查克·诺里斯不需要治疗,敌人出于尊敬会主动给他加血。', '', '', '', ''), -('suggest_something_toxic', 'Chuck Norris can solo a raid... with no gear... and while AFK', 0, 0, '', 'Chuck Norris peut faire une raid tout seul... sans équipement... et en étant AFK', 'Chuck Norris kann einen Raid solo machen… ohne Ausrüstung… und während er AFK ist.', '查克·诺里斯能单刷团队副本……不穿装备……还挂机!', '', '', '', ''), -('suggest_something_toxic', 'Chuck Norris doesn’t need to dodge, the game dodges him', 0, 0, '', 'Chuck Norris n’a pas besoin d’esquiver, c’est le jeu qui l’évite', 'Chuck Norris muss nicht ausweichen, das Spiel weicht ihm aus.', '查克·诺里斯不需要躲闪,游戏会主动避开他。', '', '', '', ''), -('suggest_something_toxic', 'Chuck Norris once used a low-level weapon, and the game gave him the best loot', 0, 0, '', 'Chuck Norris a déjà utilisé une arme de bas niveau, et le jeu lui a donné le meilleur butin', 'Chuck Norris hat einmal eine Waffe mit niedrigem Level benutzt, und das Spiel hat ihm das beste Loot gegeben.', '查克·诺里斯用一把低级武器,游戏直接送他顶级装备。', '', '', '', ''), -('suggest_something_toxic', 'When Chuck Norris enters the instance, the loot automatically drops into his inventory', 0, 0, '', 'Quand Chuck Norris entre dans l’instance, le butin tombe automatiquement dans son inventaire.', 'Wenn Chuck Norris eine Instanz betritt, fällt das Loot automatisch in sein Inventar.', '查克·诺里斯一进副本,战利品自动掉进他背包。', '', '', '', ''), -('suggest_something_toxic', 'what is ligma', 0, 0, '', 'C’est quoi ligma', 'Was ist Ligma?', '啥是ligma?', '', '', '', ''), -('suggest_something_toxic', 'what is sugma', 0, 0, '', 'C’est quoi sugma', 'Was ist Sugma?', '啥是sugma?', '', '', '', ''), -('suggest_something_toxic', 'ligma balls', 0, 0, '', 'ligma balls', 'Ligma Balls', 'ligma balls', '', '', '', ''), -('suggest_something_toxic', 'sugma balls', 0, 0, '', 'sugma balls', 'Sugma Balls', 'sugma balls', '', '', '', ''), -('suggest_something_toxic', 'I EAT ASS', 0, 0, '', 'JE MANGE DES CULS', 'ICH ESSE ÄRSCHE', '我吃屁股!', '', '', '', ''), -('suggest_something_toxic', 'I want to shove %random_inventory_item_link up my ass', 0, 0, '', 'Je veux enfoncer %random_inventory_item_link dans mon cul.', 'Ich will mir %random_inventory_item_link in den Arsch stecken.', '我想把%random_inventory_item_link塞进我屁股里!', '', '', '', ''), -('suggest_something_toxic', 'I want to shove %random_inventory_item_link up your ass', 0, 0, '', 'Je veux te fourrer %random_inventory_item_link dans le cul.', 'Ich will dir %random_inventory_item_link in den Arsch schieben.', '我想把%random_inventory_item_link塞进你屁股里!', '', '', '', ''), -('suggest_something_toxic', 'Darnasses', 0, 0, '', 'Darnasses', 'Darnasses', '达纳苏斯的蠢货', '', '', '', ''), -('suggest_something_toxic', 'seems like your suffering from sugma', 0, 0, '', 'Il semble que tu souffres de sugma.', 'Sieht so aus, als würdest du unter Sugma leiden.', '看起来你得了“傻瓜病”啊', '', '', '', ''), -('suggest_something_toxic', 'deez nutz in ur mouth', 0, 0, '', 'deez nutz dans ta bouche', 'Diese Nüsse in deinem Mund.', '嘴里塞满了我的“坚果”吧', '', '', '', ''), -('suggest_something_toxic', 'cool boner, bro', 0, 0, '', 'Belle bite bro', 'Coole Latte, Bro.', '老兄,你的“装备”真不错啊', '', '', '', ''), -('suggest_something_toxic', 'ERP?', 0, 0, '', 'ERP?', 'ERP?', '来一场角色扮演吗?', '', '', '', ''), -('suggest_something_toxic', 'i tried everything, but at the end ERP did the trick', 0, 0, '', 'J’ai tout essayé, mais finalement l’ERP a fait l’affaire.', 'Ich habe alles versucht, aber am Ende hat ERP den Trick gemacht.', '我啥都试过了,最后还是角色扮演最管用', '', '', '', ''), -('suggest_something_toxic', 'I want to bonk in %zone_name', 0, 0, '', 'Je veux ken dans %zone_name.', 'Ich will in %zone_name bumsen.', '我想在%zone_name来一发', '', '', '', ''), -('suggest_something_toxic', 'looking for female gnome with gorilla pet for erp in %zone_name', 0, 0, '', 'Je recherche une gnome femelle avec un gorille de compagnie pour du jeu de rôle érotique dans %zone_name.', 'Suche weiblichen Gnom mit Gorilla-Haustier für ERP in %zone_name.', '在%zone_name找个带猩猩宠物的女性侏儒一起角色扮演', '', '', '', ''), -('suggest_something_toxic', 'i may understand an asshole, but a pervert?', 0, 0, '', 'Je peux comprendre un imbécile, mais un pervers ?', 'Einen Idioten kann ich verstehen, aber einen Perversen?', '我能理解一个混蛋,但一个变态?', '', '', '', ''), -('suggest_something_toxic', 'there is no gyat in %zone_name', 0, 0, '', 'Il n’y a pas de gyat dans %zone_name.', 'Es gibt kein Gyat in %zone_name.', '%zone_name里啥也没有啊', '', '', '', ''), -('suggest_something_toxic', 'I’m killing all the animals in %zone_name. Fuck the animals!!!', 0, 0, '', 'Je tue tous les animaux dans %zone_name. NIQUE les animaux enculé de vegan !!!', 'Ich töte alle Tiere in %zone_name. Scheiß auf die Tiere!!!', '我在%zone_name把所有动物都杀了!去他娘的动物!', '', '', '', ''), -('suggest_something_toxic', 'good think i got 3 legs', 0, 0, '', 'Je suis doté de 3 jambes', 'Gut, dass ich drei Beine habe.', '还好我有三条“腿”', '', '', '', ''), -('suggest_something_toxic', 'dont be mad im goonin like a sigma', 0, 0, '', 'Hey, ne sois pas fâché, je vais y aller comme un sigma.', 'Reg dich nicht auf, ich goone wie ein Sigma.', '别生气,我可是像头领狼一样在行动', '', '', '', ''), -('suggest_something_toxic', 'try finger, but hole', 0, 0, '', 'Essai ton doigt mais dans le cul ?', 'try finger, but hole', '试试用手指,不过是哪个洞呢?', '', '', '', ''), -('suggest_something_toxic', 'You somehow managed to mess that up, impressive', 0, 0, '', 'Tu as réussi à tout gâcher, c’est impressionnant.', 'Du hast es irgendwie geschafft, es zu vermasseln. Beeindruckend.', '你居然能把这事搞砸,真是厉害', '', '', '', ''), -('suggest_something_toxic', 'I could do this blindfolded, but I don’t need to', 0, 0, '', 'Je pourrais le faire les yeux bandés, mais ce n’est pas nécessaire.', 'Ich könnte das blind machen, aber das ist nicht nötig.', '我闭着眼睛都能搞定,根本不用睁眼', '', '', '', ''), -('suggest_something_toxic', 'You sure you know how to play this game?', 0, 0, '', 'Est-ce que tu es sûr de savoir comment jouer à ce jeu?', 'Bist du sicher, dass du weißt, wie man dieses Spiel spielt?', '你确定你会玩这个游戏吗?', '', '', '', ''), -('suggest_something_toxic', 'It’s like you’re trying to lose on purpose', 0, 0, '', 'Tu sembles vouloir perdre délibérément.', 'Es sieht so aus, als würdest du absichtlich verlieren wollen.', '你这操作像是故意想输啊', '', '', '', ''), -('suggest_something_toxic', 'I’ve seen better plays from level 1 characters', 0, 0, '', 'J’ai vu des meilleures performances de la part de personnages de niveau 1.', 'Ich habe bessere Spielzüge von Level-1-Charakteren gesehen.', '我见过1级的菜鸟都比你玩得好', '', '', '', ''), -('suggest_something_toxic', 'You’re like a speed bump in this game', 0, 0, '', 'Tu es comme un dos d’âne dans ce jeu.', 'Du bist wie ein Bremshügel in diesem Spiel.', '你在这游戏里就是个绊脚石', '', '', '', ''), -('suggest_something_toxic', 'Maybe you should try a different hobby', 0, 0, '', 'Peut-être que tu devrais essayer une autre passion.', 'Vielleicht solltest du ein anderes Hobby ausprobieren.', '也许你该换个兴趣,游戏不适合你', '', '', '', ''), -('suggest_something_toxic', 'Wow, your gameplay is like watching paint dry', 0, 0, '', 'Wow, ton gameplay c’est comme regarder de la peinture sécher', 'Wow, dein Gameplay ist wie beim Trocknen von Farbe zuzusehen.', '哇,你的玩法跟看油漆干一样无聊', '', '', '', ''), -('suggest_something_toxic', 'I think my alt could carry you through this', 0, 0, '', 'Je pense que mon ALT pourrait te carry', 'Ich glaube, mein Alter könnte dich hier durchziehen.', '我觉得我的小号都能带你飞过这关', '', '', '', ''), -('suggest_something_toxic', 'Are you sure you’re not trolling us?', 0, 0, '', 'T’est sure que tu nous troll pas ?', 'Bist du sicher, dass du uns nicht trollst?', '你确定你不是在逗我们玩?', '', '', '', ''), -('suggest_something_toxic', 'I think the enemy team is actually doing better with you on our side', 0, 0, '', 'Je crois que l’équipe adverse se débrouille mieux avec toi dans notre équipe.', 'Ich glaube, das gegnerische Team spielt besser, solange du in unserem Team bist.', '我觉得有你在我们这边,敌方队伍反而更强', '', '', '', ''), -('suggest_something_toxic', 'Maybe play the game instead of watching the scenery', 0, 0, '', 'Peut-être vaut-il mieux jouer au jeu au lieu de regarder le paysage.', 'Vielleicht solltest du das Spiel spielen, anstatt die Landschaft zu bewundern.', '别老盯着风景看,认真玩游戏吧', '', '', '', ''), -('suggest_something_toxic', 'I think you missed every single ability there', 0, 0, '', 'Je pense que vous avez manqué chaque capacité là.', 'Ich glaube, du hast jede einzelne Fähigkeit verfehlt.', '我觉得你一个技能都没打中', '', '', '', ''), -('suggest_something_toxic', 'Are you lost or just pretending to be bad?', 0, 0, '', 'T’es perdu ou tu fais simplement semblant d’être mauvais?', 'Bist du verloren oder tust du nur so, als wärst du schlecht?', '你是迷路了,还是故意装菜?', '', '', '', ''), -('suggest_something_toxic', 'I’ve seen level 1 characters take less damage than that', 0, 0, '', 'J’ai vu des personnages de niveau 1 prendre moins de dégâts que ça.', 'Ich habe Level-1-Charaktere gesehen, die weniger Schaden genommen haben als du.', '我见过1级的小号都比你挨的打少', '', '', '', ''), -('suggest_something_toxic', 'That was embarrassing for both of us', 0, 0, '', 'C’était gênant pour nous deux.', 'Das war peinlich für uns beide.', '这操作真让我们俩都丢脸', '', '', '', ''), -('suggest_something_toxic', 'I’ve seen better teamwork in a solo game', 0, 0, '', 'J’ai déjà vu une meilleure coopération dans un jeu solo.', 'Ich habe in einem Solo-Spiel bessere Teamarbeit gesehen.', '我单人游戏时看到的配合都比这强', '', '', '', ''), -('suggest_something_toxic', 'I’m just going to stand back and watch you fail', 0, 0, '', 'Je vais juste reculer et te regarder creuver.', 'Ich werde einfach zurücktreten und zusehen, wie du versagst.', '我就站这儿看你怎么失败', '', '', '', ''), -('suggest_something_toxic', 'Did you really think that was a good idea?', 0, 0, '', 'Penses-tu vraiment que c’était une bonne idée?', 'Hast du wirklich gedacht, das wäre eine gute Idee?', '你真觉得那是个好主意?', '', '', '', ''), -('suggest_something_toxic', 'I can’t believe you managed to fail that simple task', 0, 0, '', 'Je n’arrive pas à croire que tu aies échoué à cette tâche si simple.', 'Ich kann nicht glauben, dass du an dieser einfachen Aufgabe gescheitert bist.', '我真不敢相信你连这么简单的任务都能搞砸', '', '', '', ''), -('suggest_something_toxic', 'Are you here to help, or just to waste time?', 0, 0, '', 'Êtes-vous là pour aider, ou simplement perdre du temps ?', 'Bist du hier, um zu helfen, oder nur, um Zeit zu verschwenden?', '你是来帮忙的,还是来浪费时间的?', '', '', '', ''), -('suggest_something_toxic', 'It’s a good thing I’m here to carry this team', 0, 0, '', 'Heureusement que je suis là pour porter cette équipe', 'Gut, dass ich hier bin, um dieses Team zu tragen.', '还好有我在,不然这队伍早就完了', '', '', '', ''), -('suggest_something_toxic', 'I swear you make this harder than it needs to be', 0, 0, '', 'Tu sais, parfois tu rends ça plus difficile que nécessaire.', 'Ich schwöre, du machst das schwieriger, als es sein muss.', '我发誓,你把事情搞得比实际难多了', '', '', '', ''), -('suggest_something_toxic', 'How do you keep failing the simplest things?', 0, 0, '', 'Comment est-ce que je continue à échouer dans les tâches les plus simples?', 'Wie schaffst du es, selbst an den einfachsten Dingen zu scheitern?', '你怎么连最简单的事都能搞砸?', '', '', '', ''), -('suggest_something_toxic', 'Maybe try watching the game, instead of just playing', 0, 0, '', 'Peut-être que tu devrais essayer de regarder le match, au lieu de simplement y jouer.', 'Vielleicht solltest du das Spiel nur anschauen, anstatt es zu spielen.', '也许你该先看看别人怎么玩,而不是瞎搞', '', '', '', ''), -('suggest_something_toxic', 'You could at least try to pretend you’re good', 0, 0, '', 'Tu pourrais au moins essayer de faire semblant d’être compétent.', 'Du könntest wenigstens so tun, als wärst du gut.', '你至少可以假装自己很厉害吧', '', '', '', ''), -('suggest_something_toxic', 'If you keep this up, we’ll be here all day', 0, 0, '', 'Si tu continues comme ça, on va y passer la journée entière.', 'Wenn du so weitermachst, sitzen wir hier den ganzen Tag.', '你再这样下去,我们得在这耗一整天', '', '', '', ''), -('suggest_something_toxic', 'Honestly, that was the worst decision ever', 0, 0, '', 'Franchement, c’était la pire décision que j’aie jamais prise.', 'Ganz ehrlich, das war die schlechteste Entscheidung aller Zeiten.', '老实说,这是我见过的最烂决定', '', '', '', ''), -('suggest_something_toxic', 'I don’t think you’re cut out for this game', 0, 0, '', 'Je ne pense pas que tu sois fait pour ce jeu.', 'Ich glaube nicht, dass dieses Spiel das Richtige für dich ist.', '我觉得你真不适合玩这游戏', '', '', '', ''), -('suggest_something_toxic', 'What was the point of that move?', 0, 0, '', 'Quel était l’intérêt de ce coup ?', 'Was sollte diese Aktion bringen?', '你那操作到底想干嘛?', '', '', '', ''), -('suggest_something_toxic', 'You’re like a burden on this team', 0, 0, '', 'Tu es comme un fardeau pour cette équipe.', 'Du bist wie ein Klotz am Bein für dieses Team.', '你就是这队伍的累赘', '', '', '', ''), -('suggest_something_toxic', 'You really think you’re a pro, huh?', 0, 0, '', 'Tu te prends vraiment pour un pro, hein ?', 'Du hältst dich echt für einen Profi, oder?', '你真觉得自己是大神啊?', '', '', '', ''), -('suggest_something_toxic', 'Maybe it’s time to just quit and save us all the trouble', 0, 0, '', 'Il serait temps de déco et nous épargner tes problèmes.', 'Vielleicht solltest du einfach aufhören und uns allen Ärger ersparen.', '也许你该退出,省得给我们添麻烦', '', '', '', ''), - - --- random generic toxic item links --- usable placeholders: --- %prefix --- %random_taken_quest_or_item_link --- %random_inventory_item_link --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('suggest_toxic_links', '%prefix %random_taken_quest_or_item_link', 0, 0, '', '.', '', '', '', '', '', ''), -('suggest_toxic_links', '%prefix %random_inventory_item_link', 0, 0, '', '', '', '', '', '', '', ''), - - --- thunderfury spam --- usable placeholders: --- %thunderfury_link -('thunderfury_spam', '%thunderfury_link', 0, 0, '', '.', '', '', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link%thunderfury_link', 0, 0, '', '', '', '', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link%thunderfury_link%thunderfury_link', 0, 0, '', '', '', '', '', '', '', ''), -('thunderfury_spam', 'I think I just heard %thunderfury_link', 0, 0, '', 'J’ai entendu %thunderfury_link', 'Ich glaube, ich habe gerade %thunderfury_link gehört.', '我好像刚听到了%thunderfury_link', '', '', '', ''), -('thunderfury_spam', 'I think I heard %thunderfury_link', 0, 0, '', 'Je viens juste d’entendre %thunderfury_link', 'Ich glaube, ich habe %thunderfury_link gehört.', '我觉得我听到了%thunderfury_link', '', '', '', ''), -('thunderfury_spam', 'I definitely heard %thunderfury_link', 0, 0, '', 'Je suis sûr d’avoir entendu parler de %thunderfury_link.', 'Ich bin mir sicher, dass ich %thunderfury_link gehört habe.', '我确定听到了%thunderfury_link', '', '', '', ''), -('thunderfury_spam', 'I dunno but I’m pretty sure I heard %thunderfury_link', 0, 0, '', 'Je ne sais pas, mais je suis presque certain d’avoir entendu %thunderfury_link.', 'Keine Ahnung, aber ich bin mir ziemlich sicher, dass ich %thunderfury_link gehört habe.', '我不确定,但我几乎肯定听到了%thunderfury_link', '', '', '', ''), -('thunderfury_spam', 'Did you just say %thunderfury_link', 0, 0, '', 'Qui vient de parler de %thunderfury_link?', 'Hast du gerade %thunderfury_link gesagt?', '你刚刚是不是说了%thunderfury_link?', '', '', '', ''), -('thunderfury_spam', 'did someone say %thunderfury_link', 0, 0, '', 'Oh, vous avez mentionné %thunderfury_link ?', 'Hat jemand gerade %thunderfury_link erwähnt?', '有人提到%thunderfury_link了吗?', '', '', '', ''), -('thunderfury_spam', 'Did someone say %thunderfury_link ?', 0, 0, '', 'Qui a mentionné %thunderfury_link ?', 'Hat jemand %thunderfury_link gesagt?', '有人说了%thunderfury_link吗?', '', '', '', ''), -('thunderfury_spam', 'Someone said %thunderfury_link', 0, 0, '', 'Quelqu’un a mentionné %thunderfury_link.', 'Jemand hat %thunderfury_link erwähnt.', '有人提到%thunderfury_link了!', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link is coming out of the closet', 0, 0, '', '%thunderfury_link sort du placard', '%thunderfury_link kommt aus dem Schrank heraus.', '%thunderfury_link终于现身了!', '', '', '', ''), -('thunderfury_spam', 'could swear it was a %thunderfury_link, might have been %thunderfury_link tho', 0, 0, '', 'Je pourrais jurer que c’était un %thunderfury_link, mais ça aurait aussi pu être %thunderfury_link en fait.', 'Ich könnte schwören, es war ein %thunderfury_link, aber es könnte auch %thunderfury_link gewesen sein.', '我敢发誓那是%thunderfury_link,不过也可能是%thunderfury_link吧。', '', '', '', ''), -('thunderfury_spam', 'Why use %thunderfury_link when %thunderfury_link is clearly way more OP', 0, 0, '', 'Pourquoi utiliser %thunderfury_link quand %thunderfury_link est clairement beaucoup plus puissant.', 'Warum %thunderfury_link benutzen, wenn %thunderfury_link eindeutig viel stärker ist?', '为什么用%thunderfury_link,%thunderfury_link明显更强啊!', '', '', '', ''), -('thunderfury_spam', 'Did I just hear %thunderfury_link?', 0, 0, '', 'Ai-je juste entendu %thunderfury_link?', 'Habe ich gerade %thunderfury_link gehört?', '我刚才是听到%thunderfury_link了吗?', '', '', '', ''), -('thunderfury_spam', 'No way... is that a %thunderfury_link I hear?', 0, 0, '', 'Pas possible... Est-ce que j’entends %thunderfury_link ?', 'Unmöglich... Habe ich da etwa %thunderfury_link gehört?', '不会吧……我听到了%thunderfury_link?', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link? Sounds like something only legends wield!', 0, 0, '', '%thunderfury_link? Ça a l’air d’être quelque chose que seuls les légendes manient!', '%thunderfury_link? Klingt nach etwas, das nur Legenden führen!', '%thunderfury_link?听起来只有传说中的英雄才能驾驭!', '', '', '', ''), -('thunderfury_spam', 'I can feel the power of %thunderfury_link in the air!', 0, 0, '', 'Je peux sentir la puissance de %thunderfury_link dans l’air !', 'Ich kann die Macht von %thunderfury_link in der Luft spüren!', '我能感受到空气中%thunderfury_link的力量!', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link is the *real* MVP', 0, 0, '', '%thunderfury_link est le *vrai* MVP.', '%thunderfury_link ist der *wahre* MVP.', '%thunderfury_link才是真正的MVP!', '', '', '', ''), -('thunderfury_spam', 'Can someone get me a %thunderfury_link? Asking for a friend... maybe even two!', 0, 0, '', 'Est-ce que quelqu’un peut me trouver un %thunderfury_link ? Je demande pour un ami... peut-être même deux !', 'Kann mir jemand ein %thunderfury_link besorgen? Frage für einen Freund... oder zwei!', '谁能帮我搞把%thunderfury_link?替朋友问的……可能还得两把!', '', '', '', ''), -('thunderfury_spam', 'Hey, did %thunderfury_link just *smite* something? I swear I felt the ground shake.', 0, 0, '', 'Hey, est-ce que %thunderfury_link vient de *foudroyer* quelque chose ? Je te jure que j’ai senti la terre trembler.', 'Hey, hat %thunderfury_link gerade etwas *erschlagen*? Ich schwöre, ich habe den Boden beben gespürt.', '嘿,%thunderfury_link刚是不是劈了什么?我发誓感觉地面都在抖!', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link on the battlefield... It’s like an instant flex.', 0, 0, '', '%thunderfury_link sur le champ de bataille... C’est comme montrer sa puissance instantanément.', '%thunderfury_link auf dem Schlachtfeld... Das ist purer Flex.', '%thunderfury_link在战场上……简直就是瞬间秀肌肉!', '', '', '', ''), -('thunderfury_spam', 'I *think* I heard %thunderfury_link... Or was that just a myth?', 0, 0, '', '*Je crois* avoir entendu %thunderfury_link... Ou peut-être que c’était juste un mythe?', '*Ich glaube*, ich habe %thunderfury_link gehört... Oder war das nur ein Mythos?', '我*好像*听到了%thunderfury_link……还是只是个传说?', '', '', '', ''), -('thunderfury_spam', 'Wait, did I just see someone wielding %thunderfury_link?!', 0, 0, '', 'Attends, est-ce que je viens de voir quelqu’un manier %thunderfury_link ?!', 'Warte mal, habe ich gerade jemanden gesehen, der %thunderfury_link führt?!', '等等,我刚是不是看到有人拿着%thunderfury_link?!', '', '', '', ''), -('thunderfury_spam', 'The power of %thunderfury_link is *undeniable*', 0, 0, '', 'Le pouvoir de %thunderfury_link est *indéniable*', 'Die Macht von %thunderfury_link ist *unbestreitbar*', '%thunderfury_link的力量*无可否认*!', '', '', '', ''), -('thunderfury_spam', 'I’m 99% sure that was %thunderfury_link. 1% might have been a random NPC, tho.', 0, 0, '', 'Je suis à 99% certain que c’était %thunderfury_link. Le 1% restant aurait pu être un PNJ aléatoire, cependant.', 'Ich bin zu 99% sicher, dass das %thunderfury_link war. Die restlichen 1% könnten ein zufälliger NPC gewesen sein.', '我99%确定那是%thunderfury_link。不过1%可能是随机NPC吧。', '', '', '', ''), -('thunderfury_spam', '%thunderfury_link in action! Who needs an army when you’ve got that kind of power?', 0, 0, '', '%thunderfury_link en action ! Qui a besoin d’une armée lorsqu’on a ce genre de puissance ?', '%thunderfury_link in Aktion! Wer braucht eine Armee, wenn man so eine Macht hat?', '%thunderfury_link出手了!有这种力量谁还需要军队?', '', '', '', ''), -('thunderfury_spam', 'I heard %thunderfury_link is what really brings all the boys to the yard.', 0, 0, '', 'J’ai entendu dire que %thunderfury_link, c’est ce qui attire vraiment tous les garçons dans la cour.', 'Ich habe gehört, dass %thunderfury_link wirklich alle Jungs in den Hof lockt.', '听说%thunderfury_link才是真正吸引大家的东西!', '', '', '', ''), -('thunderfury_spam', 'Why even use %thunderfury_link when %thunderfury_link is clearly the superior option?', 0, 0, '', 'Pourquoi même utiliser %thunderfury_link quand %thunderfury_link est clairement l’option supérieure ?', 'Warum überhaupt %thunderfury_link benutzen, wenn %thunderfury_link klar die bessere Option ist?', '干嘛还用%thunderfury_link,%thunderfury_link明显是更好的选择啊!', '', '', '', ''), -('thunderfury_spam', 'Is it me or does %thunderfury_link have a “smack talk” passive effect?', 0, 0, '', 'Est-ce moi ou est-ce que %thunderfury_link a un effet passif de provocation ?', 'Bin ich der Einzige, oder hat %thunderfury_link eine passive “Trash Talk”-Fähigkeit?', '是我感觉错了吗,%thunderfury_link是不是自带嘲讽被动效果?', '', '', '', ''), -('thunderfury_spam', 'I thought I heard %thunderfury_link, but it was just the sound of sheer awesome.', 0, 0, '', 'J’ai cru entendre %thunderfury_link, mais en fait c’était juste le son de la pure puissance.', 'Ich dachte, ich hätte %thunderfury_link gehört, aber es war nur der Klang reiner Epikness.', '我以为听到了%thunderfury_link,结果只是纯粹的震撼之声。', '', '', '', ''), -('thunderfury_spam', 'Oh, %thunderfury_link... Where *have* you been all my life?', 0, 0, '', 'Oh, %thunderfury_link ... Où as-tu été tout ce temps, ma belle épée ?', 'Oh, %thunderfury_link... Wo *warst* du mein ganzes Leben lang?', '哦,%thunderfury_link……我这辈子怎么才遇到你?', '', '', '', ''), -('thunderfury_spam', 'Did someone say %thunderfury_link? I think I need to go pick mine up.', 0, 0, '', '%thunderfury_link? Oh là là, j’entends toujours parler de cet incroyable objet! Je crois bien qu’il est temps pour moi d’aller le chercher.', 'Hat jemand %thunderfury_link gesagt? Ich glaube, ich muss meine jetzt auch abholen.', '有人提到%thunderfury_link了?我觉得我也得去弄一把。', '', '', '', ''), -('thunderfury_spam', 'That was definitely the sound of %thunderfury_link! I can feel the epicness.', 0, 0, '', 'C’était certainement le son de %thunderfury_link ! Je peux sentir toute son épopée.', 'Das war definitiv der Klang von %thunderfury_link! Ich kann die Epikness spüren.', '那绝对是%thunderfury_link的声音!我能感受到那份史诗感!', '', '', '', ''), -('thunderfury_spam', 'Is it just me, or does %thunderfury_link have a tendency to make everyone else *look* weak?', 0, 0, '', 'C’est moi ou bien %thunderfury_link a tendance à faire paraître les autres faibles?', 'Bin ich der Einzige, oder lässt %thunderfury_link alle anderen *schwach* aussehen?', '是我一个人觉得吗,%thunderfury_link让其他人都显得*好弱*?', '', '', '', ''), -('thunderfury_spam', 'You know what they say... the louder the %thunderfury_link, the bigger the bragging rights!', 0, 0, '', 'Tu sais ce qu’on dit... plus le %thunderfury_link rugit, plus grand sont les droits de vantardise!', 'Du weißt, was man sagt... je lauter das %thunderfury_link, desto größer die Prahlrechte!', '都知道……%thunderfury_link声音越大,吹牛的资本就越足!', '', '', '', ''), - - --- random WTS --- usable placeholders: --- %%item_formatted_link --- %item_formatted_link --- %item_count --- %cost_gold --- %my_class --- %my_race --- %my_level -('suggest_sell', 'WTS %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold.', 'Verkaufe %item_formatted_link für %cost_gold.', '卖%item_formatted_link,只要%cost_gold!', '', 'Quiero vender %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'Who wants %item_formatted_link for %cost_gold?', 0, 0, '', 'Qui veut %item_formatted_link pour %cost_gold?', 'Wer will %item_formatted_link für %cost_gold?', '谁想要%item_formatted_link?只要%cost_gold!', '', '¿Quién quiere %item_formatted_link por %cost_gold?', '', ''), -('suggest_sell', 'Anyone wants %item_formatted_link? Only %cost_gold.', 0, 0, '', 'Est-ce que quelqu’un veut %item_formatted_link ? Seulement %cost_gold.', 'Will jemand %item_formatted_link? Nur %cost_gold.', '有人要%item_formatted_link吗?只要%cost_gold。', '', '¿Alguien quiere %item_formatted_link? Solo %cost_gold.', '', ''), -('suggest_sell', 'Just %cost_gold for %item_formatted_link!', 0, 0, '', 'Juste %cost_gold pour %item_formatted_link!', 'Nur %cost_gold für %item_formatted_link!', '只要%cost_gold就能拿走%item_formatted_link!', '', '¡Solo %cost_gold para %item_formatted_link!', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold.', 'Verkaufe %item_formatted_link für %cost_gold.', '出售%item_formatted_link,价格%cost_gold。', '', 'Vendo %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', '%item_formatted_link is yours just for %cost_gold!', 0, 0, '', '%item_formatted_link est à toi pour seulement %cost_gold !', '%item_formatted_link gehört dir für nur %cost_gold!', '%item_formatted_link归你了,只要%cost_gold!', '', '%item_formatted_link es tuyo solo por %cost_gold!', '', ''), -('suggest_sell', 'Ridiculus price of %cost_gold for %item_formatted_link!', 0, 0, '', 'Le prix ridicule de %cost_gold pour %item_formatted_link !', 'Lächerlicher Preis von %cost_gold für %item_formatted_link!', '白菜价%cost_gold卖%item_formatted_link!', '', '¡Precio ridículo de %cost_gold para %item_formatted_link_link!', '', ''), -('suggest_sell', 'Wanna sell %item_formatted_link for %cost_gold.', 0, 0, '', 'Je veux vendre %item_formatted_link pour %cost_gold.', 'Möchte %item_formatted_link für %cost_gold verkaufen.', '想卖掉%item_formatted_link,价格%cost_gold。', '', 'Quiero vender %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'Who needs %item_formatted_link? Only %cost_gold.', 0, 0, '', 'Qui a besoin de %item_formatted_link? Seulement %cost_gold.', 'Wer braucht %item_formatted_link? Nur %cost_gold.', '谁需要%item_formatted_link?只要%cost_gold!', '', '¿Quién necesita %item_formatted_link? Solo %cost_gold.', '', ''), -('suggest_sell', 'Anyone needs %item_formatted_link for %cost_gold?', 0, 0, '', 'Besoin du %item_formatted_link pour %cost_gold?', 'Braucht jemand %item_formatted_link für %cost_gold?', '有人需要%item_formatted_link吗?价格%cost_gold。', '', '¿Alguien necesita %item_formatted_link_link por %cost_gold?', '', ''), -('suggest_sell', '%cost_gold for %item_formatted_link. Less than AH!', 0, 0, '', '%cost_gold pour %item_formatted_link. Moins cher que l’HD !', '%cost_gold für %item_formatted_link. Günstiger als im AH!', '%item_formatted_link只要%cost_gold,比拍卖行还便宜!', '', '%cost_gold para %item_formatted_link. ¡Menos que en las subastas!', '', ''), -('suggest_sell', '%item_formatted_link is expensive, but I’d sell it for %cost_gold.', 0, 0, '', '%item_formatted_link est cher, mais je le vendrais pour %cost_gold.', '%item_formatted_link ist teuer, aber ich würde es für %cost_gold verkaufen.', '%item_formatted_link挺贵的,但我愿意%cost_gold卖给你。', '', '%item_formatted_link es caro, pero lo vendería por %cost_gold.', '', ''), -('suggest_sell', 'You’ll never find %item_formatted_link cheaper than %cost_gold!', 0, 0, '', 'Tu ne trouveras jamais %item_formatted_link moins cher que %cost_gold !', 'Du wirst %item_formatted_link niemals günstiger als für %cost_gold finden!', '你绝对找不到比%cost_gold更便宜的%item_formatted_link!', '', '¡Nunca encontrarás %item_formatted_link más barato que %cost_gold!', '', ''), -('suggest_sell', 'Need more than %item_formatted_link!', 0, 0, '', 'J’ai besoin de plus de %item_formatted_link !', 'Ich brauche mehr als %item_formatted_link!', '我需要更多的%item_formatted_link!', '', '¡Necesito más %item_formatted_link!', '', ''), -('suggest_sell', 'I have %item_formatted_link and need more.', 0, 0, '', 'J’ai %item_formatted_link et j’en ai besoin de plus.', 'Ich habe %item_formatted_link und brauche mehr.', '我有%item_formatted_link,但还需要更多。', '', 'Tengo %item_formatted_link y necesito más. ¿Alguien me vende alguno?', '', ''), -('suggest_sell', 'Have %item_formatted_link. Who wants to buy for %cost_gold?', 0, 0, '', 'J’ai %item_formatted_link. Qui veut acheter pour %cost_gold?', 'Ich habe %item_formatted_link. Wer will es für %cost_gold kaufen?', '我有%item_formatted_link,谁想买?只要%cost_gold!', '', 'Tengo %item_formatted_link. ¿Quién quiere comprarlo por %cost_gold?', '', ''), -('suggest_sell', 'Anyone WTB %item_formatted_link for %cost_gold?', 0, 0, '', 'Quelqu’un veut acheter %item_formatted_link pour %cost_gold?', 'Will jemand %item_formatted_link für %cost_gold kaufen?', '有人想买%item_formatted_link吗?价格%cost_gold!', '', '¿Alguien compra %item_formatted_link por %cost_gold?', '', ''), -('suggest_sell', 'What about %item_formatted_link? For %cost_gold.', 0, 0, '', 'Qu’en est-il de %item_formatted_link ? Pour %cost_gold.', 'Was ist mit %item_formatted_link? Für %cost_gold.', '%item_formatted_link怎么样?只要%cost_gold。', '', '¿Qué pasa con %item_formatted_link? Por el oro.', '', ''), -('suggest_sell', 'Who said I am a bastard? %item_formatted_link for %cost_gold is a good price.', 0, 0, '', 'Qui a dit que je suis un bâtard ? %item_formatted_link pour %cost_gold, c’est un bon prix.', 'Wer hat gesagt, dass ich ein Gauner bin? %item_formatted_link für %cost_gold ist ein guter Preis.', '谁说我是坑货?%item_formatted_link卖%cost_gold已经很公道了!', '', '¿Quién ha dicho que soy un rata? %item_formatted_link por %cost_gold es una ganga.', '', ''), -('suggest_sell', 'I am selling %item_formatted_link? Just %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link à seulement %cost_gold.', 'Ich verkaufe %item_formatted_link? Nur %cost_gold.', '我在卖%item_formatted_link,只要%cost_gold!', '', '¿Vendo %item_formatted_link? Solo %cost_gold.', '', ''), -('suggest_sell', 'LFG for farming. You can still buy %item_formatted_link I have for %cost_gold.', 0, 0, '', 'Je cherche des gens pour farmer. Tu peux toujours acheter %item_formatted_link que j’ai pour %cost_gold.', 'LFG für Farmruns. Du kannst immer noch %item_formatted_link für %cost_gold kaufen.', '组队刷怪,有兴趣的可以买我的%item_formatted_link,只要%cost_gold。', '', 'LFG para farmear. Todavía puedes comprar %item_formatted_link que tengo por %cost_gold.', '', ''), -('suggest_sell', 'Sold almost everything today. Still have %item_formatted_link for %cost_gold.', 0, 0, '', 'J’ai presque tout vendu aujourd’hui. Il me reste seulement %item_formatted_link pour %cost_gold.', 'Habe heute fast alles verkauft. Habe nur noch %item_formatted_link für %cost_gold.', '今天几乎都卖光了,还剩%item_formatted_link,卖%cost_gold。', '', 'Se vendió casi todo hoy. Todavía tengo %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'What use for trade chat? Of course to sell %item_formatted_link for %cost_gold.', 0, 0, '', 'Pourquoi utiliser le chat de commerce ? Eh bien, évidemment pour vendre mon %item_formatted_link au prix de %cost_gold.', 'Wofür gibt es den Handelschat? Natürlich um %item_formatted_link für %cost_gold zu verkaufen.', '交易频道有什么用?当然是卖我的%item_formatted_link,价格%cost_gold!', '', '¿De qué sirve el chat comercial? Por supuesto, para vender %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'Can anyone beat the price of %cost_gold for %item_formatted_link?', 0, 0, '', 'Est-ce que quelqu’un peut battre le prix de %cost_gold pour %item_formatted_link ?', 'Kann jemand den Preis von %cost_gold für %item_formatted_link unterbieten?', '有人能比%cost_gold更便宜卖%item_formatted_link吗?', '', '¿Alguien puede superar el precio de %cost_gold por %item_formatted_link?', '', ''), -('suggest_sell', 'Wanna stop trade chat? Just buy %item_formatted_link? For %cost_gold!', 0, 0, '', 'Tu veux arrêter le chat commercial ? Il suffit d’acheter %item_formatted_link ! Pour seulement %cost_gold !', 'Willst du den Handelschat stoppen? Kauf einfach %item_formatted_link für %cost_gold!', '想让交易频道安静?买我的%item_formatted_link吧,只要%cost_gold!', '', '¿Quieres detener el chat comercial? ¿Solo compra %item_formatted_link? ¡Por el oro!', '', ''), -('suggest_sell', 'Everybody spams in trade chat. Me too - %cost_gold for %item_formatted_link!', 0, 0, '', 'Tout le monde spamme dans le chat commercial. Moi aussi - %cost_gold pour %item_formatted_link !', 'Jeder spammt im Handelschat. Ich auch – %cost_gold für %item_formatted_link!', '交易频道都在刷屏,我也来——%item_formatted_link卖%cost_gold!', '', 'Todo el mundo envía spam en el chat y yo también! ¡%item_formatted_link por %cost_gold!', '', ''), -('suggest_sell', 'Is %item_formatted_link any use? Just selling it for %cost_gold.', 0, 0, '', 'Est-ce que %item_formatted_link est utile ? Je le vends juste pour %cost_gold.', 'Ist %item_formatted_link überhaupt nützlich? Verkaufe es nur für %cost_gold.', '%item_formatted_link有用吗?随便卖了,价格%cost_gold。', '', '¿Es %item_formatted_link útil? Solo lo vendo por %cost_gold.', '', ''), -('suggest_sell', 'I have %item_formatted_link ready to sell you for %cost_gold.', 0, 0, '', 'J’ai %item_formatted_link prêt à te vendre pour %cost_gold.', 'Ich habe %item_formatted_link zum Verkauf für %cost_gold.', '我有%item_formatted_link,可以卖给你,只要%cost_gold。', '', 'Tengo %item_formatted_link listo a la venta por %cost_gold.', '', ''), -('suggest_sell', 'Did nothing yesterday but have got %item_formatted_link. Selling it for %cost_gold.', 0, 0, '', 'Je n’ai rien fait hier mais j’ai %item_formatted_link Je le vends pour %cost_gold', 'Habe gestern nichts gemacht, aber %item_formatted_link bekommen. Verkaufe es für %cost_gold.', '昨天啥也没干,但搞到了%item_formatted_link,卖%cost_gold。', '', 'No hice nada ayer pero consegu’i %item_formatted_link. Lo vendo por %cost_gold.', '', ''), -('suggest_sell', 'Farmed yesterday and got %item_formatted_link. Anyone wtb for %cost_gold?', 0, 0, '', 'J’ai farmé hier et j’ai obtenu %item_formatted_link. Quelqu’un est intéressé pour %cost_gold ?', 'Gestern gefarmt und %item_formatted_link bekommen. Will jemand es für %cost_gold kaufen?', '昨天刷怪拿到了%item_formatted_link,有人想买吗?价格%cost_gold。', '', 'Ayer farmee y conseguí mucho %item_formatted_link. ¿Alguien lo quiere comprar por %cost_gold?', '', ''), -('suggest_sell', 'Bought %item_formatted_link yesterday. Anyone needs it for %cost_gold?', 0, 0, '', 'J’ai acheté %item_formatted_link hier. Est-ce que quelqu’un en a besoin pour %cost_gold?', 'Habe gestern %item_formatted_link gekauft. Braucht es jemand für %cost_gold?', '昨天买了%item_formatted_link,有人需要吗?只要%cost_gold。', '', 'Ayer compré %item_formatted_link. ¿Alguien lo quiere por %cost_gold?', '', ''), -('suggest_sell', 'Who asked for %item_formatted_link? The price is the same - %cost_gold.', 0, 0, '', 'Qui a demandé %item_formatted_link? Le prix est le même - %cost_gold.', 'Wer hat nach %item_formatted_link gefragt? Der Preis bleibt derselbe – %cost_gold.', '谁问%item_formatted_link的?价格没变,还是%cost_gold。', '', '¿Quién pidió %item_formatted_link? El precio es el mismo: %cost_gold.', '', ''), -('suggest_sell', 'I sill have %item_formatted_link. WTB for %cost_gold?', 0, 0, '', 'J’ai toujours %item_formatted_link. Je cherche à l’acheter pour %cost_gold.', 'Ich habe immer noch %item_formatted_link. Will jemand es für %cost_gold kaufen?', '我还有%item_formatted_link,有人想买吗?价格%cost_gold。', '', 'Todavía tengo %item_formatted_link. ¿Alguien lo compra por %cost_gold?', '', ''), -('suggest_sell', 'I used to have more than %item_formatted_link. Now needs to sell it for %cost_gold.', 0, 0, '', 'J’avais plus de %item_formatted_link avant. Maintenant, je dois le vendre pour %cost_gold.', 'Früher hatte ich mehr als %item_formatted_link. Jetzt muss ich es für %cost_gold verkaufen.', '我以前有好多%item_formatted_link,现在得卖了,价格%cost_gold。', '', 'Solía tener más de %item_formatted_link. Ahora necesito venderlo por %cost_gold.', '', ''), -('suggest_sell', 'I wish I have more than %item_formatted_link. You could buy it for %cost_gold anyways.', 0, 0, '', 'J’aimerais avoir plus que %item_formatted_link. De toute façon, tu pourrais l’acheter pour %cost_gold.', 'Ich wünschte, ich hätte mehr als %item_formatted_link. Du könntest es trotzdem für %cost_gold kaufen.', '我希望能有更多%item_formatted_link,反正你可以先买这个,价格%cost_gold。', '', 'Desearía tener más de %item_formatted_link. Podrías comprarlo por %cost_gold de todos modos.', '', ''), -('suggest_sell', 'What use for your gold? To buy my %item_formatted_link for %cost_gold.', 0, 0, '', 'Que vais-je faire de ton or ? Acheter mon %item_formatted_link pour %cost_gold.', 'Wofür dein Gold? Um mein %item_formatted_link für %cost_gold zu kaufen.', '你的金币有啥用?当然是买我的%item_formatted_link啦,价格%cost_gold。', '', '¿De qué sirve tu oro? Para comprar mi %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'Please spare some gold for me. You can buy %item_formatted_link for %cost_gold.', 0, 0, '', 'S’il vous plaît, épargnez-moi de l’or. Vous pouvez acheter %item_formatted_link pour %cost_gold', 'Bitte gib mir etwas Gold. Du kannst %item_formatted_link für %cost_gold kaufen.', '行行好,给我点金币吧。你可以花 %cost_gold 买下 %item_formatted_link。', '', 'Por favor, dame algo de oro. Puedes comprar %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'Is %cost_gold is a good price for %item_formatted_link?', 0, 0, '', 'Est-ce que %cost_gold, c’est un bon prix pour %item_formatted_link?', 'Ist %cost_gold ein guter Preis für %item_formatted_link?', '%cost_gold 买 %item_formatted_link,这个价格咋样?', '', '¿%cost_gold es un buen precio para %item_formatted_link?', '', ''), -('suggest_sell', 'Just bought yesterday %item_formatted_links, but do not need it anymore. Anyone wants for %cost_gold?', 0, 0, '', 'Hier, j’ai acheté %item_formatted_links, mais je n’en ai plus besoin. Est-ce que quelqu’un le veut pour %cost_gold?', 'Gestern habe ich %item_formatted_links gekauft, brauche es aber nicht mehr. Will es jemand für %cost_gold?', '昨天刚买的 %item_formatted_links,现在用不上了,有人要吗?只要 %cost_gold!', '', 'Ayer compré %item_formatted_link, pero ya no los necesito. ¿Alguien lo quiere por %cost_gold?', '', ''), -('suggest_sell', 'I am going to post %item_formatted_link on the AH but you can buy it now cheaper just for %cost_gold.', 0, 0, '', 'Je vais mettre en vente %item_formatted_link sur l’HV, mais tu peux l’acheter maintenant à un prix réduit, pour seulement %cost_gold.', 'Ich werde %item_formatted_link ins AH stellen, aber du kannst es jetzt günstiger für %cost_gold kaufen.', '我正打算把 %item_formatted_link 挂拍卖行,但你现在就可以便宜买走,只要 %cost_gold!', '', 'Voy a poner %item_formatted_link en la casa de subastas pero puedes comprarlo ahora más barato solo por %cost_gold.', '', ''), -('suggest_sell', 'Why the #!@ have I bought %item_formatted_link? Anyone needs it for %cost_gold?', 0, 0, '', 'Pourquoi diable ai-je acheté %item_formatted_link? Est-ce que quelqu’un en a besoin pour %cost_gold?', 'Warum zum Teufel habe ich %item_formatted_link gekauft? Braucht es jemand für %cost_gold?', '我咋就脑抽买了 %item_formatted_link 呢?有人需要吗?只要 %cost_gold!', '', '¿Por qué #!@ he comprado %item_formatted_link? ¿Alguien lo necesita por %cost_gold?', '', ''), -('suggest_sell', 'WTS %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold.', 'Verkaufe %item_formatted_link für %cost_gold.', '出售 %item_formatted_link,只要 %cost_gold!', '', 'Quiero vender %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', 'Who wants %item_formatted_link for %cost_gold?', 0, 0, '', 'Qui veut %item_formatted_link pour %cost_gold ?', 'Wer will %item_formatted_link für %cost_gold?', '谁想要 %item_formatted_link?只要 %cost_gold!', '', '¿Quién quiere %item_formatted_link por %cost_gold?', '', ''), -('suggest_sell', 'Anyone wants %item_formatted_link? Only %cost_gold.', 0, 0, '', 'Qui veut %item_formatted_link ? Seulement %cost_gold.', 'Will jemand %item_formatted_link? Nur %cost_gold.', '有人要 %item_formatted_link 吗?只卖 %cost_gold!', '', '¿Alguien quiere %item_formatted_link? Solo %cost_gold.', '', ''), -('suggest_sell', 'Just %cost_gold for %item_formatted_link!', 0, 0, '', 'Juste %cost_gold pour %item_formatted_link !', 'Nur %cost_gold für %item_formatted_link!', '只要 %cost_gold 就能拿下 %item_formatted_link!', '', '¡Solo %cost_gold para %item_formatted_link!', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends cet %item_formatted_link pour %cost_gold.', 'Verkaufe %item_formatted_link für %cost_gold.', '卖 %item_formatted_link,只需 %cost_gold!', '', 'Vendo %item_formatted_link por %cost_gold.', '', ''), -('suggest_sell', '%item_formatted_link is yours just for %cost_gold!', 0, 0, '', '%item_formatted_link est à toi pour seulement %cost_gold!', '%item_formatted_link gehört dir für nur %cost_gold!', '%item_formatted_link 只要 %cost_gold 就是你的了!', '', '%item_formatted_link es tuyo solo por %cost_gold!', '', ''), -('suggest_sell', 'WTS %item_formatted_link for %cost_gold. DM if interested.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold en or. Envoyez-moi un message si vous êtes intéressé(e).', 'Verkaufe %item_formatted_link für %cost_gold. Schreibe mir bei Interesse.', '卖 %item_formatted_link,价格 %cost_gold,有意私聊!', '', 'Quiero vender %item_formatted_link por %cost_gold. Envíame un mensaje si te interesa.', '', ''), -('suggest_sell', 'Need to sell %item_formatted_link, %cost_gold or best offer.', 0, 0, '', 'Je dois vendre %item_formatted_link, pour %cost_gold ou meilleure offre.', 'Muss %item_formatted_link verkaufen, %cost_gold oder bestes Angebot.', '急卖 %item_formatted_link,标价 %cost_gold,可议价!', '', 'Necesito vender %item_formatted_link, %cost_gold o la mejor oferta.', '', ''), -('suggest_sell', '%item_formatted_link available for %cost_gold. Get it now!', 0, 0, '', '%item_formatted_link disponible pour %cost_gold. Tu devrais le prendre maintenant!', '%item_formatted_link verfügbar für %cost_gold. Greif jetzt zu!', '%item_formatted_link 只要 %cost_gold,赶紧入手吧!', '', '%item_formatted_link disponible por %cost_gold. ¡Consíguelo ahora!', '', ''), -('suggest_sell', 'Got %item_formatted_link, asking %cost_gold.', 0, 0, '', 'J’ai trouvé %item_formatted_link, et on me demande %cost_gold.', 'Habe %item_formatted_link gefunden, verlange %cost_gold.', '有 %item_formatted_link,卖 %cost_gold。', '', 'Tengo %item_formatted_link, pido %cost_gold.', '', ''), -('suggest_sell', 'Selling %item_formatted_link, only %cost_gold left.', 0, 0, '', 'Je vends %item_formatted_link, il ne reste que %cost_gold.', 'Verkaufe %item_formatted_link, nur noch %cost_gold verfügbar.', '卖 %item_formatted_link,剩最后一件,只要 %cost_gold!', '', 'Vendo %item_formatted_link, solo queda %cost_gold.', '', ''), -('suggest_sell', 'Anyone want %item_formatted_link for %cost_gold? DM me!', 0, 0, '', '%Item_formatted_link à %cost_gold, ça vous tente ? Envoyez-moi un message en privé !', '%item_formatted_link für %cost_gold gefällig? Schreibt mir eine DM!', '有人要 %item_formatted_link 吗?只要 %cost_gold,私聊我!', '', '¿Alguien quiere %item_formatted_link por %cost_gold? ¡Envíame un mensaje!', '', ''), -('suggest_sell', 'Selling %item_formatted_link at %cost_gold, grab it while you can!', 0, 0, '', 'Je vends %item_formatted_link à %cost_gold, dépêchez-vous de le saisir avant qu’il ne soit trop tard!', 'Verkaufe %item_formatted_link für %cost_gold, schnappt es euch solange es geht!', '卖 %item_formatted_link,只要 %cost_gold,欲购从速!', '', 'Vendo %item_formatted_link a %cost_gold, ¡agárralo mientras puedas!', '', ''), -('suggest_sell', '%item_formatted_link for %cost_gold, hurry up before it’s gone!', 0, 0, '', '%item_formatted_link pour %cost_gold, dépêchez-vous avant qu’il ne soit trop tard!', '%item_formatted_link für %cost_gold, beeilt euch bevor es weg ist!', '%item_formatted_link 只要 %cost_gold,快来买,不然就没了!', '', '%item_formatted_link por %cost_gold, ¡apúrate antes de que se vaya!', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, who’s in?', 0, 0, '', 'Qui veut acheter %item_formatted_link pour %cost_gold ?', 'Verkaufe %item_formatted_link für %cost_gold, wer ist dabei?', '卖 %item_formatted_link,只要 %cost_gold,谁来?', '', 'Vendo %item_formatted_link por %cost_gold, ¿quién está dentro?', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, first come, first serve!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, premier arrivé, premier servi !', 'Verkaufe %item_formatted_link für %cost_gold, wer zuerst kommt, mahlt zuerst!', '卖 %item_formatted_link,价格 %cost_gold,先到先得!', '', 'Vendo %item_formatted_link por %cost_gold, ¡el primero que llega se lo lleva!', '', ''), -('suggest_sell', 'Only %cost_gold for %item_formatted_link, limited time offer!', 0, 0, '', 'Seulement %cost_gold pour %item_formatted_link, une offre à durée limitée!', 'Nur %cost_gold für %item_formatted_link, zeitlich begrenztes Angebot!', '仅需 %cost_gold 就能买到 %item_formatted_link,限时优惠!', '', 'Solo %cost_gold para %item_formatted_link, ¡oferta por tiempo limitado!', '', ''), -('suggest_sell', '%item_formatted_link available for %cost_gold, hit me up if you need it!', 0, 0, '', 'Lien pour %item_formatted_link disponible pour %cost_gold en or, n’hésite pas à me contacter si tu en as besoin ! ', '%item_formatted_link für %cost_gold verfügbar, schreibt mich an wenn ihr es braucht!', '%item_formatted_link 卖 %cost_gold,需要的联系我!', '', '%item_formatted_link disponible por %cost_gold, ¡contáctame si lo necesitas!', '', ''), -('suggest_sell', 'Anyone interested in %item_formatted_link for %cost_gold?', 0, 0, '', 'Quelqu’un est intéressé par %item_formatted_link à %cost_gold ?', 'Hat jemand Interesse an %item_formatted_link für %cost_gold?', '有人对 %item_formatted_link 感兴趣吗?只要 %cost_gold!', '', '¿Alguien interesado en %item_formatted_link por %cost_gold?', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, need to clear inventory.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, je dois vider mon inventaire.', 'Verkaufe %item_formatted_link für %cost_gold, muss mein Inventar leeren.', '卖 %item_formatted_link,价格 %cost_gold,急清背包!', '', 'Vendo %item_formatted_link por %cost_gold, necesito vaciar mi inventario.', '', ''), -('suggest_sell', 'Get %item_formatted_link for %cost_gold, it’s a great deal!', 0, 0, '', 'Procurez-vous %item_formatted_link pour %cost_gold, c’est une bonne affaire !', 'Holt euch %item_formatted_link für %cost_gold, ein super Angebot!', '买 %item_formatted_link 只要 %cost_gold,超值啊!', '', 'Consigue %item_formatted_link por %cost_gold, ¡es una gran oferta!', '', ''), -('suggest_sell', '%item_formatted_link for %cost_gold, get it while it’s hot!', 0, 0, '', 'Fais-toi plaisir avec %item_formatted_link pour %cost_gold, c’est le moment ou jamais!', 'Holt euch %item_formatted_link für %cost_gold, solange es noch heiß ist!', '%item_formatted_link 只要 %cost_gold,趁热快买!', '', '%item_formatted_link por %cost_gold, ¡cómpralo mientras esté caliente!', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, don’t miss out!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, ne ratez pas cette occasion!', 'Verkaufe %item_formatted_link für %cost_gold, verpasst diese Gelegenheit nicht!', '卖 %item_formatted_link,价格 %cost_gold,别错过!', '', 'Vendo %item_formatted_link por %cost_gold, ¡no te lo pierdas!', '', ''), -('suggest_sell', 'Only %cost_gold for %item_formatted_link, hit me up for details!', 0, 0, '', 'Seulement %cost_gold pour %item_formatted_link, n’hésitez pas à me contacter pour plus de détails !', 'Nur %cost_gold für %item_formatted_link, schreibt mir für Details!', '仅 %cost_gold 就能买 %item_formatted_link,详情私聊!', '', 'Solo %cost_gold para %item_formatted_link, ¡contáctame para más detalles!', '', ''), -('suggest_sell', '%item_formatted_link is available for %cost_gold, let’s trade!', 0, 0, '', '%item_formatted_link est disponible pour %cost_gold, ça te dirait de faire un échange?', '%item_formatted_link ist für %cost_gold verfügbar, lasst uns handeln!', '%item_formatted_link 卖 %cost_gold,来交易吧!', '', '%item_formatted_link está disponible por %cost_gold, ¡hagamos un intercambio!', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, message me for more info.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, n’hésitez pas à me contacter pour plus d’informations.', 'Verkaufe %item_formatted_link für %cost_gold, schreibt mir für mehr Infos.', '卖 %item_formatted_link,价格 %cost_gold,更多信息私聊。', '', 'Vendo %item_formatted_link por %cost_gold, envíame un mensaje para más información.', '', ''), -('suggest_sell', 'Looking to sell %item_formatted_link, %cost_gold takes it!', 0, 0, '', 'Je cherche à vendre %item_formatted_link, et je le laisse pour seulement %cost_gold !', 'Möchte %item_formatted_link verkaufen, für %cost_gold gehört es dir!', '想卖 %item_formatted_link,%cost_gold 就能拿走!', '', 'Busco vender %item_formatted_link, ¡%cost_gold lo consigue!', '', ''), -('suggest_sell', 'I’m selling %item_formatted_link, %cost_gold only.', 0, 0, '', 'Je vends %item_formatted_link, seulement %cost_gold.', 'Ich verkaufe %item_formatted_link, nur %cost_gold.', '我在卖 %item_formatted_link,只要 %cost_gold。', '', 'Estoy vendiendo %item_formatted_link, solo %cost_gold.', '', ''), -('suggest_sell', 'Got %item_formatted_link for %cost_gold, let’s make a deal!', 0, 0, '', 'J’ai mis la main sur %item_formatted_link pour %cost_gold pièces d’or, ça t’intéresse de conclure un marché avec moi ?', 'Habe %item_formatted_link für %cost_gold, lass uns einen Deal machen!', '有 %item_formatted_link,卖 %cost_gold,来谈个价吧!', '', 'Tengo %item_formatted_link por %cost_gold, ¡hagamos un trato!', '', ''), -('suggest_sell', 'Selling %item_formatted_link, %cost_gold takes it home!', 0, 0, '', 'Je vends %item_formatted_link, %cost_gold et c’est à toi de le ramener chez toi !', 'Verkaufe %item_formatted_link, %cost_gold und es gehört dir!', '卖 %item_formatted_link,%cost_gold 就能带回家!', '', 'Vendo %item_formatted_link, ¡%cost_gold lo lleva a casa!', '', ''), -('suggest_sell', 'WTS %item_formatted_link for %cost_gold, anyone interested?', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, ça intéresse quelqu’un ?', 'Verkaufe %item_formatted_link für %cost_gold, interessiert?', '出售 %item_formatted_link,价格 %cost_gold,有人感兴趣吗?', '', 'Vendo %item_formatted_link por %cost_gold, ¿alguien interesado?', '', ''), -('suggest_sell', 'Need to sell %item_formatted_link fast, %cost_gold.', 0, 0, '', 'Besoin de vendre %item_formatted_link rapidement, %cost_gold.', 'Muss %item_formatted_link schnell verkaufen, %cost_gold.', '急卖 %item_formatted_link,只要 %cost_gold!', '', 'Necesito vender %item_formatted_link rápido, %cost_gold.', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, best price around!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, c’est le meilleur prix du coin !', 'Verkaufe %item_formatted_link für %cost_gold, bester Preis in der Gegend!', '卖 %item_formatted_link,价格 %cost_gold,绝对是最低价!', '', 'Vendo %item_formatted_link por %cost_gold, ¡el mejor precio!', '', ''), -('suggest_sell', '%item_formatted_link for %cost_gold, who’s buying?', 0, 0, '', '%item_formatted_link pour %cost_gold, qui est intéressé(e) ?', '%item_formatted_link für %cost_gold, wer kauft?', '%item_formatted_link 卖 %cost_gold,谁来买?', '', '%item_formatted_link por %cost_gold, ¿quién compra?', '', ''), -('suggest_sell', '%item_formatted_link for %cost_gold, let’s make it happen!', 0, 0, '', '%item_formatted_link à %cost_gold, on se lance !', '%item_formatted_link für %cost_gold, lass es uns machen!', '%item_formatted_link 只要 %cost_gold,成交吧!', '', '%item_formatted_link por %cost_gold, ¡hagámoslo realidad!', '', ''), -('suggest_sell', 'Selling %item_formatted_link for %cost_gold, grab it before it’s gone!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, dépêchez-vous avant qu’il ne soit trop tard !', 'Verkaufe %item_formatted_link für %cost_gold, holt es euch bevor es weg ist!', '卖 %item_formatted_link,价格 %cost_gold,赶紧买,不然就没了!', '', 'Vendo %item_formatted_link por %cost_gold, ¡agárralo antes de que se vaya!', '', ''), -('suggest_sell', 'Want %item_formatted_link? %cost_gold gets you one!', 0, 0, '', 'Tu veux %item_formatted_link? Ça te coûte %cost_gold pour en obtenir un !', 'Willst du %item_formatted_link? %cost_gold und es gehört dir!', '想要 %item_formatted_link 吗?%cost_gold 就能买到!', '', '¿Quieres %item_formatted_link? ¡%cost_gold te consigue uno!', '', ''), -('suggest_sell', '%item_formatted_link for %cost_gold, quick sale!', 0, 0, '', '%item_formatted_link en vente à %cost_gold, une super affaire à saisir rapidement !', '%item_formatted_link für %cost_gold, schneller Verkauf!', '%item_formatted_link 卖 %cost_gold,速卖!', '', '%item_formatted_link por %cost_gold, ¡venta rápida!', '', ''), - - --- response LFG/LFM channel --- usable placeholders: --- %quest_links --- %other_name --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('response_lfg_quests_channel', 'I have %quest_links', 0, 0, '', 'J’ai %quest_links', 'Ich habe %quest_links', '我有 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I also have %quest_links', 0, 0, '', 'J’ai aussi des %quest_links.', 'Ich habe auch %quest_links.', '我也有 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I also have %quest_links, I am in %zone_name right now', 0, 0, '', 'J’ai aussi des %quest_links, je suis actuellement dans la zone %zone_name', 'Ich habe auch %quest_links, ich bin gerade in %zone_name.', '我也有 %quest_links,现在在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', '%other_name, I also have %quest_links', 0, 0, '', 'Oh, j’ai aussi %quest_links', '%other_name, ich habe auch %quest_links.', '%other_name,我也有 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', '%other_name, I also have %quest_links, I am in %zone_name right now', 0, 0, '', '%other_name, je possède également %quest_links, je me trouve actuellement dans la zone %zone_name.', '%other_name, ich habe auch %quest_links, ich bin gerade in %zone_name.', '%other_name,我也有 %quest_links,现在在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'I am up for %quest_links, I am in %zone_name right now', 0, 0, '', 'Je suis partant pour %quest_links, je suis actuellement dans la zone de %zone_name.', 'Ich bin dabei für %quest_links, ich bin gerade in %zone_name.', '我可以做 %quest_links,现在在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'I am up for %quest_links, I am %my_role', 0, 0, '', 'Je suis prêt pour %quest_links, je suis un %my_role.', 'Ich bin bereit für %quest_links, ich bin ein %my_role.', '我可以做 %quest_links,我是 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', '%other_name, I am up for %quest_links, I am in %zone_name right now', 0, 0, '', '%other_name, je suis prêt pour %liens_quête, je suis actuellement à %nom_zone', '%other_name, ich bin bereit für %quest_links, ich bin gerade in %zone_name.', '%other_name,我可以做 %quest_links,现在在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', '%other_name, I am up for %quest_links, I am %my_role', 0, 0, '', '%other_name, je suis partant pour %quest_links, je suis %mon rôle', '%other_name, ich bin bereit für %quest_links, ich bin %my_role.', '%other_name,我可以做 %quest_links,我是 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone need %quest_links? I’m in %zone_name.', 0, 0, '', 'Besoin de %quest_links? Je me trouve dans %zone_name.', 'Braucht jemand %quest_links? Ich bin in %zone_name.', '有人需要做 %quest_links 吗?我在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for more for %quest_links in %zone_name.', 0, 0, '', 'À la recherche de plus de lieux pour %quest_links dans %zone_name.', 'Suche mehr Leute für %quest_links in %zone_name.', '找人一起在 %zone_name 做 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'Need some help with %quest_links in %zone_name.', 0, 0, '', 'Besoin d’aide avec les %quest_links dans %zone_name.', 'Brauche Hilfe bei %quest_links in %zone_name.', '在 %zone_name 做 %quest_links 需要帮忙。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone want to do %quest_links with me in %zone_name?', 0, 0, '', 'Qui veut faire des %quest_links avec moi dans la zone %zone_name?', 'Wer will %quest_links mit mir in %zone_name machen?', '有人想和我一起在 %zone_name 做 %quest_links 吗?', '', '', '', ''), -('response_lfg_quests_channel', 'I have %quest_links, looking for group in %zone_name.', 0, 0, '', 'J’ai %quest_links, je cherche un groupe dans la zone %zone_name.', 'Ich habe %quest_links, suche eine Gruppe in %zone_name.', '我有 %quest_links,在 %zone_name 找队伍。', '', '', '', ''), -('response_lfg_quests_channel', 'Who needs %quest_links in %zone_name?', 0, 0, '', 'Qui a besoin de %quest_links dans %zone_name ?', 'Wer braucht %quest_links in %zone_name?', '谁在 %zone_name 需要做 %quest_links?', '', '', '', ''), -('response_lfg_quests_channel', 'Up for %quest_links in %zone_name, who’s in?', 0, 0, '', 'Qui est partant pour des %quest_links à %zone_name ?', 'Bereit für %quest_links in %zone_name, wer ist dabei?', '准备在 %zone_name 做 %quest_links,谁要一起?', '', '', '', ''), -('response_lfg_quests_channel', '%my_role looking for group to complete %quest_links in %zone_name.', 0, 0, '', 'Je cherche un groupe pour terminer %quest_links dans la zone %zone_name. Qui est partant?', '%my_role sucht eine Gruppe, um %quest_links in %zone_name abzuschließen.', '我是 %my_role,找队伍在 %zone_name 完成 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I need %quest_links, anyone available in %zone_name?', 0, 0, '', 'J’ai besoin de %quest_links, est-ce qu’il y a quelqu’un disponible dans la région de %zone_name?', 'Ich brauche %quest_links, ist jemand in %zone_name verfügbar?', '我需要做 %quest_links,有人在 %zone_name 能帮忙吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for more people for %quest_links in %zone_name.', 0, 0, '', 'Je recherche plus de personnes pour %quest_links dans la région de %zone_name.', 'Suche mehr Leute für %quest_links in %zone_name.', '在 %zone_name 找更多人一起做 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I’m doing %quest_links in %zone_name, need more help.', 0, 0, '', 'Je suis en train de faire des %quest_links dans %zone_name, j’aurais besoin de davantage d’aide.', 'Ich mache gerade %quest_links in %zone_name, brauche mehr Hilfe.', '我正在 %zone_name 做 %quest_links,还需要人手帮忙。', '', '', '', ''), -('response_lfg_quests_channel', 'Any %my_role interested in %quest_links in %zone_name?', 0, 0, '', 'Des %my_role intéressés par des %quest_links dans la zone %zone_name?', 'Gibt es %my_role, die an %quest_links in %zone_name interessiert sind?', '有 %my_role 对 %zone_name 的 %quest_links 感兴趣吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Help needed for %quest_links in %zone_name, looking for more.', 0, 0, '', 'J’ai besoin d’aide pour %quest_links dans %zone_name, j’en cherche davantage.', 'Brauche Hilfe für %quest_links in %zone_name, suche weitere Mitspieler.', '在 %zone_name 做 %quest_links 需要帮忙,找更多人一起!', '', '', '', ''), -('response_lfg_quests_channel', 'I have %quest_links, %my_role in %zone_name, who’s in?', 0, 0, '', 'J’ai %quest_links, en tant que %my_role dans %zone_name, qui est partant?', 'Ich habe %quest_links, %my_role in %zone_name, wer ist dabei?', '我在 %zone_name 有 %quest_links,我是 %my_role,谁想加入?', '', '', '', ''), -('response_lfg_quests_channel', 'Who wants to do %quest_links in %zone_name? I am %my_role.', 0, 0, '', 'Qui veut faire des %quest_links dans la zone %zone_name? Je suis %my_role.', 'Wer will %quest_links in %zone_name machen? Ich bin %my_role.', '谁想在 %zone_name 做 %quest_links?我是 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone doing %quest_links right now? I’m in %zone_name.', 0, 0, '', 'Est-ce que quelqu’un est en train de faire des %quest_links en ce moment ? Je suis dans la zone %zone_name.', 'Macht gerade jemand %quest_links? Ich bin in %zone_name.', '有人现在在做 %quest_links 吗?我在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', '%quest_links needed in %zone_name, any takers?', 0, 0, '', ' %quest_links est nécessaire dans la zone %zone_name, des volontaires ?', '%quest_links wird in %zone_name benötigt, jemand dabei?', '在 %zone_name 需要完成 %quest_links,有人愿意加入吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Up for %quest_links in %zone_name, looking for group.', 0, 0, '', 'À la recherche de %quest_links dans %zone_name, je suis à la recherche d’un groupe.', 'Bereit für %quest_links in %zone_name, suche eine Gruppe.', '想在 %zone_name 做 %quest_links,找队伍一起!', '', '', '', ''), -('response_lfg_quests_channel', 'I need %quest_links and some company, %zone_name anyone?', 0, 0, '', 'J’ai besoin de %quest_links et de compagnie, %zone_name quelqu’un ?', 'Ich brauche %quest_links und Gesellschaft, jemand in %zone_name?', '我需要完成 %quest_links,想找人一起,%zone_name 有没有人?', '', '', '', ''), -('response_lfg_quests_channel', 'Who wants to join me in %zone_name for %quest_links?', 0, 0, '', 'Qui veut me rejoindre dans la %zone_name pour %quest_links ?', 'Wer will sich mir in %zone_name für %quest_links anschließen?', '谁想在 %zone_name 跟我一起做 %quest_links?', '', '', '', ''), -('response_lfg_quests_channel', 'Any %my_role available for %quest_links in %zone_name?', 0, 0, '', 'Il y a des postes de %my_role disponibles pour les %quest_links dans la zone %zone_name?', 'Gibt es %my_role für %quest_links in %zone_name?', '有 %my_role 能来 %zone_name 帮忙做 %quest_links 吗?', '', '', '', ''), -('response_lfg_quests_channel', 'I have %quest_links and I am %my_role, need more people in %zone_name.', 0, 0, '', 'J’ai %quest_links et je suis %my_role, j’ai besoin de plus de monde dans la zone %zone_name.', 'Ich habe %quest_links und bin %my_role, brauche mehr Leute in %zone_name.', '我有 %quest_links,我是 %my_role,在 %zone_name 需要更多人手。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for people to do %quest_links in %zone_name.', 0, 0, '', 'Je recherche des personnes pour faire des %quest_links dans la zone %zone_name.', 'Suche Leute für %quest_links in %zone_name.', '在 %zone_name 找人一起做 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I am %my_role and have %quest_links, looking for group in %zone_name.', 0, 0, '', 'Je suis %my_role et j’ai %quest_links, je cherche un groupe dans la zone %zone_name.', 'Ich bin %my_role und habe %quest_links, suche eine Gruppe in %zone_name.', '我是 %my_role,有 %quest_links,在 %zone_name 找队伍。', '', '', '', ''), -('response_lfg_quests_channel', 'Need help with %quest_links in %zone_name, any %my_role available?', 0, 0, '', 'Besoin d’aide avec les %quest_links dans la zone de %zone_name, est-ce qu’il y a un %my_role disponible pour m’aider?', 'Brauche Hilfe mit %quest_links in %zone_name, ist ein %my_role verfügbar?', '在 %zone_name 做 %quest_links 需要帮助,有 %my_role 能来吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Any %my_role interested in completing %quest_links with me in %zone_name?', 0, 0, '', 'Des %my_role intéressées pour compléter des %quest_links avec moi dans la zone %zone_name?', 'Ist ein %my_role daran interessiert, %quest_links mit mir in %zone_name abzuschließen?', '有 %my_role 想跟我一起在 %zone_name 完成 %quest_links 吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Let’s complete %quest_links together in %zone_name.', 0, 0, '', 'Complétons ensemble les %quest_links dans %zone_name.', 'Lass uns %quest_links gemeinsam in %zone_name abschließen.', '咱们一起在 %zone_name 完成 %quest_links 吧!', '', '', '', ''), -('response_lfg_quests_channel', 'I am in %zone_name and have %quest_links, who wants to join?', 0, 0, '', 'Je suis actuellement dans %zone_name et j’ai %quest_links. Qui vient se joindre à moi ?', 'Ich bin in %zone_name und habe %quest_links, wer möchte mitmachen?', '我在 %zone_name,有 %quest_links,谁想加入?', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for more %my_role to complete %quest_links in %zone_name.', 0, 0, '', 'Je recherche plus de personnes pour compléter les %my_role nécessaires dans les %quest_links de la zone %zone_name.', 'Ich suche weitere %my_role, um %quest_links in %zone_name abzuschließen.', '在 %zone_name 找更多 %my_role 一起完成 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I have %quest_links and I need %my_role in %zone_name.', 0, 0, '', 'J’ai %quest_links et j’ai besoin de %my_role dans %zone_name.', 'Ich habe %quest_links und brauche %my_role in %zone_name.', '我有 %quest_links,在 %zone_name 需要 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone up for %quest_links? I am %my_role in %zone_name.', 0, 0, '', 'Qui est partant pour %quest_links? Je suis %my_role dans %zone_name.', 'Hat jemand Lust auf %quest_links? Ich bin %my_role in %zone_name.', '有人想做 %quest_links 吗?我是 %my_role,在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for a group to do %quest_links in %zone_name, %my_role needed.', 0, 0, '', 'Je recherche un groupe pour faire des %quest_links dans la région de %zone_name, j’ai besoin de jouer en tant que %my_role.', 'Ich suche eine Gruppe für %quest_links in %zone_name, %my_role wird gebraucht.', '在 %zone_name 找队伍做 %quest_links,需要 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone need help with %quest_links in %zone_name? I’m %my_role.', 0, 0, '', 'Besoin d’aide avec les %quest_links dans la %zone_name? Je suis là pour aider en tant que %my_role.', 'Braucht jemand Hilfe mit %quest_links in %zone_name? Ich bin %my_role.', '有人在 %zone_name 需要帮忙做 %quest_links 吗?我是 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'I have %quest_links to complete, need %my_role in %zone_name.', 0, 0, '', 'J’ai %quest_links à terminer, j’ai besoin de %my_role dans la zone %zone_name.', 'Ich muss %quest_links abschließen und brauche %my_role in %zone_name.', '我有 %quest_links 要完成,在 %zone_name 需要 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'I’m in %zone_name looking for %quest_links help, who’s in?', 0, 0, '', 'Je suis dans %zone_name à la recherche d’aide pour %quest_links, qui est partant?', 'Ich bin in %zone_name und suche Hilfe für %quest_links, wer ist dabei?', '我在 %zone_name 找人帮忙做 %quest_links,谁愿意加入?', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for a group to finish %quest_links in %zone_name.', 0, 0, '', 'Je cherche un groupe pour terminer les %quest_links dans la zone %zone_name.', 'Ich suche eine Gruppe, um %quest_links in %zone_name abzuschließen.', '在 %zone_name 找队伍完成 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone looking for %quest_links in %zone_name? I’m %my_role.', 0, 0, '', 'Quelqu’un recherche %quest_links dans la zone de %zone_name? Je suis %my_role.', 'Sucht jemand nach %quest_links in %zone_name? Ich bin %my_role.', '有人在 %zone_name 找 %quest_links 吗?我是 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Let’s do %quest_links together! %zone_name is waiting.', 0, 0, '', 'Allons-y ensemble pour %quest_links ! %zone_name nous attend.', 'Lass uns %quest_links gemeinsam machen! %zone_name wartet.', '咱们一起做 %quest_links 吧!%zone_name 正等着我们!', '', '', '', ''), -('response_lfg_quests_channel', 'Need a group for %quest_links, %zone_name, anyone interested?', 0, 0, '', 'Besoin d’un groupe pour %quest_links dans la zone %zone_name, des intéressés ?', 'Ich brauche eine Gruppe für %quest_links in %zone_name, jemand interessiert?', '在 %zone_name 需要队伍做 %quest_links,有人感兴趣吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Who wants to join for %quest_links? I’m in %zone_name.', 0, 0, '', 'Qui veut se joindre pour %quest_links? Je suis actuellement dans la zone %zone_name.', 'Wer möchte bei %quest_links mitmachen? Ich bin in %zone_name.', '谁想加入做 %quest_links?我在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'I’m in %zone_name, looking for people to complete %quest_links with.', 0, 0, '', 'Je suis dans %zone_name, à la recherche de joueurs pour compléter les missions %quest_links avec moi.', 'Ich bin in %zone_name und suche Leute, um %quest_links abzuschließen.', '我在 %zone_name,找人一起完成 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'Need a hand with %quest_links in %zone_name, any %my_role?', 0, 0, '', 'Besoin d’aide avec %quest_links dans %zone_name, quelqu’un de mon rôle?', 'Brauche Hilfe mit %quest_links in %zone_name, ist ein %my_role verfügbar?', '在 %zone_name 做 %quest_links 需要帮忙,有 %my_role 吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for group to finish %quest_links, %zone_name is the place to be.', 0, 0, '', 'Je cherche un groupe pour finir %quest_links, %zone_name est l’endroit où il faut être.', 'Ich suche eine Gruppe, um %quest_links abzuschließen, %zone_name ist der richtige Ort dafür.', '找队伍完成 %quest_links,%zone_name 就是好地方。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone want to help with %quest_links in %zone_name? I’m %my_role.', 0, 0, '', 'Est-ce que quelqu’un veut aider avec les %quest_links dans la région de %zone_name? Je suis %my_role.', 'Möchte jemand mit %quest_links in %zone_name helfen? Ich bin %my_role.', '有人想在 %zone_name 帮忙做 %quest_links 吗?我是 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for %my_role to join me for %quest_links in %zone_name.', 0, 0, '', 'Je recherche %my_role pour me rejoindre pour des %quest_links dans la %zone_name.', 'Ich suche einen %my_role, der sich mir für %quest_links in %zone_name anschließt.', '在 %zone_name 找 %my_role 一起做 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I’ve got %quest_links, %my_role needed for %zone_name.', 0, 0, '', 'J’ai %quest_links, %my_role requis pour %zone_name.', 'Ich habe %quest_links, %my_role wird für %zone_name benötigt.', '我有 %quest_links,在 %zone_name 需要 %my_role。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking to finish %quest_links in %zone_name, need more people.', 0, 0, '', 'Je cherche à finir les %quest_links dans %zone_name, j’ai besoin de plus de personnes.', 'Ich möchte %quest_links in %zone_name abschließen, brauche aber mehr Leute.', '想在 %zone_name 完成 %quest_links,需要更多人手。', '', '', '', ''), -('response_lfg_quests_channel', 'I need help with %quest_links, who’s around in %zone_name?', 0, 0, '', 'J’ai besoin d’aide avec %quest_links, qui est autour dans %zone_name?', 'Ich brauche Hilfe mit %quest_links, wer ist in %zone_name in der Nähe?', '我需要帮忙做 %quest_links,%zone_name 附近有谁在?', '', '', '', ''), -('response_lfg_quests_channel', 'Need more people to complete %quest_links, %zone_name.', 0, 0, '', 'J’ai besoin de plus de personnes pour compléter %quest_links dans la zone %zone_name.', 'Ich brauche mehr Leute, um %quest_links in %zone_name abzuschließen.', '需要更多人完成 %quest_links,在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for group in %zone_name to finish %quest_links.', 0, 0, '', 'Je cherche un groupe dans la %zone_name pour terminer les %quest_links.', 'Ich suche eine Gruppe in %zone_name, um %quest_links abzuschließen.', '在 %zone_name 找队伍完成 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'Who wants to do %quest_links in %zone_name? I’m ready to go.', 0, 0, '', 'Qui veut faire des %quest_links dans la zone de %zone_name? Je suis prêt à y aller.', 'Wer möchte %quest_links in %zone_name machen? Ich bin bereit.', '谁想在 %zone_name 做 %quest_links?我已经准备好了。', '', '', '', ''), -('response_lfg_quests_channel', 'Anyone want to join me for %quest_links? I’m %my_role in %zone_name.', 0, 0, '', 'Qui veut se joindre à moi pour %quest_links? Je suis %my_role dans la zone %zone_name.', 'Möchte jemand mit mir %quest_links machen? Ich bin %my_role in %zone_name.', '有人想跟我一起做 %quest_links 吗?我是 %my_role,在 %zone_name。', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for %my_role to do %quest_links with in %zone_name.', 0, 0, '', 'Je recherche %my_role pour faire %quest_links avec dans la zone %zone_name.', 'Ich suche einen %my_role, um %quest_links gemeinsam in %zone_name zu erledigen.', '在 %zone_name 找 %my_role 一起做 %quest_links。', '', '', '', ''), -('response_lfg_quests_channel', 'I’m in %zone_name, anyone need help with %quest_links?', 0, 0, '', 'Je suis dans %zone_name, est-ce que quelqu’un a besoin d’aide avec %quest_links?', 'Ich bin in %zone_name, braucht jemand Hilfe mit %quest_links?', '我在 %zone_name,有人需要帮忙做 %quest_links 吗?', '', '', '', ''), -('response_lfg_quests_channel', 'Need more for %quest_links in %zone_name, any takers?', 0, 0, '', 'Besoin de renfort pour les %quest_links dans %zone_name, des volontaires ?', 'Brauche mehr Leute für %quest_links in %zone_name, wer ist dabei?', '在 %zone_name 做 %quest_links 需要更多人,有人愿意吗?', '', '', '', ''), -('response_lfg_quests_channel', 'I need help with %quest_links in %zone_name, who’s up for it?', 0, 0, '', 'J’ai besoin d’aide avec les %quest_links dans la zone %zone_name, qui est partant pour m’accompagner?', 'Ich brauche Hilfe mit %quest_links in %zone_name, wer ist dabei?', '在 %zone_name 做 %quest_links 需要帮忙,谁愿意一起?', '', '', '', ''), -('response_lfg_quests_channel', 'Looking for more to finish %quest_links, %zone_name.', 0, 0, '', 'Recherche compagnions pour %quest_links dans la zone %zone_name.', 'Ich suche noch mehr Leute, um %quest_links in %zone_name abzuschließen.', '找更多人完成 %quest_links,在 %zone_name。', '', '', '', ''), - --- response LFG/LFM whisper --- usable placeholders: --- %quest_links --- %other_name --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('response_lfg_quests_whisper', 'Hey, I’m up for %quest_links', 0, 0, '', 'Salut, je suis partant pour %quest_links', 'Hey, ich bin dabei für %quest_links', '嘿,我想做 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey, I could do %quest_links with you', 0, 0, '', 'Salut, je pourrais faire %quest_links avec toi', 'Hey, ich könnte %quest_links mit dir machen', '嘿,我可以跟你一起做 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey, I also have %quest_links', 0, 0, '', 'Salut, j’ai aussi %quest_links.', 'Hey, ich habe auch %quest_links.', '嘿,我也有 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey %other_name, I’m up for %quest_links', 0, 0, '', 'Salut %other_name, je suis prêt pour %quest_links', 'Hey %other_name, ich bin bereit für %quest_links', '嘿,%other_name,我想做 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey %other_name, I could do %quest_links with you', 0, 0, '', 'Salut %other_name, je pourrais faire %quest_links avec toi.', 'Hey %other_name, ich könnte %quest_links mit dir machen.', '嘿,%other_name,我可以跟你一起做 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey %other_name, I also have %quest_links', 0, 0, '', 'Salut %other_name, j’ai aussi des %quest_links', 'Hey %other_name, ich habe auch %quest_links', '嘿,%other_name,我也有 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'wanna group up for %quest_links ?', 0, 0, '', 'Tu veux qu’on se groupe pour faire les %quest_links ensemble ?', 'Willst du eine Gruppe für %quest_links bilden?', '想组队一起做 %quest_links 吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'I am up for %quest_links, I am in %zone_name right now', 0, 0, '', 'Je suis partant pour des %quest_links, je me trouve en ce moment dans %zone_name.', 'Ich bin dabei für %quest_links, ich bin gerade in %zone_name.', '我想做 %quest_links,我现在在 %zone_name。', '', '', '', ''), -('response_lfg_quests_whisper', 'I am up for %quest_links, I am %my_role', 0, 0, '', 'Je suis prêt pour des %quest_links, je suis %my_role.', 'Ich bin bereit für %quest_links, ich bin %my_role.', '我想做 %quest_links,我是 %my_role。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey, I’m down for %quest_links', 0, 0, '', 'Hey, je suis partant pour %quest_links', 'Hey, ich bin bereit für %quest_links', '嘿,我愿意做 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'I can help with %quest_links if you need', 0, 0, '', 'Je peux t’aider avec les %quest_links si tu en as besoin.', 'Ich kann mit %quest_links helfen, wenn du es brauchst.', '如果你需要,我可以帮你做 %quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Let’s do %quest_links together!', 0, 0, '', 'Faisons %quest_links ensemble!', 'Lass uns %quest_links zusammen machen!', '咱们一起做 %quest_links 吧!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links to do too', 0, 0, '', 'J’ai aussi %quest_links à faire.', 'Ich habe auch %quest_links zu erledigen.', '我也有 %quest_links 要做。', '', '', '', ''), -('response_lfg_quests_whisper', 'I’m available for %quest_links if you want', 0, 0, '', 'Je suis disponible pour %quest_links si tu veux', 'Ich bin verfügbar für %quest_links, wenn du willst.', '如果你想做 %quest_links,我有空。', '', '', '', ''), -('response_lfg_quests_whisper', 'I’m down for %quest_links, just let me know', 0, 0, '', 'Je suis partant pour %quest_links, il me suffit de me dire', 'Ich bin bereit für %quest_links, sag mir einfach Bescheid.', '我愿意做 %quest_links,告诉我一声就行。', '', '', '', ''), -('response_lfg_quests_whisper', 'Looking for %quest_links? I’m in %zone_name', 0, 0, '', 'À la recherche de %quest_links? Je me trouve à %zone_name.', 'Suchst du nach %quest_links? Ich bin in %zone_name.', '在找 %quest_links 吗?我在 %zone_name。', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey, I need %quest_links too, let’s go', 0, 0, '', 'Salut, j’ai besoin de %quest_links aussi, allons-y', 'Hey, ich brauche auch %quest_links, lass uns gehen.', '嘿,我也需要 %quest_links,一起走吧!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links, want to join me?', 0, 0, '', 'J’ai %quest_links, tu veux te joindre à moi?', 'Ich habe %quest_links, willst du mitmachen?', '我有%quest_links,要不要一起来?', '', '', '', ''), -('response_lfg_quests_whisper', 'I could use %quest_links, let’s do this', 0, 0, '', 'Je peux faire %quest_links, allons-y', 'Ich könnte %quest_links gebrauchen, lass es uns machen.', '我正好需要做%quest_links,一起搞定吧!', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey, want to join for %quest_links?', 0, 0, '', 'Salut, tu veux te joindre à moi pour %quest_links?', 'Hey, willst du mitmachen für %quest_links?', '嘿,想一起做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'I can help with %quest_links if you need', 0, 0, '', 'Je peux t’aider avec %quest_links si besoin', 'Ich kann mit %quest_links helfen, wenn du es brauchst', '如果需要帮忙,我可以助你一臂之力,%quest_links没问题!', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey %other_name, up for %quest_links?', 0, 0, '', 'Salut %other_name, partant pour %quest_links?', 'Hey %other_name, bereit für %quest_links?', '嘿,%other_name,有兴趣做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Want to group up for %quest_links %other_name?', 0, 0, '', 'Tu veux faire équipe pour %quest_links %other_name?', 'Willst du eine Gruppe für %quest_links bilden, %other_name?', '想组队做%quest_links吗,%other_name?', '', '', '', ''), -('response_lfg_quests_whisper', 'Let’s do %quest_links together, %other_name', 0, 0, '', 'Faisons les %quest_links ensemble, %other_name.', 'Lass uns %quest_links zusammen machen, %other_name', '咱们一起把%quest_links搞定吧,%other_name!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’m %my_role and ready for %quest_links', 0, 0, '', 'Je suis %my_role, prêt pour %quest_links.', 'Ich bin %my_role und bereit für %quest_links.', '我是%my_role,已经准备好做%quest_links了!', '', '', '', ''), -('response_lfg_quests_whisper', 'I can join for %quest_links, %my_role here', 0, 0, '', 'Je peux vous rejoindre pour %quest_links, %my_role ici.', 'Ich kann mich für %quest_links anschließen, %my_role hier.', '我可以加入做%quest_links,我是%my_role!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’m up for %quest_links, %my_role!', 0, 0, '', 'Je suis prêt pour %quest_links, %my_role!', 'Ich bin bereit für %quest_links, %my_role!', '我准备好做%quest_links了,我是%my_role!', '', '', '', ''), -('response_lfg_quests_whisper', 'Looking to do %quest_links with a %my_role', 0, 0, '', 'Je cherche à faire des %quest_links avec mon rôle.', 'Ich suche eine Gruppe für %quest_links mit einem %my_role.', '想找人一起做%quest_links,我是%my_role。', '', '', '', ''), -('response_lfg_quests_whisper', 'Anyone need help with %quest_links?', 0, 0, '', 'Besoin d’aide avec %quest_links?', 'Braucht jemand Hilfe mit %quest_links?', '有人需要帮忙做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Who else is up for %quest_links?', 0, 0, '', 'Qui d’autre est partant pour %quest_links?', 'Wer ist noch bereit für %quest_links?', '还有谁想做%quest_links?', '', '', '', ''), -('response_lfg_quests_whisper', 'Anyone down for %quest_links in %zone_name?', 0, 0, '', 'Qui est partant pour des %quest_links dans la région %zone_name ?', 'Wer hat Lust auf %quest_links in %zone_name?', '有人想在%zone_name做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links, %my_role on the way', 0, 0, '', 'J’ai %quest_links, %my_role en route.', 'Ich habe %quest_links, %my_role ist unterwegs.', '我有%quest_links,我是%my_role,正在赶来!', '', '', '', ''), -('response_lfg_quests_whisper', 'I need %quest_links too, who’s in?', 0, 0, '', 'J’ai besoin de %quest_links aussi, qui est partant?', 'Ich brauche auch %quest_links, wer ist dabei?', '我也需要做%quest_links,谁一起?', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve been grinding %quest_links, need a hand?', 0, 0, '', 'J’ai bossé dur sur %quest_links, tu as besoin d’aide ?', 'Ich habe %quest_links gegrindet, brauchst du Hilfe?', '我一直在刷%quest_links,需要帮忙吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'I’m ready for %quest_links, anyone else?', 0, 0, '', 'Je suis prêt pour les %quest_links, quelqu’un d’autre est prêt aussi ?', 'Ich bin bereit für %quest_links, noch jemand?', '我准备好做%quest_links了,还有人吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Can anyone join me for %quest_links?', 0, 0, '', 'Est-ce que quelqu’un veut se joindre à moi pour %quest_links?', 'Kann jemand mit mir %quest_links machen?', '有人能和我一起做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Looking for help with %quest_links, %my_role here', 0, 0, '', 'Je recherche de l’aide avec %quest_links, je suis %my_role ici. En quoi puis-je vous aider?', 'Ich suche Hilfe für %quest_links, %my_role hier.', '找人帮忙做%quest_links,我是%my_role!', '', '', '', ''), -('response_lfg_quests_whisper', 'I can help out with %quest_links, anyone need me?', 0, 0, '', 'Je peux aider avec %quest_links, quelqu’un a besoin de moi ?', 'Ich kann mit %quest_links helfen, braucht mich jemand?', '我可以帮忙做%quest_links,有人需要我吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Anyone else doing %quest_links? Let’s go', 0, 0, '', 'Quelqu’un d’autre fait des %quest_links? Allons-y', 'Macht noch jemand %quest_links? Auf geht’s.', '还有人在做%quest_links吗?一起上吧!', '', '', '', ''), -('response_lfg_quests_whisper', 'Anyone need %quest_links help? %my_role here', 0, 0, '', 'Besoin d’aide pour %quest_links? C’est %my_role qui s’en charge par ici.', 'Braucht jemand Hilfe bei %quest_links? %my_role ist hier.', '有人需要帮忙做%quest_links吗?我这里是%my_role!', '', '', '', ''), -('response_lfg_quests_whisper', 'Who’s up for some %quest_links in %zone_name?', 0, 0, '', 'Qui est partant pour quelques %quest_links dans la %zone_name ?', 'Wer hat Lust auf ein paar %quest_links in %zone_name?', '谁想在%zone_name做点%quest_links?', '', '', '', ''), -('response_lfg_quests_whisper', 'I can join for %quest_links if you want', 0, 0, '', 'Je peux me joindre pour %quest_links si tu veux.', 'Ich kann für %quest_links mitmachen, wenn du willst.', '如果你愿意,我可以加入做%quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Looking for a %my_role to do %quest_links', 0, 0, '', 'Je suis à la recherche d’un %my_role pour réaliser les %quest_links.', 'Ich suche einen %my_role, um %quest_links zu machen.', '找一个%my_role一起做%quest_links。', '', '', '', ''), -('response_lfg_quests_whisper', 'Any %my_role up for %quest_links?', 0, 0, '', 'Des volontaires pour %my_role pour les %quest_links?', 'Irgendwelche %my_role für %quest_links?', '有%my_role想做%quest_links的吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links, anyone want to join me?', 0, 0, '', 'J’ai %quest_links, quelqu’un veut se joindre à moi?', 'Ich habe %quest_links, will jemand mitmachen?', '我有%quest_links,有人想一起吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey %other_name, up for some %quest_links?', 0, 0, '', 'Salut %other_name, partant pour quelques %quest_links?', 'Hey %other_name, Lust auf ein paar %quest_links?', '嘿,%other_name,有兴趣做点%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Let’s team up for %quest_links, %other_name', 0, 0, '', 'Partons à l’aventure pour %quest_links, %other_name!', 'Lass uns für %quest_links zusammenarbeiten, %other_name!', '咱们组队做%quest_links吧,%other_name!', '', '', '', ''), -('response_lfg_quests_whisper', 'I need %quest_links, anyone else want to do it?', 0, 0, '', 'J’ai besoin de %quest_links, est-ce que quelqu’un d’autre veut le faire avec moi ?', 'Ich brauche %quest_links, will jemand anderes es mit mir machen?', '我需要做%quest_links,还有人想一起吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'I need %quest_links too, anyone else around?', 0, 0, '', 'J’ai besoin de %quest_links aussi, est-ce qu’il y a d’autres personnes ici?', 'Ich brauche auch %quest_links, ist noch jemand hier?', '我也需要做%quest_links,附近有人吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Looking for %quest_links in %zone_name, who’s with me?', 0, 0, '', 'À la recherche de %quest_links dans %zone_name, qui est partant avec moi ?', 'Suche nach %quest_links in %zone_name, wer ist dabei?', '在%zone_name找人做%quest_links,谁跟我一起?', '', '', '', ''), -('response_lfg_quests_whisper', 'Anyone available for %quest_links?', 0, 0, '', 'Quelqu’un de disponible pour %quest_links ?', 'Ist jemand verfügbar für %quest_links?', '有人有空做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Hey, I could join you for %quest_links!', 0, 0, '', 'Hey, je pourrais me joindre à vous pour les %quest_links!', 'Hey, ich könnte mich euch für %quest_links anschließen!', '嘿,我可以加入你们一起做%quest_links!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links too, let’s team up', 0, 0, '', 'J’ai aussi %quest_links, unissons nos forces!', 'Ich habe auch %quest_links, lasst uns zusammenarbeiten!', '我也有%quest_links,咱们组队吧!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links to do, want to join?', 0, 0, '', 'J’ai %quest_links à accomplir, ça te dit de m’accompagner ?', 'Ich habe %quest_links zu erledigen, willst du mitmachen?', '我有%quest_links要做,要不要一起?', '', '', '', ''), -('response_lfg_quests_whisper', 'Any %my_role up for %quest_links in %zone_name?', 0, 0, '', 'Y a-t-il des %my_role partants pour des %quest_links dans %zone_name?', 'Gibt es %my_role, die für %quest_links in %zone_name bereit sind?', '有%my_role想在%zone_name做%quest_links吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Let’s finish %quest_links together, %other_name', 0, 0, '', 'Terminons ensemble %quest_links, %other_name.', 'Lass uns %quest_links zusammen abschließen, %other_name.', '咱们一起把%quest_links做完吧,%other_name!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’m up for %quest_links, anyone else want to join?', 0, 0, '', 'Je suis partant pour %quest_links, est-ce qu’il y a d’autres personnes qui veulent se joindre ?', 'Ich bin bereit für %quest_links, will noch jemand mitmachen?', '我准备好做%quest_links了,还有人想加入吗?', '', '', '', ''), -('response_lfg_quests_whisper', 'Who else is doing %quest_links? I’m in', 0, 0, '', 'Qui d’autre fait les %quest_links ? J’y participe également!', 'Wer macht noch %quest_links? Ich bin dabei!', '还有谁在做%quest_links?我也参加!', '', '', '', ''), -('response_lfg_quests_whisper', 'I’ve got %quest_links in %zone_name, need help?', 0, 0, '', 'J’ai %quest_links dans %zone_name, besoin d’aide ?', 'Ich habe %quest_links in %zone_name, brauche Hilfe?', '我在%zone_name有%quest_links,需要帮忙吗?', '', '', '', ''), - - --- response WTB channel --- usable placeholders: --- %formatted_item_links --- %other_name --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('response_wtb_items_channel', '%other_name, I could sell you %formatted_item_links', 0, 0, '', 'Ah, %formatted_item_links, je pourrais vous les vendre.', 'Ah, %formatted_item_links, ich könnte sie dir verkaufen.', '%other_name,我可以卖给你%formatted_item_links。', '', '', '', ''), -('response_wtb_items_channel', 'I could potentially sell %formatted_item_links', 0, 0, '', 'Je pourrais potentiellement vendre %formatted_item_links.', 'Ich könnte möglicherweise %formatted_item_links verkaufen.', '我可能有%formatted_item_links可以卖。', '', '', '', ''), -('response_wtb_items_channel', 'Think I could sell %formatted_item_links', 0, 0, '', 'Penses-tu que je pourrais vendre les %formatted_item_links ?', 'Denkst du, ich könnte %formatted_item_links verkaufen?', '我觉得我可以卖%formatted_item_links。', '', '', '', ''), -('response_wtb_items_channel', '%other_name, I could potentially sell %formatted_item_links', 0, 0, '', 'Hé toi, je pourrais potentiellement te vendre %formatted_item_links', 'Hey %other_name, ich könnte dir möglicherweise %formatted_item_links verkaufen.', '嘿,%other_name,我可能有%formatted_item_links可以卖给你。', '', '', '', ''), -('response_wtb_items_channel', '%other_name, Think I could sell %formatted_item_links', 0, 0, '', '%other_name, tu crois que je pourrais vendre %formatted_item_links?', '%other_name, denkst du, ich könnte %formatted_item_links verkaufen?', '%other_name,你觉得我能卖%formatted_item_links吗?', '', '', '', ''), - - - - --- response WTB channel --- usable placeholders: --- %formatted_item_links --- %other_name --- %my_role --- %zone_name --- %area_name --- %my_class --- %my_race --- %my_level -('response_wtb_items_whisper', 'I could sell you %formatted_item_links', 0, 0, '', 'Je pourrais te vendre des %formatted_item_links.', 'Ich könnte dir %formatted_item_links verkaufen.', '我可以卖给你%formatted_item_links。', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I got %formatted_item_links for sale', 0, 0, '', 'Salut, j’ai %formatted_item_links à vendre.', 'Hey, ich habe %formatted_item_links zum Verkauf.', '嘿,我有%formatted_item_links要卖!', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I could potentially sell %formatted_item_links', 0, 0, '', 'Salut, je pourrais potentiellement vendre %formatted_item_links', 'Hey, ich könnte möglicherweise %formatted_item_links verkaufen.', '嘿,我可能有%formatted_item_links可以卖给你。', '', '', '', ''), -('response_wtb_items_whisper', 'I could sell you %formatted_item_links', 0, 0, '', 'Je pourrais te vendre %formatted_item_links.', 'Ich könnte dir %formatted_item_links verkaufen.', '我可以卖给你%formatted_item_links。', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I got %formatted_item_links for sale', 0, 0, '', 'Salut, j’ai %formatted_item_links à vendre.', 'Hey, ich habe %formatted_item_links zum Verkauf.', '嘿,我有%formatted_item_links要卖!', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I could potentially sell %formatted_item_links', 0, 0, '', 'Hey, je pourrais potentiellement vendre ces %formatted_item_links.', 'Hey, ich könnte möglicherweise %formatted_item_links verkaufen.', '嘿,我可能能卖给你%formatted_item_links。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, are you interested?', 0, 0, '', 'J’ai des %formatted_item_links, ça t’intéresse ?', 'Ich habe %formatted_item_links, bist du interessiert?', '我有%formatted_item_links,你感兴趣吗?', '', '', '', ''), -('response_wtb_items_whisper', 'Looking to sell %formatted_item_links, what’s your offer?', 0, 0, '', 'Je cherche à vendre %formatted_item_links, quelle est votre offre ?', 'Ich möchte %formatted_item_links verkaufen, was bietest du?', '我想卖掉%formatted_item_links,你能开个什么价?', '', '', '', ''), -('response_wtb_items_whisper', 'I can sell you %formatted_item_links for a fair price', 0, 0, '', 'Je peux vous vendre des %formatted_item_links à un prix raisonnable.', 'Ich kann dir %formatted_item_links zu einem fairen Preis verkaufen.', '我可以以公道价格卖给你%formatted_item_links。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links in stock, how much are you offering?', 0, 0, '', 'J’ai %formatted_item_links en stock, combien proposez-vous ?', 'Ich habe %formatted_item_links auf Lager, wie viel bietest du?', '我有%formatted_item_links库存,你能出多少钱?', '', '', '', ''), -('response_wtb_items_whisper', 'I’m selling %formatted_item_links for %cost_gold, let me know if you’re interested', 0, 0, '', 'Je vends des %formatted_item_links pour %cost_gold, dis-moi si ça t’intéresse.', 'Ich verkaufe %formatted_item_links für %cost_gold, sag mir Bescheid, wenn du interessiert bist.', '我在卖%formatted_item_links,价格%cost_gold,感兴趣就告诉我。', '', '', '', ''), -('response_wtb_items_whisper', 'Got %formatted_item_links for sale, what are you willing to pay?', 0, 0, '', 'J’ai %formatted_item_links à vendre, combien es-tu prêt à payer?', 'Ich habe %formatted_item_links zu verkaufen, wie viel würdest du zahlen?', '我有%formatted_item_links要卖,你愿意出多少钱?', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links, how does %cost_gold sound for you?', 0, 0, '', 'J’ai %formatted_item_links, est-ce que %cost_gold te semble raisonnable ?', 'Ich habe %formatted_item_links, klingt %cost_gold fair für dich?', '我有%formatted_item_links,%cost_gold这个价格你觉得咋样?', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links for %cost_gold, message me if you want it', 0, 0, '', 'Je vends des %formatted_item_links pour %cost_gold, contactez-moi si vous êtes intéressé.', 'Verkaufe %formatted_item_links für %cost_gold, schreib mir, wenn du es willst.', '卖%formatted_item_links,价格%cost_gold,想买就私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, hit me up if you’re interested', 0, 0, '', 'J’ai des %formatted_item_links, contactez-moi si ça vous intéresse', 'Ich habe %formatted_item_links, melde dich, wenn du interessiert bist.', '我有%formatted_item_links,感兴趣就联系我。', '', '', '', ''), -('response_wtb_items_whisper', 'I can part with %formatted_item_links if you make the right offer', 0, 0, '', 'Je peux me séparer de %formatted_item_links si ton offre est intéressante.', 'Ich kann mich von %formatted_item_links trennen, wenn dein Angebot gut ist.', '如果你出价合适,我可以把%formatted_item_links卖给你。', '', '', '', ''), -('response_wtb_items_whisper', 'Looking to get rid of %formatted_item_links, hit me up', 0, 0, '', 'Je cherche à me débarrasser de %formatted_item_links, contactez-moi', 'Ich möchte %formatted_item_links loswerden, schreib mir.', '我想处理掉%formatted_item_links,有意就找我。', '', '', '', ''), -('response_wtb_items_whisper', 'Got %formatted_item_links for %cost_gold, are you buying?', 0, 0, '', 'J’ai %formatted_item_links pour %cost_gold, tu veux acheter?', 'Ich habe %formatted_item_links für %cost_gold, willst du es kaufen?', '我有%formatted_item_links,价格%cost_gold,要买吗?', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, let’s negotiate', 0, 0, '', 'Je vends %formatted_item_links, on peut discuter du prix ensemble.', 'Ich verkaufe %formatted_item_links, lass uns verhandeln.', '卖%formatted_item_links,咱们可以谈谈价格。', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I have %formatted_item_links for %cost_gold', 0, 0, '', 'Salut, j’ai des %formatted_item_links pour un coût en or de %cost_gold.', 'Hey, ich habe %formatted_item_links für %cost_gold.', '嘿,我有%formatted_item_links,价格%cost_gold。', '', '', '', ''), -('response_wtb_items_whisper', 'I can hook you up with %formatted_item_links, just name your price', 0, 0, '', 'Je peux vous procurer des liens d’articles %formatted_item_links, vous n’avez qu’à me dire votre prix.', 'Ich kann dir %formatted_item_links besorgen, nenne einfach deinen Preis.', '我能给你弄到%formatted_item_links,你开个价吧。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links available, let me know if you’re interested', 0, 0, '', 'J’ai des %formatted_item_links disponibles, fais-moi savoir si cela t’intéresse.', 'Ich habe %formatted_item_links verfügbar, sag mir Bescheid, wenn du interessiert bist.', '我有%formatted_item_links可供出售,感兴趣就告诉我。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, let’s do business', 0, 0, '', 'Je vends %formatted_item_links, on pourrait faire affaire ensemble.', 'Ich verkaufe %formatted_item_links, lass uns handeln.', '卖%formatted_item_links,咱们做笔生意吧。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, feel free to make an offer', 0, 0, '', 'J’ai %formatted_item_links, n’hésitez pas à faire une offre.', 'Ich habe %formatted_item_links, mach mir ein Angebot.', '我有%formatted_item_links,欢迎出价。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links, send me a message if you want to buy', 0, 0, '', 'J’ai des %formatted_item_links, envoie-moi un message si tu veux acheter.', 'Ich habe %formatted_item_links, schreib mir, wenn du kaufen willst willst.', '我有%formatted_item_links,想买就给我发消息。', '', '', '', ''), -('response_wtb_items_whisper', 'I can offer you %formatted_item_links for %cost_gold', 0, 0, '', 'Je peux vous proposer des %formatted_item_links pour %cost_gold.', 'Ich kann dir %formatted_item_links für %cost_gold anbieten.', '我可以给你%formatted_item_links,价格%cost_gold。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links for sale, message me for the price', 0, 0, '', 'J’ai %formatted_item_links à vendre, envoyez-moi un message pour connaître le prix.', 'Ich habe %formatted_item_links zu verkaufen, schreib mir für den Preis.', '我有%formatted_item_links要卖,私聊我问价格。', '', '', '', ''), -('response_wtb_items_whisper', 'I’m selling %formatted_item_links, get it while it’s hot', 0, 0, '', 'Je vends %formatted_item_links, profitez-en pendant que c’est chaud !', 'Ich verkaufe %formatted_item_links, schlag zu, solange es geht!', '我在卖%formatted_item_links,手快有手慢无!', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, you won’t find a better deal', 0, 0, '', 'Je vends des %formatted_item_links, tu ne trouveras pas meilleure affaire que celle-ci.', 'Ich verkaufe %formatted_item_links, ein besseres Angebot gibt es nicht.', '卖%formatted_item_links,绝对找不到比这更划算的了!', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links for sale, how much are you willing to pay?', 0, 0, '', 'J’ai des %formatted_item_links à vendre, combien es-tu prêt à payer?', 'Ich habe %formatted_item_links zu verkaufen, wie viel würdest du zahlen?', '我有%formatted_item_links要卖,你能出多少钱?', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, feel free to message me for the price', 0, 0, '', 'Je vends des %formatted_item_links, n’hésitez pas à me contacter pour connaître les prix.', 'Ich verkaufe %formatted_item_links, schreib mir für den Preis.', '卖%formatted_item_links,想知道价格就私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'Interested in %formatted_item_links? I’ve got them for sale', 0, 0, '', 'Intéressé par %formatted_item_links? J’en ai en vente.', 'Interessiert an %formatted_item_links? Ich habe sie zu verkaufen.', '对%formatted_item_links感兴趣吗?我有卖的。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, PM me if you want them', 0, 0, '', 'J’ai %formatted_item_links, envoie-moi un message si tu les veux.', 'Ich habe %formatted_item_links, schreib mir, wenn du sie willst.', '我有%formatted_item_links,想要就私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links at %cost_gold, hit me up', 0, 0, '', 'Je vends des %formatted_item_links à %cost_gold, n’hésitez pas à me contacter', 'Ich verkaufe %formatted_item_links für %cost_gold, schreib mir.', '卖%formatted_item_links,价格%cost_gold,有意就找我。', '', '', '', ''), -('response_wtb_items_whisper', 'I’m selling %formatted_item_links, make me an offer', 0, 0, '', 'Je vends des %formatted_item_links, faites-moi une offre.', 'Ich verkaufe %formatted_item_links, mach mir ein Angebot.', '我在卖%formatted_item_links,给我开个价吧。', '', '', '', ''), -('response_wtb_items_whisper', 'Got %formatted_item_links, message me for the details', 0, 0, '', 'J’ai des %formatted_item_links, envoie-moi un message pour obtenir les détails', 'Ich habe %formatted_item_links, schreib mir für die Details.', '我有%formatted_item_links,想了解详情就私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I’ve got %formatted_item_links, hit me up if you’re interested', 0, 0, '', 'Salut, j’ai des %formatted_item_links, fais-moi signe si cela t’intéresse.', 'Hey, ich habe %formatted_item_links, sag Bescheid, wenn du interessiert bist.', '嘿,我有%formatted_item_links,感兴趣就联系我。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, let’s make a deal', 0, 0, '', 'Je vends des %formatted_item_links, on peut s’arranger sur le prix.', 'Ich verkaufe %formatted_item_links, lass uns einen Deal machen.', '卖%formatted_item_links,咱们谈个交易吧。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, let me know if you want them', 0, 0, '', 'Je vends des %formatted_item_links, fais-moi savoir si tu en veux.', 'Ich verkaufe %formatted_item_links, sag mir Bescheid, wenn du sie haben willst.', '卖%formatted_item_links,想要就告诉我。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, I’ll make you a good deal', 0, 0, '', 'J’ai des %formatted_item_links, je vais te faire une bonne affaire', 'Ich habe %formatted_item_links, ich mache dir ein gutes Angebot.', '我有%formatted_item_links,给你个好价格。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links, make me an offer and it’s yours', 0, 0, '', 'J’ai %formatted_item_links, fais-moi une offre et il est à toi.', 'Ich habe %formatted_item_links, mach mir ein Angebot und sie gehören dir.', '我有%formatted_item_links,开个价,合适就归你了。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, I’m open to offers', 0, 0, '', 'Je vends des %formatted_item_links, je suis ouvert aux offres.', 'Ich verkaufe %formatted_item_links, bin für Angebote offen.', '卖%formatted_item_links,价格好商量。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, message me if you’re interested', 0, 0, '', 'J’ai %formatted_item_links, n’hésitez pas à me contacter si vous êtes intéressé(e).', 'Ich habe %formatted_item_links, schreib mir, wenn du interessiert bist.', '我有%formatted_item_links,感兴趣就私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'I can sell you %formatted_item_links for a good price', 0, 0, '', 'Je peux te vendre des %formatted_item_links à un bon prix.', 'Ich kann dir %formatted_item_links zu einem guten Preis verkaufen.', '我可以便宜点卖给你%formatted_item_links。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links for sale, I’ll consider any offers', 0, 0, '', 'J’ai des %formatted_item_links à vendre, je considérerai toutes les offres.', 'Ich habe %formatted_item_links zu verkaufen, ich nehme alle Angebote in Betracht.', '我有%formatted_item_links要卖,任何报价我都会考虑。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links for sale, message me for details', 0, 0, '', 'J’ai %formatted_item_links à vendre, envoyez-moi un message pour plus de détails.', 'Ich habe %formatted_item_links zu verkaufen, schreib mir für Details.', '我有%formatted_item_links要卖,详情私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links for sale, make me an offer', 0, 0, '', 'J’ai des %formatted_item_links à vendre, faites-moi une offre.', 'Ich habe %formatted_item_links zu verkaufen, mach mir ein Angebot.', '我有%formatted_item_links要卖,给我开个价。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, how much are you willing to pay?', 0, 0, '', 'J’ai des %formatted_item_links, combien es-tu prêt à payer?', 'Ich habe %formatted_item_links, wie viel würdest du zahlen?', '我有%formatted_item_links,你愿意出多少钱?', '', '', '', ''), -('response_wtb_items_whisper', 'I’m selling %formatted_item_links, message me for more info', 0, 0, '', 'Je vends %formatted_item_links, n’hésitez pas à me contacter pour plus d’informations.', 'Ich verkaufe %formatted_item_links, schreib mir für mehr Infos.', '我在卖%formatted_item_links,想了解更多就私聊我。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links, let me know if you’re interested', 0, 0, '', 'J’ai %formatted_item_links, fais-moi savoir si cela t’intéresse.', 'Ich habe %formatted_item_links, sag mir Bescheid, wenn du interessiert bist.', '我有%formatted_item_links,感兴趣就告诉我。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, let’s talk numbers', 0, 0, '', 'Je vends des %formatted_item_links, parlons chiffres.', 'Ich verkaufe %formatted_item_links, lass uns über Zahlen reden.', '卖%formatted_item_links,咱们聊聊价格吧。', '', '', '', ''), -('response_wtb_items_whisper', 'I can sell you %formatted_item_links, just let me know what you’re offering', 0, 0, '', 'Je peux vous vendre %formatted_item_links, il suffit de me dire ce que vous proposez.', 'Ich kann dir %formatted_item_links verkaufen, sag mir einfach, was du bietest.', '我可以卖给你%formatted_item_links,告诉我你能出啥价。', '', '', '', ''), -('response_wtb_items_whisper', 'Hey, I’ve got %formatted_item_links for sale, hit me up if you’re interested', 0, 0, '', 'Salut, j’ai des %formatted_item_links à vendre, fais-moi signe si ça t’intéresse', 'Hey, ich habe %formatted_item_links zu verkaufen, sag Bescheid, wenn du interessiert bist.', '嘿,我有%formatted_item_links要卖,感兴趣就找我。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, PM me for the price', 0, 0, '', 'J’ai %formatted_item_links, contactez-moi en privé pour connaître le prix.', 'Ich habe %formatted_item_links, schreib mir privat für den Preis.', '我有%formatted_item_links,私聊我问价格。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, I’m open to offers', 0, 0, '', 'Je vends %formatted_item_links, je suis ouvert aux offres.', 'Ich verkaufe %formatted_item_links, bin für Angebote offen.', '卖%formatted_item_links,价格可以商量。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, feel free to message me', 0, 0, '', 'J’ai des %formatted_item_links, n’hésite pas à me contacter.', 'Ich habe %formatted_item_links, schreib mir einfach.', '我有%formatted_item_links,随时可以联系我。', '', '', '', ''), -('response_wtb_items_whisper', 'I can offer %formatted_item_links, just let me know if you’re interested', 0, 0, '', 'Je peux vous proposer %formatted_item_links, dites-moi si cela vous intéresse.', 'Ich kann %formatted_item_links anbieten, sag mir Bescheid, wenn du interessiert bist.', '我可以提供%formatted_item_links,感兴趣就告诉我。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, I can work with any reasonable offer', 0, 0, '', 'Je vends des %formatted_item_links, je peux m’adapter à toute offre raisonnable.', 'Ich verkaufe %formatted_item_links, bin für jedes vernünftige Angebot offen.', '卖%formatted_item_links,合理报价我都能接受。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, hit me up for more info', 0, 0, '', 'J’ai des %formatted_item_links, n’hésitez pas à me contacter pour plus d’informations.', 'Ich habe %formatted_item_links, schreib mir für mehr Infos.', '我有%formatted_item_links,想知道更多就找我。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links, make me an offer', 0, 0, '', 'J’ai %formatted_item_links, proposez-moi quelque chose.', 'Ich habe %formatted_item_links, mach mir ein Angebot.', '我有%formatted_item_links,给我开个价吧。', '', '', '', ''), -('response_wtb_items_whisper', 'Selling %formatted_item_links, shoot me a message if you’re interested', 0, 0, '', 'Je vends des %formatted_item_links, envoie-moi un message si ça t’intéresse', 'Ich verkaufe %formatted_item_links, schreib mir, wenn du interessiert bist.', '卖%formatted_item_links,感兴趣就给我发消息。', '', '', '', ''), -('response_wtb_items_whisper', 'I’ve got %formatted_item_links for sale, let’s talk business', 0, 0, '', 'J’ai des %formatted_item_links à vendre, parlons affaires.', 'Ich habe %formatted_item_links zu verkaufen, lass uns über das Geschäft reden.', '我有%formatted_item_links要卖,咱们谈谈生意吧。', '', '', '', ''), -('response_wtb_items_whisper', 'I have %formatted_item_links, let me know if you want them', 0, 0, '', 'J’ai des %formatted_item_links, tu veux que je te les envoie ?', 'Ich habe %formatted_item_links, sag mir Bescheid, wenn du sie willst.', '我有%formatted_item_links,想要就告诉我。', '', '', '', ''), - --- ///////////////////////////// --- generic events - --- quest -('quest_accept', 'Quest accepted', 0, 0, '', 'Quête accepté!', 'Quest angenommen', '任务已接受。', '', 'Misión aceptada', '', 'Квест взят'), -('quest_remove', 'Quest removed', 0, 0, '', 'Quête supprimée', 'Quest entfernt', '任务已删除。', '', 'Misión eliminada', '', 'Квест отменен'), -('quest_cant_take', 'I can’t take this quest', 0, 0, '', 'Je ne peux pas prendre cette quête.', 'Ich kann diese Quest nicht annehmen.', '我无法接受这个任务。', '', 'No puedo tomar esta búsqueda', '', 'Я не могу взять этот квест'), -('quest_error_talk', 'I can’t talk with the quest giver', 0, 0, '', 'Je ne peux pas parler à ce donneur de quête', 'Ich kann nicht mit dem Questgeber sprechen.', '我无法与任务发布者交谈。', '', 'No puedo hablar con el propietario de la mision', '', ''), -('quest_error_completed', 'I have already completed %quest', 0, 0, '', 'J’ai déjà accompli %quête', 'Ich habe %quest bereits abgeschlossen.', '我已经完成了%quest。', '', 'Ya he completado la mision %quest', '', ''), -('quest_error_have_quest', 'I already have %quest', 0, 0, '', 'Je possède déjà %quest', 'Ich habe %quest bereits.', '我已经拥有%quest。', '', 'Ya tengo la mision %quest', '', ''), -('quest_error_cant_take', 'I can’t take %quest', 0, 0, '', 'Je ne peux pas accomplir cette %quest.', 'Ich kann %quest nicht annehmen.', '我无法接受%quest。', '', 'No puedo aceptar la mision %quest', '', ''), -('quest_error_log_full', 'I can’t take %quest because my quest log is full', 0, 0, '', 'Je ne peux pas prendre %quest, car mon journal de quêtes est plein.', 'Ich kann %quest nicht annehmen, da mein Questlog voll ist.', '我没法接%quest,任务日志已经满了!', '', 'No puedo aceptar la mision %quest porque mi registro de misiones esta completo', '', ''), -('quest_error_bag_full', 'I can’t take %quest because my bag is full', 0, 0, '', 'Je ne peux pas prendre %quest car mon sac est plein.', 'Ich kann %quest nicht annehmen, weil meine Tasche voll ist.', '我没法接%quest,背包已经塞满了!', '', 'No puedo aceptar la mision %quest porque mi inventario esta lleno', '', ''), -('quest_accepted', 'I have accepted %quest', 0, 0, '', 'J’ai accepté la %quête.', 'Ich habe %quest angenommen.', '我已经接下了%quest任务!', '', 'He aceptado la mision %quest', '', ''), -('quest_status_incomplete', 'I have not completed the quest %quest', 0, 0, '', 'Je n’ai pas encore terminé la quête %quest.', 'Ich habe die Quest %quest noch nicht abgeschlossen.', '我还没完成%quest任务呢。', '', 'No he completado la mision %quest', '', ''), -('quest_status_available', 'Quest %quest available', 0, 0, '', 'Il y a une %quête disponible.', 'Quest %quest verfügbar.', '%quest任务可以接了!', '', 'Mision %quest disponible', '', ''), -('quest_status_failed', 'I have failed the quest %quest', 0, 0, '', 'J’ai échoué à la quête %quest.', 'Ich habe die Quest %quest nicht geschafft.', '我失败了%quest任务。', '', 'He fallado la mision %quest', '', ''), -('quest_status_unable_to_complete', 'I am unable to turn in the quest %quest', 0, 0, '', 'Je ne peux pas terminer la quête %quest.', 'Ich kann die Quest %quest nicht abgeben.', '我没法交%quest任务。', '', 'No puedo entregar la mision %quest', '', ''), -('quest_status_completed', 'I have completed the quest %quest', 0, 0, '', 'J’ai terminé la quête %quest', 'Ich habe die Quest %quest abgeschlossen.', '我已经完成了%quest任务!', '', 'He completado la mision %quest', '', ''), -('quest_status_complete_single_reward', 'I have completed the quest %quest and received %item', 0, 0, '', 'J’ai terminé la quête %quest et j’ai reçu %item.', 'Ich habe die Quest %quest abgeschlossen und %item erhalten.', '我完成了%quest任务,拿到了%item!', '', 'He completado la mision %quest y he recibido %item', '', ''), -('quest_status_complete_pick_reward', 'Which reward should I pick for completing the quest %quest?%rewards', 0, 0, '', 'Quelle récompense devrais-je choisir pour avoir complété la quête %quest? %rewards', 'Welche Belohnung soll ich für das Abschließen der Quest %quest wählen? %rewards', '完成%quest任务后,我该选哪个奖励呢?%rewards', '', '¿Que recompensa deberia escoger por completar la mision %quest?%rewards', '', ''), -('quest_choose_reward', 'Okay, I will pick %item as a reward', 0, 0, '', 'D’accord, je vais choisir %item comme récompense.', 'Okay, ich werde %item als Belohnung nehmen.', '好吧,我就选%item作为奖励吧。', '', 'De acuerdo, voy a escoger %item de recompensa', '', ''), - --- stuff -('hello', 'Hello', 0, 0, '', 'Salut!', 'Hallo', '你好!', '', 'Hola', '', 'Привет'), -('hello', 'Hello!', 0, 0, '', 'Salut!', 'Hallo!', '你好啊!', '', '¡Hola!', '', 'Привет!'), -('hello', 'Hi', 0, 0, '', 'Salut! Comment ça va?', 'Hi! Wie geht’s?', '嗨!', '', 'Hola', '', 'Прив'), -('hello', 'Hi!', 0, 0, '', 'Salut!', 'Hi!', '嗨!', '', '¡Hola!', '', 'Прив!'), -('hello', 'Hello there!', 0, 0, '', 'Salut toi!', 'Hallo du!', '嘿,你好!', '', '¡Hola!', '', 'Здарова!'), - -('hello_follow', 'Hello, I follow you!', 0, 0, '', 'Bonjour, je te suis !', 'Hallo, ich folge dir!', '你好,我跟着你走!', '', '¡Hola, te sigo!', '', 'Привет, иду за тобой!'), -('hello_follow', 'Hello, lead the way!', 0, 0, '', 'Bonjour, je vais vous montrer le chemin !', 'Hallo, führe den Weg!', '你好,带路吧!', '', '¡Hola, guía el camino!', '', 'Привет, веди!'), -('hello_follow', 'Hi, lead the way!', 0, 0, '', 'Salut, montre le chemin!', 'Hi, zeig mir den Weg!', '嗨,带路吧!', '', '¡Hola, guía el camino!', '', 'Прив, веди!'), - -('join_group', 'Hey %player, do you want join my group?', 0, 0, '', 'Salut %player, est-ce que tu veux rejoindre mon groupe ?', 'Hey %player, möchtest du meiner Gruppe beitreten?', '嘿,%player,要不要加入我的队伍?', '', 'Oye %player, ¿Quieres unirte a mi grupo?', '', ''), -('join_raid', 'Hey %player, do you want join my group?', 0, 0, '', 'Salut %player, est-ce que tu veux rejoindre mon groupe?', 'Hey %player, möchtest du meinem Schlachtzug beitreten?', '嘿,%player,要不要加入我的团队?', '', 'Oye %player, ¿Quieres unirte a mi banda?', '', ''), - -('logout_cancel', 'Logout cancelled!', 0, 0, '', 'Annulation de la déconnexion !', 'Abmeldung abgebrochen!', '退出取消了!', '', 'Cerrar sesión cancelado!', '', 'Логаут отменен!'), -('logout_start', 'I’m logging out!', 0, 0, '', 'Je me déconnecte !', 'Ich melde mich ab!', '我要下线了!', '', '¡Me estoy desconectando!', '', 'Я выхожу из игры!'), - -('goodbye', 'Goodbye!', 0, 0, '', 'Au revoir!', 'Auf Wiedersehen!', '再见!', '', '¡Adiós!', '', 'Пока!'), -('goodbye', 'Bye bye!', 0, 0, '', 'Au revoir!', 'Tschüss!', '拜拜!', '', '¡Adiós!', '', 'Пока пока!'), -('goodbye', 'See you later!', 0, 0, '', 'À plus tard !', 'Bis später!', '回头见!', '', '¡Hasta luego!', '', 'Увидимся позже!'), - --- ///////////////////////////// --- replies - --- replies 0 -('reply', 'what was that %s?', 0, 0, '', 'c’était quoi ce %s?', 'Was war das %s?', '那是什么,%s?', '', '¿qué has dicho %s?', '', ''), -('reply', 'not sure I understand %s?', 0, 0, '', 'Je ne suis pas sûre de comprendre %s.', 'Bin mir nicht sicher, ob ich %s verstehe?', '我不确定我懂了%s的意思?', '', '¿no estoy seguro de entenderte %s?', '', ''), -('reply', 'uh... no clue what yer talkin bout', 0, 0, '', 'Oh, je ne sais vraiment pas de quoi vous parlez.', 'Äh... keine Ahnung, wovon du redest.', '呃……完全不知道你在说啥。', '', 'uh... ni idea de lo que estás hablando', '', ''), -('reply', 'you talkin to me %s?', 0, 0, '', 'Tu me parles, %s ?', 'Redest du mit mir, %s?', '你在跟我说话吗,%s?', '', '¿Me hablas a mí %s?', '', ''), -('reply', 'whaaaa?', 0, 0, '', 'Oh là là, qu’est-ce qui se passe?', 'Waaaaas?', '啥???', '', '¿quéaaa?', '', ''), -('reply', 'huh?', 0, 0, '', 'heein ?', 'Hä?', '哈?', '', '¿eh?', '', ''), -('reply', 'what?', 0, 0, '', 'Quoi ?', 'Was?', '啥?', '', '¿qué?', '', ''), -('reply', 'are you talking?', 0, 0, '', 'Tu es en train de parler ?', 'Redest du grade?', '你在说话吗?', '', '¿estás hablando?', '', ''), -('reply', 'whatever dude', 0, 0, '', 'on s’en fou mec', 'Ist mir egal, Alter.', '随便吧,兄弟。', '', 'Menudo tio...', '', ''), -('reply', 'you lost me', 0, 0, '', 'tu m’as perdu', 'Ich hab den Faden verloren.', '你把我整懵了。', '', 'No entiendo ni papa', '', ''), -('reply', 'Bla bla bla...', 0, 0, '', 'bla bla bla ...', 'Bla bla bla...', '叽里呱啦……', '', 'Bla bla bla...', '', ''), -('reply', 'What did you say, %s?', 0, 0, '', 'Qu’est-ce que tu as dit, %s?', 'Was hast du gesagt, %s?', '你说了啥,%s?', '', '¿Qué me estas contando %s?', '', ''), -('reply', 'Concentrate on the game, %s!', 0, 0, '', 'Concentre-toi sur le jeu, %s!', 'Konzentriere dich aufs Spiel, %s!', '专心点,%s!', '', '¡Concéntrate en el juego, %s!', '', ''), -('reply', 'Chatting with you %s is so great! I always wanted to meet you', 0, 0, '', 'Discuter avec toi %s est tellement génial! J’ai toujours voulu te rencontrer.', 'Mit dir zu quatschen, %s, ist so cool! Ich wollte dich schon immer mal treffen.', '跟%s你聊天真是太棒了!我早就想认识你了!', '', '¡%s, chatear contigo es genial! Siempre quise conocerte', '', ''), -('reply', 'These chat-messages are freaking me out! I feel like I know you all!', 0, 0, '', 'Ces messages de chat me rendent dingue! J’ai l’impression de tous vous connaître!', 'Diese Chat-Nachrichten machen mich fertig! Ich habe das Gefühl, euch alle zu kennen!', '这些聊天信息让我有点慌!感觉好像都认识你们!', '', '¡Estos mensajes de chat me están asustando! ¿De que cojones estais hablando?', '', ''), -('reply', 'YEAH RIGHT! HAHA SURE!!!', 0, 0, '', 'Bien sûr! Haha, ouais, bien sûr!', 'JA GENAU! HAHA SICHER!!!', '对对对!哈哈,肯定的!!!', '', '¡SÍ, CORRECTO! ¡¡¡JAJA SEGURO!!!', '', ''), -('reply', 'I believe you!!!', 0, 0, '', 'Je te crois !!!', 'Ich glaube dir!!!', '我信你!!!', '', '¡Si, claro!', '', ''), -('reply', 'OK, uhuh LOL', 0, 0, '', 'D’accord, euhuh MDR', 'OK, uhuh LOL', '好吧,嗯嗯,哈哈', '', 'OK, uhuh LOL', '', ''), -('reply', 'Why is everybody always saying the same things???', 0, 0, '', 'Pourquoi est-ce que tout le monde répète toujours les mêmes choses ???', 'Warum sagt eigentlich jeder immer dasselbe???', '为什么大家老是说一样的话???', '', '¿Por qué todo el mundo siempre dice lo mismo?', '', ''), -('reply', 'Hey %s....oh nevermind!', 0, 0, '', 'Hey %s....oh, laisse tomber!', 'Hey %s... ach, vergiss es!', '嘿,%s……算了,不说了!', '', 'Oye %s... ¡oh, no importa!', '', ''), -('reply', 'What are you talking about %s', 0, 0, '', 'De quoi parles-tu %s', 'Worüber redest du %s?', '你在说啥啊,%s?', '', 'De qué estás hablando %s', '', ''), -('reply', 'Who said that? I resemble that remark', 0, 0, '', 'Qui a dit ça ? Je ressemble à ce commentaire', 'Wer hat das gesagt? Ich fühle mich angesprochen', '谁说的?这话咋听着像说我呢?', '', '¿Quién ha dicho eso? ¿Estais hablando de mi?', '', ''), -('reply', 'wtf are you all talking about', 0, 0, '', 'Qu’est-ce que vous racontez tous en fait?', 'WTF redet ihr alle?', '你们都在扯啥啊?', '', '', '', ''), -('reply', 'fr fr no cap on a stack', 0, 0, '', '', 'Echt jetzt, kein Witz, auf Ehrenbruderbasis', '真没骗你,兄弟,绝不撒谎!', '', '', '', ''), -('reply', 'your not getting jack shit', 0, 0, '', 'Tu ne comprends que dalle.', 'Du kriegst gar nichts.', '你啥也得不到!', '', '', '', ''), -('reply', 'swag', 0, 0, '', '', '', '真帅!', '', '', '', ''), -('reply', 'ty!', 0, 0, '', 'De rien !', 'Danke!', '谢谢!', '', '', '', ''), -('reply', 'no', 0, 0, '', 'non', 'nein', '不', '', '', '', ''), -('reply', 'Yep', 0, 0, '', 'ouai', 'Ja', '嗯', '', '', '', ''), -('reply', 'f', 0, 0, '', '', '', '唉', '', '', '', ''), -('reply', '%s no shit xD', 0, 0, '', '%s sans déconner xD', '%s kein Witz xD', '%s,真的假的xD', '', '', '', ''), -('reply', 'why is that', 0, 0, '', 'Pourquoi cela?', 'Warum das?', '为啥啊?', '', '', '', ''), -('reply', 'lmao', 0, 0, '', 'Je me marre à en pleurer.', 'Ich lache mich schlapp.', '笑死我了!', '', '', '', ''), -('reply', 'thought i should remain silent, was getting confused from chat again', 0, 0, '', 'Je pensais que je devrais rester silencieux, je commençais à me perdre dans la conversation encore une fois.', 'Dachte, ich sollte lieber schweigen, hab mich schon wieder im Chat verloren.', '我觉得我还是闭嘴吧,聊天又把我整糊涂了。', '', '', '', ''), -('reply', 'i can get realy jealous', 0, 0, '', 'Je peux devenir vraiment jaloux.', 'Ich kann echt eifersüchtig werden.', '我可会吃醋了!', '', '', '', ''), -('reply', '%s you can’t hear the dripping sarcasm in my text', 0, 0, '', '%s, tu ne peux pas entendre le sarcasme qui goutte de mon texte?', '%s, du hörst den triefenden Sarkasmus in meinem Text nicht?', '%s,你听不出我文字里满满的讽刺吗?', '', '', '', ''), -('reply', 'he said no homo, it’s fine', 0, 0, '', 'Il a dit no homo, tout va bien.', 'Er hat "no homo" gesagt, also passt das.', '他说没别的意思,那就没事。', '', '', '', ''), -('reply', 'dwarf moment', 0, 0, '', '', 'Zwergen-Moment', '矮人时刻!', '', '', '', ''), -('reply', 'Yes %s', 0, 0, '', 'Oui %s', 'Ja %s', '好的,%s', '', '', '', ''), -('reply', 'interesting...', 0, 0, '', 'Intéressant...', 'Interessant...', '有意思……', '', '', '', ''), -('reply', 'lol', 0, 0, '', 'Ah ah!', 'Haha!', '哈哈!', '', '', '', ''), -('reply', '%s fuck you man :D', 0, 0, '', '%s va te faire enculer :D', '%s fick dich, Mann :D', '%s,去你的吧 :D', '', '', '', ''), -('reply', ':^)', 0, 0, '', '', '', ':^)', '', '', '', ''), -('reply', 'ty', 0, 0, '', 'Merci à toi!', 'Danke', '谢谢!', '', '', '', ''), -('reply', '%s well said', 0, 0, '', '%s Bien dit.', '%s gut gesagt.', '%s,说得好!', '', '', '', ''), -('reply', 'yay', 0, 0, '', '', '', '耶!', '', '', '', ''), -('reply', 'yeah', 0, 0, '', '', '', '对对对!', '', '', '', ''), -('reply', 'ooooooh', 0, 0, '', '', '', '哦哦哦!', '', '', '', ''), -('reply', 'hmm', 0, 0, '', '', '', '嗯……', '', '', '', ''), -('reply', 'yeah right', 0, 0, '', 'Oui, c’est ça.', 'Ja, genau.', '得了吧,真的?', '', '', '', ''), -('reply', 'you made me gag wtf', 0, 0, '', 'Tu m’as vraiment dégouté, c’est quoi ce délire ?', 'Du hast mich richtig würgen lassen, wtf.', '你让我恶心死了,搞什么啊!', '', '', '', ''), -('reply', 'hot', 0, 0, '', ' !', 'heiß!', '真火爆!', '', '', '', ''), -('reply', 'hoes mad', 0, 0, '', 'Les pétasses sont folles', 'Die Weiber drehen durch.', '一群人气炸了!', '', '', '', ''), -('reply', 'what have you been eating %s', 0, 0, '', 'Qu’est-ce que tu as mangé %s', 'Was hast du gegessen, %s?', '%s,你到底吃了啥?', '', '', '', ''), -('reply', 'wtf', 0, 0, '', '', '', '啥情况?!', '', '', '', ''), -('reply', 'i will attempt to understand that comment', 0, 0, '', 'Je vais essayer de comprendre ce commentaire', 'Ich werde versuchen, diesen Kommentar zu verstehen.', '我试着理解你这话啥意思……', '', '', '', ''), -('reply', '*confused*', 0, 0, '', '*Je suis un peu perdu.*', '*Verwirrt*', '*一脸懵逼*', '', '', '', ''), -('reply', 'fuck yea', 0, 0, '', 'D’accord !', 'Verdammt ja!', '太棒了!', '', '', '', ''), -('reply', '0/10 would not read again', 0, 0, '', 'Hennetement, 0/10 je ne te relirais pas.', '0/10 würde ich nicht noch einmal lesen.', '0/10,完全不想再看一次。', '', '', '', ''), -('reply', '10/10 would read again', 0, 0, '', '10/10 Absolument, je te relirais sans hésiter!', '10/10 würde ich definitiv noch einmal lesen!', '10/10,绝对会再看一遍!', '', '', '', ''), -('reply', '6/10 would read', 0, 0, '', 'Je dirais que c’est un 6/10 en termes de lecture.', '6/10 würde ich lesen.', '6/10,还行吧,能看。', '', '', '', ''), -('reply', '7/10 would', 0, 0, '', '7/10 serait mon verdict.', '7/10 wäre mein Urteil.', '7/10,不错啦。', '', '', '', ''), -('reply', 'based', 0, 0, '', '', 'basiert', '真有道理!', '', '', '', ''), -('reply', 'oh yeah maybe', 0, 0, '', 'Oh oui, peut-être que oui.', 'Oh ja, vielleicht.', '哦,对,可能吧。', '', '', '', ''), -('reply', 'yeah so what', 0, 0, '', 'Ouais, donc quoi ?', 'Ja, und?', '对啊,那又咋样?', '', '', '', ''), - --- replies 1 -('reply', 'hey %s i havent forgotten you', 0, 1, '', 'salut %s je ne t’ai pas oublié', 'hey %s ich habe dich nicht vergessen', '嘿,%s,我可没忘了你!', '', 'hey %s no te he olvidado', '', ''), -('reply', 'you piss me off %s', 0, 1, '', 'Tu me saoules %s', 'du gehst mir auf die Nerven %s', '%s,你真让我火大!', '', 'me cabreas %s', '', ''), -('reply', 'im gunna get you this time %s', 0, 1, '', 'je vais t’avoir cette fois %s', 'diesmal kriege ich dich %s', '这次我非抓住你不可,%s!', '', 'voy a atraparte esta vez %s', '', ''), -('reply', 'better watch your back %s', 0, 1, '', 'Fais mieux de surveiller tes arrières %s', 'pass besser auf deinen Rücken auf %s', '小心你的背后,%s!', '', 'mejor cuida tu espalda %s', '', ''), -('reply', 'i did not like last round so much', 0, 1, '', 'Je n’ai pas aimé notre dernier round.', 'die letzte Runde hat mir nicht gefallen', '上一轮真不咋样。', '', 'no me gustó tanto la última ronda', '', ''), -('reply', 'i sucked last round thanks to %s', 0, 1, '', 'J’ai été nul lors de la dernière manche à cause de %s.', 'ich war in der letzten Runde schlecht dank %s', '上一轮我表现烂透了,都怪%s!', '', 'chupé la última ronda gracias a %s', '', ''), -('reply', 'prepare to die %s', 0, 1, '', 'Prépare-toi à mourir %s', 'mach dich bereit zu sterben %s', '准备受死吧,%s!', '', 'prepárate para morir %s', '', ''), -('reply', 'dont appreciate you killin me %s', 0, 1, '', 'Je n’apprécie pas que tu me tues %s.', 'ich finde es nicht lustig, dass du mich tötest %s', '%s,杀我可不厚道啊!', '', 'no aprecio que me mates %s', '', ''), -('reply', '%s, i hate you', 0, 1, '', 'Oh, %s je te déteste.', 'oh %s ich hasse dich', '%s,我恨你!', '', '%s, te odio', '', ''), -('reply', 'grrrrrr, ill get you this time %s', 0, 1, '', 'grrrrrr, je vais t’avoir cette fois %s', 'grrrrrr, diesmal kriege ich dich %s', '嗷嗷,这次我非得干掉你,%s!', '', 'grrrrrr, te libraste esta vez %s', '', ''), -('reply', 'well fuck you', 0, 1, '', 'Eh bien va te faire mettre', 'na dann fick dich', '去你的吧!', '', 'vamos a la mierda', '', ''), -('reply', '%s i’ll vomit into your fkin mouth', 0, 1, '', '%s je vais te gerber dans la bouche', '%s ich werde dir in den verdammten Mund kotzen', '%s,我要吐你一脸!', '', '', '', ''), -('reply', 'dont fucking judge me', 0, 1, '', 'Ne me juge pas putain.', 'verdammt noch mal, beurteile mich nicht', '别他妈评头论足!', '', '', '', ''), -('reply', 'Yo momma so fat, she can’t even fit through the Dark Portal', 0, 1, '', 'Ta mère est tellement grosse qu’elle ne peut même pas passer à travers le Portail des Ténèbres.', 'deine Mutter ist so fett, dass sie nicht mal durch das Dunkle Portal passt', '你妈胖得连黑暗之门都挤不进去!', '', '', '', ''), - --- replies 2 -('reply', 'wtf', 0, 2, '', 'wtf', 'wtf', '啥情况?!', '', 'wtf', '', ''), -('reply', 'wtf??', 0, 2, '', 'wtf??', 'wtf??', '啥情况??', '', 'wtf??', '', ''), -('reply', 'low life', 0, 2, '', 'low life', 'low life', '卑微的家伙!', '', 'low life', '', ''), -('reply', 'wth', 0, 2, '', 'wth', 'wth', '搞啥呢?', '', 'wth', '', ''), -('reply', 'sucked', 0, 2, '', 'nul à chier', 'war richtig schlecht', '太烂了!', '', 'chupate esa', '', ''), -('reply', 'REMATCH!!! im taking him down', 0, 2, '', 'REVANCHE!!! Je vais le terrasser', 'REVANCHE!!! Ich mache ihn fertig', '再来一局!!我要把他干掉!', '', '¡¡REVANCHA!!! lo estoy derribando', '', ''), -('reply', 'pathetic, i got killed by %s', 0, 2, '', 'Pathétique, je me suis fait tuer par %s.', 'Peinlich, ich wurde von %s getötet.', '真丢人,被%s干掉了!', '', 'patético, me mató %s', '', ''), -('reply', 'ok I’m done', 0, 2, '', 'Ok j’en fini.', 'Okay, ich bin raus.', '好了,我受够了!', '', '', '', ''), - --- replies 3 -('reply', 'hehe, i nailed %s?', 0, 3, '', 'Héhé, j’ai cloué %s?', 'Hehe, ich hab %s erwischt?', '嘿嘿,我搞定了%s?', '', 'jeje, ¿acerté a %s?', '', ''), -('reply', 'that was too easy, killin %s', 0, 3, '', 'c’était trop facile, de tuer %s', 'Es war viel zu einfach, %s zu erledigen.', '太简单了,干掉%s跟玩儿似的!', '', 'fue demasiado fácil matar a %s', '', ''), -('reply', 'gotcha mofo', 0, 3, '', 'je t’ai eu enfoiré', 'Hab dich, du Mistkerl!', '抓到你了,混蛋!', '', 'te pille hijo de perra', '', ''), -('reply', 'ha ha', 0, 3, '', 'Ha ha', 'Haha', '哈哈!', '', 'ja, ja', '', ''), -('reply', 'loser', 0, 3, '', 'loser', 'Verlierer', '输家!', '', 'loser', '', ''), -('reply', 'i killed %s and yer all next dudes', 0, 3, '', 'J’ai éliminé %s et vous êtes tous les prochains, les gars.', 'Ich habe %s erledigt und ihr seid die nächsten!', '我干掉了%s,你们下一个,伙计们!', '', 'yo maté a %s y a todos los siguientes tipos', '', ''), -('reply', 'oh yeah i owned him', 0, 3, '', 'Oh ouais, je l’ai complètement battu', 'Oh ja, er gehörte mir', '哦耶,我完全碾压了他!', '', 'oh, sí, lo tenía', '', ''), -('reply', 'im a killin machine', 0, 3, '', 'Je suis une machine à tuer.', 'Ich bin eine Tötungsmaschine.', '我就是杀戮机器!', '', 'soy una máquina de matar', '', ''), -('reply', '%s, this reminds me of a Slayer song...all this bloodshed', 0, 3, '', 'Oh ça, ça me rappelle une chanson de Slayer... tout ce carnage', '%s, das erinnert mich an einen Slayer-Song... all dieses Blutvergießen.', '%s,这让我想起一首杀手乐队的歌……全是血腥杀戮!', '', '%s, esto me recuerda a una canción de Slayer... todo este derramamiento de sangre', '', ''), -('reply', 'sorry, %s. can we do the scene again?', 0, 3, '', 'Désolé, %s. On peut refaire la scène?', 'Sorry, %s. Können wir die Szene nochmal spielen?', '抱歉,%s。咱们能再来一次吗?', '', 'lo siento, %s. ¿Podemos hacer la escena de nuevo?', '', ''), -('reply', 'so....how do you like being worm food %s???', 0, 3, '', 'Alors, comment aimes-tu être de la bouffe pour vers de terre %s ??', 'Na, %s, wie gefällt es dir als Wurmfutter?', '嘿……%s,变成虫子粮食的感觉咋样啊???', '', 'así que... ¿cómo te gusta ser comida de gusanos %s???', '', ''), -('reply', 'yer supposed to be dead, %s its part of the game!!!!!', 0, 3, '', 'Je suis mort, %s, c’est une partie du jeu !!!!', 'Du solltest tot sein, %s, das gehört zum Spiel!!!!!', '你应该早就死了,%s,这是游戏规则!!!!', '', '¡¡Se supone que estás muerto, %s es parte del juego!!!!!', '', ''), -('reply', 'sorry, %s. that looked as good as an Andy Worhol painting!', 0, 3, '', 'Désolé, %s. Cela ressemblait aussi bien qu’une peinture d’Andy Warhol!', 'Sorry, %s. Das sah aus wie ein Andy Warhol-Gemälde!', '抱歉,%s。这场景美得像安迪·沃霍尔的画作!', '', 'lo siento, %s. ¡Eso se veía tan bien como una pintura de Andy Worhol!', '', ''), -('reply', '%s, ill use the rubber bullets next time !', 0, 3, '', 'D’accord, %s la prochaine fois j’utiliserai les balles en caoutchouc !', 'Okay, %s, nächstes Mal benutze ich Gummigeschosse!', '%s,下次我用橡皮弹好了!', '', '%s, ¡usaré las balas de goma la próxima vez!', '', ''), -('reply', 'whatsamatter, %s?? lose your head? hahaha gotta keep cool!!', 0, 3, '', 'Qu’est-ce qu’il se passe, %s ? Tu perds la tête ? Haha, faut garder son calme !', 'Was ist los, %s?? Kopf verloren? Hahaha, bleib cool!!', '咋了,%s??掉了脑袋?哈哈,保持冷静!!', '', '¿qué pasa, %s?? ¿has perdido la cabeza? jajaja tengo que mantener la calma!!', '', ''), -('reply', 'i had to do it, %s. You understand. The Director said so!!', 0, 3, '', 'Il fallait que je le fasse, %s. Tu comprends. Le Directeur l’a dit !!', 'Ich musste es tun, %s. Du verstehst doch. Der Regisseur hat das gesagt!!', '我不得不这么做,%s。你懂的,是导演要求的!!', '', 'Tenía que hacerlo, %s. Entiendelo... ¡¡Dios me lo ordenó!!', '', ''), -('reply', 'hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', 0, 3, '', 'Hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', 'Hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', '嘿,%s.......哇哈哈哈哈哈哈哈哈哈', '', 'hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', '', ''), -('reply', '%s, i enjoyed that one!! Lets play it again Sam', 0, 3, '', '%s, j’ai adoré celui-là!! On rejoue Sam', '%s, das hat mir gefallen!! Lass uns das nochmal spielen, Sam.', '%s,这局我玩得爽!!再来一次吧,山姆!', '', '%s, ¡disfruté ese! Juguemos de nuevo Sam', '', ''), -('reply', 'hey, %s! ju can start callin me scarface.. ju piece of CHIT!!!!', 0, 3, '', 'Hé, %s ! tu peux commencer à m’appeler Scarface .. sac à merde !!!!', 'Hey, %s! Du kannst mich ab jetzt Scarface nennen.. du Stück SCHEIßE!!!!', '嘿,%s!以后你可以叫我刀疤脸……你个小废物!!!!', '', '¡oye, %s! ¡Puedes empezar a llamarme caracortada... ¡¡¡un trozo de CHIT!!!', '', ''), -('reply', 'are you talking to me %s??', 0, 3, '', 'Tu me parles, %s?', 'Redest du mit mir, %s??', '你是在跟我说话吗,%s??', '', '¿me estás hablando a mí %s?', '', ''), -('reply', '%s get it right this time, dont stand in front of my bullets.', 0, 3, '', 'Désolé %s, ne te mets pas devant mes balles cette fois-ci.', 'Diesmal richtig, %s, stell dich nicht vor meine Kugeln.', '%s,这次别搞错了,别站在我的子弹前面。', '', '%s hazlo bien esta vez, no te pares frente a mis balas.', '', ''), -('reply', '%s, what are you laying around for??? hehe', 0, 3, '', 'Oups, %s qu’est-ce que tu fais à trainer là??? hihi', 'Oh, %s, warum liegst du da rum??? hehe', '%s,你躺在这儿干嘛呢???嘿嘿', '', '%s, ¿para qué estás tirado? jeje', '', ''), -('reply', 'laughed real hard', 0, 3, '', 'J ai vraiment bien rigoler.', 'Ich hab wirklich gut gelacht.', '笑得我肚子都疼了!', '', '', '', ''), - --- replies 4 -('reply', 'hi %s', 0, 4, '', 'Salut %s', 'Hi %s', '嗨,%s', '', 'hola %s', '', ''), -('reply', 'oh, hi %s', 0, 4, '', 'oh, salut %s', 'Oh, hi %s', '哦,嗨,%s', '', 'oh, hola %s', '', ''), -('reply', 'wazzup %s!!!', 0, 4, '', 'Salut %s, ça roule !!!', 'Was geht, %s!!!', '咋样啊,%s!!!', '', 'wazzup %s!!!', '', ''), -('reply', 'hi', 0, 4, '', 'Salut! Comment ça va?', 'Hi!?', '嗨!', '', 'hi', '', ''), -('reply', 'wazzup', 0, 4, '', 'Salut! Comment ça va?', 'Was geht?', '最近咋样?', '', 'wazzup', '', ''), -('reply', 'hello %s', 0, 4, '', 'Salut %s', 'Hallo %s', '你好,%s', '', 'hola %s', '', ''), -('reply', 'hi %s, do i know you?', 0, 4, '', 'Salut %s, est-ce qu’on se connait ?', 'Hi %s, kennen wir uns?', '嗨,%s,我们认识吗?', '', 'hola %s, ¿te conozco?', '', ''), -('reply', 'hey %s', 0, 4, '', 'Salut %s', 'Hey %s', '嘿,%s', '', 'hey %s', '', ''), -('reply', 'hai %s', 0, 4, '', 'Salut %s', 'Hai %s', '嗨,%s', '', 'hai %s', '', ''), -('reply', 'wth', 0, 6, '', 'wth', 'Was zur Hölle?', '搞什么鬼?', '', 'wth', '', ''), -('reply', 'wtf', 0, 6, '', 'wtf', 'Was zum Teufel?', '什么情况?!', '', 'que cojones!', '', ''), -('reply', 'this is bs', 0, 6, '', 'c’est vraiment de la merde', 'Das ist totaler Mist', '这简直是胡扯!', '', 'esto es una mierda', '', ''), -('reply', 'admin', 0, 6, '', 'admin', 'Admin', '管理员', '', 'admin', '', ''), -('reply', 'hey %s quit abusing your admin', 0, 6, '', 'Hey %s, arrête d’abuser de tes droits d’admin.', 'Hey %s, hör auf, deine Adminrechte zu missbrauchen.', '嘿,%s,别滥用你的管理员权限!', '', 'hey %s deja de abusar de tu poder de administrador', '', ''), -('reply', 'leave me alone admin!', 0, 6, '', 'Laisse-moi tranquille, admin !', 'Lass mich in Ruhe, Admin!', '管理员,别烦我!', '', '¡déjame en paz administrador!', '', ''), -('reply', 'you suck admin', 0, 6, '', 't’es une merde l’admin.', 'Du bist schlecht, Admin.', '管理员,你真烂!', '', 'apestas administrador', '', ''), -('reply', 'thats my name, what you want %s', 0, 5, '', 'c’est mon nom, qu’est-ce que tu veux %s', 'Das ist mein Name, was willst du %s?', '那是我的名字,你想干嘛,%s?', '', 'ese es mi nombre, ¿que quieres %s?', '', ''), -('reply', 'yes???', 0, 5, '', 'Oui???', 'Ja???', '啥事???', '', 'sí???', '', ''), -('reply', 'uh... what', 0, 5, '', 'Heein ... quoi ???', 'Äh... was?', '呃……啥?', '', 'uh... qué', '', ''), -('reply', 'you talkin to me %s?', 0, 5, '', 'Tu me parles, %s?', 'Redest du mit mir, %s?', '你跟我说话呢,%s?', '', '¿Me hablas a mí %s?', '', ''), - --- taunts -('taunt', 'I have puppies under my armor!', 0, 0, '', 'J’ai des chiots sous mon armure!', 'Ich habe Welpen unter meiner Rüstung!', '我的盔甲底下藏着小狗崽呢!', '', '¡Tengo cachorros bajo mi armadura!', '', ''), -('taunt', 'Bite me, !', 0, 0, '', 'Va te faire voir, !', 'Beiß mich, !', '来咬我啊,!', '', '¡Muérdeme, !', '', ''), -('taunt', 'Hey ! Guess what your mom said last night!', 0, 0, '', 'Hey ! Devine ce que ta mère a dit hier soir!', 'Hey ! Rate mal, was deine Mutter letzte Nacht gesagt hat!', '嘿,!猜猜你妈昨晚说了啥!', '', '¡Oye, ! ¡Adivina lo que me dijo tu madre anoche!', '', ''), -('taunt', ', you’re so ugly you couldn’t score in a monkey whorehouse with a bag of bananas!', 0, 0, '', ', tu es tellement moche que tu n’arriverais même pas à marquer dans une maison close pour singes avec un sac de bananes!', ', du bist so hässlich, du würdest nicht mal in einem Affenbordell mit einem Sack Bananen landen!', ',你丑得连猴子妓院里带一袋香蕉都捞不到妹子!', '', ', ¡eres tan feo que no podrías anotar en un burdel de monos con una bolsa de plátanos!', '', ''), -('taunt', 'Shut up , you’ll never be the man your mother is!!', 0, 0, '', ', tais-toi, tu ne seras jamais l’homme que ta mère est !!', 'Halt die Klappe, , du wirst niemals so männlich sein wie deine Mutter es ist!!', '闭嘴,,你永远也比不上你妈那股子爷们儿气!', '', 'Cállate , ¡nunca serás el hombre que es tu madre!', '', ''), -('taunt', 'Your mother was a hampster and your father smelt of elderberries!!!!', 0, 0, '', 'Ta mère était un hamster et ton père sentait la myrtille!!!!', 'Deine Mutter war ein Hamster und dein Vater roch nach Holunderbeeren!!!!', '你妈是只仓鼠,你爸身上一股接骨木浆果味儿!!!!', '', 'Tu madre era un hámster y tu padre olía a bayas de saúco!!!!', '', ''), -('taunt', 'I don’t want to talk to you no more, you empty headed animal food trough wiper!!!', 0, 0, '', 'Je ne veux plus te parler, espèce de chiffon mangeur de nourriture pour animaux!!!', 'Ich will nicht mehr mit dir reden, du hohlköpfiger Tierfuttertrog-Wischer!!!', '我不想再跟你废话了,你个脑子空空的喂牲口槽的废物!!!', '', '¡¡No quiero hablar más contigo, limpiaparabrisas de cabeza vacía!', '', ''), -('taunt', 'I fart in your general direction!!!', 0, 0, '', 'Je te pète à la figure !!!', 'Ich pupse in deine allgemeine Richtung!!!', '我朝你大方向放了个屁!!!', '', '¡¡Me tiro un pedo en tu dirección general!!!', '', ''), -('taunt', 'Go and boil your bottom, you son of a silly person!!!', 0, 0, '', 'Va faire bouillir ton derrière, espèce de fils d’une personne stupide !!!', 'Geh kacken du Vollidiot', '去把你屁股煮熟吧,你个傻子的崽!!!', '', '¡Ve y hierve tu trasero, hijo de una persona tonta!', '', ''), -('taunt', 'What are you going to do , bleed on me? HAVE AT YOU!', 0, 0, '', 'Que comptes-tu faire, , me saigner dessus ? VIENS TE BATTRE !', 'Was willst du tun, , mich bluten lassen? Viel Spaß!', '你能拿我咋样,,往我身上淌血吗?来干一架吧!', '', '¿Qué vas a hacer , llorar?', '', ''), -('taunt', 'M-O-O-N! That spells aggro!', 0, 0, '', 'M-O-O-N! Ça, ça annonce des ennuis!', 'M-O-O-N! Das bedeutet Ärger!', 'M-O-O-N!这就意味着开打!', '', '¡LUNA! ¡Eso deletrea agresividad!', '', ''), -('taunt', 'You’re about as useful as a one-legged man in an ass kicking contest.', 0, 0, '', 'You know, tu es aussi utile qu’un homme à une jambe dans un concours de coups de pied aux fesses.', 'Du bist so nützlich wie ein einbeiniger Mann in einem Arschtritt-Wettbewerb.', '你这人跟独腿男参加踢屁股比赛一样没用。', '', 'Eres tan útil como un hombre con una sola pierna en un concurso de patadas en el culo.', '', ''), -('taunt', 'Hey ! Stop hitting on them, they’re not your type. They aren’t inflatable.', 0, 0, '', 'Hé ! Arrête d’essayer de les draguer, Elles ne sont pas de ton style. Elles ne sont pas gonflables.', 'Hey ! Hör auf, sie anzubaggern, die sind nicht dein Typ. Die sind nicht aufblasbar.', '嘿,!别再勾搭了,人家不是你那款,不是充气娃娃。', '', '¡Oye, ! Deja de coquetear con ellos, no son tu tipo. No son inflables.', '', ''), -('taunt', ' you’re so far outta your league, you’re playing a different sport.', 0, 0, '', ' tu es tellement dépassé, tu joues à un autre sport.', ', du bist so weit aus deiner Liga, du spielst eine ganz andere Sportart.', ',你完全不是一个量级的,简直在玩不同的运动。', '', ' estás tan lejos de tu liga, estás jugando un deporte diferente.', '', ''), -('taunt', 'You made a big mistake today , you got out of bed.', 0, 0, '', ', tu as commis une grosse erreur aujourd’hui en sortant du lit.', ', du hast heute einen großen Fehler gemacht, du bist aus dem Bett gestiegen.', '你今天犯了大错,,你不该起床的。', '', 'Cometiste un gran error hoy , te levantaste de la cama.', '', ''), -('taunt', 'I wanna try turning into a horse, but I need help. I’ll be the front, you be yourself.', 0, 0, '', 'Je veux essayer de me transformer en cheval, mais j’ai besoin d’aide. Tu seras à l’avant, et je serai juste moi-même.', 'Ich will versuchen, mich in ein Pferd zu verwandeln, aber ich brauche Hilfe. Ich bin die Vorderseite und du bleibst du selbst.', '我想试试变身成马,但需要帮忙。我来当前面,你就做你自己吧。', '', 'Quiero intentar convertirme en un caballo, pero necesito ayuda. Yo seré el frente, tú mismo.', '', ''), -('taunt', 'Can I borrow your face for a few days? My ass is going on holiday....', 0, 0, '', 'Je peux emprunter ton visage pendant quelques jours ? Mon derrière part en vacances...', 'Kann ich dein Gesicht für ein paar Tage leihen? Mein Hintern macht Urlaub...', '能借你的脸用几天吗?我的屁股要去度假……', '', '¿Me prestas tu cara por unos días? Mi culo se va de vacaciones...', '', ''), -('taunt', 'I’d like to give you a going away present... First you do your part.', 0, 0, '', 'Je voudrais te donner un cadeau d’au revoir... Tout d’abord, toi tu fais ta part.', 'Ich würde dir gerne ein Abschiedsgeschenk machen... aber Zuerst machst du deinen Teil.', '我想送你个临别礼物……先干好你的份内事吧。', '', 'Me gustaría darte un regalo de despedida... Primero haz tu parte.', '', ''), -('taunt', 'Before you came along we were hungry, Now we’re just fed up.', 0, 0, '', 'Avant ton arrivée, on avait faim. Maintenant, on en a juste assez.', 'Bevor du kamst, hatten wir Hunger. Jetzt ist uns schlecht.', '你来之前我们还饿着肚子,现在都快被你气饱了。', '', 'Antes de que llegaras teníamos hambre, ahora estamos hartos', '', ''), -('taunt', 'I like you. People say I have no taste, but I like you.', 0, 0, '', 'Je t’aime bien. Les gens disent que je n’ai pas de goût, mais je t’aime bien.', 'Ich mag dich. Die Leute sagen, ich hätte keinen Geschmack, aber ich mag dich.', '我挺喜欢你的。别人都说我没品,但我就是喜欢你。', '', 'Me gustas. La gente dice que no tengo gusto, pero me gustas.', '', ''), -('taunt', 'I think you have an inferiority complex, but that’s okay, it’s justified.', 0, 0, '', 'Je pense que tu as un complexe d’infériorité, mais il n’y a pas de problème, c’est justifié.', 'Ich glaube, du hast einen Minderwertigkeitskomplex, aber das ist okay, der ist berechtigt.', '我觉得你有自卑情结,不过没关系,这情结挺合理。', '', 'Creo que tienes complejo de inferioridad, pero está bien, está justificado', '', ''), -('taunt', 'Hence rotten thing! Or I shall shake thy bones out of thy garments.', 0, 0, '', 'Miserable vermine! Sinon, je vais te secouer jusqu’à ce que tes os sortent de tes vêtements.', 'Fort mit dir, du faules Ding! Oder ich werde deine Gebeine aus deinen Kleidern schütteln.', '滚吧,臭东西!不然我把你骨头从衣服里抖出来!', '', '¡Por lo tanto, cosa podrida! o te sacudiré los huesos de las vestiduras.', '', ''), -('taunt', 'I can’t believe I’m wasting my time with you!', 0, 0, '', 'Je n’arrive pas à croire que je perds mon temps avec toi!', 'Ich kann nicht glauben, dass ich meine Zeit mit dir verschwende!', '我真不敢相信我在你身上浪费时间!', '', '¡No puedo creer que estoy perdiendo el tiempo contigo!', '', ''), -('taunt', 'I love it when someone insults me, it means I don’t have to be nice anymore.', 0, 0, '', 'j’adore quand quelqu’un m’insulte, ça veut dire que je n’ai plus besoin d’être gentil.', 'Ich liebe es, wenn mich jemand beleidigt. Das bedeutet, dass ich nicht mehr nett sein muss.', '我喜欢被人骂,这意味着我再也不用装客气了。', '', 'Me encanta cuando alguien me insulta, significa que ya no tengo que ser amable', '', ''), -('taunt', 'Thou leathern-jerkin, crystal-button, knot-pated, agatering, puke-stocking, caddis-garter, smooth-tongue, Spanish pouch!', 0, 0, '', 'Whoo, ça va swinguer, toi en ta peau de cuir, tes boutons de cristal, ton crâne noueux, tes chausses de vomi, tes jarretières en galon, ta langue de miel, et ta bourse espagnole !', 'Du Lederwams, Kristallknopf, Knotenpflaster, Achatring, Kotzstrumpf, Köcherfliegen-Strumpfband, Glattzunge, spanischer Beutel!', '你个皮夹克、水晶扣、脑壳疙瘩、玛瑙戒、呕吐袜、粗布吊带、油嘴滑舌、西班牙钱袋!', '', '¡Tú, jubón de cuero, botón de cristal, anudado, ágata, medias de vómito, liga de caddis, lengua suave, bolsa española!', '', ''), -('taunt', 'Thou qualling bat-fowling malt-worm!', 0, 0, '', 'Espèce de vermine chauve-souris qui infeste l’orge maltée!', 'Du quälender Fledermausvogel-Malzwurm!', '你个令人作呕的蝙蝠猎手麦芽虫!', '', '¡Tú, gusano de malta cazador de murciélagos!', '', ''), -('taunt', 'Thou art truely an idol of idiot-worshippers!', 0, 0, '', 'Tu es vraiment l’idole des adorateurs d’idiots!', 'Du bist wahrlich das Idol der Idiotenanbeter!', '你真是白痴崇拜者的偶像!', '', '¡Eres verdaderamente un ídolo de los adoradores de idiotas!', '', ''), -('taunt', 'Thou misbegotten knotty-pated wagtail!', 0, 0, '', 'Espèce de galopin au cerveau embrouillé!', 'Du missratene, knotige Bachstelze!', '你个天生脑壳疙瘩的摇尾小鸟!', '', '¡Maldito lavandero nudoso!', '', ''), -('taunt', 'Thou whoreson mandrake, thou art fitter to be worn in my cap than to wait at my heels!', 0, 0, '', '', 'Du Hurensohn Alraune, es ist besser für dich, in meiner Mütze getragen zu werden, als hinter mir zu lauern!', '你个曼陀罗贱种,戴在我帽子上都比跟在我脚后跟合适!', '', '¡Hijo de puta mandrágora, eres mejor para que te lleve en mi gorra que para que me pise los talones!', '', ''), -('taunt', 'You! You scullion! You rampallian! You fustilarian! I’ll tickle your catastrophe!', 0, 0, '', 'Toi! Toi l’écuyer! Toi le gredin! Toi le vaurien! Je vais titiller ta catastrophe!', 'Du! Du Küchenjunge! Du Randalierer! Du Fäustling! Ich werde eine Katastrophe auslösen!', '你!下贱厨子!无赖!废物!我来挠挠你的倒霉事!', '', '¡Tú! ¡Scullion! ¡Ramaliano! ¡Fustilarista! ¡Te haré cosquillas en tu catástrofe!', '', ''), -('taunt', 'Oh ! Thou infectious ill-nurtured flax-wench!', 0, 0, '', 'Oh ! Toi, vilaine lavandière malveillante et infecte!', 'Oh ! Du ansteckendes, schlecht gepflegtes Flachsweib!', '哦,!你个恶毒又传染的亚麻贱女!', '', '¡Oh, ! ¡Tú, contagiosa moza del lino mal educada!', '', ''), -('taunt', 'We leak in your chimney, !', 0, 0, '', 'On fuit dans ta cheminée, !', 'Wir haben ein Leck in deinem Schornstein, !', '我们在你烟囱里漏水了,!', '', '¡Tenemos una fuga en tu chimenea, !', '', ''), -('taunt', 'Oh thou bootless fen-sucked canker-blossom!', 0, 0, '', 'Oh mon impuissant aspiré par les marais, fleur de chancre!', 'Oh du nutzlose, vom Moor ausgesaugte Krebsblüte!', '哦,你个无用的沼泽吸血疮花!', '', '¡Oh tú, flor de cancro chupada por el pantano sin botas!', '', ''), -('taunt', 'Were I like thee I’d throw away myself!', 0, 0, '', 'Si j’étais comme toi, je me jetterais en l’air !', 'Wäre ich wie du, würde ich mich selbst wegwerfen!', '如果我像你一样,我早就把自己扔了!', '', '¡Si yo fuera como tú, me tiraría a la basura!', '', ''), -('taunt', 'O teach me , how I should forget to think!', 0, 0, '', 'Oh, enseigne-moi, , comment oublier de trop réfléchir!', 'O lehre mich, wie ich vergessen kann zu Denken!', '哦,教教我吧,,怎样才能忘记思考!', '', '¡Oh, enséñame , cómo debo olvidarme de pensar!', '', ''), -('taunt', 'Truly thou art damned, like an ill-roasted egg, all on one side!', 0, 0, '', 'Franchement, tu es foutu, comme un œuf mal cuit, tout d’un côté!', 'Wahrlich, du bist verdammt, wie ein schlecht gebratenes Ei, alles auf einer Seite!', '你真该死,就像个烤坏的蛋,全都焦在一边!', '', '¡Verdaderamente estás condenado, como un huevo mal asado, todo de un lado!', '', ''), -('taunt', 'You starvelling, you eel-skin, you dried neat’s-tongue, you bull’s-pizzle, you stock-fish- O for breath to utter what is like thee!! -you tailor’s-yard, you sheath, you bow-case, you vile standing tuck!', 0, 0, '', '', 'Du Verhungernder, du Aalhaut, du vertrocknete Rinderzunge, du Ochsenziemer, du Stockfisch – oh, hätte ich Atem, um auszusprechen, was dir gleicht!! – du Schneiderlein, du Schwertscheide, du Bogenfutteral, du schäbiges Stehaufmännchen!', '你个饿殍、鳗鱼皮、干瘪牛舌、公牛鞭、腌鱼——哦,我要是有气力说出跟你有多像就好了!——你个裁缝尺、剑鞘、弓套、卑劣的站立家伙!', '', 'Estás muerto de hambre, piel de anguila, te secaste la lengua limpiamente, toro-pizzle, pescado-O para respirar pronunciar lo que es como tú !! ¡Tú, sastrería, vaina, estuche de arco, vil flaco de pie!', '', ''), -('taunt', 'Fie! Drop thee into the rotten mouth of Death!', 0, 0, '', 'Que le diable t’emporte dans la gueule pourrie de la Mort!', 'Pfui! Wirf dich in den faulen Rachen des Todes!', '呸!掉进死亡的腐烂大嘴去吧!', '', '¡Fie! ¡Déjate caer en la podrida boca de la Muerte!', '', ''), -('taunt', ', you are a fishmonger!', 0, 0, '', ', tu es poissonnier!', ', du bist ein Fischhändler!', ',你就是个卖鱼的!', '', ', ¡eres un pescadero!', '', ''), -('taunt', 'I shall live to knock thy brains out!', 0, 0, '', 'Je vivrai pour te cogner le cerveau!', 'Ich lebe, um dir den Schädel einzuschlagen!', '我会活着把你的脑浆打出来!', '', '¡Viviré para romperte los sesos!', '', ''), -('taunt', 'Most shallow are you, !! Thou art worms-meat in respect of a good piece of flesh, indeed!!', 0, 0, '', 'Comme tu es superficiel !! Tu n’es que de la chair à vers comparé à un bon morceau de viande, en effet!!', 'Du bist wirklich sehr oberflächlich, !! Im Vergleich zu einem guten Stück Fleisch bist du wahrlich Wurmversäuchtes Fleisch!!', '你真肤浅,!!跟一块好肉比,你就是个虫子啃的货,的确如此!!', '', '¡¡Eres el más superficial, !! ¡Eres carne de gusano con respecto a un buen trozo de carne, de hecho!', '', ''), -('taunt', 'Vile wretch! O , thou odiferous hell-hated pignut!', 0, 0, '', 'Misérable vermine! Ô , toi l’infâme pignouf puant des enfers!', 'Abscheulicher Schurke! O , du stinkender, höllisch verhasster Schweinshund!', '卑劣的混蛋!哦,,你个地狱厌弃的臭猪头!', '', '¡Miserable vil! ¡Oh, , fétido cerdo odiado por el infierno!', '', ''), -('taunt', '! Thy kiss is as comfortless as frozen water to a starved snake!', 0, 0, '', '! Ton baiser est aussi réconfortant que de l’eau gelée pour un serpent affamé!', '! Dein Kuss ist für eine ausgehungerte Schlange so trostlos wie gefrorenes Wasser!', '!你的吻就像给一条饿蛇喂冰水一样毫无安慰!', '', '! ¡Tu beso es tan desconsolador como el agua helada para una serpiente hambrienta!', '', ''), -('taunt', 'I scorn you, scurvy companion. What, you poor, base, rascally, cheating, lack-linen mate! Away, you moldy rogue, away!', 0, 0, '', 'Je vous méprise, misérable compagnon. Quoi, toi, pauvre, vil, scélérat, tricheur, compagnon usé de toile! Déguerpisse, espèce de coquin moisissure, déguerpisse!', 'Ich verachte dich, schäbiger Geselle. Was, du armer, gemeiner, schurkischer, betrügerischer, leinenloser Kumpel? Weg, du schimmeliger Schurke, weg!', '我鄙视你,卑劣的家伙。什么,你个穷酸、下贱、无赖、骗子、破布般的同伴!滚吧,你个发霉的混账,滚!', '', 'Te desprecio, compañero escorbuto. ¡Qué, pobre, vil, pícaro, tramposo, compañero falto de lino! ¡Fuera, mohoso granuja, fuera!', '', ''), -('taunt', 'Out of my sight! Thou dost infect my eyes !', 0, 0, '', 'Hors de ma vue! Tu infectes mes yeux !', 'Aus meinen Augen! Du infizierst meine Augen !', '从我眼前消失!你污染了我的眼睛,!', '', '¡Fuera de mi vista! ¡Me infectas los ojos !', '', ''), -('taunt', 'PLAY TIME!!!!', 0, 0, '', 'L’HEURE DE LA MORT !', 'SPIELZEIT!!!', '开战时间!!!!', '', '¡¡¡HORA DE JUGAR!!!!', '', ''), -('taunt', 'None shall pass!', 0, 0, '', 'Vous ne passerez pas !', 'Keiner kommt durch!', '谁也别想通过!', '', '¡Ninguno pasará!', '', ''), -('taunt', 'We’re under attack! A vast, ye swabs! Repel the invaders!', 0, 0, '', 'Nous sommes attaqués! Visez bien, bande de moussaillons! Repoussez les envahisseurs!', 'Wir werden angegriffen! Ein gewaltiger Angriff, ihr Wichser! Schlagt die Eindringlinge zurück!', '我们遭到攻击!一大波敌人,蠢货们!击退入侵者!', '', '¡Estamos bajo ataque! ¡Un vasto, hisopos! ¡Repeler a los invasores!', '', ''), -('taunt', 'None may challenge the Brotherhood!', 0, 0, '', 'Personne ne peut défier la Confrérie!', 'Niemand darf die Bruderschaft herausfordern!', '没人能挑战兄弟会!', '', '¡Nadie puede desafiar a la Hermandad!', '', ''), -('taunt', 'Foolsss...Kill the one in the dress!', 0, 0, '', 'Imbéciles... Tuez celui en robe!', 'Narren ... bringt die in den Kleidern um!', '蠢货们……干掉那个穿长袍的!', '', 'Tontosss... ¡Matad al del vestido!', '', ''), -('taunt', 'I’ll feed your soul to Hakkar himself! ', 0, 0, '', 'Je vais nourrir ton âme à Hakkar lui-même!', 'Ich werde Hakkar persönlich mit deiner Seele füttern!', '我会把你的灵魂喂给哈卡本人!', '', '¡Le daré de comer con tu alma al mismísimo Hakkar! ', '', ''), -('taunt', 'Pride heralds the end of your world! Come, mortals! Face the wrath of the !', 0, 0, '', ' m’apporte une grande fierté! Venez, mortels ! Affrontez la colère de !', 'Stolz läutet das Ende eurer Welt ein! Kommt, Sterbliche! Stellt euch dem Zorn der !', '骄傲预示着你们世界的终结!来吧,凡人!面对的怒火!', '', '¡El orgullo anuncia el fin de tu mundo! ¡Venid, mortales! ¡Enfréntate a la ira de la !', '', ''), -('taunt', 'All my plans have led to this!', 0, 0, '', 'Tous mes projets ont mené à cela!', 'Alle meine Pläne haben hierher geführt!', '我所有的计划都指向这一刻!', '', '¡Todos mis planes me han llevado a esto!', '', ''), -('taunt', 'Ahh! More lambs to the slaughter!', 0, 0, '', 'Oh non! Encore des agneaux pour l’abattoir!', 'Ahh! Noch mehr Lämmer zur Schlachtbank!', '啊哈!又有羔羊来送死了!', '', '¡Ahh! ¡Más corderos al matadero!', '', ''), -('taunt', 'Another day, another glorious battle!', 0, 0, '', 'Une autre journée, une autre bataille glorieuse !', 'Ein neuer Tag, eine neue glorreiche Schlacht!', '又是新的一天,又是一场光荣的战斗!', '', '¡Otro día, otra batalla gloriosa!', '', ''), -('taunt', 'So, business... or pleasure?', 0, 0, '', 'Alors, affaires... ou plaisir ?', 'Also, geschäftlich … oder privat?', '那么,是公事……还是私事?', '', 'Entonces, ¿negocios... o placer?', '', ''), -('taunt', 'You are not prepared!', 0, 0, '', 'Tu n’es pas prêt!', 'Du bist nicht vorbereitet!', '你们还没准备好!', '', '¡No estás preparado!', '', ''), -('taunt', 'The ’s final conquest has begun! Once again the subjugation of this world is within our grasp. Let none survive! ', 0, 0, '', 'La dernière conquête de a commencé! Encore une fois, la soumission de ce monde est à portée de main. Que personne ne survive!', 'Die letzte Eroberung der hat begonnen! Erneut liegt die Unterwerfung dieser Welt in unserer Hand. Lasst niemanden überleben!', '的最终征服开始了!这个世界的臣服再次近在咫尺,一个也别想活!', '', '¡La conquista final de ha comenzado! Una vez más, la subyugación de este mundo está a nuestro alcance. ¡Que nadie sobreviva! ', '', ''), -('taunt', 'Your death will be a painful one. ', 0, 0, '', 'Ta mort sera une douloureuse.', 'Dein Tod wird schmerzhaft sein.', '你的死会非常痛苦。', '', 'Tu muerte será dolorosa. ', '', ''), -('taunt', 'Cry for mercy! Your meaningless lives will soon be forfeit. ', 0, 0, '', 'Pleurez miséricorde! Vos vies insignifiantes seront bientôt perdues.', 'Fleht um Gnade! Eure bedeutungslosen Leben werden bald enden.', '求饶吧!你们无意义的生命马上就要终结了。', '', '¡Llora por piedad! Sus vidas sin sentido pronto se perderán. ', '', ''), -('taunt', 'Abandon all hope! The has returned to finish what was begun so many years ago. This time there will be no escape! ', 0, 0, '', 'Abandonne tout espoir! La est de retour pour finir ce qui a été commencé il y a tant d’années. Cette fois, il n’y aura pas d’échappatoire!', 'Gebt alle Hoffnung auf! Die ist zurückgekehrt, um zu beenden, was vor so vielen Jahren begonnen wurde. Dieses Mal gibt es kein Entkommen!', '放弃所有希望吧!回来了,要完成多年前未竟的事业。这次,你们无处可逃!', '', '¡Abandona toda esperanza! La ha regresado para terminar lo que comenzó hace tantos años. ¡Esta vez no habrá escapatoria! ', '', ''), -('taunt', 'Alert! You are marked for Extermination! ', 0, 0, '', 'Alerte ! Vous êtes désigné pour extermination !', 'Alarm! Du bist zur Auslöschung markiert!', '警报!你们被标记为清除目标!', '', '¡Alerta! ¡Estás marcado para el exterminio! ', '', ''), -('taunt', 'The is for guests only...', 0, 0, '', 'Le est réservé aux invités uniquement...', 'Die ist nur für Gäste...', '仅对访客开放……', '', 'La es solo para invitados...', '', ''), -('taunt', 'Ha ha ha! You are hopelessly outmatched!', 0, 0, '', 'Ha ha ha! Tu es totalement dépassé!', 'Ha ha ha! Du bist hoffnungslos unterlegen!', '哈哈哈!你们完全不是对手!', '', '¡Ja, ja, ja! ¡Estás irremediablemente superado!', '', ''), -('taunt', 'I will crush your delusions of grandeur! ', 0, 0, '', 'Je vais briser tes illusions de grandeur!', 'Ich werde deinen Größenwahn zerschmettern!', '我会粉碎你自大的幻想!', '', '¡Aplastaré tus delirios de grandeza! ', '', ''), -('taunt', 'Forgive me, for you are about to lose the game.', 0, 0, '', 'Pardonnez-moi, mais vous êtes sur le point de perdre la partie.', 'Verzeih mir, aber du wirst dieses Spiel verlieren.', '原谅我,因为你们马上就要输了。', '', 'Perdóname, porque estás a punto de perder el juego.', '', ''), -('taunt', 'Struggling only makes it worse.', 0, 0, '', 'Se battre ne fait qu’empirer les choses.', 'Widerstand macht es nur schlimmer.', '挣扎只会让事情变得更糟。', '', 'La lucha solo lo empeora.', '', ''), -('taunt', 'Vermin! Leeches! Take my blood and choke on it!', 0, 0, '', 'Créatures immondes ! Sangsues ! Prenez mon sang et étouffez-y !', 'Ungeziefer! Blutegel! Nehmt mein Blut und erstickt daran!', '害虫!吸血鬼!喝我的血,噎死你们!', '', '¡Alimañas! ¡Sanguijuelas! ¡Toma mi sangre y atragantate con ella!', '', ''), -('taunt', 'Not again... NOT AGAIN!', 0, 0, '', 'Pas encore... PAS ENCORE !', 'Nicht schon wieder... NICHT SCHON WIEDER!', '别再来了……别再来了!', '', 'No otra vez... ¡NO OTRA VEZ!', '', ''), -('taunt', 'My blood will be the end of you!', 0, 0, '', 'Mon sang sera ta fin!', 'Mein Blut wird dein Ende sein!', '我的鲜血将是你的终结!', '', '¡Mi sangre será tu fin!', '', ''), -('taunt', 'Good, now you fight me!', 0, 0, '', 'Bien, maintenant combat moi !', 'Gut, jetzt kämpfst du gegen mich!', '好极了,现在来跟我打吧!', '', 'Bien, ¡ahora pelea conmigo!', '', ''), -('taunt', 'Get da move on, guards! It be killin’ time!', 0, 0, '', 'Allez, les gardes, on se presse! C’est l’heure de tuer!', 'Los, Wachen! Zeit zu töten!', '快点动起来,卫兵们!是时候大开杀戒了!', '', '¡Muévanse, guardias! ¡Será para matar el tiempo!', '', ''), -('taunt', 'Don’t be delayin’ your fate. Come to me now. I make your sacrifice quick.', 0, 0, '', 'Ne retarde pas ton destin. Viens à moi maintenant. Je rends ton sacrifice rapide.', 'Verzögere dein Schicksal nicht. Komm jetzt zu mir. Ich mache dein Ende schnell.', '别拖延你的命运。现在过来,我会让你的牺牲快一点。', '', 'No demores tu destino. Ven a mí ahora. Hago que tu sacrificio sea rápido.', '', ''), -('taunt', 'You be dead soon enough!', 0, 0, '', 'Tu seras bientôt mort !', 'Du wirst bald tot sein!', '你很快就会死翘翘!', '', '¡Estarás muerto lo suficientemente pronto!', '', ''), -('taunt', 'Mua-ha-ha!', 0, 0, '', 'Mua-ha-ha!', 'Mua-ha-ha!', '哇哈哈哈!', '', '¡Mua-ja-ja!', '', ''), -('taunt', 'I be da predator! You da prey...', 0, 0, '', 'Je suis le prédateur! Tu es la proie...', 'Ich bin der Jäger! Du bist die Beute...', '我是猎手!你是猎物……', '', '¡Soy un depredador! Eres presa...', '', ''), -('taunt', 'You gonna leave in pieces!', 0, 0, '', 'Tu vas finir en morceaux !', 'Du wirst diesen Ort in Stücken verlassen!', '你会碎成渣离开这里!', '', '¡Te vas a ir en pedazos!', '', ''), -('taunt', 'Death comes. Will your conscience be clear? ', 0, 0, '', 'La mort arrive. Est-ce que ta conscience sera claire ?', 'Der Tod kommt. hast du ein reines Gewissen?', '死亡降临了。你的良心能安宁吗?', '', 'La muerte llega. ¿Tu conciencia estará limpia? ', '', ''), -('taunt', 'Your behavior will not be tolerated.', 0, 0, '', 'Votre comportement ne sera pas toléré.', 'Dein Verhalten wird nicht toleriert.', '你的行为不可容忍。', '', 'Tu comportamiento no será tolerado', '', ''), -('taunt', 'The Menagerie is for guests only.', 0, 0, '', 'La Ménagerie est réservée aux invités seulement.', 'Die Menagerie ist nur für Gäste.', '展览馆仅对访客开放。', '', 'The Menagerie es solo para invitados', '', ''), -('taunt', 'Hmm, unannounced visitors, Preparations must be made... ', 0, 0, '', 'Des visiteurs non annoncés, il me faut faire des préparatifs...', 'Hmm, unangekündigte Besucher, Vorbereitungen müssen getroffen werden...', '嗯,未经通报的访客,必须做好准备……', '', 'Hmm, visitantes inesperados, se deben hacer preparativos... ', '', ''), -('taunt', 'Hostile entities detected. Threat assessment protocol active. Primary target engaged. Time minus thirty seconds to re-evaluation.', 0, 0, '', 'Entités hostiles détectées. Protocole d’évaluation des menaces activé. Cible principale engagée. Temps moins trente secondes avant réévaluation.', 'Feindliche Einheiten entdeckt. Bedrohungsbewertungsprotokoll aktiv. Primärziel angegriffen. Zeit bis zur Neubewertung: minus dreißig Sekunden.', '侦测到敌对实体。威胁评估协议已激活。主要目标锁定。距离重新评估还有三十秒。', '', 'Entidades hostiles detectadas. Protocolo de evaluación de amenazas activo. Objetivo principal comprometido. Tiempo menos treinta segundos para la reevaluación.', '', ''), -('taunt', 'New toys? For me? I promise I won’t break them this time!', 0, 0, '', 'De nouveaux jouets ? Pour moi ? Je te promets que cette fois, je ne les casserai pas !', 'Neue Spielzeuge? Für mich? Ich verspreche, ich werde sie dieses Mal nicht kaputt machen!', '新玩具?给我玩的?我保证这次不会弄坏它们!', '', '¿Juguetes nuevos? ¿Para mi? ¡Te prometo que no los romperé esta vez!', '', ''), -('taunt', 'I’m ready to play!', 0, 0, '', 'Je suis prêt à jouer !', 'Ich bin bereit zu spielen!', '我准备好玩一玩了!', '', '¡Estoy listo para jugar!', '', ''), -('taunt', 'Shhh... it will all be over soon.', 0, 0, '', 'Chut... tout sera bientôt fini.', 'Shhh... es wird bald vorbei sein.', '嘘……这一切很快就会结束。', '', 'Shhh... todo terminará pronto.', '', ''), -('taunt', 'Aaaaaughibbrgubugbugrguburgle!', 0, 0, '', 'Aaaaaughibbrgubugbugrguburgle!', 'Aaaaaughibbrgubugbugrguburgle!', '啊啊啊啊咕噜咕噜咕噜咕!', '', '¡Aaaaaughibbrgubugbugrguburgle!', '', ''), -('taunt', 'RwlRwlRwlRwl!', 0, 0, '', 'RwlRwlRwlRwl!', 'RwlRwlRwlRwl!', '嗷嗷嗷嗷!', '', 'RwlRwlRwlRwl!', '', ''), -('taunt', 'You too, shall serve!', 0, 0, '', 'Toi aussi, tu serviras !', 'Auch du wirst dienen!', '你也一样,必须效忠!', '', '¡Tú también, debes servir!', '', ''), -('taunt', 'Tell me... tell me everything! Naughty secrets! I’ll rip the secrets from your flesh!', 0, 0, '', 'Dis-moi... dis-moi tout ! Les petits secrets coquins ! Je vais arracher les secrets de ta chair !', 'Sag mir... sag mir alles! Schmutzige Geheimnisse! Ich werde dir deine Geheimnisse aus deinem Fleisch reißen!', '告诉我……把一切都告诉我!那些小秘密!我会从你的血肉中撕出真相!', '', 'Cuéntame... ¡cuéntame todo! ¡Secretos traviesos! ¡Arrancaré los secretos de tu carne!', '', ''), -('taunt', 'Prepare yourselves, the bells have tolled! Shelter your weak, your young and your old! Each of you shall pay the final sum! Cry for mercy, the reckoning has come!', 0, 0, '', 'Préparez-vous, les cloches ont sonné ! Protégez vos faibles, vos jeunes et vos anciens ! Chacun d’entre vous paiera le prix final ! Pleurez miséricorde, l’heure du jugement est arrivée!', 'Bereitet euch vor, die Glocken haben geläutet! Schützt eure Schwachen, eure Jungen und eure Alten! Jeder von euch wird den endgültigen Preis zahlen! Fleht um Gnade, die Abrechnung ist gekommen!', '准备好吧,丧钟已敲响!保护你们的弱者、孩子和老人!你们每个人都将付出最终代价!哭求怜悯吧,清算时刻到了!', '', '¡Prepárense, las campanas han doblado! ¡Abriga a tus débiles, a tus jóvenes ya tus viejos! ¡Cada uno de ustedes pagará la suma final! Clama por misericordia, el ajuste de cuentas ha llegado!', '', ''), -('taunt', 'Where in Bonzo’s brass buttons am I?', 0, 0, '', 'Où diable suis-je?', 'Wo zur Hölle bin ich?', '我到底在邦佐的黄铜扣子哪里?', '', '¿Dónde estoy en los botones de latón de Bonzo?', '', ''), -('taunt', 'I can bear it no longer! Goblin King! Goblin King! Wherever you may be! Take this far away from me!', 0, 0, '', 'Je n’en peux plus! Roi des Gobelins! Roi des Gobelins! Où que tu sois! Emmène cette %cible% loin de moi!', 'Ich ertrage es nicht mehr! Goblinkönig! Goblinkönig! Wo immer du bist! Nimm diesen weit weg von mir!', '我再也受不了了!地精之王!地精之王!不管你在哪!把这个带离我远点!', '', '¡No puedo soportarlo más! ¡Rey de los duendes! ¡Rey de los duendes! ¡WHERE quiera que estés! ¡Lleva a este lejos de mí!', '', ''), -('taunt', 'You have thirteen hours in which to solve the labyrinth, before your baby brother becomes one of us... forever.', 0, 0, '', 'Tu as treize heures pour résoudre le labyrinthe, avant que ton petit frère ne devienne l’un d’entre nous... pour toujours.', 'Du hast dreizehn Stunden, um das Labyrinth zu lösen, bevor dein kleiner Bruder einer von uns wird... für immer.', '你有十三小时来解开迷宫,否则你的小弟弟将永远成为我们的一员……', '', 'Tienes trece horas para resolver el laberinto, antes de que tu hermanito se convierta en uno de nosotros... para siempre', '', ''), -('taunt', 'So, the is a piece of cake, is it? Well, let’s see how you deal with this little slice... ', 0, 0, '', 'Alors, la est du gâteau, vous dites? Hmm, voyons comment vous vous en sortez avec cette petite portion...', 'Also, die ist ein Kinderspiel, ja? Nun, sehen wir mal, wie du mit diesem kleinen Stück klarkommst...', '所以,对你来说是小菜一碟,对吧?好吧,看看你怎么应付这小块“蛋糕”……', '', 'Entonces, la es pan comido, ¿verdad? Bueno, veamos cómo lidias con esta pequeña porción... ', '', ''), -('taunt', 'Back off, I’ll take you on, headstrong to take on anyone, I know that you are wrong, and this is not where you belong', 0, 0, '', 'Alors, recule, je vais m’occuper de toi, déterminé à affronter n’importe qui, je sais que tu as tort, et ce n’est pas là que tu appartiens', 'Zurück, ich nehme dich auf, entschlossen, es mit jedem aufzunehmen, ich weiß, dass du falsch liegst, und das ist nicht der Ort, an den du gehörst', '退后,我来对付你,胆大包天敢挑战任何人,我知道你错了,这里不是你该来的地方', '', 'Atrás, te enfrentaré, testarudo para enfrentar a cualquiera, sé que estás equivocado, y este no es el lugar al que perteneces', '', ''), -('taunt', 'Show me whatcha got!', 0, 0, '', 'Montre-moi ce que t’as dans le ventre!', 'Zeig mir, was du drauf hast!', '让我瞧瞧你有啥能耐!', '', '¡Muéstrame lo que tienes!', '', ''), -('taunt', 'To the death!', 0, 0, '', '% Sorry, I can’t translate that.', 'Bis zum Tod!', '决一死战!', '', '¡Hasta la muerte!', '', ''), -('taunt', 'Twin blade action, for a clean close shave every time.', 0, 0, '', 'Lame jumelle, pour un rasage de près impeccable à chaque fois.', 'Doppelklingenaktion, für eine saubere Rasur jedes Mal.', '双刃出击,每次都能干净利落!', '', 'Acción de cuchilla doble, para un afeitado limpio y apurado en todo momento.', '', ''), -('taunt', 'Bring it on!', 0, 0, '', 'Allez, viens ici !', 'Los geht’s!', '来吧!', '', '¡Adelante!', '', ''), -('taunt', 'You’re goin’ down!', 0, 0, '', 'Tu vas tomber !', 'Du gehst unter!', '你死定了!', '', '¡Vas a caer!', '', ''), -('taunt', 'Stabby stab stab!', 0, 0, '', 'Stabby stab stab!', 'Stech, stech, stech!', '刺刺刺!', '', '¡Puñalada puñalada puñalada!', '', ''), -('taunt', 'Let’s get this over quick, time is mana.', 0, 0, '', 'Finissons-en vite , le temps c’est du mana.', 'Lass uns das schnell beenden, Zeit ist Mana.', '咱们速战速决,时间就是魔法!', '', 'Terminemos con esto rápido, el tiempo es maná.', '', ''), -('taunt', 'I do not think you realise the gravity of your situation.', 0, 0, '', 'Tu ne réalise pas encore la gravité de ta situation.', 'Ich glaube nicht, dass du die Schwere deiner Lage begreifst.', '我想你还没意识到你处境的严重性。', '', 'No creo que te des cuenta de la gravedad de tu situación.', '', ''), -('taunt', 'I will bring honor to my family and my kingdom!', 0, 0, '', 'Je vais apporter honneur à ma famille et à mon royaume!', 'Ich werde meiner Familie und meinem Königreich Ehre bringen!', '我会为我的家族和王国带来荣耀!', '', '¡Llevaré honor a mi familia y mi reino!', '', ''), -('taunt', 'Light, give me strength!', 0, 0, '', 'Lumière, donne-moi de la force!', 'Licht, gib mir Kraft!', '圣光,赐予我力量!', '', '¡Luz, dame fuerza!', '', ''), -('taunt', 'My church is the field of battle - time to worship...', 0, 0, '', 'Mon terrain de bataille, c’est mon église - il est temps de rendre hommage...', 'Das Schlachtfeld ist meine Kirche - Zeit für die Anbetung...', '战场就是我的教堂——是时候膜拜了……', '', 'Mi iglesia es el campo de batalla - hora de adorar...', '', ''), -('taunt', 'I hold you in contempt...', 0, 0, '', 'Je te méprise...', 'Ich verachte dich...', '我鄙视你……', '', 'Te tengo en desacato...', '', ''), -('taunt', 'Face the hammer of justice!', 0, 0, '', 'Affronte le marteau de la justice!', 'Stell dich dem Hammer der Gerechtigkeit!', '面对正义之锤吧!', '', '¡Enfréntate al martillo de la justicia!', '', ''), -('taunt', 'Prove your worth in the test of arms under the Light!', 0, 0, '', 'Prouve ta valeur dans le test des armes sous la Lumière!', 'Beweise deinen Wert in der Prüfung der Waffen unter dem Licht!', '在圣光的见证下证明你的武艺价值!', '', '¡Demuestra tu valía en la prueba de las armas bajo la Luz!', '', ''), -('taunt', 'All must fall before the might and right of my cause, you shall be next!', 0, 0, '', 'Tous doivent tomber devant la puissance et la justesse de ma cause, tu seras le prochain!', 'Alle müssen vor der Macht und dem Recht meiner Sache fallen, du bist der Nächste!', '所有人都将在我的正义与力量前倒下,你就是下一个!', '', 'Todo debe caer ante el poder y el derecho de mi causa, ¡tú serás el próximo!', '', ''), -('taunt', 'Prepare to die!', 0, 0, '', 'Prépare-toi à mourir!', 'Mach dich bereit zu sterben!', '准备受死吧!', '', '¡Prepárate para morir!', '', ''), -('taunt', 'The beast with me is nothing compared to the beast within...', 0, 0, '', 'La bête qui est avec moi n’est rien comparée à la bête qui est en moi...', 'Die Bestie an meiner Seite ist nichts im Vergleich zur Bestie in mir...', '我身边的野兽与我内心的猛兽相比简直不值一提……', '', 'La bestia conmigo no es nada comparada con la bestia interior...', '', ''), -('taunt', 'Witness the firepower of this fully armed huntsman!', 0, 0, '', 'Découvrez la puissance de feu de ce chasseur entièrement armé!', 'Erlebe die Feuerkraft dieses voll bewaffneten Jägers!', '见识一下全副武装猎人的火力吧!', '', '¡Sea testigo de la potencia de fuego de este cazador completamente armado!', '', ''), - --- combat events -('critical health', 'Heal me! Quick!', 0, 0, '', 'Je vais te soigner tout de suite !', 'Heil mich! Schnell!', '快治疗我!快点!', '', '¡Cúrame! ¡Rápido!', '', ''), -('critical health', 'Almost dead! Heal me!', 0, 0, '', 'Presque mort ! Soignez-moi !', 'Fast tot! Heil mich!', '快不行了!治疗我!', '', '¡Estoy casi muerto! ¡Cúrame!', '', ''), -('critical health', 'Help! Heal me!', 0, 0, '', 'S’il te plaît, aide-moi! Soigne-moi!', 'Hilfe! Heil mich!', '救命!治疗我!', '', '¡Ayuda! ¡Cúrame!', '', ''), -('critical health', 'Somebody! Heal me!', 0, 0, '', 'Quelqu’un ! Soigne-moi !', 'Irgendjemand! Heil mich!', '谁来!治疗我!', '', '¡Alguien! ¡Cúrame!', '', ''), -('critical health', 'Heal! Heal! Heal!', 0, 0, '', 'Guérir! Guérir! Guérir!', 'Heilung! Heilung! Heilung!', '治疗!治疗!治疗!', '', '¡Curame! ¡Saname! ¡Socorro!', '', ''), -('critical health', 'I am dying! Heal! Aaaaarhg!', 0, 0, '', 'Je meurs! Au secours! Aaaaarhg!', 'Ich sterbe! Heilung! Aaaaarhg!', '我要死了!治疗!啊啊啊啊!', '', '¡Me estoy muriendo! ¡Saname! ¡Aaaaarhg!', '', ''), -('critical health', 'Heal me!', 0, 0, '', 'Soigne moi !', 'Heil mich!', '快治疗我!', '', '¡Cúrame!', '', ''), -('critical health', 'I will die. I will die. I will die. Heal!', 0, 0, '', 'Je vais mourir. Je vais mourir. Je vais mourir. Soigne-moi !', 'Ich werde sterben. Ich werde sterben. Ich werde sterben. Heilung!', '我要死了!我要死了!快治疗!', '', 'Voy a morir. Voy a morir. Voy a morir. ¡Saname!', '', ''), -('critical health', 'Healers, where are you? I am dying!', 0, 0, '', 'Healers, où êtes-vous? Je suis en train de mourir!', 'Heiler, wo seid ihr? Ich sterbe!', '治疗呢?你们在哪儿?我快挂了!', '', 'Sanadores, ¿dónde estais? ¡Me estoy muriendo!', '', ''), -('critical health', 'Oh the pain. Heal me quick!', 0, 0, '', 'Oh la douleur. Guérissez-moi vite!', 'Oh, der Schmerz. Heil mich schnell!', '哎哟,好痛!快给我治疗!', '', 'Ouch, qué dolor. ¡Cúrame rápido!', '', ''), - -('low health', 'Need heal', 0, 0, '', 'Besoin de soins', 'Brauche Heilung', '需要治疗!', '', 'Necesito una cura', '', ''), -('low health', 'Low health', 0, 0, '', 'Faible santé', 'Wenig Leben', '血量低了!', '', 'Salud baja', '', ''), -('low health', 'Drop a heal. Please.', 0, 0, '', 'HEAL. S’il te plaît.', 'Heilung bitte.', '来个治疗,求你了!', '', 'Una sanación por favor.', '', ''), -('low health', 'Could somebody drop a heal on me?', 0, 0, '', 'Est-ce que quelqu’un pourrait me soigner?', 'Kann mich jemand heilen?', '谁能给我加点血啊?', '', '¿Alguien podría curarme?', '', ''), -('low health', 'Hey! Better heal me now than rez later', 0, 0, '', 'Salut! Mieux vaut me soigner maintenant que me ressusciter plus tard.', 'Hey! Heilt mich lieber jetzt als mich später zu wiederbeleben.', '嘿!现在治好我总比待会儿复活强吧!', '', '¡Oye! Mejor curarme ahora que resucitarme después', '', ''), -('low health', 'I am sorry. Need another heal', 0, 0, '', 'Je suis désolé. J’ai besoin d’une autre guérison.', 'Tut mir leid. Brauche nochmal Heilung.', '抱歉,又需要治疗了!', '', 'Lo siento. Necesito otra curación', '', ''), -('low health', 'Damn mobs. Heal me please', 0, 0, '', 'Maudits mobs. Guéris moi s’il te plait', 'Verdammte Mobs. Heilt mich bitte.', '该死的怪物,拜托治疗一下!', '', '¡Malditos bichos! ¡Una curita por favor!', '', ''), -('low health', 'One more hit and I am done for. Heal please', 0, 0, '', 'Un coup de plus et c’est fini. Guéris moi s’il te plait', 'Ein Treffer noch und ich bin erledigt. Heilung bitte.', '再挨一下我就完了,求治疗!', '', 'Un golpe más y estoy acabado. Cura por favor', '', ''), -('low health', 'Are there any healers?', 0, 0, '', 'Y a-t-il des guérisseurs?', 'Gibt es hier Heiler?', '有治疗吗?', '', '¿Hay sanadores?', '', ''), -('low health', 'Why do they always punch me in the face? Need heal', 0, 0, '', 'Pourquoi est-ce qu’ils me frappent toujours au visage? J’ai besoin de guérison', 'Warum schlagen sie mir immer ins Gesicht? Brauche Heilung.', '为什么总是打我脸?需要治疗!', '', '¿Por qué siempre me golpean en la cara? Necesito sanacion', '', ''), -('low health', 'Can anybody heal me a bit?', 0, 0, '', 'Soignez moi, même juste un peu ?', 'Kann mich jemand ein bisschen heilen?', '谁能给我加一点血?', '', '¿Alguien puede curarme un poco?', '', ''), - -('low mana', 'OOM', 0, 0, '', 'Plus de mana !', 'Kein Mana mehr!', '没蓝了!', '', 'No tengo mana!', '', ''), -('low mana', 'I am out of mana', 0, 0, '', 'Je n’ai plus de mana', 'Ich habe kein Mana mehr', '我没蓝了!', '', 'No tengo maná', '', ''), -('low mana', 'Damn I wasted all my mana on this', 0, 0, '', 'Damn j’ai gaspillé toute ma mana sur ça.', 'Verdammt, ich habe mein ganzes Mana darauf verschwendet.', '该死,我的蓝全浪费在这上面了!', '', 'Me cago en todo! Malgasté todo mi maná enseguida', '', ''), -('low mana', 'You should wait until I drink or regenerate my mana', 0, 0, '', 'Tu devrais attendre que je boive ou régénère ma mana.', 'Du solltest warten, bis ich trinke oder Mana regeneriere.', '等我喝点水或者回点蓝再继续吧!', '', 'Deberías esperar hasta que beba o regenere mi maná', '', ''), -('low mana', 'Low mana', 0, 0, '', 'Bas niveau de mana.', 'Wenig Mana.', '蓝量低了!', '', 'Maná bajo', '', ''), -('low mana', 'No mana. Again?', 0, 0, '', 'Pas de mana. Encore ?', 'Kein Mana. Schon wieder?', '没蓝了,又是这样?', '', 'Sin maná. ¿Otra vez?', '', ''), -('low mana', 'Low mana. Wanna drink', 0, 0, '', 'Plus de mana, je prendrai bien un peu d eau', 'Kein Mana mehr, ich sollte etwas trinken.', '蓝量低了,想喝点水!', '', 'Maná bajo. Quiero beber', '', ''), -('low mana', 'Do we have a vending machine? Out of mana again', 0, 0, '', 'Nous avons une machine distributrice ? Encore à court de mana.', 'Haben wir einen Automaten? Schon wieder kein Mana.', '有自动售卖机吗?又没蓝了!', '', '¿Tenemos una máquina expendedora? Sin maná otra vez', '', ''), -('low mana', 'My mana is history', 0, 0, '', 'Mon mana est à sec.', 'Mein Mana ist Geschichte.', '我的蓝已经见底了!', '', 'Mi maná es historia', '', ''), -('low mana', 'I’d get some drinks next time. Out of mana', 0, 0, '', 'Je prendrai des verres la prochaine fois. Plus de mana.', 'Ich hole mir nächstes Mal ein paar Tränke. Kein Mana mehr.', '下次一定要多带点喝的,没蓝了!', '', '¡No tengo maná! Tengo que comprar bebidas la próxima vez...', '', ''), -('low mana', 'Where is my mana?', 0, 0, '', 'Oh là là, désolé, mais je ne sais pas du tout où se trouve ta mana. As-tu vérifié tous tes sacs et tes poches? Peut-être que tu l’as laissé quelque part dans la maison ou dans ton sac à dos. Il est toujours embêtant de perdre quelque chose d’aussi précieux que sa mana, j’espère que tu vas la retrouver bientôt!', 'Wo ist mein Mana?', '我的蓝去哪儿了?', '', '¿Dónde está mi maná?', '', ''), - -('low ammo', 'I have few left!', 0, 0, '', 'Il ne me reste que quelques !', 'Ich habe nur noch wenig !', '我只剩一点了!', '', '¡Me quedan pocas !', '', ''), -('low ammo', 'I need more !', 0, 0, '', 'J’ai besoin de plus de !', 'Ich brauche mehr !', '我需要更多的!', '', '¡Necesito más !', '', ''), -('low ammo', '100 left!', 0, 0, '', 'Il me reste 100 !', 'Ich habe noch 100 übrig!', '还剩100个!', '', '¡Quedan 100 !', '', ''), - -('no ammo', 'That’s it! No !', 0, 0, '', 'C’est ça! Pas de !', 'Das war’s! Keine mehr!', '完了!没了!', '', '¡Eso es todo! ¡Sin !', '', ''), -('no ammo', 'And you have my bow... Oops, no !', 0, 0, '', 'Et vous avez mon arc... Oops, pas de !', 'Und du hast meinen Bogen... Ups, keine !', '你拿了我的弓……呃,没了!', '', 'Y tú tienes mi arco... ¡Uy, sin !', '', ''), -('no ammo', 'Need ammo!', 0, 0, '', 'Besoin de munitions !', 'Brauche Munition!', '需要弹药!', '', '¡Necesito munición!', '', ''), - -('aoe', 'Oh god!', 0, 0, '', 'Oh mon Dieu!', 'Oh Gott!', '天哪!', '', '¡Oh, Dios!', '', ''), -('aoe', 'I am scared', 0, 0, '', 'Je suis effrayé.', 'Ich habe Angst.', '我好害怕!', '', 'Tengo miedo', '', ''), -('aoe', 'We are done for', 0, 0, '', 'Nous sommes foutus.', 'Wir sind erledigt.', '我们完了!', '', 'Hemos terminado', '', ''), -('aoe', 'This is over', 0, 0, '', 'C’est terminé', 'Das war’s.', '这下完了!', '', 'Esto se acabó', '', ''), -('aoe', 'This ends now', 0, 0, '', 'Cela se termine maintenant.', 'Das endet jetzt.', '现在就结束了!', '', 'Esto termina ahora', '', ''), -('aoe', 'Could somebody cast blizzard or something?', 0, 0, '', 'Est-ce que quelqu’un pourrait lancer une tempête de neige ou quelque chose du genre, s’il vous plaît?', 'Kann jemand Blizzard anrufen?', '谁能扔个暴风雪啥的吗?', '', '¿Alguien podría lanzar ventisca o algo así?', '', ''), -('aoe', 'Damn. The tank aggroed all the mobs around', 0, 0, '', 'Maudit tank a attiré tous les ennemis autour.', 'Verdammt. Der Tank hat alle Mobs um uns herum gepullt.', '该死,坦克拉了周围所有的怪!', '', '¡Me cago en todo! El tanque atrajo a todos los mobs...', '', ''), -('aoe', 'We gonna die. We gonna die. We gonna die.', 0, 0, '', 'On va mourir. On va mourir. On va mourir.', 'Wir werden sterben. Wir werden sterben. Wir werden sterben.', '我们要死了!我们要死了!我们要死了!', '', 'Vamos a morir. Vamos a morir. Vamos a morir.', '', ''), -('aoe', 'Whoa! So many toys to play with', 0, 0, '', 'Oh là là ! Tellement de jouets avec lesquels jouer !', 'Wow! So viele Spielzeuge zum Spielen!', '哇!这么多小怪可以玩!', '', '¡Vaya! Tantos juguetes con los que jugar', '', ''), -('aoe', 'I gonna kill them all!', 0, 0, '', 'Je vais les tuer tous !', 'Ich werde sie alle töten!', '我要干掉他们所有人!', '', '¡Voy a matarlos a todos!', '', ''), -('aoe', 'If the tank dies we are history', 0, 0, '', 'Si le tank meurt, c’est la fin pour nous.', 'Wenn der Tank stirbt, war es das für uns.', '如果坦克挂了,我们就完蛋了!', '', 'Si el tanque muere somos historia', '', ''), -('aoe', 'Aaaaaargh!', 0, 0, '', 'Aaaaaaah!', 'Aaaaaargh!', '啊啊啊啊!', '', '¡Aaaaaargh!', '', ''), -('aoe', 'LEEEEERROOOYYYYYYYYYYYY JENNKINNNSSSSSS!!!!!!!', 0, 0, '', 'LEEROOOOOOOYYYYYYYYYYY JENNKINNNSSSSSSS!!!!!!!', 'CAAAAAARL!!!!!!!', '李洛伊·詹金斯!!!', '', 'LEEEEERROOOYYYYYYYYYYYY JENNKINNNSSSSSS!!!!!!!', '', ''), -('aoe', 'Right. What do we have in AOE?', 0, 0, '', 'Alors, qu’est-ce qu’on a dans AOE?', 'Okay. Was haben wir für AOE?', '好吧,我们有什么群攻技能?', '', 'Claro. ¿Qué tenemos en AOE?', '', ''), -('aoe', 'This gets interesting', 0, 0, '', 'Cela devient intéressant', 'Das wird interessant.', '这下有意思了!', '', 'Esto se pone interesante', '', ''), -('aoe', 'Cool. Get them in one place for a good flamestrike', 0, 0, '', 'Cool. Rassemble-les au même endroit pour un bon Coup de Flamme.', 'Cool. Bringt sie alle zusammen für einen ordentlichen Flammenstoß.', '酷!把他们聚一起,给我来个烈焰冲击!', '', 'Genial! Ponlos todos juntos para un buen ataque en area', '', ''), -('aoe', 'Kill! Kill! Kill!', 0, 0, '', 'TUER! TUER! TUER!.', 'TÖTEN! TÖTEN! TÖTEN!', '杀!杀!杀!', '', '¡Matar! ¡Matar! ¡Matar!', '', ''), -('aoe', 'I think my pants are wet', 0, 0, '', 'Je crois que mon pantalon est mouillé.', 'Ich glaube, meine Hose ist nass.', '我感觉我的裤子湿了……', '', 'Creo que mis pantalones están mojados', '', ''), -('aoe', 'We are history', 0, 0, '', 'Nous sommes l’histoire.', 'Wir sind Geschichte.', '我们完蛋了!', '', 'Somos historia', '', ''), -('aoe', 'I hope healers are ready. Leeeeroy!', 0, 0, '', 'J’espère que les heal sont prêt. Leeeeroy!', 'Ich hoffe, die Heiler sind bereit. CAAAAAARL!', '希望治疗准备好了,李洛伊!', '', 'Espero que los sanadores estén listos. ¡Leeeroy!', '', ''), -('aoe', 'I hope they won’t come for me', 0, 0, '', 'J’espère qu’ils ne viendront pas me chercher.', 'Ich hoffe, sie kommen nicht zu mir.', '希望他们别冲着我来!', '', 'Espero que no vengan por mí', '', ''), -('aoe', 'Oh no. I can’t see at this slaugther', 0, 0, '', 'Oh non. Je ne peux pas regarder ce massacre.', 'Oh nein. Ich kann dieses Gemetzel nicht ansehen.', '哦不,我看不下这场屠杀!', '', 'Oh, no. No puedo ver esta matanza', '', ''), - --- on looting -('loot', 'I hope there will be some money', 0, 0, '', 'J’espère qu’il y aura de l’argent.', 'Ich hoffe, es gibt etwas Geld.', '希望能有点金币!', '', 'Espero que haya algo de dinero...', '', ''), -('loot', 'Loot! Loot!', 0, 0, '', 'Butin! Butin!', 'Beute! Beute!', '战利品!战利品!', '', '¡Botín! ¡Botín!', '', ''), -('loot', 'My precious', 0, 0, '', 'Mon précieux', 'Mein Schatz', '我的宝贝!', '', 'Mi tesoro...', '', ''), -('loot', 'I hope there is a shiny epic item waiting for me there', 0, 0, '', 'j’espère qu’il y a un objet épique brillant qui m’attend ici', 'Ich hoffe, dort wartet ein glänzendes episches Item auf mich.', '希望那儿有件闪闪发光的史诗装备等着我!', '', 'Espero que haya un objeto épico brillante esperándome allí', '', ''), -('loot', 'I have deep pockets and bags', 0, 0, '', 'J’ai de grandes poches et des sacs profonds.', 'Ich habe tiefe Taschen und große Beutel.', '我的口袋和背包都很大!', '', 'Tengo bolsillos profundos y infinitos', '', ''), -('loot', 'All is mine!', 0, 0, '', 'Tout m’appartient!', 'Alles gehört mir!', '全是我的!', '', '¡Todo es mío!', '', ''), -('loot', 'Hope no gray shit today', 0, 0, '', 'J’espère qu’il n’y aura pas de merde grise aujourd’hui.', 'Hoffentlich heute kein grauer Mist.', '希望今天别出垃圾灰装!', '', 'Espero que hoy no haya mierda gris', '', ''), -('loot', 'This loot is MINE!', 0, 0, '', 'Ce butin est MIEN !', 'Diese Beute gehört MIR!', '这战利品是我的!', '', '¡Este botín es MÍO!', '', ''), -('loot', 'Looting is disgusting but I need money', 0, 0, '', 'Le pillage est répugnant mais j’ai besoin d’argent', 'Plündern ist ekelhaft, aber ich brauche Geld.', '摸尸体真恶心,但我需要金币!', '', 'El saqueo es repugnante pero necesito dinero...', '', ''), -('loot', 'Gold!', 0, 0, '', 'ARGENT !', 'GOLD!', '金币!', '', '¡Oro!', '', ''), -('loot', 'OK. Let’s see what they’ve got', 0, 0, '', 'D’accord. Voyons ce qu’ils ont.', 'Okay. Schauen wir mal, was sie haben.', '好吧,看看他们身上有啥!', '', 'OK. Veamos qué tienen', '', ''), -('loot', 'Do not worry. I will loot eveything', 0, 0, '', 'Ne t’inquiète pas. je vais tout piller', 'Keine Sorge. Ich werde alles looten.', '别担心,我会把所有东西都搜刮干净!', '', 'No te preocupes. Saquearé todo', '', ''), -('loot', 'I am loot ninja', 0, 0, '', 'Je suis un ninja du butin', 'Ich bin ein Loot-Ninja.', '我是战利品忍者!', '', 'Soy un ladron de tesoros', '', ''), -('loot', 'Do I neeed to roll?', 0, 0, '', 'Est-ce que je dois lancer les dés?', 'Muss ich würfeln?', '我需要roll点吗?', '', '¿Necesito lanzar dados?', '', ''), -('loot', 'Somebody explain me, where they did put all this stuff?', 0, 0, '', 'Quelqu’un m’explique, où ils ont mis tout ça?', 'Kann mir jemand erklären, wo sie das ganze Zeug hingetan haben?', '谁能告诉我,他们把这些东西都藏哪儿了?', '', 'Alguien me explica, ¿dónde pusieron todas estas cosas?', '', ''), -('loot', 'No, I won’t loot gray shit', 0, 0, '', 'No way, je ne vais pas voler de la merde grise.', 'Nein, ich loote keinen grauen Mist.', '不行,我才不捡这些破烂灰色装备!', '', 'No, no saquearé mierda gris', '', ''), -('loot', 'I’m first. I’m first. I’m first.', 0, 0, '', 'Je suis le premier. Je suis le premier. Je suis le premier.', 'Ich bin zuerst! Ich bin zuerst! Ich bin zuerst!', '我先来!我先来!我先来!', '', '¡Es mio! ¡Es mio! ¡Es mio!', '', ''), -('loot', 'Give me your money!', 0, 0, '', 'Désolé, je ne peux pas faire ça.', 'Gib mir dein Geld!', '把你的金币都交出来!', '', '¡Dame todo tu dinero!', '', ''), -('loot', 'My pockets are empty, I need to fill them', 0, 0, '', 'Mes poches sont vides, il faut que je les remplisse.', 'Meine Taschen sind leer, ich muss sie füllen.', '我的口袋空空如也,得赶紧装点东西!', '', 'Mis bolsillos están vacíos y necesitan llenarse!', '', ''), -('loot', 'I’ve got a new bag for this', 0, 0, '', 'J’ai un nouveau sac pour cela.', 'Ich habe eine neue Tasche dafür.', '我刚弄了个新背包,正好装这些!', '', 'Tengo una nueva bolsa para esto', '', ''), -('loot', 'I hope I won’t aggro anybody while looting', 0, 0, '', 'J’espère ne pas aggro, je vais loot !', 'Ich hoffe, ich ziehe keine Aggro, während ich loote.', '希望我捡东西的时候别引到什么怪物!', '', 'Espero no ofender a nadie mientras saqueo', '', ''), -('loot', 'Please don’t watch. I am looting', 0, 0, '', 'S’il vous plaît, ne regardez pas. je loot', 'Bitte nicht hinschauen. Ich plündere.', '别盯着看,我在捡装备呢!', '', 'Por favor, no mires. Estoy saqueando', '', ''), -('loot', 'Ha! You won’t get any piece of it!', 0, 0, '', 'Ha! Tu ne verras rien de cela !', 'Ha! Davon bekommst du nichts ab!', '哈!你别想分到半点东西!', '', '¡Ja! ¡No obtendrás nada de eso!', '', ''), -('loot', 'Looting is cool', 0, 0, '', 'Le pillage c’est cool', 'Looten ist cool.', '捡战利品真是爽!', '', 'Saquear es genial', '', ''), -('loot', 'I like new gear', 0, 0, '', 'J’aime les nouveaux équipements.', 'Ich liebe neue Ausrüstung.', '我超喜欢新装备!', '', 'Me gusta el equipo nuevo', '', ''), -('loot', 'I’l quit if there is nothing valuable again', 0, 0, '', 'j’arrêterai s’il n’y a plus rien de précieux', 'Ich höre auf, wenn es wieder nichts Wertvolles gibt.', '要是再没什么值钱的东西,我就走人了!', '', 'Me doy por vencido si no hay nada util', '', ''), -('loot', 'I hope it is be a pretty ring', 0, 0, '', 'j’espère que c’est une jolie bague', 'Ich hoffe, es ist ein hübscher Ring.', '希望这次能捡到个漂亮的戒指!', '', 'Espero que sea un anillo bonito', '', ''), -('loot', 'I’l rip the loot from you', 0, 0, '', 'Je t’arracherai le butin', 'Ich werde dir die Beute entreißen.', '我要把战利品从你手里抢过来!', '', 'Te arrancaré el botín', '', ''), -('loot', 'Everybody stay off. I’m going to loot', 0, 0, '', 'Tout le monde, restez loin. Je vais piller.', 'Alle zurückbleiben. Ich loote jetzt.', '都别靠近,我要开始捡东西了!', '', '¡Alejaos todos! Voy a lootear', '', ''), -('loot', 'Sweet loot', 0, 0, '', 'Je me suis procuré un butin incroyable !', 'Tolle Beute!', '真是甜美的战利品!', '', 'Dulce botín', '', ''), -('loot', 'The Roll God! Give me an epic today', 0, 0, '', 'Le Dieu des Lancés! Fais-moi vivre une épopée aujourd’hui', 'Oh Würfelgott! Gib mir heute ein Epic!', '掷骰之神!今天赐我一件史诗装备吧!', '', '¡El Dios de los dados! Dame un epico hoy', '', ''), -('loot', 'Please give me new toys', 0, 0, '', 'S’il te plaît, donne-moi de nouveaux jouets', 'Bitte gib mir neue Spielsachen.', '求你了,给我点新玩意儿吧!', '', 'Por favor, dame nuevos juguetes', '', ''), -('loot', 'I hope they carry tasties', 0, 0, '', 'J’espère qu’ils auront des délices.', 'Ich hoffe, sie haben etwas Leckeres dabei.', '希望他们身上带了点好吃的!', '', 'Espero que caiga algo sabrosos', '', ''), -('loot', 'The gold is mine. I’ll leave everyting, I promise', 0, 0, '', 'L’or m’appartient. Je laisserai tout, je te le promets.', 'Das Gold gehört mir. Ich lasse den Rest liegen, versprochen.', '金币是我的,其他都留给你们,我保证!', '', 'El oro es mío. Dejaré todo lo demas, lo prometo!', '', ''), -('loot', 'No, I can’t resist', 0, 0, '', 'Non, je ne peux pas résister.', 'Nein, ich kann nicht widerstehen.', '不,我忍不住了!', '', 'No, no puedo resistir', '', ''), -('loot', 'I want more!', 0, 0, '', 'Oh, vous en voulez plus!', 'Ich will mehr!', '我还想要更多!', '', '¡Quiero más!', '', ''), - --- wait signals -('wait_travel_close', 'I am close, wait for me!', 0, 0, '', 'Je suis presque là, attends-moi !', 'Ich bin fast da, wartet auf mich!', '我就在附近,等我一下!', '', '¡Estoy cerca, espérame!', '', 'Я тут рядом, подожди!'), -('wait_travel_close', 'I’m not far, please wait!', 0, 0, '', 'Je ne suis pas loin, s’il vous plaît patientez !', 'Ich bin nicht weit, bitte wartet!', '我不远了,请稍等我!', '', 'No estoy lejos, ¡esperame por favor!', '', 'Я недалеко, погодите!'), -('wait_travel_medium', 'I’m heading to your location', 0, 0, '', 'Je me dirige vers votre emplacement.', 'Ich bin auf dem Weg zu euch.', '我正朝你的位置赶去!', '', 'Me dirijo a tu ubicación', '', 'Двигаюсь к тебе'), -('wait_travel_medium', 'I’m coming to you', 0, 0, '', 'Je viens vers vous.', 'Ich komme zu euch.', '我正向你靠近!', '', 'Voy hacia ti', '', 'Иду к тебе'), -('wait_travel_far', 'I’m traveling to your location', 0, 0, '', 'Je voyage vers votre destination.', 'Ich reise zu eurem Standort.', '我正在前往你的位置!', '', 'Voy a viajar a tu ubicación', '', 'Направляюсь к тебе'), -('wait_travel_far', 'I’m trying to get to you', 0, 0, '', 'Je suis en train de l’atteindre', 'Ich versuche, zu euch zu kommen.', '我正努力赶到你那儿!', '', 'Estoy tratando de llegar a ti', '', 'Пытаюсь до тебя добраться'), - --- commands -('equip_command', 'Equipping %item', 0, 0, '', 'Équipement %item.', 'Rüste %item aus.', '装备上 %item', '', 'Me he equipado %item', '', ''), -('unequip_command', '%item unequipped', 0, 0, '', '%item Déséquipé', '%item abgelegt.', '卸下了 %item', '', 'Me he quitado %item', '', ''), -('auto_learn_spell', 'I have learned the spells: %spells', 0, 0, '', 'Je viens d’apprendre: %spells', 'Ich habe folgende Zauber gelernt: %spells', '我学会了这些法术:%spells', '', 'He aprendido los hechizos: %spells', '', ''), - -('use_command_item_cooldown', '%item is in cooldown', 0, 0, '', 'L’objet %item est en temps de recharge.', '%item ist auf Abklingzeit.', '%item 还在冷却中', '', '%item esta recargandose', '', ''), -('use_command_item_not_owned', 'I don’t have %item in my inventory', 0, 0, '', 'Je n’ai pas %item dans mon inventaire.', 'Ich habe %item nicht in meinem Inventar.', '我的背包里没有 %item', '', 'No tengo %item en mi inventario', '', ''), -('use_command_invalid_item', 'The item with the id %item does not exist', 0, 0, '', 'L’article avec l’identifiant %item n’existe pas.', 'Der Gegenstand mit der ID %item existiert nicht.', 'ID 为 %item 的物品不存在', '', 'El objeto con el id %item no existe', '', ''), -('use_command_socket', 'Socketing %gem into %item', 0, 0, '', 'J’encastre le %gem dans %item.', 'Setze %gem in %item ein.', '将 %gem 镶嵌到 %item 上', '', 'Insertando %gem en %item', '', ''), -('use_command_item_error', 'I can’t use %item', 0, 0, '', 'Je ne peux pas utiliser %item.', 'Ich kann %item nicht benutzen.', '我无法使用 %item', '', 'No puedo usar %item', '', ''), - -('following', 'Following', 0, 0, '', 'Je suis', 'Folge dir', '跟随中', '', 'Te sigo', '', 'Следую'), -('staying', 'Staying', 0, 0, '', 'Je reste ici', 'Bleibe stehen', '待命中', '', 'Me quedo aqui', '', 'Стою'), -('fleeing', 'Fleeing', 0, 0, '', 'Je m’enfui', 'Fliehe', '逃跑中', '', 'Huyendo', '', 'Убегаю'), -('fleeing_far', 'I won’t flee with you, you are too far away', 0, 0, '', 'Je ne vais pas partir avec toi, tu es trop loin.', 'Ich werde nicht mit dir fliehen, du bist zu weit weg.', '我不会跟你一起逃,你离我太远了!', '', 'No huiré contigo, estás demasiado lejos', '', 'Я с тобой не побегу, ты слишком далеко'), -('grinding', 'Grinding', 0, 0, '', 'Grinding', 'Farme', '刷怪中', '', 'Farmeando', '', 'Гриндю'), -('attacking', 'Attacking', 0, 0, '', 'Attaque en cours !', 'Greife an!', '攻击中', '', 'Atacando', '', 'Атакую'), - -('error_far', 'It is too far away', 0, 0, '', 'C’est trop loin.', 'Das ist zu weit entfernt.', '距离太远了', '', 'Estas demasiado lejos', '', 'Слишком далеко'), -('error_water', 'It is under water', 0, 0, '', 'Cela se trouve sous l’eau', 'Das ist unter Wasser.', '目标在水下', '', 'Estas debajo del agua', '', 'Это под водой'), -('error_cant_go', 'I can’t go there', 0, 0, '', 'Je ne peux pas y aller.', 'Ich kann dort nicht hingehen.', '我去不了那里', '', 'No puedo ir allí', '', 'Я не могу туда пройти'), -('error_guild', 'I’m not in your guild!', 0, 0, '', 'Je ne fais pas partie de votre guilde!', 'Ich bin nicht in eurer Gilde!', '我不在你的公会里!', '', '¡No estoy en tu hermandad!', '', 'Я не в твоей гильдии'), -('error_gbank_found', 'Can not find a guild bank nearby', 0, 0, '', 'Je ne trouve pas de banque de guilde à proximité.', 'Ich kann keine Gildenbank in der Nähe finden.', '附近找不到公会银行', '', 'No se puede encontrar un banco de hermandad cercano', '', 'Не могу найти гильд банк рядом'), -('error_cant_put', 'I can’t put ', 0, 0, '', 'Je ne peux pas déposer ', 'Ich kann das nicht ablegen.', '我无法存放', '', 'No puedo depositar ', '', 'Я не могу положить'), -('error_gbank_rights', 'I have no rights to put items in the first guild bank tab', 0, 0, '', 'Je n’ai pas le droit de mettre des objets dans le premier onglet de la banque de guilde.', 'Ich habe keine Rechte, Gegenstände in den ersten Reiter der Gildenbank zu legen.', '我没有权限将物品放入公会银行的第一个标签页', '', 'No tengo derechos para depositar objetos en la primera pestaña del banco de hermandad', '', 'Нет прав чтобы класть вещи в первую вкладку гильд банка'), - -('gbank_put', ' put to guild bank', 0, 0, '', ' déposé dans la banque de guilde.', 'in die Gildenbank gelegt.', '已存入公会银行', '', ' depositado en el banco de hermandad', '', ' теперь в гильд банке'), -('free_moving', 'Free moving', 0, 0, '', 'Déplacement libre', 'Freie Bewegung', '自由移动中', '', 'Moviendome libremente', '', ''), -('guarding', 'Guarding', 0, 0, '', 'Je garle la position.', 'Ich bewache die Position.', '守卫中', '', 'Protegiendo la posición', '', ''), -('use_command', 'Using ', 0, 0, '', 'Utilisation de ', 'Benutze ', '使用 ', '', 'Usando ', '', ''), -('command_target_unit', 'on %unit', 0, 0, '', 'sur %unit', 'auf %unit', '对 %unit', '', 'en %unit', '', ''), - -('use_command_remaining', '(%amount available)', 0, 0, '', '(%amount restant)', '(%amount verfügbar)', '(剩余 %amount)', '', '(%amount restante)', '', ''), -('use_command_last', '(the last one)', 0, 0, '', '(La dernière fois)', '(der letzte)', '(最后一个)', '', '(el último)', '', ''), -('use_command_socket_error', 'The socket does not fit', 0, 0, '', 'Le socket ne s’adapte pas.', 'Der Sockel passt nicht.', '插槽不匹配', '', 'La ranura no sirve', '', ''), - -('command_target_trade', 'on trade item', 0, 0, '', 'Sur le commerce de l’article', 'auf Handelsware', '对交易物品', '', 'en objeto comerciado', '', ''), -('command_target_self', 'on myself', 0, 0, '', 'sur moi-même', 'auf mich selbst', '对自己', '', 'en mi', '', ''), -('command_target_item', 'on %item', 0, 0, '', 'sur %item', 'auf %item', '对 %item', '', 'en %item', '', ''), -('command_target_go', 'on %gameobject', 0, 0, '', 'sur %gameobject', 'auf %gameobject', '对 %gameobject', '', 'en %gameobject', '', ''), - -('loot_command', 'Looting %item', 0, 0, '', 'Ramassage de %item', 'Plündere %item', '拾取 %item', '', 'Despojando %item', '', ''), - -('cast_spell_command_summon', 'Summoning ', 0, 0, '', 'Invocation de ', 'Beschwöre ', '召唤 ', '', 'Invocando a ', '', ''), -('cast_spell_command_summon_error_members', 'I don’t have enough party members around to cast a summon', 0, 0, '', 'Je n’ai pas assez de membres de groupe autour de moi pour lancer une invocation.', 'Ich habe nicht genug Gruppenmitglieder in der Nähe, um eine Beschwörung zu durchzuführen.', '我周围没有足够的队友,无法施放召唤术。', '', 'No tengo suficientes miembros de grupo alrededor para invocar', '', ''), -('cast_spell_command_summon_error_target', 'Failed to find the summon target', 0, 0, '', 'Je n’ai pas réussi à trouver la cible à invoquer.', 'Beschwörungsziel nicht gefunden.', '找不到召唤目标!', '', 'No he podido encontrar al objetivo de la invocación', '', ''), -('cast_spell_command_summon_error_combat', 'I can’t summon while I’m in combat', 0, 0, '', 'Je ne peux pas invoquer pendant que je suis en combat', 'Ich kann im Kampf nicht beschwören.', '战斗中无法施放召唤术!', '', 'No puedo invocar durante un combate', '', ''), -('cast_spell_command_error_unknown_spell', 'I don’t know the spell %spell', 0, 0, '', 'Je ne connais pas le sort %spell.', 'Ich kenne den Zauber %spell nicht.', '我不会这个技能 %spell。', '', 'No conozco el hechizo %spell', '', ''), -('cast_spell_command_spell', 'Casting %spell', 0, 0, '', 'Lancement du sort %spell', 'Wirke %spell', '正在施放 %spell', '', 'Lanzando %spell', '', ''), -('cast_spell_command_craft', 'Crafting %spell', 0, 0, '', 'Je suis en train de fabriquer le sort %spell.', 'Stelle %spell her.', '正在制作 %spell', '', 'Creando %spell', '', ''), -('cast_spell_command_error', 'Cannot cast %spell', 0, 0, '', 'Je ne peux pas lancer le sort %spell.', 'Kann %spell nicht wirken.', '无法施放 %spell!', '', 'No puedo lanzar %spell', '', ''), -('cast_spell_command_error_failed', 'Failed to cast %spell', 0, 0, '', 'J’ai échoué à lancer %spell.', 'Zauber %spell fehlgeschlagen.', '施放 %spell 失败了!', '', 'Fallo al lanzar %spell', '', ''), -('cast_spell_command_amount', ' |cffffff00(x%amount left)|r', 0, 0, '', ' |cffffff00(x%amount restant)|r.', '|cffffff00(x%amount übrig)|r.', ' |cffffff00(剩余 x%amount)|r', '', ' |cffffff00(x%amount restante)|r', '', ''), - -('dummy_end', 'dummy', 0, 0, '', 'dummy', 'dummy', '', '', '', '', ''); +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 + (1, 'string_unknown_area', 'the middle of nowhere', 0, 0, '', 'au milieu de nulle part', 'am Arsch der Welt', '荒无人烟之地', '', '', '', 'посреди нигде'), + (2, 'string_unknown_area', 'an undisclosed location', 0, 0, '', 'un lieu non divulgué', 'an einem geheimen Ort', '某个不为人知的地方', '', '', '', 'в неопределенном месте'), + (3, 'string_unknown_area', 'somewhere', 0, 0, '', 'Quelque part.', 'irgendwo', '某个地方', '', '', '', 'где-то'), + (4, 'string_empty_link', 'something', 0, 0, '', 'quelque chose', 'irgendwas', '某样东西', '', '', '', 'что-то'), + (5, 'broadcast_looting_item_normal', 'wonder what %item_link tastes like', 0, 0, '', 'Je me demande à quoi %item_link peut bien goûter.', 'frag mich, wie %item_link wohl schmeckt', '想知道这%item_link吃起来啥味儿', '', '', '', 'интересно, какой на вкус %item_link'), + (6, 'broadcast_looting_item_poor', 'nooo, I got %item_link', 0, 0, '', 'Oh non, j’ai reçu %item_link', 'neeein, ich hab %item_link bekommen', '不!我捡了个%item_link', '', '', '', 'нет, мне выпал %item_link'), + (7, 'broadcast_looting_item_poor', 'aww not this trash again %item_link', 0, 0, '', 'Encore cette poubelle %item_link.', 'ah nee, nicht schon wieder dieser Müll: %item_link', '哎,又是这破烂%item_link', '', '', '', 'опять этот хлам %item_link'), + (8, 'broadcast_looting_item_poor', 'seem to be looting trash %item_link', 0, 0, '', 'Il semble que je fouille dans les ordures %item_link.', 'scheint so, als würde ich nur Müll looten: %item_link', '看来我捡的都是垃圾%item_link', '', '', '', 'похоже, я лутаю мусор %item_link'), + (9, 'broadcast_looting_item_poor', 'well, it’s better than nothing I guess %item_link', 0, 0, '', 'Eh bien, c’est toujours mieux que rien, je suppose %item_link', 'naja, besser als nix, schätze ich: %item_link', '好吧,有%item_link总比没有强', '', '', '', ''), + (10, 'broadcast_looting_item_poor', 'not sure what to do with %item_link', 0, 0, '', 'Je ne suis pas certain de ce qu’il faut faire avec %item_link.', 'keine Ahnung, was ich mit %item_link anfangen soll', '不知道这%item_link能干啥', '', '', '', 'не знаю, что делать с %item_link'), + (11, 'broadcast_looting_item_normal', 'well, I can keep looting %item_link all day', 0, 0, '', 'Eh bien, je peux passer la journée à looter %item_link!', 'naja, ich kann den ganzen Tag %item_link looten', '好吧,我能捡%item_link捡一天', '', '', '', 'могу лутать %item_link весь день'), + (12, 'broadcast_looting_item_normal', 'another day, another %item_link', 0, 0, '', 'Encore une journée, encore un %item_link', 'neuer Tag, neuer %item_link', '又是新的一天,又捡了个%item_link', '', '', '', 'еще один день, еще один %item_link'), + (13, 'broadcast_looting_item_normal', 'looted some %item_link', 0, 0, '', 'J’ai pillé quelques %item_link.', 'hab %item_link gelootet', '捡了点%item_link', '', '', '', 'получил немного %item_link'), + (14, 'broadcast_looting_item_normal', 'some %item_link is alright', 0, 0, '', 'Certains %item_link passent bien', 'ein %item_link ist okay', '有点%item_link还凑合', '', '', '', 'немного %item_link — это нормально'), + (15, 'broadcast_looting_item_uncommon', 'not bad, I just got %item_link', 0, 0, '', 'Pas mal du tout, je viens juste de recevoir %item_link', 'nicht schlecht, hab grad %item_link bekommen.', '不错,刚捡到个%item_link', '', '', '', 'неплохо, только что получил %item_link'), + (16, 'broadcast_looting_item_uncommon', 'just looted %item_link in %zone_name', 0, 0, '', 'Je viens de piller %item_link dans %zone_name.', 'gerade %item_link in %zone_name gelootet', '刚在%zone_name捡到个%item_link', '', '', '', 'только что залутал %item_link в %zone_name'), + (17, 'broadcast_looting_item_uncommon', 'I could put that to good use %item_link', 0, 0, '', 'Je pourrais bien utiliser ça %item_link', 'kann ich gut gebrauchen, %item_link', '这%item_link我能派上用场', '', '', '', 'могу найти применение %item_link'), + (18, 'broadcast_looting_item_uncommon', 'money money money %item_link', 0, 0, '', 'L’argent, l’argent, l’argent! %item_link', 'Gold, Gold, Gold: %item_link', '发财了发财了!%item_link', '', '', '', 'деньги, деньги, деньги %item_link'), + (19, 'broadcast_looting_item_uncommon', 'got %item_link', 0, 0, '', 'J’ai eu %item_link.', 'hab %item_link bekommen', '拿到了%item_link', '', '', '', 'получил %item_link'), + (20, 'broadcast_looting_item_rare', '%item_link is hunter bis', 0, 0, '', 'Ah, %item_link c’est le meilleur équipement pour les chasseurs.', '%item_link ist BiS für Jäger', '%item_link简直是猎人的极品装备', '', '', '', '%item_link — лучший для охотника'), + (21, 'broadcast_looting_item_rare', '%item_link is %my_class bis', 0, 0, '', '%item_link est %my_class bis.', '%item_link ist BiS für %my_class', '%item_link对%my_class来说是极品', '', '', '', '%item_link — лучший для %my_class'), + (22, 'broadcast_looting_item_rare', 'RNG is not bad today %item_link', 0, 0, '', 'La RNG n’est pas mauvaise aujourd’hui %item_link.', 'RNG meint es heute gut: %item_link', '今天人品不错,捡到个%item_link', '', '', '', 'сегодня удача на моей стороне %item_link'), + (23, 'broadcast_looting_item_rare', 'sweet %item_link, freshly looted', 0, 0, '', 'Doux %item_link, fraîchement looté', 'nice, %item_link frisch gelootet', '太棒了,新鲜出炉的%item_link', '', '', '', 'классный %item_link, только что залутал'), + (24, 'broadcast_looting_item_rare', 'wow, I just got %item_link', 0, 0, '', 'wow, je viens de recevoir %item_link', 'wow, hab gerade %item_link bekommen', '哇,我刚捡到个%item_link', '', '', '', 'вау, только что получил %item_link'), + (25, 'broadcast_looting_item_rare', '%item_link is hunter bis', 0, 0, '', 'Ah, %item_link, c’est le meilleur équipement pour les chasseurs.', '%item_link ist BiS für Jäger', '%item_link是猎人的顶级装备', '', '', '', '%item_link — лучший для охотника'), + (26, 'broadcast_looting_item_rare', '%item_link is %my_class bis', 0, 0, '', '%item_link est %my_class bis.', '%item_link ist BiS für %my_class', '%item_link对%my_class来说是顶级货', '', '', '', '%item_link — лучший для %my_class'), + (27, 'broadcast_looting_item_rare', 'RNG is not bad today %item_link', 0, 0, '', 'Aujourd’hui, la RNG n’est pas mauvaise %item_link.', 'RNG meint es heute gut: %item_link', '今天运气真好,捡到%item_link', '', '', '', 'сегодня удача на моей стороне %item_link'), + (28, 'broadcast_looting_item_rare', 'sweet %item_link, freshly looted', 0, 0, '', 'Oh, %item_link, fraîchement pillé', 'nice, %item_link frisch gelootet', '太好了,刚捡到个%item_link', '', '', '', 'классный %item_link, только что залутал'), + (29, 'broadcast_looting_item_epic', 'OMG, look at what I just got %item_link!!!', 0, 0, '', 'OMG, regardez ce que je viens d’obtenir %item_link !!!', 'OMG, guckt mal, was ich gerade bekommen hab: %item_link!!!', '天啊,快看我刚捡到的%item_link!!!', '', '', '', 'ОГО, посмотрите, что я только что получил %item_link!!!'), + (30, 'broadcast_looting_item_legendary', 'No @#$@% way! This can not be, I got %item_link, this is insane', 0, 0, '', 'Pas moyen! Ça ne peut pas être vrai, j’ai obtenu %item_link, c’est fou!', 'Keine @#$@% Chance! Ich hab %item_link bekommen, Wahnsinn.', '不是吧!这不可能,我竟然捡到%item_link,太疯狂了!', '', '', '', 'Нет @#$@%! Не может быть, я получил %item_link, это безумие'), + (31, 'broadcast_looting_item_artifact', 'No @#$@% way! This can not be, I got %item_link, this is insane', 0, 0, '', 'Pas possible! Ça ne peut pas être vrai, j’ai eu %item_link, c’est fou', 'Keine @#$@% Chance! Das kann nicht sein - ich hab %item_link bekommen, völlig irre.', '天哪!这不可能,我拿到了%item_link,简直不敢相信!', '', '', '', 'Нет @#$@%! Не может быть, я получил %item_link, это безумие'), + (32, 'broadcast_looting_item_poor', 'Seriously? Another %item_link? My luck is broken.', 0, 0, '', 'Sérieusement? Encore un %item_link? La poisse me suit, c’est pas possible.', 'Im Ernst? Noch ein %item_link? Mein Glück ist kaputt.', '不是吧?又是个%item_link?我的运气真是烂透了。', '', '', '', ''), + (33, 'broadcast_looting_item_poor', 'This %item_link is not even worth the pixels it’s printed on.', 0, 0, '', 'Cet %item_link ne vaut même pas les pixels sur lesquels il est imprimé.', 'Dieser %item_link ist nicht mal die Pixel wert, auf denen er angezeigt wird.', '这%item_link连显示它的像素都不值。', '', '', '', ''), + (34, 'broadcast_looting_item_poor', 'I got %item_link... Ugh, here we go again.', 0, 0, '', 'J’ai de nouveau %item_link... Oh là là, c’est reparti.', 'Ich hab %item_link... ugh, schon wieder.', '我又捡到个%item_link...哎,又来了。', '', '', '', ''), + (35, 'broadcast_looting_item_poor', 'I don’t even know why I’m picking up %item_link, it’s useless.', 0, 0, '', 'Je me demande pourquoi je prends %item_link, c’est inutile.', 'Keine Ahnung, warum ich %item_link überhaupt aufhebe, es ist nutzlos.', '我都不知道为啥要捡%item_link,完全没用。', '', '', '', ''), + (36, 'broadcast_looting_item_poor', 'Look at this shiny %item_link... too bad it’s garbage.', 0, 0, '', 'Regarde ce magnifique %item_link... dommage que ce soit de la camelote.', 'Schaut euch diesen glänzenden %item_link an... schade, ist trotzdem Müll.', '看看这闪亮的%item_link...可惜就是个垃圾。', '', '', '', ''), + (37, 'broadcast_looting_item_poor', 'This %item_link isn’t even worth the paper it’s written on.', 0, 0, '', '%item_link ne vaut même pas le papier sur lequel il est écrit.', 'Dieser %item_link ist nicht mal das Papier wert, auf dem er stehen würde.', '这%item_link连写它的羊皮纸都不值。', '', '', '', ''), + (38, 'broadcast_looting_item_poor', 'I guess %item_link is better than nothing? Barely.', 0, 0, '', 'Je suppose que %item_link vaut mieux que rien ? À peine.', 'Ich schätze, %item_link ist besser als gar nix? Gerade so.', '我想%item_link总比没有强吧?勉强。', '', '', '', ''), + (39, 'broadcast_looting_item_poor', 'Not sure what to do with %item_link, maybe I’ll just sell it for 1 silver.', 0, 0, '', 'Je ne suis pas sûr de ce que faire avec %item_link, peut-être que je vais juste le vendre pour 1 pièce d’argent.', 'Kein Plan, was ich mit %item_link machen soll, vielleicht verkauf ich es für 1 Silber.', '不知道%item_link能干嘛,或许卖1个银币得了。', '', '', '', ''), + (40, 'broadcast_looting_item_poor', 'Another %item_link... well, at least I’m getting something.', 0, 0, '', 'Encore un %item_link... eh bien, au moins je reçois quelque chose.', 'Noch ein %item_link... naja, immerhin krieg ich was.', '又是个%item_link...好吧,至少有点收获。', '', '', '', ''), + (41, 'broadcast_looting_item_poor', 'I hope %item_link is actually worth something because it looks awful.', 0, 0, '', 'J’espère que %item_link vaut en fait quelque chose parce que ça a l’air affreux.', 'Hoffentlich ist %item_link was wert, es sieht nämlich furchtbar aus.', '希望%item_link能值点钱,因为它看起来真垃圾。', '', '', '', ''), + (42, 'broadcast_looting_item_poor', 'Is %item_link a joke? Someone’s laughing at me.', 0, 0, '', '%item_link c’est une blague ? Quelqu’un se moque de moi.', 'Ist %item_link ein Witz? Irgendwer lacht mich aus.', '这%item_link是开玩笑吗?谁在嘲笑我啊!', '', '', '', ''), + (43, 'broadcast_looting_item_poor', 'Why do I keep getting %item_link... did I anger RNG?', 0, 0, '', 'Pourquoi est-ce que je continue à obtenir %item_link... est-ce que j’ai fâché le RNG?', 'Warum krieg ich ständig %item_link... hab ich RNG verärgert?', '为啥我老是拿到%item_link……我是不是得罪了随机之神?', '', '', '', ''), + (44, 'broadcast_looting_item_poor', 'It’s like %item_link is following me around. I don’t want it!', 0, 0, '', 'C’est comme si %item_link me suivait partout. Je ne veux pas de ça !', 'Als würde %item_link mir nachlaufen. Ich will das nicht!', '这%item_link就像缠着我一样,我才不要呢!', '', '', '', ''), + (45, 'broadcast_looting_item_poor', 'There’s %item_link, again. What a surprise.', 0, 0, '', 'Encore %item_link. Quelle surprise.', 'Da ist %item_link schon wieder. Was für eine Überraschung.', '又是%item_link,真是“惊喜”啊。', '', '', '', ''), + (46, 'broadcast_looting_item_poor', 'I guess %item_link is good for vendor trash?', 0, 0, '', 'I présume qu’ %item_link est bon pour vendre à un marchand ?', 'Ich schätze, %item_link taugt als Händler-Müll?', '看来%item_link只能当垃圾卖给商人了?', '', '', '', ''), + (47, 'broadcast_looting_item_poor', 'Noooo, not %item_link again! RNG, please!', 0, 0, '', 'Oh non, pas encore %item_link! RNG s’il te plaît!', 'Neeeein, nicht schon wieder %item_link! RNG, bitte!', '不!别又是%item_link!随机之神,求放过!', '', '', '', ''), + (48, 'broadcast_looting_item_poor', 'Well, at least %item_link isn’t completely useless, right?', 0, 0, '', 'Eh bien, au moins %item_link n’est pas complètement inutile, n’est-ce pas?', 'Naja, ganz nutzlos ist %item_link immerhin nicht, oder?', '好吧,至少%item_link还不算完全没用,对吧?', '', '', '', ''), + (49, 'broadcast_looting_item_poor', 'Another %item_link? RNG, do you even care?', 0, 0, '', 'Encore %item_link ? La RNG est-ce que ça te dit quelque chose au moins?', 'Noch ein %item_link? RNG, kümmert dich das überhaupt?', '又一个%item_link?随机之神,你在乎过我的感受吗?', '', '', '', ''), + (50, 'broadcast_looting_item_poor', 'Maybe %item_link will be worth something in 10 years.', 0, 0, '', 'Peut-être que %item_link vaudra quelque chose dans 10 ans.', 'Vielleicht ist %item_link in 10 Jahren was wert.', '或许%item_link十年后能值点钱吧。', '', '', '', ''), + (51, 'broadcast_looting_item_poor', 'I guess I’ll just admire how %item_link looks... and throw it away.', 0, 0, '', 'Je suppose que je vais juste admirer %item_link ... et le jeter.', 'Ich bewundere einfach mal, wie %item_link aussieht... und schmeiß es weg.', '我就欣赏一下%item_link的样子……然后扔了吧。', '', '', '', ''), + (52, 'broadcast_looting_item_poor', 'Who needs %item_link? Not me, that’s for sure.', 0, 0, '', 'Qui a besoin de %item_link ? Pas moi en tout cas, ça c’est sûr.', 'Wer braucht %item_link? Ich sicher nicht.', '谁需要%item_link啊?反正我肯定不要。', '', '', '', ''), + (53, 'broadcast_looting_item_poor', 'Maybe %item_link will be useful... just kidding, it’s trash.', 0, 0, '', 'Peut-être que %item_link sera utile... non, je plaisante, c’est nul.', 'Vielleicht ist %item_link nützlich... haha, doch nur Müll.', '也许%item_link有用……开玩笑的,就是个垃圾。', '', '', '', ''), + (54, 'broadcast_looting_item_poor', 'The universe has given me %item_link. Great.', 0, 0, '', 'L’univers m’a donné %item_link. Génial.', 'Das Universum schenkt mir %item_link. Toll.', '宇宙赐予了我%item_link,棒极了。', '', '', '', ''), + (55, 'broadcast_looting_item_poor', 'Well, I have %item_link. Can’t say I’m thrilled.', 0, 0, '', 'Eh bien, j’ai %item_link. Franchement, je ne suis pas super emballé.', 'Gut, ich hab %item_link. Begeistert bin ich nicht.', '好吧,我拿到了%item_link,可一点也高兴不起来。', '', '', '', ''), + (56, 'broadcast_looting_item_poor', 'What am I even going to do with %item_link? Can’t even sell it.', 0, 0, '', 'Qu’est-ce que je vais bien pouvoir faire avec %item_link ? Je ne peux même pas le vendre.', 'Was soll ich überhaupt mit %item_link anfangen? Nicht mal verkaufbar.', '我拿%item_link能干啥?连卖都卖不掉。', '', '', '', ''), + (57, 'broadcast_looting_item_poor', 'Let’s add %item_link to my growing pile of disappointment.', 0, 0, '', 'Ajoutons %item_link à ma pile croissante de déceptions.', 'Packen wir %item_link zu meinem wachsenden Enttäuschungsstapel.', '把%item_link扔进我那堆失望的垃圾里吧。', '', '', '', ''), + (58, 'broadcast_looting_item_poor', 'Thanks for %item_link... it’s exactly what I didn’t need.', 0, 0, '', 'Merci pour %item_link... c’est exactement ce qu’il me fallait pas.', 'Danke für %item_link... genau das, was ich nicht gebraucht hab.', '谢谢给了我%item_link……这正是我不需要的东西。', '', '', '', ''), + (59, 'broadcast_looting_item_poor', 'Another %item_link... RNG, we need to talk.', 0, 0, '', 'Encore un %item_link... RNG, on doit avoir une discussion.', 'Noch ein %item_link... RNG, wir müssen reden.', '又一个%item_link……随机之神,咱们得谈谈。', '', '', '', ''), + (60, 'broadcast_looting_item_poor', 'I’ve looted so many %item_link, I could open a store with them.', 0, 0, '', 'J’ai récupéré tellement de %item_link, je pourrais ouvrir un magasin avec.', 'Ich hab so viele %item_link gelootet, ich könnte damit einen Laden aufmachen.', '我捡了这么多%item_link,够开个杂货铺了。', '', '', '', ''), + (61, 'broadcast_looting_item_poor', 'A shiny %item_link... maybe for the trash pile.', 0, 0, '', 'Un brillant %item_link... peut-être destiné à la poubelle.', 'Ein glänzender %item_link... kommt wohl auf den Müllhaufen.', '一个闪亮的%item_link……大概只能扔垃圾堆了。', '', '', '', ''), + (62, 'broadcast_looting_item_poor', 'It’s %item_link, everybody. Don’t get too excited.', 0, 0, '', 'C’est %item_link, tout le monde. Pas de quoi s’affoler.', 'Es ist %item_link, Leute. Nicht zu früh freuen.', '各位,这是%item_link,别太激动哦。', '', '', '', ''), + (63, 'broadcast_looting_item_poor', 'Well, at least it’s %item_link... could be worse.', 0, 0, '', 'Eh bien, au moins c’est %item_link... ça pourrait être pire.', 'Naja, wenigstens ist es %item_link... könnte schlimmer sein.', '好吧,至少是%item_link……还能更糟呢。', '', '', '', ''), + (64, 'broadcast_looting_item_poor', 'I’ll take %item_link over nothing.', 0, 0, '', 'Je préfère %item_link à rien du tout.', 'Lieber %item_link als gar nix.', '有%item_link总比啥也没有强。', '', '', '', ''), + (65, 'broadcast_looting_item_poor', 'Another %item_link. Not the worst.', 0, 0, '', 'Encore un %item_link. Pas le pire.', 'Noch ein %item_link. Nicht das Schlimmste.', '又一个%item_link,还不算最差。', '', '', '', ''), + (66, 'broadcast_looting_item_poor', 'Hey, %item_link... it’s okay, I suppose.', 0, 0, '', 'Salut, %item_link... c’est correct, je suppose.', 'Hey, %item_link... ist schon okay, denk ich.', '嘿,%item_link……还行吧,我觉得。', '', '', '', ''), + (67, 'broadcast_looting_item_normal', 'Hey, %item_link. Not bad, not great, but I’ll take it.', 0, 0, '', 'Salut, %item_link. Pas mal, pas super, mais je vais le prendre.', 'Hey, %item_link. Nicht schlecht, nicht gut, aber ich nehme es.', '嘿,%item_link。不算好也不算差,拿了再说。', '', '', '', ''), + (68, 'broadcast_looting_item_normal', 'Could be worse... I just looted %item_link.', 0, 0, '', 'Ça aurait pu être pire... Je viens juste de mettre la main sur %item_link.', 'Könnte schlimmer sein... hab gerade %item_link gelootet.', '还行吧……刚捡到个%item_link。', '', '', '', ''), + (69, 'broadcast_looting_item_normal', 'I guess %item_link will come in handy for something.', 0, 0, '', 'Je suppose que %item_link va être utile pour quelque chose.', '%item_link wird für irgendwas nützlich sein, denke ich.', '我觉得%item_link总能派上点用场。', '', '', '', ''), + (70, 'broadcast_looting_item_normal', 'Another day, another %item_link. Can’t complain.', 0, 0, '', 'Encore une journée, encore un %item_link. Je ne vais pas me plaindre.', 'Neuer Tag, neuer %item_link. Kann mich nicht beschweren.', '又是新的一天,又捡到个%item_link,挺好。', '', '', '', ''), + (71, 'broadcast_looting_item_normal', 'Well, %item_link is alright, I guess.', 0, 0, '', 'Hmm, %item_link, c’est pas mal, je suppose.', 'Naja, %item_link geht klar, würd ich sagen.', '嗯,%item_link还行吧,我觉得。', '', '', '', ''), + (72, 'broadcast_looting_item_normal', 'Just got %item_link. It’s not the best, but it’s something.', 0, 0, '', 'Je viens de récupérer %item_link. Ce n’est pas le meilleur, mais c’est mieux que rien.', 'Gerade %item_link bekommen. Nicht das Beste, aber etwas.', '刚拿到%item_link,不是最好的,但也算有点收获。', '', '', '', ''), + (73, 'broadcast_looting_item_normal', 'Could be worse, at least it’s %item_link.', 0, 0, '', 'Ça pourrait être pire, au moins c’est %item_link.', 'Könnte schlimmer sein, immerhin ist es %item_link.', '还能更糟呢,至少是%item_link。', '', '', '', ''), + (74, 'broadcast_looting_item_normal', '%item_link is good enough, I’m not mad.', 0, 0, '', '%item_link est assez bon, je ne suis pas fâché.', '%item_link ist gut genug, ich bin nicht sauer.', '%item_link还不错,我没啥好抱怨的。', '', '', '', ''), + (75, 'broadcast_looting_item_normal', 'It’s no legendary, but %item_link will do.', 0, 0, '', 'Ce n’est pas légendaire, mais %item_link fera l’affaire.', 'Kein Legendary, aber %item_link reicht.', '虽然不是传说装备,但%item_link也凑合了。', '', '', '', ''), + (76, 'broadcast_looting_item_normal', 'Not a bad drop... %item_link is alright.', 0, 0, '', 'Pas mal du tout cette trouvaille... %item_link est pas mal du tout.', 'Kein schlechter Drop... %item_link ist okay.', '掉落还行……%item_link不错。', '', '', '', ''), + (77, 'broadcast_looting_item_normal', 'I’ll take %item_link over nothing.', 0, 0, '', 'Je préfère %item_link à rien du tout.', 'Lieber %item_link als gar nix.', '有%item_link总比啥也没有强。', '', '', '', ''), + (78, 'broadcast_looting_item_normal', 'Not the best, but %item_link is a nice addition.', 0, 0, '', 'Ce n’est pas le meilleur, mais %item_link est une belle addition.', 'Nicht das Beste, aber %item_link ist eine nette Ergänzung.', '不是最好的,但%item_link也是个不错的收获。', '', '', '', ''), + (79, 'broadcast_looting_item_normal', 'Getting %item_link again. I guess it’s better than nothing.', 0, 0, '', 'Encore en train de récupérer %item_link. Je suppose que c’est mieux que rien.', 'Schon wieder %item_link. Besser als nix, denke ich.', '又拿了个%item_link,总比啥也没有好。', '', '', '', ''), + (80, 'broadcast_looting_item_normal', '%item_link again, huh? I’m not thrilled, but I’ll live.', 0, 0, '', 'Encore %item_link, hein? Je ne suis pas super enthousiaste, mais bon, je vais m’en sortir.', '%item_link schon wieder, hm? Nicht begeistert, aber ich überlebe.', '又是%item_link?不太激动,不过还能接受。', '', '', '', ''), + (81, 'broadcast_looting_item_normal', 'Another %item_link? I’m okay with that.', 0, 0, '', 'Encore un %item_link? Ça me va.', 'Noch ein %item_link? Passt schon.', '又一个%item_link?我觉得还行。', '', '', '', ''), + (82, 'broadcast_looting_item_normal', 'Could’ve gotten worse, but %item_link isn’t so bad.', 0, 0, '', 'Ça aurait pu être pire, mais %item_link n’est pas si mal.', 'Hätte schlimmer kommen können, %item_link ist gar nicht so schlecht.', '本来可能更糟,但%item_link还算可以。', '', '', '', ''), + (83, 'broadcast_looting_item_normal', 'It’s %item_link... but at least it’s something.', 0, 0, '', 'C’est %item_link... mais au moins c’est quelque chose.', 'Es ist %item_link... aber immerhin etwas.', '是%item_link……但至少是个东西。', '', '', '', ''), + (84, 'broadcast_looting_item_normal', 'I’ll take %item_link, could be worse.', 0, 0, '', 'Je vais prendre %item_link, ça aurait pu être pire.', 'Ich nehm %item_link, könnte schlimmer sein.', '我就拿%item_link吧,总比更糟的好。', '', '', '', ''), + (85, 'broadcast_looting_item_normal', 'Looted %item_link, nothing special but okay.', 0, 0, '', 'J’ai pillé %item_link, rien de spécial mais bon.', '%item_link gelootet, nix Besonderes, aber okay.', '捡了个%item_link,没啥特别的,但还行。', '', '', '', ''), + (86, 'broadcast_looting_item_normal', 'Not what I wanted, but %item_link will do for now.', 0, 0, '', 'Ce n’est pas exactement ce que je voulais, mais %item_link fera l’affaire pour l’instant.', 'Nicht das, was ich wollte, aber %item_link reicht fürs Erste.', '不是我想要的,但%item_link先凑合用吧。', '', '', '', ''), + (87, 'broadcast_looting_item_normal', 'Another %item_link. Not amazing, but it’ll do.', 0, 0, '', 'Encore un %item_link. Pas extraordinaire, mais ça fera l’affaire.', 'Noch ein %item_link. Nicht der Wahnsinn, aber geht klar.', '又一个%item_link,不算惊艳,但能用。', '', '', '', ''), + (88, 'broadcast_looting_item_normal', 'Looted %item_link, could be worse.', 0, 0, '', 'J’ai pillé %item_link, ça aurait pu être pire.', '%item_link gelootet, könnte schlimmer sein.', '捡了个%item_link,还不算太糟。', '', '', '', ''), + (89, 'broadcast_looting_item_normal', 'Well, %item_link is better than a poke in the eye.', 0, 0, '', 'Eh bien, %item_link vaut mieux que de se faire poke dans l’œil.', 'Naja, %item_link ist besser als ein Stich ins Auge.', '好吧,%item_link总比被戳眼强。', '', '', '', ''), + (90, 'broadcast_looting_item_normal', 'I’ll take %item_link and move on.', 0, 0, '', 'Je vais prendre %item_link et continuer.', 'Ich nehm %item_link und zieh weiter.', '拿上%item_link,继续前行吧。', '', '', '', ''), + (91, 'broadcast_looting_item_uncommon', 'Nice! A solid %item_link for the collection.', 0, 0, '', 'Super! Un bel %item_link pour la collection.', 'Nice! Ein solider %item_link für die Sammlung.', '不错!一个靠谱的%item_link,收好了!', '', '', '', ''), + (92, 'broadcast_looting_item_uncommon', 'I’ll take that %item_link any day.', 0, 0, '', 'Je prendrais bien ce lien de %item_link n’importe quel jour.', 'Den %item_link nehm ich jederzeit.', '这%item_link我随时都乐意拿!', '', '', '', ''), + (93, 'broadcast_looting_item_uncommon', 'Not bad, %item_link might come in handy.', 0, 0, '', 'Pas mal, %item_link pourrait s’avérer utile.', 'Nicht schlecht, %item_link könnte nützlich werden.', '还不错,%item_link说不定能派上用场。', '', '', '', ''), + (94, 'broadcast_looting_item_uncommon', 'Got myself a %item_link, now we’re talking.', 0, 0, '', 'Je me suis procuré %item_link, ça va être la folie maintenant !', 'Hab mir %item_link geholt, jetzt gehts los.', '搞到个%item_link,这下有戏了!', '', '', '', ''), + (95, 'broadcast_looting_item_uncommon', 'That %item_link is better than most, I’ll take it.', 0, 0, '', 'Ce %item_link est meilleur que la plupart, je vais le prendre.', 'Dieser %item_link ist besser als die meisten, nehm ich.', '这%item_link比大多数都好,我收下了!', '', '', '', ''), + (96, 'broadcast_looting_item_uncommon', 'I could use %item_link for something good.', 0, 0, '', 'Je pourrais utiliser %item_link pour quelque chose de bien.', 'Mit %item_link kann ich was Gutes anfangen.', '我能用%item_link干点好事。', '', '', '', ''), + (97, 'broadcast_looting_item_uncommon', 'Not bad, %item_link will work just fine.', 0, 0, '', 'Pas mal, %item_link va parfaitement convenir.', 'Nicht schlecht, %item_link passt schon.', '不错,%item_link用起来正好。', '', '', '', ''), + (98, 'broadcast_looting_item_uncommon', 'Another %item_link, at least it’s not useless.', 0, 0, '', 'Encore un %item_link, au moins ce n’est pas inutile.', 'Noch ein %item_link, immerhin nicht nutzlos.', '又一个%item_link,至少不是没用的。', '', '', '', ''), + (99, 'broadcast_looting_item_uncommon', 'That %item_link might make a decent profit.', 0, 0, '', 'Ce %item_link pourrait me rapporter un joli profit.', 'Mit %item_link lässt sich vielleicht gut verdienen.', '这%item_link说不定能卖个好价钱。', '', '', '', ''), + (100, 'broadcast_looting_item_uncommon', 'A solid %item_link, now the day is looking better.', 0, 0, '', 'Un bon %item_link, ça rend la journée plus belle maintenant.', 'Ein solider %item_link, der Tag sieht gleich besser aus.', '一个靠谱的%item_link,今天心情都变好了。', '', '', '', ''), + (101, 'broadcast_looting_item_uncommon', 'Well, I didn’t expect %item_link, but I’ll take it.', 0, 0, '', 'Eh bien, je ne m’attendais pas à %item_link, mais je vais le prendre.', 'Hab %item_link nicht erwartet, aber ich nehm es.', '嗯,没想到会拿到%item_link,但我收下了。', '', '', '', ''), + (102, 'broadcast_looting_item_uncommon', 'Not sure what to do with %item_link, but I’m happy to have it.', 0, 0, '', 'Je ne suis pas sûr de ce qu’il faut faire avec %item_link, mais je suis content de l’avoir.', 'Kein Plan, was ich mit %item_link machen soll aber ich freue mich drüber.', '不太确定%item_link干啥用,但我很开心能拿到。', '', '', '', ''), + (103, 'broadcast_looting_item_uncommon', 'Not my first choice, but I’ll happily accept %item_link.', 0, 0, '', 'Ce n’était pas mon premier choix, mais je vais volontiers accepter %item_link.', 'Nicht meine erste Wahl, aber %item_link nehm ich gern.', '不是我的首选,但我很乐意收下%item_link。', '', '', '', ''), + (104, 'broadcast_looting_item_uncommon', 'That %item_link is exactly what I needed today.', 0, 0, '', 'Ce %item_link est exactement ce dont j’avais besoin aujourd’hui.', 'Genau das hab ich heute gebraucht: %item_link.', '这%item_link正是我今天需要的!', '', '', '', ''), + (105, 'broadcast_looting_item_uncommon', 'I’ll never say no to a nice %item_link.', 0, 0, '', 'Je ne dis jamais non à une belle %item_link.', 'Zu einem schönen %item_link sag ich nie nein.', '对一个不错的%item_link,我从不说不。', '', '', '', ''), + (106, 'broadcast_looting_item_uncommon', 'Another %item_link. I’ll take it, no complaints here.', 0, 0, '', 'Encore un %item_link. Je le prends, pas de soucis ici.', 'Noch ein %item_link. Nehm ich, keine Beschwerden.', '又一个%item_link,我收下,没啥好抱怨的。', '', '', '', ''), + (107, 'broadcast_looting_item_uncommon', 'At least %item_link has some use. Not the worst loot.', 0, 0, '', 'Au moins %item_link a une certaine utilité. Ce n’est pas le pire butin.', 'Immerhin hat %item_link irgendeinen Nutzen. Nicht der schlimmste Loot.', '至少%item_link有点用,不是最差的战利品。', '', '', '', ''), + (108, 'broadcast_looting_item_uncommon', '%item_link might just be the one thing I need for the day.', 0, 0, '', 'Hmm, le lien %item_link pourrait bien être exactement ce dont j’ai besoin pour la journée.', '%item_link ist vielleicht genau das, was ich heute brauche.', '或许%item_link正是我今天需要的东西。', '', '', '', ''), + (109, 'broadcast_quest_accepted_generic', 'I have just taken the %quest_link', 0, 0, '', 'Je viens de commencer %quest_link.', 'Ich hab gerade %quest_link angenommen', '我刚接了%quest_link任务。', '', '', '', 'Я только что взял %quest_link'), + (110, 'broadcast_quest_accepted_generic', 'just accepted %quest_link', 0, 0, '', 'Je viens d’accepter %quest_link', 'gerade %quest_link angenommen', '刚接受了%quest_link任务。', '', '', '', 'только что принял %quest_link'), + (111, 'broadcast_quest_accepted_generic', '%quest_link gonna try to complete this one', 0, 0, '', '%quest_link Je vais essayer de terminer celle-ci.', '%quest_link - versuch das mal abzuschließen', '%quest_link,这个任务我得试着完成。', '', '', '', '%quest_link попробую выполнить это задание'), + (112, 'broadcast_quest_accepted_generic', 'took %quest_link in %zone_name', 0, 0, '', 'J’ai pris %quest_link dans %zone_name.', '%quest_link in %zone_name angenommen', '在%zone_name接下了%quest_link任务。', '', '', '', 'взял %quest_link в %zone_name'), + (113, 'broadcast_quest_accepted_generic', 'I just picked up %quest_link, time to get to work!', 0, 0, '', 'Je viens de prendre %quest_link, c’est parti pour se mettre au travail !', 'Gerade %quest_link abgeholt, Zeit ranzuklotzen!', '刚接到%quest_link,是时候开干了!', '', '', '', ''), + (114, 'broadcast_quest_accepted_generic', 'Got %quest_link, let’s see what this one is about.', 0, 0, '', 'J’ai obtenu %quest_link, voyons de quoi il s’agit.', '%quest_link eingesackt, mal sehen, worum es geht.', '拿到了%quest_link,看看这是个啥任务。', '', '', '', ''), + (115, 'broadcast_quest_accepted_generic', 'Just grabbed %quest_link, let’s do this!', 0, 0, '', 'Je viens de prendre %quest_link, allons-y!', '%quest_link geschnappt, Los gehts!', '刚抢到%quest_link,走起!', '', '', '', ''), + (116, 'broadcast_quest_accepted_generic', 'Another quest, another %quest_link. Let’s go!', 0, 0, '', 'Encore une autre quête, encore %quest_link. Allons-y!', 'Neue Quest, %quest_link. Los!', '又一个任务,又是%quest_link,走吧!', '', '', '', ''), + (117, 'broadcast_quest_accepted_generic', 'Looks like it’s %quest_link time, wish me luck.', 0, 0, '', 'Il semblerait que c’est l’heure de faire %quest_link, souhaitez-moi bonne chance.', 'Sieht so aus, als wäre jetzt %quest_link dran, wünscht mir Glück.', '看来是时候做%quest_link了,祝我好运吧。', '', '', '', ''), + (118, 'broadcast_quest_accepted_generic', 'Accepted %quest_link, let’s see how tough this one is.', 0, 0, '', 'Accepté %quest_link, eh bien, voyons à quel point celle-ci est difficile.', '%quest_link angenommen, mal sehen, wie hart die ist.', '接受了%quest_link,看看这任务有多难。', '', '', '', ''), + (119, 'broadcast_quest_accepted_generic', 'Picked up %quest_link, time to go make some progress.', 0, 0, '', 'J’ai récupéré %quest_link, il est temps d’avancer et de progresser.', '%quest_link mitgenommen, Zeit für Fortschritt.', '拿下%quest_link,该去干点活儿了。', '', '', '', ''), + (120, 'broadcast_quest_accepted_generic', 'Just took %quest_link. Let’s see how hard this one is.', 0, 0, '', 'Je viens de commencer %quest_link. Voyons à quel point elle est difficile.', 'Gerade %quest_link genommen. Mal sehen, wie schwer die ist.', '刚接了%quest_link,看看这任务有多棘手。', '', '', '', ''), + (121, 'broadcast_quest_accepted_generic', 'Accepted %quest_link, let’s hope there’s no surprise boss at the end.', 0, 0, '', 'Accepté %quest_link, espérons qu’il n’y ait pas de boss surprise à la fin.', '%quest_link angenommen, hoffentlich kein Überraschungsboss am Ende.', '接了%quest_link,希望最后别蹦出个惊喜首领。', '', '', '', ''), + (122, 'broadcast_quest_accepted_generic', 'On my way to complete %quest_link. Here we go!', 0, 0, '', 'En route pour terminer ma quête %quest_link! C’est parti!', 'Unterwegs, %quest_link abzuschließen. Auf gehts!', '正在赶去完成%quest_link,走起!', '', '', '', ''), + (123, 'broadcast_quest_accepted_generic', 'I guess I’m doing %quest_link now. Let’s hope it’s worth it.', 0, 0, '', 'Je suppose que je me lance dans %quest_link maintenant. Espérons que cela en vaille la peine.', 'Dann mach ich jetzt wohl %quest_link. Hoffentlich lohnt sichs.', '看来现在得做%quest_link,希望值回票价。', '', '', '', ''), + (124, 'broadcast_quest_accepted_generic', 'Picked up %quest_link. Let’s get moving!', 0, 0, '', 'J’ai récupéré %quest_link. Allez, on se bouge !', '%quest_link eingesackt. Weiter gehts!', '拿到了%quest_link,赶紧行动起来!', '', '', '', ''), + (125, 'broadcast_quest_accepted_generic', 'I’ve just taken %quest_link. This should be a piece of cake.', 0, 0, '', 'Je viens juste de prendre %quest_link. Ça devrait être facile comme tout.', 'Hab gerade %quest_link angenommen. Sollte Kinderkram sein.', '刚接了%quest_link,这应该很简单。', '', '', '', ''), + (126, 'broadcast_quest_accepted_generic', 'I accepted %quest_link, now to knock it out of the park.', 0, 0, '', 'J’ai accepté %quest_link, maintenant il va falloir assurer comme jamais.', '%quest_link angenommen, jetzt rock ich das.', '我接了%quest_link,现在得干得漂亮。', '', '', '', ''), + (127, 'broadcast_quest_accepted_generic', 'Took %quest_link in %zone_name. Let’s do this!', 0, 0, '', 'J’ai pris %quest_link dans %zone_name. Allons-y!', '%quest_link in %zone_name angenommen. Los gehts!', '在%zone_name接了%quest_link,干吧!', '', '', '', ''), + (128, 'broadcast_quest_accepted_generic', 'On my way to finish %quest_link. Let’s make it quick!', 0, 0, '', 'En route pour terminer %quest_link. Allez, on se dépêche !', 'Auf dem Weg, %quest_link zu beenden. Machen wirs schnell!', '正在去完成%quest_link,赶紧搞定!', '', '', '', ''), + (129, 'broadcast_quest_accepted_generic', 'Accepted %quest_link. Wonder what awaits me in %zone_name.', 0, 0, '', 'Quête %quest_link acceptée. Je me demande ce qui m’attend dans la zone %zone_name.', '%quest_link angenommen. Mal sehen, was in %zone_name auf mich wartet.', '接了%quest_link,不知道%zone_name会有啥等着我。', '', '', '', ''), + (130, 'broadcast_quest_accepted_generic', 'Got %quest_link, hope it doesn’t take forever.', 0, 0, '', 'J’ai attrapé %quest_link, j’espère que ça ne prendra pas une éternité.', '%quest_link bekommen, hoffentlich dauert das nicht ewig.', '拿到了%quest_link,希望别拖太久。', '', '', '', ''), + (131, 'broadcast_quest_accepted_generic', 'Just grabbed %quest_link. Here we go again.', 0, 0, '', 'Je viens de récupérer %quest_link. C’est reparti pour une nouvelle aventure.', '%quest_link geschnappt. Schon wieder von vorn.', '刚抢到%quest_link,又开始了。', '', '', '', ''), + (132, 'broadcast_quest_accepted_generic', 'Looks like %quest_link is the next one on my list.', 0, 0, '', 'Il semblerait que %quest_link soit le prochain sur ma liste.', 'Es sieht so aus, als ob %quest_link als nächstes dran ist.', '看来%quest_link是我列表上的下一个任务。', '', '', '', ''), + (133, 'broadcast_quest_accepted_generic', 'Off to complete %quest_link. I hope this one is fun.', 0, 0, '', 'En route pour accomplir %quest_link. J’espère que celle-ci est amusante.', 'Unterwegs, %quest_link abzuschließen. Hoffe, die macht Spaß.', '去完成%quest_link,希望这任务有点意思。', '', '', '', ''), + (134, 'broadcast_quest_accepted_generic', 'I’m diving into %quest_link. This will be an adventure!', 0, 0, '', 'Je plonge dans %quest_link. Ça s’annonce comme une aventure passionnante !', 'Ich stürze mich in %quest_link. Das wird ein Abenteuer!', '我一头扎进%quest_link,这会是一场冒险!', '', '', '', ''), + (135, 'broadcast_quest_accepted_generic', 'Accepted %quest_link in %zone_name. Let’s see what this brings!', 0, 0, '', '%Quête acceptée dans %zone_name. Voyons ce que cela nous réserve !', '%quest_link in %zone_name angenommen. Mal sehen, was das bringt!', '在%zone_name接了%quest_link,看看会发生啥!', '', '', '', ''), + (136, 'broadcast_quest_accepted_generic', 'Just took %quest_link. Time to see what I’m up against.', 0, 0, '', 'Je viens de prendre %quest_link. C’est l’heure de voir à quoi je dois faire face.', 'Gerade %quest_link genommen. Zeit nachzuschauen, was mich erwartet.', '刚接了%quest_link,是时候看看我要面对啥了。', '', '', '', ''), + (137, 'broadcast_quest_accepted_generic', 'Another %quest_link. Let’s get this done quickly!', 0, 0, '', 'Encore une %quest_link. On va régler ça rapidement!', 'Nochmal %quest_link. Ziehen wirs schnell durch!', '又一个%quest_link,赶紧搞定吧!', '', '', '', ''), + (138, 'broadcast_quest_accepted_generic', 'I just took %quest_link. This will be a fun one!', 0, 0, '', 'Je viens de prendre %quest_link. Ça promet d’être amusant !', 'Ich hab gerade %quest_link genommen. Das wird spaßig!', '刚接了%quest_link,这任务应该挺有趣!', '', '', '', ''), + (139, 'broadcast_quest_accepted_generic', 'Taking on %quest_link in %zone_name. Let’s go!', 0, 0, '', 'Je me lance dans %quest_link dans la région de %zone_name. C’est parti !', '%quest_link in %zone_name angenommen. Los!', '在%zone_name挑战%quest_link,走起!', '', '', '', ''), + (140, 'broadcast_quest_accepted_generic', 'Grabbed %quest_link. Can’t wait to see what happens next.', 0, 0, '', 'Attrapé %quest_link. Hâte de voir ce qui se passe ensuite.', '%quest_link geschnappt. Kann es kaum erwarten, was als Nächstes kommt.', '抢到%quest_link,迫不及待想知道接下来会发生啥。', '', '', '', ''), + (141, 'broadcast_quest_accepted_generic', 'Took %quest_link. Time to dive in and get it done.', 0, 0, '', 'J’ai pris %quest_link. Il est temps de s’y mettre et de le réaliser.', '%quest_link genommen. Abtauchen und durchziehen.', '接了%quest_link,是时候埋头苦干了。', '', '', '', ''), + (142, 'broadcast_quest_accepted_generic', 'Alright, %quest_link is on. Let’s get to work!', 0, 0, '', 'D’accord, %quest_link est en marche. Au travail !', 'Alles klar, %quest_link ist aktiv. An die Arbeit!', '好吧,%quest_link开始了,干活去!', '', '', '', ''), + (143, 'broadcast_quest_accepted_generic', 'On to %quest_link. Let’s hope it’s not too hard.', 0, 0, '', 'En route vers %quest_link. Espérons que ce ne soit pas trop difficile.', 'Weiter zu %quest_link. Hoffentlich nicht zu hart.', '前往做%quest_link,希望别太难。', '', '', '', ''), + (144, 'broadcast_quest_accepted_generic', 'Just grabbed %quest_link. Should be an easy one!', 0, 0, '', 'Je viens de saisir %quest_link. Ça devrait être une mission facile !', 'Gerade %quest_link geschnappt. Sollte easy sein!', '刚抢到%quest_link,应该挺简单的!', '', '', '', ''), + (145, 'broadcast_quest_accepted_generic', 'I’ve accepted %quest_link. Let’s make it quick!', 0, 0, '', 'J’ai accepté %quest_link. Allons-y rapidement!', 'Ich hab %quest_link angenommen. Machen wirs fix!', '我接了%quest_link,赶紧完成吧!', '', '', '', ''), + (146, 'broadcast_quest_accepted_generic', 'Took %quest_link, now to see what it’s all about.', 0, 0, '', 'J’ai pris %quest_link, maintenant je vais voir de quoi il s’agit.', '%quest_link genommen, mal schauen, worum es geht.', '接了%quest_link,现在看看这是个啥任务。', '', '', '', ''), + (147, 'broadcast_quest_accepted_generic', 'Just accepted %quest_link, let’s get moving!', 0, 0, '', 'Je viens d’accepter la %quest_link, allons-y !', 'Gerade %quest_link angenommen, weiter gehts!', '刚接受了%quest_link,动起来吧!', '', '', '', ''), + (148, 'broadcast_quest_update_add_kill_objective_completed', 'Finally done with the %quest_obj_name for %quest_link', 0, 0, '', 'Enfin terminé avec la quête %quest_obj_name pour %quest_link.', 'Endlich fertig mit %quest_obj_name für %quest_link', '终于完成了%quest_link的%quest_obj_name任务目标!', '', '', '', 'Наконец-то закончил %quest_obj_name для %quest_link'), + (149, 'broadcast_quest_update_add_kill_objective_completed', 'finally got %quest_obj_available/%quest_obj_required of %quest_obj_name for the %quest_link', 0, 0, '', 'J’ai enfin obtenu %quest_obj_available/%quest_obj_required de %quest_obj_name pour %quest_link.', 'endlich %quest_obj_available/%quest_obj_required von %quest_obj_name für %quest_link bekommen', '终于在%quest_link任务中拿到了%quest_obj_available/%quest_obj_required个%quest_obj_name!', '', '', '', 'наконец-то получил %quest_obj_available/%quest_obj_required %quest_obj_name для %quest_link'), + (150, 'broadcast_quest_update_add_kill_objective_completed', '%quest_obj_full_formatted for the %quest_link, at last', 0, 0, '', 'Enfin, j’ai terminé la %quest_obj_full_formatted pour %quest_link.', '%quest_obj_full_formatted für %quest_link, endlich', '终于搞定了%quest_link的%quest_obj_full_formatted!', '', '', '', '%quest_obj_full_formatted для %quest_link, наконец-то'), + (151, 'broadcast_quest_update_add_kill_objective_progress', 'Oof, got %quest_obj_available/%quest_obj_required %quest_obj_name for %quest_link', 0, 0, '', 'Oof, j’ai obtenu %quest_obj_available/%quest_obj_required %quest_obj_name pour %quest_link.', 'Uff, %quest_obj_available/%quest_obj_required %quest_obj_name für %quest_link bekommen', '呼,完成了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name。', '', '', '', 'Ох, получил %quest_obj_available/%quest_obj_required %quest_obj_name для %quest_link'), + (152, 'broadcast_quest_update_add_kill_objective_progress', 'still need %quest_obj_missing more of %quest_obj_name for %quest_link', 0, 0, '', 'Il me manque encore %quest_obj_missing de %quest_obj_name pour %quest_link.', 'brauche noch %quest_obj_missing von %quest_obj_name für %quest_link', '还差%quest_obj_missing个%quest_obj_name才能完成%quest_link。', '', '', '', 'еще нужно %quest_obj_missing %quest_obj_name для %quest_link'), + (153, 'broadcast_quest_update_add_kill_objective_progress', '%quest_obj_full_formatted, still working on %quest_link', 0, 0, '', 'Je suis toujours en train de travailler sur %quest_obj_full_formatted, ça prend un peu de temps mais je vais y arriver.', '%quest_obj_full_formatted, arbeite noch an %quest_link', '%quest_obj_full_formatted完成了,还在努力做%quest_link。', '', '', '', '%quest_obj_full_formatted, все еще работаю над %quest_link'), + (154, 'broadcast_quest_update_add_kill_objective_completed', '%quest_obj_name complete for %quest_link, what a grind!', 0, 0, '', '%quest_obj_name complété pour %quest_link, quelle corvée !', '%quest_obj_name für %quest_link fertig, was für ein Grind!', '%quest_obj_name完成了%quest_link,真是累死人!', '', '', '', ''), + (155, 'broadcast_quest_update_add_kill_objective_progress', 'Getting close to completing %quest_obj_name for %quest_link, just need %quest_obj_missing more.', 0, 0, '', 'Je suis sur le point de terminer %quest_obj_name pour %quest_link, il me manque juste %quest_obj_missing.', 'Kurz vor Abschluss von %quest_obj_name für %quest_link, fehlen nur noch %quest_obj_missing.', '快完成%quest_link的%quest_obj_name了,只差%quest_obj_missing个。', '', '', '', ''), + (156, 'broadcast_quest_update_add_kill_objective_progress', '%quest_obj_name is almost done, need just %quest_obj_missing more for %quest_link.', 0, 0, '', 'Il ne manque presque rien à %quest_obj_name, j’ai juste besoin de %quest_obj_missing de plus pour %quest_link.', '%quest_obj_name fast durch, brauche nur noch %quest_obj_missing für %quest_link.', '%quest_obj_name快完成了,%quest_link还差%quest_obj_missing个。', '', '', '', ''), + (157, 'broadcast_quest_update_add_kill_objective_progress', 'So far, %quest_obj_available/%quest_obj_required %quest_obj_name completed for %quest_link.', 0, 0, '', 'Jusqu’à présent, %quest_obj_available/%quest_obj_required %quest_obj_name ont été complétés pour %quest_link.', 'Bisher %quest_obj_available/%quest_obj_required %quest_obj_name für %quest_link erledigt.', '目前完成了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name。', '', '', '', ''), + (158, 'broadcast_quest_update_add_kill_objective_completed', 'Finally finished %quest_obj_name for %quest_link! That took a while!', 0, 0, '', 'J’ai enfin terminé %quest_obj_name pour %quest_link! Ça m’a pris un moment!', 'Endlich %quest_obj_name für %quest_link fertig! Das hat gedauert!', '终于完成了%quest_link的%quest_obj_name,花了好长时间!', '', '', '', ''), + (159, 'broadcast_quest_update_add_kill_objective_progress', 'Still going strong, got %quest_obj_available/%quest_obj_required of %quest_obj_name for %quest_link.', 0, 0, '', 'Toujours en pleine forme, j’ai réussi à obtenir %quest_obj_available/%quest_obj_required de %quest_obj_name pour %quest_link.', 'Läuft, hab %quest_obj_available/%quest_obj_required von %quest_obj_name für %quest_link.', '进展顺利,拿到了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name。', '', '', '', ''), + (160, 'broadcast_quest_update_add_kill_objective_completed', 'Yes! Completed %quest_obj_name for %quest_link. On to the next one!', 0, 0, '', 'Oui! J’ai fini %quest_obj_name pour %quest_link. Passons à la prochaine!', 'Yes! %quest_obj_name für %quest_link abgeschlossen. Weiter gehts!', '好耶!完成了%quest_link的%quest_obj_name,下一目标!', '', '', '', ''), + (161, 'broadcast_quest_update_add_kill_objective_progress', 'Made progress on %quest_obj_name, %quest_obj_available/%quest_obj_required completed for %quest_link.', 0, 0, '', 'J’ai fait des progrès sur l’objectif de quête, j’ai terminé %quest_obj_available/%quest_obj_required pour %quest_link.', 'Fortschritt bei %quest_obj_name, %quest_obj_available/%quest_obj_required erledigt für %quest_link.', '在%quest_obj_name上有了进展,完成了%quest_link的%quest_obj_available/%quest_obj_required。', '', '', '', ''), + (162, 'broadcast_quest_update_add_kill_objective_progress', '%quest_obj_full_formatted... almost there for %quest_link!', 0, 0, '', '%quest_obj_full_formatted... presque arrivé pour %quest_link!', '%quest_obj_full_formatted... fast geschafft für %quest_link!', '%quest_obj_full_formatted…快搞定%quest_link了!', '', '', '', ''), + (163, 'broadcast_quest_update_add_kill_objective_progress', 'Just need %quest_obj_missing more %quest_obj_name for %quest_link, almost done!', 0, 0, '', 'J’ai juste besoin de quelques %quest_obj_missing supplémentaires de %quest_obj_name pour %quest_link, presque terminé !', 'Brauche nur noch %quest_obj_missing %quest_obj_name für %quest_link, fast fertig!', '只差%quest_obj_missing个%quest_obj_name就能完成%quest_link,快好了!', '', '', '', ''), + (164, 'broadcast_quest_update_add_kill_objective_completed', 'Finally done with %quest_obj_name for %quest_link, what a relief!', 0, 0, '', 'Enfin terminé avec %quest_obj_name pour %quest_link, quel soulagement !', 'Endlich %quest_obj_name für %quest_link geschafft, was für eine Erleichterung!', '终于搞定了%quest_link的%quest_obj_name,真是松了一口气!', '', '', '', ''), + (165, 'broadcast_quest_update_add_kill_objective_progress', 'Got %quest_obj_available/%quest_obj_required of %quest_obj_name for %quest_link, making progress!', 0, 0, '', 'J’ai obtenu %quest_obj_available/%quest_obj_required de %quest_obj_name pour %quest_link, je progresse !', 'Hab %quest_obj_available/%quest_obj_required von %quest_obj_name für %quest_link, geht voran!', '拿到了%quest_link的%quest_obj_available/%quest_obj_required个%quest_obj_name,进展不错!', '', '', '', ''), + (166, 'broadcast_quest_update_add_kill_objective_completed', 'The %quest_obj_name for %quest_link is complete! Now for the next task.', 0, 0, '', 'La %quest_obj_name pour %quest_link est terminée ! Maintenant, passons à la tâche suivante.', '%quest_obj_name für %quest_link ist komplett! Weiter zur nächsten Aufgabe.', '%quest_link的%quest_obj_name完成了!接下来是下一个目标。', '', '', '', ''), + (167, 'broadcast_quest_update_add_kill_objective_completed', '100% of %quest_obj_name done for %quest_link, now onto the rewards!', 0, 0, '', 'J’ai terminé 100% de %quest_obj_name pour %quest_link, passons maintenant aux récompenses!', '100% von %quest_obj_name für %quest_link erledigt, ab zu den Belohnungen!', '完成了 %quest_obj_name 的 100% 进度,为 %quest_link,接下来就是领取奖励了!', '', '', '', ''), + (168, 'broadcast_quest_update_add_kill_objective_progress', 'Still need %quest_obj_missing more of %quest_obj_name for %quest_link, almost done!', 0, 0, '', 'J’ai encore besoin de %quest_obj_missing de %quest_obj_name pour %quest_link, presque terminé !', 'Es fehlen noch %quest_obj_missing von %quest_obj_name für %quest_link, fast fertig!', '还差 %quest_obj_missing 个 %quest_obj_name 就能完成 %quest_link,快搞定了!', '', '', '', ''), + (169, 'broadcast_quest_update_add_kill_objective_progress', 'Working on %quest_obj_name for %quest_link, currently %quest_obj_available/%quest_obj_required done.', 0, 0, '', 'Je travaille sur la quête %quest_obj_name pour %quest_link, j’ai actuellement accompli %quest_obj_available sur %quest_obj_required étapes.', 'Arbeite an %quest_obj_name für %quest_link, aktuell %quest_obj_available/%quest_obj_required erledigt.', '正在为 %quest_link 搞定 %quest_obj_name,目前完成了 %quest_obj_available/%quest_obj_required。', '', '', '', ''), + (170, 'broadcast_quest_update_add_kill_objective_completed', '%quest_obj_full_formatted for %quest_link. That was a long one!', 0, 0, '', '%quest_obj_full_formatted pour %quest_link. Quelle aventure épique!', '%quest_obj_full_formatted für %quest_link. Das war ein langer Brocken!', '完成了 %quest_obj_full_formatted,为 %quest_link。这任务真够长的!', '', '', '', ''), + (171, 'broadcast_quest_update_add_kill_objective_progress', 'Just need a few more of %quest_obj_name for %quest_link, and I’m done!', 0, 0, '', 'J’ai juste besoin de quelques %quest_obj_name de plus pour %quest_link, et j’ai fini!', 'Brauche nur noch ein paar %quest_obj_name für %quest_link, dann bin ich durch!', '就差几个 %quest_obj_name 就能完成 %quest_link,马上就搞定啦!', '', '', '', ''), + (172, 'broadcast_quest_update_add_kill_objective_completed', 'Another objective completed! %quest_obj_full_formatted for %quest_link', 0, 0, '', 'Encore un objectif accompli! %quest_obj_full_formatted pour %quest_link', 'Noch ein Ziel erledigt! %quest_obj_full_formatted für %quest_link', '又一个目标完成!%quest_obj_full_formatted,为 %quest_link。', '', '', '', ''), + (173, 'broadcast_quest_update_add_kill_objective_progress', 'Still grinding on %quest_obj_name for %quest_link, almost there with %quest_obj_available/%quest_obj_required.', 0, 0, '', 'Je continue de travailler sur %quest_obj_name pour %quest_link, j’y suis presque avec %quest_obj_available/%quest_obj_required.', 'Grinde weiter %quest_obj_name für %quest_link, mit %quest_obj_available/%quest_obj_required fast da.', '还在为 %quest_link 刷 %quest_obj_name,快到了,目前 %quest_obj_available/%quest_obj_required。', '', '', '', ''), + (174, 'broadcast_quest_update_add_kill_objective_completed', 'Objective done! %quest_obj_full_formatted for %quest_link. On to the next one!', 0, 0, '', 'Objectif accompli ! J’ai rempli l’objectif pour la quête « %quest_link ». Passons à la suivante !', 'Ziel erledigt! %quest_obj_full_formatted für %quest_link. Weiter zum nächsten!', '目标完成!%quest_obj_full_formatted,为 %quest_link。下一个!', '', '', '', ''), + (175, 'broadcast_quest_update_add_item_objective_completed', 'Finally done with the %item_link for %quest_link', 0, 0, '', 'Enfin fini avec l’%item_link pour %quest_link.', 'Endlich fertig mit %item_link für %quest_link', '终于搞定了 %item_link,为 %quest_link!', '', '', '', 'Наконец-то закончил с %item_link для %quest_link'), + (176, 'broadcast_quest_update_add_item_objective_completed', 'finally got %quest_obj_available/%quest_obj_required of %item_link for the %quest_link', 0, 0, '', 'J’ai enfin obtenu %quest_obj_available/%quest_obj_required de %item_link pour la %quest_link.', 'endlich %quest_obj_available/%quest_obj_required von %item_link für %quest_link bekommen', '终于收集到 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link!', '', '', '', 'наконец-то получил %quest_obj_available/%quest_obj_required %item_link для %quest_link'), + (177, 'broadcast_quest_update_add_item_objective_completed', '%quest_obj_full_formatted for the %quest_link, at last', 0, 0, '', 'La quête est enfin terminée pour %quest_link.', '%quest_obj_full_formatted für %quest_link, endlich', '终于完成了 %quest_obj_full_formatted,为 %quest_link!', '', '', '', '%quest_obj_full_formatted для %quest_link, наконец-то'), + (178, 'broadcast_quest_update_add_item_objective_progress', 'Oof, got %quest_obj_available/%quest_obj_required %item_link for %quest_link', 0, 0, '', 'Ouf, j’ai obtenu %quest_obj_available sur %quest_obj_required %item_link pour la quête %quest_link.', 'Uff, %quest_obj_available/%quest_obj_required %item_link für %quest_link bekommen', '哎呀,搞到 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link。', '', '', '', 'Ох, получил %quest_obj_available/%quest_obj_required %item_link для %quest_link'), + (179, 'broadcast_quest_update_add_item_objective_progress', 'still need %quest_obj_missing more of %item_link for %quest_link', 0, 0, '', 'Il me faut %quest_obj_missing encore plus de %item_link pour la quête %quest_link.', 'brauche noch %quest_obj_missing %item_link für %quest_link', '还差 %quest_obj_missing 个 %item_link 就能完成 %quest_link。', '', '', '', 'еще нужно %quest_obj_missing %item_link для %quest_link'), + (180, 'broadcast_quest_update_add_item_objective_progress', '%quest_obj_full_formatted, still working on %quest_link', 0, 0, '', 'Désolé, je suis toujours en train de travailler sur %quest_link.', '%quest_obj_full_formatted, arbeite noch an %quest_link', '完成了 %quest_obj_full_formatted,还在为 %quest_link 努力。', '', '', '', '%quest_obj_full_formatted, все еще работаю над %quest_link'), + (181, 'broadcast_quest_update_add_item_objective_completed', 'Completed the %item_link objective for %quest_link. Finally!', 0, 0, '', 'J’ai terminé l’objectif %item_link de la quête %quest_link. Enfin!', 'Das Ziel mit %item_link für %quest_link abgeschlossen. Endlich!', '完成了 %item_link 目标,为 %quest_link。终于搞定!', '', '', '', ''), + (182, 'broadcast_quest_update_add_item_objective_progress', 'Making progress! %quest_obj_available/%quest_obj_required %item_link for %quest_link.', 0, 0, '', 'Je progresse! J’ai actuellement %quest_obj_available sur les %quest_obj_required %item_link requis pour la quête %quest_link.', 'Es geht voran! %quest_obj_available/%quest_obj_required %item_link für %quest_link.', '有进展!目前有 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link。', '', '', '', ''), + (183, 'broadcast_quest_update_add_item_objective_progress', '%quest_obj_name is halfway done, need %quest_obj_missing more %item_link for %quest_link.', 0, 0, '', '%quest_obj_name est à moitié terminée, il me manque %quest_obj_missing %item_link pour %quest_link.', '%quest_obj_name ist zur Hälfte durch, brauche noch %quest_obj_missing %item_link für %quest_link.', '%quest_obj_name 完成了一半,还差 %quest_obj_missing 个 %item_link 为 %quest_link。', '', '', '', ''), + (184, 'broadcast_quest_update_add_item_objective_progress', 'Still need %quest_obj_missing of %item_link for %quest_link, but getting closer!', 0, 0, '', 'Il me manque encore %quest_obj_missing de %item_link pour %quest_link, mais je me rapproche !', 'Es fehlen noch %quest_obj_missing von %item_link für %quest_link, aber ich komme näher!', '还差 %quest_obj_missing 个 %item_link 为 %quest_link,不过越来越近了!', '', '', '', ''), + (185, 'broadcast_quest_update_add_item_objective_completed', 'Finally finished %item_link for %quest_link, now time for the next task!', 0, 0, '', 'J’ai enfin terminé %item_link pour la quête %quest_link, il est temps de passer à la prochaine tâche !', 'Endlich %item_link für %quest_link fertig, weiter mit der nächsten Aufgabe!', '终于完成了 %item_link 为 %quest_link,接下来是下一个任务!', '', '', '', ''), + (186, 'broadcast_quest_update_add_item_objective_progress', 'Almost there! Got %quest_obj_available/%quest_obj_required %item_link for %quest_link.', 0, 0, '', 'On y est presque! J’ai obtenu %quest_obj_available/%quest_obj_required %item_link pour %quest_link.', 'Fast da! %quest_obj_available/%quest_obj_required %item_link für %quest_link geholt.', '快搞定了!已经拿到 %quest_obj_available/%quest_obj_required 个 %item_link,为 %quest_link。', '', '', '', ''), + (187, 'broadcast_quest_update_add_item_objective_completed', 'Yes! Completed %item_link for %quest_link. Now moving on to the next objective.', 0, 0, '', 'Oui ! J’ai terminé %item_link pour %quest_link. Je passe maintenant à l’objectif suivant.', 'Yes! %item_link für %quest_link abgeschlossen. Weiter zum nächsten Ziel.', '好嘞!完成了 %item_link 为 %quest_link。下一个目标走起!', '', '', '', ''), + (188, 'broadcast_quest_update_add_item_objective_progress', 'Still need %quest_obj_missing more %item_link for %quest_link, but progress is being made!', 0, 0, '', 'Il me manque encore %quest_obj_missing %item_link pour la quête %quest_link, mais je progresse!', 'Brauche noch %quest_obj_missing %item_link für %quest_link, aber es geht voran!', '还差 %quest_obj_missing 个 %item_link 为 %quest_link,不过已经在进步了!', '', '', '', ''), + (189, 'broadcast_quest_update_add_item_objective_progress', 'Made good progress on %item_link for %quest_link, need %quest_obj_missing more.', 0, 0, '', 'J’ai bien avancé sur %item_link pour %quest_link, mais il me manque encore %quest_obj_missing.', 'Guter Fortschritt bei %item_link für %quest_link, brauche noch %quest_obj_missing.', '在 %item_link 上有不错进展,为 %quest_link,还差 %quest_obj_missing 个。', '', '', '', ''), + (190, 'broadcast_quest_update_add_item_objective_completed', '100% of %item_link done for %quest_link! Time to move forward.', 0, 0, '', 'J’ai complété 100% de %item_link pour %quest_link! Il est temps de passer à la suite.', '100% von %item_link für %quest_link erledigt! Weiter gehts.', '100% 完成 %item_link 为 %quest_link!该继续前进了。', '', '', '', ''), + (191, 'broadcast_quest_update_add_item_objective_progress', 'Got %quest_obj_available/%quest_obj_required %item_link for %quest_link. I can feel the finish line!', 0, 0, '', 'J’ai obtenu %quest_obj_available/%quest_obj_required %item_link pour %quest_link. Je sens que je touche au but!', 'Hab %quest_obj_available/%quest_obj_required %item_link für %quest_link. Das Ziel ist in Sicht!', '拿到 %quest_obj_available/%quest_obj_required 个 %item_link 为 %quest_link,终点就在眼前!', '', '', '', ''), + (192, 'broadcast_quest_update_add_item_objective_completed', 'That’s it, %item_link complete for %quest_link. What a relief!', 0, 0, '', 'C’est bon, %item_link complet pour %quest_link. Quel soulagement !', 'Geschafft, %item_link für %quest_link komplett. Was für eine Erleichterung!', '搞定,完成了 %item_link 为 %quest_link。松了一口气!', '', '', '', ''), + (193, 'broadcast_quest_update_add_item_objective_progress', 'Working hard on %item_link for %quest_link, still need %quest_obj_missing more.', 0, 0, '', 'Je travaille dur sur %item_link pour %quest_link, mais j’ai encore besoin de %quest_obj_missing de plus.', 'Arbeite hart an %item_link für %quest_link, brauche noch %quest_obj_missing.', '正在努力搞 %item_link 为 %quest_link,还差 %quest_obj_missing 个。', '', '', '', ''), + (194, 'broadcast_quest_update_add_item_objective_progress', 'Almost finished with %item_link for %quest_link, just %quest_obj_missing more left.', 0, 0, '', 'Presque terminé avec %item_link pour %quest_link, il ne reste plus que %quest_obj_missing.', 'Fast fertig mit %item_link für %quest_link, es fehlen nur noch %quest_obj_missing.', '快搞定 %item_link 为 %quest_link了,只剩 %quest_obj_missing 个。', '', '', '', ''), + (195, 'broadcast_quest_update_add_item_objective_completed', 'Completed %item_link for %quest_link, the grind is real!', 0, 0, '', 'J’ai terminé %item_link pour %quest_link, la galère!', '%item_link für %quest_link abgeschlossen, der Grind ist real!', '完成了 %item_link 为 %quest_link,刷得真辛苦!', '', '', '', ''), + (196, 'broadcast_quest_update_add_item_objective_progress', 'Still chipping away at %item_link for %quest_link, %quest_obj_available/%quest_obj_required done so far.', 0, 0, '', 'Je suis toujours en train de travailler sur %item_link pour %quest_link, j’ai accompli %quest_obj_available/%quest_obj_required jusqu’à présent.', 'Arbeite weiter an %item_link für %quest_link, bisher %quest_obj_available/%quest_obj_required erledigt.', '还在慢慢搞 %item_link 为 %quest_link,目前完成了 %quest_obj_available/%quest_obj_required。', '', '', '', ''), + (197, 'broadcast_quest_update_add_item_objective_progress', '%item_link for %quest_link is getting closer to completion, %quest_obj_available/%quest_obj_required done.', 0, 0, '', '%item_link pour la quête %quest_link approche de la fin, j’ai accompli %quest_obj_available/%quest_obj_required objectifs.', '%item_link für %quest_link kommt der Fertigstellung näher, %quest_obj_available/%quest_obj_required erledigt.', '%item_link 为 %quest_link 快完成了,目前完成 %quest_obj_available/%quest_obj_required。', '', '', '', ''), + (198, 'broadcast_quest_update_add_item_objective_completed', 'Yes! %item_link finished for %quest_link! That one took a bit longer.', 0, 0, '', 'Oui! %item_link terminé pour %quest_link! Celui-là a pris un peu plus de temps.', 'Yes! %item_link für %quest_link fertig! Das hat etwas länger gedauert.', '好嘞!%item_link 为 %quest_link 完成!这个花了点时间。', '', '', '', ''), + (199, 'broadcast_quest_update_add_item_objective_progress', 'I have %quest_obj_available/%quest_obj_required of %item_link for %quest_link, still going strong!', 0, 0, '', 'J’ai %quest_obj_available/%quest_obj_required de %item_link pour %quest_link, et je continue sur ma lancée!', 'Ich habe %quest_obj_available/%quest_obj_required %item_link für %quest_link, läuft!', '我有 %quest_obj_available/%quest_obj_required 个 %item_link 为 %quest_link,继续努力!', '', '', '', ''), + (200, 'broadcast_quest_update_add_item_objective_completed', 'Done with %item_link for %quest_link. On to the next one!', 0, 0, '', 'Terminé avec %item_link pour %quest_link. Passons à la suite !', 'Durch mit %item_link für %quest_link. Weiter zum nächsten!', '搞定 %item_link 为 %quest_link,下一个任务走起!', '', '', '', ''), + (201, 'broadcast_quest_update_add_item_objective_completed', 'Another one down! %item_link is complete for %quest_link!', 0, 0, '', 'Encore un de fait! %item_link est complet pour %quest_link!', 'Noch eins abgehakt! %item_link für %quest_link ist durch!', '又搞定一个!%item_link 为 %quest_link 完成!', '', '', '', ''), + (202, 'broadcast_quest_update_add_item_objective_progress', 'Still grinding on %item_link for %quest_link, got %quest_obj_available/%quest_obj_required done.', 0, 0, '', 'Je continue à travailler sur %item_link pour la quête %quest_link, j’ai accompli %quest_obj_available/%quest_obj_required objectifs jusqu’à présent.', 'Grinde weiter %item_link für %quest_link, habe %quest_obj_available/%quest_obj_required geschafft.', '还在刷 %item_link 为 %quest_link,目前完成 %quest_obj_available/%quest_obj_required。', '', '', '', ''), + (203, 'broadcast_quest_update_add_item_objective_progress', 'Getting there! Need %quest_obj_missing more %item_link for %quest_link.', 0, 0, '', 'On y est presque! Il me manque %quest_obj_missing %item_link pour %quest_link.', 'Bin fast durch! Brauche noch %quest_obj_missing %item_link für %quest_link.', '快到了!还差 %quest_obj_missing 个 %item_link 为 %quest_link。', '', '', '', ''), + (204, 'broadcast_quest_update_failed_timer', 'Failed to finish %quest_link in time...', 0, 0, '', 'J’ai échoué à terminer %quest_link à temps...', 'Nicht rechtzeitig %quest_link geschafft...', '没能在时间内完成 %quest_link……', '', '', '', 'Не успел завершить %quest_link вовремя...'), + (205, 'broadcast_quest_update_failed_timer', 'Ran out of time for %quest_link :(', 0, 0, '', 'J’ai manqué de temps pour %quest_link :(', 'Zeit für %quest_link abgelaufen :(', '时间不够,没完成 %quest_link :(', '', '', '', 'Время для %quest_link вышло :('), + (206, 'broadcast_quest_update_complete', 'I have completed all objectives for %quest_link', 0, 0, '', 'J’ai terminé tous les objectifs pour %quest_link.', 'Alle Ziele für %quest_link abgeschlossen', '我已经完成了 %quest_link 的所有目标!', '', '', '', 'Я выполнил все задачи для %quest_link'), + (207, 'broadcast_quest_update_complete', 'Completed all objectives for %quest_link', 0, 0, '', 'J’ai rempli tous les objectifs pour %quest_link.', 'Alle Ziele für %quest_link erledigt', '完成了 %quest_link 的所有目标!', '', '', '', 'Выполнил все задачи для %quest_link'), + (208, 'broadcast_quest_update_complete', 'Gonna turn in the %quest_link soon, just finished all objectives', 0, 0, '', 'Je vais bientôt remettre la %quest_link, je viens de finir tous les objectifs.', 'Ich gebe %quest_link gleich ab, habe gerade alle Ziele erledigt', '马上要去交 %quest_link 了,刚刚完成所有目标!', '', '', '', 'Скоро сдам %quest_link, только что закончил все задачи'), + (209, 'broadcast_quest_turned_in', 'Yess, I have finally turned in the %quest_link', 0, 0, '', 'Oh ouiii, j’ai enfin remis la %quest_link', 'Jaaa, endlich %quest_link abgegeben', '耶,终于完成了%quest_link任务,交掉了!', '', '', '', 'Да, наконец-то сдал %quest_link'), + (210, 'broadcast_quest_turned_in', 'turned in the %quest_link', 0, 0, '', 'J’ai soumis %quest_link.', 'hab %quest_link abgegeben', '交掉了%quest_link任务!', '', '', '', 'сдал %quest_link'), + (211, 'broadcast_quest_turned_in', 'managed to finish %quest_link, just turned in', 0, 0, '', 'J’ai réussi à terminer %quest_link, je viens tout juste de le remettre.', 'Hab %quest_link geschafft, gerade abgegeben', '终于搞定%quest_link任务,刚交了!', '', '', '', 'смог завершить %quest_link, только что сдал'), + (212, 'broadcast_quest_turned_in', 'just turned in %quest_link', 0, 0, '', 'Je viens de remettre %quest_link.', 'gerade %quest_link abgegeben', '刚交了%quest_link任务!', '', '', '', 'только что сдал %quest_link'), + (213, 'broadcast_quest_turned_in', 'just turned in %quest_link in %zone_name', 0, 0, '', 'Je viens de terminer la quête %quest_link dans la zone %zone_name.', 'gerade %quest_link in %zone_name abgegeben', '刚在%zone_name交了%quest_link任务!', '', '', '', 'только что сдал %quest_link в %zone_name'), + (214, 'broadcast_quest_turned_in', 'Another quest completed! %quest_link turned in', 0, 0, '', 'Encore une quête terminée! %quest_link remise', 'Noch eine Quest abgeschlossen! %quest_link abgegeben', '又完成一个任务!%quest_link交掉了!', '', '', '', ''), + (215, 'broadcast_quest_turned_in', 'Mission accomplished! %quest_link is turned in', 0, 0, '', 'Mission accomplie ! J’ai remis %quest_link.', 'Mission erfüllt! %quest_link ist abgegeben', '任务完成!%quest_link交掉了!', '', '', '', ''), + (216, 'broadcast_quest_turned_in', 'Finally, %quest_link turned in! That was a journey!', 0, 0, '', 'Enfin, j’ai remis la %quest_link ! Quelle aventure !', 'Endlich, %quest_link abgegeben! Was für ein Ritt!', '终于交了%quest_link任务!真是一场冒险!', '', '', '', ''), + (217, 'broadcast_quest_turned_in', 'Just completed %quest_link and turned it in, feeling good!', 0, 0, '', 'Je viens de terminer %quest_link et de le rendre, je me sens bien !', 'Grade %quest_link abgeschlossen und abgegeben, fühlt sich gut an!', '刚完成%quest_link任务并交了,感觉真不错!', '', '', '', ''), + (218, 'broadcast_quest_turned_in', 'Turned in %quest_link in %zone_name. Now, on to the next challenge!', 0, 0, '', 'J’ai remis la %quest_link dans %zone_name. Maintenant, je passe au défi suivant !', '%quest_link in %zone_name abgegeben. Weiter zur nächsten Herausforderung!', '在%zone_name交了%quest_link任务,接下来迎接新的挑战!', '', '', '', ''), + (219, 'broadcast_quest_turned_in', 'I have turned in %quest_link. One more down!', 0, 0, '', 'J’ai remis %quest_link. Un de moins à faire !', 'Hab %quest_link abgegeben. Wieder eine weniger!', '我交了%quest_link任务,又搞定一个!', '', '', '', ''), + (220, 'broadcast_quest_turned_in', 'Successfully turned in %quest_link, that took a while!', 0, 0, '', 'J’ai enfin réussi à remettre la %quest_link, ça m’a pris du temps !', '%quest_link erfolgreich abgegeben, das hat gedauert!', '成功交了%quest_link任务,耗了不少时间!', '', '', '', ''), + (221, 'broadcast_quest_turned_in', 'Completed and turned in %quest_link in %zone_name. Feels great!', 0, 0, '', 'J’ai terminé et remis la %quest_link dans la zone %zone_name. Je me sens génial!', '%quest_link in %zone_name abgeschlossen und abgegeben. Fühlt sich gut an!', '在%zone_name完成了%quest_link任务并交了,感觉棒极了!', '', '', '', ''), + (222, 'broadcast_quest_turned_in', 'Quest complete! %quest_link turned in and ready for the next one!', 0, 0, '', 'Quête terminée! J’ai remis %quest_link et je suis prêt pour la prochaine!', 'Quest fertig! %quest_link abgegeben und bereit für die nächste!', '任务完成!%quest_link交掉了,准备接下一个!', '', '', '', ''), + (223, 'broadcast_quest_turned_in', 'Finally turned in %quest_link in %zone_name. That one was tough!', 0, 0, '', 'Je viens enfin de rendre la %quest_link dans la zone de %zone_name. Celle-là était vraiment coriace !', 'Endlich %quest_link in %zone_name abgegeben. Die war knackig!', '终于在%zone_name交了%quest_link任务,这任务真难!', '', '', '', ''), + (224, 'broadcast_quest_turned_in', 'That’s it, %quest_link turned in! Now to collect the reward!', 0, 0, '', 'C’est bon, la %quest_link est terminée ! Maintenant, je vais chercher ma récompense !', 'Das wars, %quest_link abgegeben! Jetzt Belohnung einsacken!', '搞定了,%quest_link交掉了!现在去领赏!', '', '', '', ''), + (225, 'broadcast_quest_turned_in', 'Finished %quest_link! Now time to turn it in and move on.', 0, 0, '', 'J’ai fini la %quest_link ! Maintenant, il est temps de la remettre et de passer à la suite.', '%quest_link fertig! Jetzt abgeben und weiter.', '完成了%quest_link任务!现在交掉,继续前行。', '', '', '', ''), + (226, 'broadcast_quest_turned_in', 'Just turned in %quest_link, feeling accomplished!', 0, 0, '', 'Je viens juste de terminer %quest_link, je me sens tellement accompli !', 'Gerade %quest_link abgegeben, fühlt sich gut an!', '刚交了%quest_link任务,感觉很有成就感!', '', '', '', ''), + (227, 'broadcast_quest_turned_in', 'I’ve turned in %quest_link. Let’s see what’s next!', 0, 0, '', 'J’ai remis %quest_link. Voyons ce qui nous attend ensuite!', 'Hab %quest_link abgegeben. Mal sehen, was als Nächstes kommt!', '我交了%quest_link任务,看看接下来有什么!', '', '', '', ''), + (228, 'broadcast_quest_turned_in', 'Just wrapped up %quest_link and turned it in. On to the next one!', 0, 0, '', 'Je viens de terminer %quest_link et de la rendre. En route pour la suite !', '%quest_link eben abgeschlossen und abgegeben. Weiter gehts!', '刚搞定%quest_link任务并交了,继续下一个!', '', '', '', ''), + (229, 'broadcast_quest_turned_in', 'Another one in the books, %quest_link turned in', 0, 0, '', 'Encore une de faite, %quest_link remise.', 'Wieder eine im Kasten, %quest_link abgegeben', '又记上一笔,%quest_link任务交掉了!', '', '', '', ''), + (230, 'broadcast_quest_turned_in', 'Turned in %quest_link! Time to collect my rewards.', 0, 0, '', 'J’ai rendu la %quest_link! Il est temps de récupérer mes récompenses.', '%quest_link abgegeben! Zeit, die Belohnung abzuholen.', '交了%quest_link任务!该去领奖励了。', '', '', '', ''), + (231, 'broadcast_quest_turned_in', 'Completed and turned in %quest_link, what a relief!', 0, 0, '', 'J’ai terminé et remis %quest_link, quel soulagement !', '%quest_link abgeschlossen und abgegeben, was für eine Erleichterung!', '完成了%quest_link任务并交了,真是松了一口气!', '', '', '', ''), + (232, 'broadcast_quest_turned_in', 'That was quick! %quest_link turned in already!', 0, 0, '', 'Déjà?! %quest_link a été rendu si rapidement!', 'Das ging fix! %quest_link schon abgegeben!', '这么快就搞定了!%quest_link任务已经交了!', '', '', '', ''), + (233, 'broadcast_quest_turned_in', 'Just turned in %quest_link in %zone_name. Ready for the next adventure!', 0, 0, '', 'Je viens de rendre %quest_link dans %zone_name. Je suis prêt pour la prochaine aventure !', 'Gerade %quest_link in %zone_name abgegeben. Bereit fürs nächste Abenteuer!', '刚在%zone_name交了%quest_link任务,准备好迎接下一次冒险了!', '', '', '', ''), + (234, 'broadcast_quest_turned_in', 'Successfully turned in %quest_link. Feels like progress!', 0, 0, '', 'J’ai réussi à rendre %quest_link. On dirait que j’avance!', '%quest_link erfolgreich abgegeben. Fühlt sich nach Fortschritt an!', '成功交了%quest_link任务,感觉自己在进步!', '', '', '', ''), + (235, 'broadcast_quest_turned_in', 'Mission complete! %quest_link is turned in!', 0, 0, '', 'Mission accomplie! %quest_link a été rendue!', 'Mission komplett! %quest_link ist abgegeben!', '任务完成!%quest_link交掉了!', '', '', '', ''), + (236, 'broadcast_quest_turned_in', 'I just finished %quest_link and turned it in, on to the next one!', 0, 0, '', 'Je viens de terminer %quest_link et de le remettre, passons à la suite!', 'Hab gerade %quest_link beendet und abgegeben, weiter zur nächsten!', '刚完成%quest_link任务并交了,接下来继续!', '', '', '', ''), + (237, 'broadcast_killed_normal', 'another %victim_name down', 0, 0, '', 'Encore une %victim_name tombée.', 'wieder %victim_name gelegt', '又干掉了一个%victim_name!', '', '', '', 'еще одна жертва — %victim_name'), + (238, 'broadcast_killed_normal', 'I keep killing %victim_name, nothing to talk about', 0, 0, '', 'Je continue de tuer %victim_name, il n’y a rien à dire à ce sujet.', 'Ich leg ständig %victim_name um, nix Besonderes', '老是杀%victim_name,没啥好说的。', '', '', '', 'Я продолжаю убивать %victim_name, нечего рассказывать'), + (239, 'broadcast_killed_normal', 'another %victim_name bites the dust', 0, 0, '', 'Encore une fois, %victim_name mord la poussière.', 'noch ein %victim_name beißt ins Gras', '又一个%victim_name倒下了!', '', '', '', 'еще один %victim_name пал'), + (240, 'broadcast_killed_normal', 'one less %victim_name in %zone_name', 0, 0, '', 'Une victime en moins dans la zone.', 'ein %victim_name weniger in %zone_name', '%zone_name少了一个%victim_name!', '', '', '', 'минус один %victim_name в %zone_name'), + (241, 'broadcast_killed_elite', 'Took down this elite bastard %victim_name!', 0, 0, '', 'J’ai descendu cet enfoiré d’élite %victim_name !', 'Hab diesen Elite-Bastard %victim_name umgehauen!', '干掉了这个精英混蛋%victim_name!', '', '', '', 'Убил этого элитного ублюдка %victim_name!'), + (242, 'broadcast_killed_player', 'killed elite %victim_name in %zone_name', 0, 0, '', 'J’ai tué l’élite %victim_name dans la zone %zone_name', 'Elite %victim_name in %zone_name gekillt', '在%zone_name干掉了精英%victim_name!', '', '', '', 'убил элиту %victim_name в %zone_name'), + (243, 'broadcast_killed_rareelite', 'Fooof, managed to take down %victim_name!', 0, 0, '', 'Oh là là, j’ai réussi à abattre %victim_name !', 'Puh, %victim_name doch noch gelegt!', '呼,终于干掉了%victim_name!', '', '', '', 'Фух, удалось завалить %victim_name!'), + (244, 'broadcast_killed_worldboss', 'That was sick! Just killed %victim_name! Can tell tales now', 0, 0, '', 'C’était fou! Je viens juste de tuer %victim_name! Je pourrais raconter des histoires maintenant.', 'Gefällt dir das %victim_name?! Willst du, dass ich drei Wochen zurückspule, als du noch gelebt hast?!', '太爽了!刚干掉%victim_name!这下有故事可以吹了!', '', '', '', 'Это было круто! Только что убил %victim_name! Теперь есть что рассказать'), + (245, 'broadcast_killed_rare', 'Yoo, I just killed %victim_name!', 0, 0, '', 'Eh, je viens juste de tuer %victim_name!', 'Yo, hab gerade %victim_name gekillt!', '嘿,刚干掉了%victim_name!', '', '', '', 'Йо, я только что убил %victim_name!'), + (246, 'broadcast_killed_player', 'killed rare %victim_name in %zone_name', 0, 0, '', 'J’ai tué un rare %victim_name dans %zone_name', 'Rare %victim_name in %zone_name gekillt', '在%zone_name干掉了稀有怪%victim_name!', '', '', '', 'убил редкого %victim_name в %zone_name'), + (247, 'broadcast_killed_unknown', 'WTF did I just kill? %victim_name', 0, 0, '', 'Qu’est-ce que je viens de tuer ? %victim_name', 'WTF hab ich da gerade umgehauen? %victim_name', '我刚杀了啥?%victim_name?', '', '', '', 'Что я только что убил? %victim_name'), + (248, 'broadcast_killed_pet', 'Just killed that pet %victim_name', 0, 0, '', 'Je viens de tuer l’animal de compagnie, %victim_name.', 'Gerade das Pet %victim_name gekillt', '刚干掉了那个宠物%victim_name。', '', '', '', 'Только что убил этого пета %victim_name'), + (249, 'broadcast_killed_player', 'Oh yeah, I just killed %victim_name', 0, 0, '', 'Oh ouais, je viens juste de tuer %victim_name', 'Oh ja, hab gerade %victim_name gekillt', '哦耶,刚干掉了%victim_name!', '', '', '', 'О да, только что убил %victim_name'), + (250, 'broadcast_killed_player', 'killed %victim_name in %zone_name', 0, 0, '', 'J’ai tué %victim_name dans %zone_name.', '%victim_name in %zone_name gekillt', '在%zone_name干掉了%victim_name!', '', '', '', 'убил %victim_name в %zone_name'), + (251, 'broadcast_killed_normal', 'another %victim_name down', 0, 0, '', 'Encore une %victim_name de moins.', 'Wieder %victim_name erlegt', '又干掉了一个%victim_name!', '', '', '', 'еще одна жертва — %victim_name'), + (252, 'broadcast_killed_normal', 'I keep killing %victim_name, nothing to talk about', 0, 0, '', 'Je continue de tuer %victim_name, rien à dire à ce sujet.', 'Ich leg ständig %victim_name um, nix Besonderes', '一直在杀%victim_name,没啥好聊的。', '', '', '', 'Я продолжаю убивать %victim_name, нечего рассказывать'), + (253, 'broadcast_killed_normal', 'another %victim_name bites the dust', 0, 0, '', 'Encore une victime %victim_name qui mord la poussière.', 'noch ein %victim_name beißt ins Gras', '又一个%victim_name倒下了!', '', '', '', 'еще один %victim_name пал'), + (254, 'broadcast_killed_normal', 'one less %victim_name in %zone_name', 0, 0, '', 'Il y a une victime %victim_name de moins dans la zone.', 'ein %victim_name weniger in %zone_name', '%zone_name少了一个%victim_name!', '', '', '', 'минус один %victim_name в %zone_name'), + (255, 'broadcast_killed_normal', 'Just took down %victim_name like it’s nothing', 0, 0, '', 'Je viens de neutraliser %victim_name comme si c’était rien.', '%victim_name so nebenbei umgehauen', '刚干掉%victim_name,轻松得跟玩儿似的!', '', '', '', ''), + (256, 'broadcast_killed_normal', '%victim_name didn’t stand a chance', 0, 0, '', '%victim_name n’avait aucune chance.', '%victim_name hatte keine Chance', '%victim_name完全不是对手!', '', '', '', ''), + (257, 'broadcast_killed_normal', 'One more %victim_name out of the way', 0, 0, '', 'Encore une victime %victim_name de moins sur notre chemin', 'Schon wieder %victim_name aus dem Weg geräumt', '又一个%victim_name被我清理掉了!', '', '', '', ''), + (258, 'broadcast_killed_normal', '%victim_name was too easy, on to the next one', 0, 0, '', '%victim_name était trop facile, passons au suivant.', '%victim_name war zu easy, weiter gehts', '%victim_name太简单了,下一只!', '', '', '', ''), + (259, 'broadcast_killed_normal', 'Another %victim_name defeated. Easy peasy', 0, 0, '', 'Encore une autre victime vaincue %victim_name. C’était du gâteau.', 'Und wieder %victim_name plattgemacht. Kinderkram', '又干掉了一个%victim_name,小菜一碟!', '', '', '', ''), + (260, 'broadcast_killed_normal', '%victim_name wasn’t tough at all', 0, 0, '', '%victim_name n’était vraiment pas du tout résistant.', '%victim_name war gar nicht so tough', '%victim_name一点都不难对付!', '', '', '', ''), + (261, 'broadcast_killed_normal', 'Down goes %victim_name, nothing special', 0, 0, '', '%victim_name tombe, rien de spécial', '%victim_name liegt, nix Besonderes', '%victim_name倒下了,没啥特别的。', '', '', '', ''), + (262, 'broadcast_killed_normal', 'That was too easy, %victim_name down', 0, 0, '', 'C’était trop facile, %victim_name est hors jeu.', 'Das war zu easy, %victim_name liegt', '太简单了,%victim_name倒下了!', '', '', '', ''), + (263, 'broadcast_killed_normal', '%victim_name dropped so fast, blink and it’s over', 0, 0, '', '%victim_name a chuté si vite, cligne des yeux et c’est déjà terminé.', '%victim_name ist so schnell umgekippt, einmal blinzeln und vorbei', '%victim_name 倒得太快了,眨眼就完事!', '', '', '', ''), + (264, 'broadcast_killed_elite', 'Took down this elite %victim_name!', 0, 0, '', 'J’ai vaincu cet élite %victim_name !', 'Diesen Elite %victim_name umgehauen!', '干掉了精英 %victim_name!', '', '', '', ''), + (265, 'broadcast_killed_elite', 'Elite %victim_name is no more', 0, 0, '', 'Elite %victim_name n’est plus.', 'Elite %victim_name existiert nicht mehr', '精英 %victim_name 已被消灭!', '', '', '', ''), + (266, 'broadcast_killed_elite', 'Took down the elite %victim_name like a champ', 0, 0, '', 'J’ai descendu l’élite %victim_name comme un champion.', 'Den Elite %victim_name weggemacht wie ein Boss', '像个冠军一样干掉了精英 %victim_name!', '', '', '', ''), + (267, 'broadcast_killed_elite', 'Elite monster %victim_name, no match for me!', 0, 0, '', 'Monstre d’élite %victim_name, pas de taille pour moi !', 'Elite-Mob %victim_name, hat keine Chance gegen mich!', '精英怪 %victim_name,根本不是我的对手!', '', '', '', ''), + (268, 'broadcast_killed_elite', 'Finally took down the mighty %victim_name', 0, 0, '', 'J’ai enfin vaincu le redoutable %victim_name.', 'Endlich den mächtigen %victim_name gelegt', '终于干掉了强大的 %victim_name!', '', '', '', ''), + (269, 'broadcast_killed_player', 'killed elite %victim_name in %zone_name', 0, 0, '', 'J’ai tué l’élite %victim_name dans la zone %zone_name.', 'Elite %victim_name in %zone_name gekillt', '在 %zone_name 干掉了精英 %victim_name!', '', '', '', 'убил элиту %victim_name в %zone_name'), + (270, 'broadcast_killed_player', 'Just got the elite %victim_name in %zone_name', 0, 0, '', 'Je viens tout juste d’obtenir l’élite %victim_name dans la région de %zone_name.', 'Hab gerade Elite %victim_name in %zone_name erwischt', '刚在 %zone_name 搞定了精英 %victim_name!', '', '', '', ''), + (271, 'broadcast_killed_player', 'Killed the elite %victim_name. Too easy', 0, 0, '', 'J’ai tué l’élite %victim_name. Trop facile.', 'Elite %victim_name gekillt. Viel zu easy', '干掉了精英 %victim_name,太简单了!', '', '', '', ''), + (272, 'broadcast_killed_player', 'Took down %victim_name in %zone_name, what a fight', 0, 0, '', 'J’ai abattu %victim_name dans %zone_name, quel combat!', '%victim_name in %zone_name umgehauen, was ein Kampf', '在 %zone_name 干掉了 %victim_name,真是一场激战!', '', '', '', ''), + (273, 'broadcast_killed_player', 'Killed elite %victim_name. Guess I’m unstoppable', 0, 0, '', 'J’ai tué l’élite %victim_name. Je suis imbattable, je crois.', 'Elite %victim_name gekillt. Ich scheine unaufhaltbar zu sein', '干掉了精英 %victim_name,我简直无人能挡!', '', '', '', ''), + (274, 'broadcast_killed_rareelite', 'Fooof, managed to take down %victim_name!', 0, 0, '', 'Whouaa, j’ai réussi à éliminer %victim_name !', 'Puh, %victim_name doch noch gelegt!', '呼,成功干掉了 %victim_name!', '', '', '', 'Фух, удалось завалить %victim_name!'), + (275, 'broadcast_killed_rareelite', 'Rare elite %victim_name has fallen', 0, 0, '', 'Un rare élite nommé %victim_name est tombé', 'Seltener Elite %victim_name liegt', '稀有精英 %victim_name 倒下了!', '', '', '', ''), + (276, 'broadcast_killed_rareelite', '%victim_name is no more. Another rare elite slain', 0, 0, '', '%victim_name n’est plus. Encore un rare élite abattu.', '%victim_name gibt es nicht mehr. Noch ein seltener Elite gelegt', '%victim_name 完了,又一个稀有精英被我干掉!', '', '', '', ''), + (277, 'broadcast_killed_rareelite', 'Rare elite %victim_name down. I’m on fire!', 0, 0, '', 'Rare elite %victim_name abattu. Je suis en feu!', 'Seltener Elite %victim_name down. Ich bin aus Feuer!', '稀有精英 %victim_name 被我干掉了,我现在状态火热!', '', '', '', ''), + (278, 'broadcast_killed_rareelite', 'I just killed the rare elite %victim_name', 0, 0, '', 'Je viens de vaincre l’élite rare %%victim_name.', 'Hab gerade den seltenen Elite %victim_name gekillt', '我刚干掉了稀有精英 %victim_name!', '', '', '', ''), + (279, 'broadcast_killed_worldboss', 'That was awesome! Just killed %victim_name! What a battle', 0, 0, '', 'C’était incroyable! Je viens juste de tuer %victim_name! Quelle bataille!', 'Das war geil! %victim_name gerade gekillt! Was ein Kampf', '太棒了!刚干掉了 %victim_name!真是场精彩的战斗!', '', '', '', ''), + (280, 'broadcast_killed_worldboss', 'World boss %victim_name is down! That was epic', 0, 0, '', 'Le boss mondial %victim_name est tombé ! C’était épique.', 'Weltenboss %victim_name ist down! Das war episch', '世界首领 %victim_name 倒下了!这场战斗太史诗了!', '', '', '', ''), + (281, 'broadcast_killed_worldboss', 'I’ve slain the world boss %victim_name. Let’s celebrate!', 0, 0, '', 'J’ai abattu le boss mondial %victim_name. On va fêter ça !', 'Hab den Weltenboss %victim_name gelegt. Zeit zum Feiern!', '我干掉了世界首领 %victim_name,来庆祝一下吧!', '', '', '', ''), + (282, 'broadcast_killed_worldboss', 'Killed %victim_name! That world boss was no joke!', 0, 0, '', 'J’ai tué %victim_name! Ce world boss était vraiment costaud!', '%victim_name gekillt! Der Weltenboss war kein Spaß!', '干掉了 %victim_name!这个世界首领可不是开玩笑的!', '', '', '', ''), + (283, 'broadcast_killed_worldboss', 'Just killed %victim_name, that was legendary', 0, 0, '', 'Je viens juste de tuer %victim_name, c’était légendaire!', 'Gerade %victim_name gekillt, das war legendär', '刚干掉了 %victim_name,简直是传奇一战!', '', '', '', ''), + (284, 'broadcast_killed_rare', 'Yoo, I just killed %victim_name!', 0, 0, '', 'Eh, je viens juste de tuer %victim_name!', 'Yo, hab gerade %victim_name gekillt!', '嘿,我刚干掉了 %victim_name!', '', '', '', 'Йо, я только что убил %victim_name!'), + (285, 'broadcast_killed_rare', 'Rare %victim_name down. That was a fun fight', 0, 0, '', 'Rare %victim_name down. C’était un combat amusant.', 'Seltener %victim_name down. War ein geiler Fight', '稀有怪 %victim_name 倒下了,这场战斗真有趣!', '', '', '', ''), + (286, 'broadcast_killed_rare', 'I just killed the rare %victim_name', 0, 0, '', 'Je viens de tuer le rare %victim_name.', 'Hab gerade den seltenen %victim_name gekillt', '我刚干掉了稀有怪 %victim_name!', '', '', '', ''), + (287, 'broadcast_killed_rare', 'Rare %victim_name defeated in %zone_name', 0, 0, '', 'Rare %victim_name vaincu dans %zone_name', 'Seltener %victim_name in %zone_name besiegt', '在 %zone_name 击败了稀有怪 %victim_name!', '', '', '', ''), + (288, 'broadcast_killed_rare', 'Just killed rare %victim_name in %zone_name. That was quick!', 0, 0, '', 'Je viens de tuer le rare %victim_name dans %zone_name. Ça a été rapide!', 'Gerade seltenen %victim_name in %zone_name gekillt. Das ging fix!', '刚在 %zone_name 干掉了稀有怪 %victim_name,速度真快!', '', '', '', ''), + (289, 'broadcast_killed_player', 'killed rare %victim_name in %zone_name', 0, 0, '', 'J’ai tué un %victim_name rare dans la zone de %zone_name.', 'Seltenen %victim_name in %zone_name gekillt', '在 %zone_name 干掉了稀有怪 %victim_name!', '', '', '', 'убил редкого %victim_name в %zone_name'), + (290, 'broadcast_killed_player', 'Got the rare %victim_name in %zone_name', 0, 0, '', 'J’ai trouvé le rare %victim_name dans %zone_name', 'Den seltenen %victim_name in %zone_name erwischt', '在 %zone_name 搞定了稀有怪 %victim_name!', '', '', '', ''), + (291, 'broadcast_killed_player', 'I killed %victim_name in %zone_name. That felt good', 0, 0, '', 'J’ai tué %victim_name dans %zone_name. Ça m’a fait du bien.', 'Hab %victim_name in %zone_name gekillt. Das tat gut', '在 %zone_name 干掉了 %victim_name,感觉真爽!', '', '', '', ''), + (292, 'broadcast_killed_player', 'Took down %victim_name in %zone_name. They didn’t stand a chance', 0, 0, '', 'J’ai vaincu %victim_name dans %zone_name. Ils n’avaient aucune chance.', '%victim_name in %zone_name umgehauen. Keine Chance gehabt', '在 %zone_name 干掉了 %victim_name,他们毫无还手之力!', '', '', '', ''), + (293, 'broadcast_killed_player', 'Just killed %victim_name in %zone_name, easy', 0, 0, '', 'Je viens de tuer %victim_name à %zone_name, c’était facile', 'Gerade %victim_name in %zone_name gekillt, easy', '刚在 %zone_name 干掉了 %victim_name,太轻松了!', '', '', '', ''), + (294, 'broadcast_killed_unknown', 'What did I just kill? %victim_name', 0, 0, '', 'Qui est-ce que je viens de tuer? %victim_name', 'Was hab ich da gerade gekillt? %victim_name', '我刚杀了啥?%victim_name?', '', '', '', ''), + (295, 'broadcast_killed_unknown', 'What kind of %victim_name is that? Never seen anything like it', 0, 0, '', 'Quel genre de %victim_name est-ce ça ? Je n’ai jamais rien vu de tel auparavant.', 'Was ist das für ein %victim_name? Sowas hab ich noch nie gesehen', '这 %victim_name 是啥玩意儿?从没见过这样的!', '', '', '', ''), + (296, 'broadcast_killed_unknown', 'Just killed something strange, %victim_name', 0, 0, '', 'Je viens de tuer quelque chose d’étrange, %victim_name', 'Hab gerade was Seltsames gekillt, %victim_name', '刚干掉了个奇怪的东西,%victim_name!', '', '', '', ''), + (297, 'broadcast_killed_unknown', '%victim_name was weird, but down they go', 0, 0, '', '%victim_name était bizarre, mais au sol ils vont', '%victim_name war seltsam, aber liegt trotzdem', '%victim_name 怪怪的,不过还是被我干掉了!', '', '', '', ''), + (298, 'broadcast_killed_pet', 'Just defeated %victim_name', 0, 0, '', 'Je viens juste de vaincre %victim_name.', 'Gerade %victim_name besiegt', '刚击败了 %victim_name!', '', '', '', ''), + (299, 'broadcast_killed_pet', 'Goodbye, %victim_name. You were just a pet', 0, 0, '', 'Au revoir, %victim_name. Tu étais qu’un animal de compagnie.', 'Machs gut, %victim_name. Warst nur ein Pet', '再见了,%victim_name,你不过是个宠物而已。', '', '', '', ''), + (300, 'broadcast_killed_pet', 'Just took down the pet %victim_name. That was unexpected', 0, 0, '', 'Je viens de m’occuper de l’animal de compagnie %victim_name. Ça c’était inattendu.', 'Hab gerade das Pet %victim_name umgehauen. Das kam unerwartet.', '刚干掉了宠物 %victim_name,真没想到!', '', '', '', ''), + (301, 'broadcast_killed_pet', 'Down goes %victim_name. Better luck next time, pet', 0, 0, '', '%victim_name mord la poussière. Meilleure chance la prochaine fois, mon pote.', '%victim_name liegt. Vielleicht nächstes Mal, Pet', '%victim_name 倒下了,下次好运吧,小宠物!', '', '', '', ''), + (302, 'broadcast_levelup_generic', '%my_level Ding!', 0, 0, '', '%my_level Ding !', 'Ding! %my_level', '%my_level 级,升级啦!', '', '', '', ''), + (303, 'broadcast_levelup_generic', 'Yess, I am level %my_level!', 0, 0, '', 'Oui, je suis au niveau %my_level !', 'Jaaa, ich bin Level %my_level!', '耶,我到 %my_level 级了!', '', '', '', 'Да, я теперь %my_level уровень!'), + (304, 'broadcast_levelup_generic', 'I just leveled up', 0, 0, '', 'Je viens de monter de niveau', 'Gerade aufgelevelt', '我刚升级了!', '', '', '', 'Я только что повысил уровень'), + (305, 'broadcast_levelup_10x', 'I am level %my_level!!!', 0, 0, '', 'Je suis au niveau %my_level !!!', 'Ich bin Level %my_level!!!', '我到 %my_level 级了!!!', '', '', '', 'Я %my_level уровень!!!'), + (306, 'broadcast_levelup_10x', 'getting stronger, already level %my_level!!!', 0, 0, '', 'Je deviens de plus en plus fort, je suis déjà au niveau %my_level !!!', 'Werde stärker, schon Level %my_level!!!', '越来越强了,已经到 %my_level 级!!!', '', '', '', 'Становлюсь сильнее, уже %my_level уровень!!!'), + (307, 'broadcast_levelup_10x', 'Just reached level %my_level!!!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level !!!', 'Gerade Level %my_level erreicht!!!', '刚达到 %my_level 级!!!', '', '', '', 'Только что достиг %my_level уровня!!!'), + (308, 'broadcast_levelup_max_level', 'OMG, finally level %my_level!!!', 0, 0, '', 'OMG, enfin le niveau %my_level!!!', 'OMG, endlich Level %my_level!!!', '天啊,终于到 %my_level 级了!!!', '', '', '', 'ОГО, наконец-то %my_level уровень!!!'), + (309, 'broadcast_levelup_max_level', '%my_level!!! can do endgame content now', 0, 0, '', '%my_level!!! je peux me permettre de faire du contenu de fin de jeu maintenant.', 'Stufe %my_level!!! Jetzt kann ich Endgame-Content machen', '%my_level 级!!!现在可以挑战终极内容了!', '', '', '', '%my_level!!! теперь могу заниматься эндгейм-контентом'), + (310, 'broadcast_levelup_max_level', 'fresh new level %my_level %my_class!!!', 0, 0, '', 'Nouveau niveau %my_level %my_class!!!', 'Frisch auf Level %my_level, %my_class!!!', '全新等级 %my_level 的 %my_class!!!', '', '', '', 'свежий новый уровень %my_level %my_class!!!'), + (311, 'broadcast_levelup_max_level', 'one more level %my_level %my_race %my_class!', 0, 0, '', 'Encore un niveau de plus %my_level pour ma race %my_race et ma classe %my_class!', 'Noch ein Level: %my_level, %my_race %my_class!', '又升了一级,%my_level 级的 %my_race %my_class!', '', '', '', 'еще один уровень %my_level %my_race %my_class!'), + (312, 'broadcast_levelup_generic', 'Ding! Another level up!', 0, 0, '', 'Ding! Encore un niveau de gagné!', 'Ding! Wieder ein Level up!', '叮!又升了一级!', '', '', '', ''), + (313, 'broadcast_levelup_generic', 'I’m officially level %my_level now!', 0, 0, '', 'Je suis maintenant officiellement au niveau %my_level !', 'Jetzt offiziell Level %my_level!', '我现在正式达到 %my_level 级了!', '', '', '', ''), + (314, 'broadcast_levelup_generic', 'Level %my_level, feeling stronger already!', 0, 0, '', 'Niveau %my_level, je me sens déjà plus fort!', 'Level %my_level, fühl mich jetzt schon stärker!', '%my_level 级,感觉已经更强了!', '', '', '', ''), + (315, 'broadcast_levelup_generic', 'Up I go! Level %my_level reached', 0, 0, '', 'Je monte ! Niveau %my_level atteint', 'Aufwärts! Level %my_level erreicht', '我升级了!达到 %my_level 级!', '', '', '', ''), + (316, 'broadcast_levelup_generic', 'That was quick, level %my_level already!', 0, 0, '', 'Déjà niveau %my_level ! Ça se passe bien !', 'Das ging fix, schon Level %my_level!', '这么快,已经到 %my_level 级了!', '', '', '', ''), + (317, 'broadcast_levelup_generic', 'Just hit level %my_level, let’s do this!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level, allons-y !', 'Gerade Level %my_level erreicht, auf gehts!', '刚达到 %my_level 级,来吧,继续干!', '', '', '', ''), + (318, 'broadcast_levelup_generic', 'Level %my_level, on to bigger things!', 0, 0, '', 'Niveau %my_level, passons à des choses plus grandes !', 'Level %my_level, jetzt kommen die großen Sachen!', '%my_level 级,接下来是更大的挑战!', '', '', '', ''), + (319, 'broadcast_levelup_generic', 'Level %my_level, I’m on fire now!', 0, 0, '', 'Niveau %my_level, je suis en feu maintenant !', 'Level %my_level, ich bin jetzt on fire!', '%my_level 级,现在我状态火热!', '', '', '', ''), + (320, 'broadcast_levelup_generic', 'Level %my_level complete, onward!', 0, 0, '', 'Niveau %my_level terminé, en avant !', 'Level %my_level voll und weiter!', '%my_level 级完成,继续前进!', '', '', '', ''), + (321, 'broadcast_levelup_generic', 'I’m now level %my_level, what’s next?', 0, 0, '', 'Je suis maintenant niveau %my_level, qu’est-ce qui suit?', 'Bin jetzt Level %my_level, was kommt als Nächstes?', '我现在是 %my_level 级,接下来干啥?', '', '', '', ''), + (322, 'broadcast_levelup_10x', 'Level %my_level!!! It’s happening!', 0, 0, '', 'Niveau %my_level!!! C’est en train de se passer!', 'Level %my_level!!! Es passiert!', '%my_level 级!!!这就发生了!', '', '', '', ''), + (323, 'broadcast_levelup_10x', 'Just hit level %my_level, this feels amazing!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level, c’est incroyable !', 'Gerade Level %my_level erreicht, fühlt sich mega an!', '刚达到 %my_level 级,感觉太棒了!', '', '', '', ''), + (324, 'broadcast_levelup_10x', 'Level %my_level and I’m only getting started!', 0, 0, '', 'Niveau %my_level et je ne fais que commencer!', 'Level %my_level und ich fange erst an!', '%my_level 级,我才刚开始呢!', '', '', '', ''), + (325, 'broadcast_levelup_10x', 'Wow, level %my_level already, I’m unstoppable!', 0, 0, '', 'Wow, niveau %my_level déjà, je suis imbattable!', 'Wow, schon Level %my_level, unaufhaltbar!', '哇,已经到 %my_level 级了,我无人能挡!', '', '', '', ''), + (326, 'broadcast_levelup_10x', 'Reaching new heights at level %my_level!!!', 0, 0, '', 'Atteignant de nouveaux sommets au niveau %my_level !!!', 'Neue Höhen mit Level %my_level!!!', '在 %my_level 级达到了新的高度!!!', '', '', '', ''), + (327, 'broadcast_levelup_10x', 'Level %my_level and still pushing forward!', 0, 0, '', 'Niveau %my_level et je continue d’avancer à fond !', 'Level %my_level und weiter Vollgas!', '%my_level 级,我还在继续前进!', '', '', '', ''), + (328, 'broadcast_levelup_10x', 'Level %my_level, I’m too strong now!', 0, 0, '', 'Niveau %my_level, je suis trop fort maintenant!', 'Level %my_level, jetzt bin ich zu stark!', '达到%my_level级,我现在强得不行了!', '', '', '', ''), + (329, 'broadcast_levelup_10x', 'Hitting %my_level, it feels so good!', 0, 0, '', 'Atteindre %my_level, ça fait tellement du bien !', 'Level %my_level erreicht, fühlt sich so gut an!', '升到%my_level级,感觉真是太爽了!', '', '', '', ''), + (330, 'broadcast_levelup_10x', 'I’m level %my_level now! Let’s go!', 0, 0, '', 'Je suis au niveau %my_level maintenant ! Allez, c’est parti !', 'Bin jetzt Level %my_level! Los gehts!', '我现在是%my_level级了!咱们走起!', '', '', '', ''), + (331, 'broadcast_levelup_10x', 'Level %my_level, still a lot to go!', 0, 0, '', 'Niveau %my_level, il me reste encore beaucoup de chemin à parcourir !', 'Level %my_level, noch viel vor mir!', '升到%my_level级,还有很长的路要走啊!', '', '', '', ''), + (332, 'broadcast_levelup_max_level', 'Finally hit level %my_level, I can do anything!', 0, 0, '', 'J’ai enfin atteint le niveau %my_level, je peux tout faire !', 'Endlich Level %my_level erreicht, ich kann alles!', '终于达到%my_level级了,我无所不能!', '', '', '', ''), + (333, 'broadcast_levelup_max_level', 'Max level %my_level, endgame here I come!', 0, 0, '', 'Niveau maximum %my_level atteint, je me rapproche de la fin du jeu !', 'Maxlevel %my_level, Endgame ich komme!', '满级%my_level,终极挑战我来了!', '', '', '', ''), + (334, 'broadcast_levelup_max_level', 'Level %my_level! Ready for the endgame!', 0, 0, '', 'Niveau %my_level ! Prêt pour la phase finale !', 'Level %my_level! Bereit fürs Endgame!', '%my_level级达成!准备好迎接终极内容了!', '', '', '', ''), + (335, 'broadcast_levelup_max_level', 'Endgame unlocked at level %my_level, bring it on!', 0, 0, '', 'Fin du jeu débloquée au niveau %my_level, ça va déménager !', 'Endgame freigeschaltet auf Level %my_level, her damit!', '%my_level级解锁终极内容,来吧,挑战我!', '', '', '', ''), + (336, 'broadcast_levelup_max_level', 'I’ve reached level %my_level, time to shine!', 0, 0, '', 'J’ai atteint le niveau %my_level, c’est le moment de briller!', 'Hab Level %my_level erreicht, Zeit zu glänzen!', '我达到%my_level级了,是时候大放异彩了!', '', '', '', ''), + (337, 'broadcast_levelup_max_level', 'Level %my_level, let’s do some endgame raiding!', 0, 0, '', 'Niveau %my_level, allons faire un raid de fin de jeu!', 'Level %my_level, lasst uns Endgame-Raids machen!', '%my_level级了,咱去刷终极团队副本吧!', '', '', '', ''), + (338, 'broadcast_levelup_max_level', 'At level %my_level, nothing can stop me now!', 0, 0, '', 'Au niveau %my_level, rien ne peut m’arrêter maintenant !', 'Mit Level %my_level hält mich nix mehr auf!', '在%my_level级,谁也挡不住我了!', '', '', '', ''), + (339, 'broadcast_levelup_max_level', 'Level %my_level, all systems go for high-level content!', 0, 0, '', 'Niveau %my_level, toutes les systèmes sont prêts pour du contenu de haut niveau !', 'Level %my_level, bereit für High-Level-Content!', '%my_level级,一切就绪,准备迎接高难度内容!', '', '', '', ''), + (340, 'broadcast_levelup_max_level', 'Yes! Level %my_level, %my_class ready for everything!', 0, 0, '', 'Oui! Niveau %my_level, %my_class prêt pour tout!', 'Yes! Level %my_level, %my_class für alles bereit!', '太棒了!%my_level级,我的%my_class准备好面对一切了!', '', '', '', ''), + (341, 'broadcast_levelup_max_level', 'Level %my_level, it’s time to take on the world!', 0, 0, '', 'Niveau %my_level, c’est le moment de conquérir le monde!', 'Level %my_level, Zeit die Welt herauszufordern!', '%my_level级,是时候征服这个世界了!', '', '', '', ''), + (342, 'broadcast_levelup_max_level', 'Reached level %my_level, let’s take on those elite bosses!', 0, 0, '', 'J’ai atteint le niveau %my_level, allons affronter ces boss d’élite!', 'Level %my_level erreicht, schnappen wir uns die Elitebosse!', '达到%my_level级了,咱们去挑战那些精英首领吧!', '', '', '', ''), + (343, 'broadcast_levelup_max_level', 'Now I’m %my_level, the real adventure begins!', 0, 0, '', 'Maintenant que je suis %my_level, la vraie aventure commence!', 'Jetzt bin ich Level %my_level, das echte Abenteuer beginnt!', '现在我是%my_level级,真正的冒险才刚刚开始!', '', '', '', ''), + (344, 'broadcast_levelup_max_level', 'Level %my_level! Time for some serious action!', 0, 0, '', 'Niveau %my_level ! Il est temps d’entrer en action sérieusement !', 'Level %my_level! Zeit für ernsthafte Action!', '%my_level级!是时候来点硬核行动了!', '', '', '', ''), + (345, 'broadcast_levelup_max_level', 'Level %my_level and still growing stronger!', 0, 0, '', 'Niveau %my_level et je continue de devenir de plus en plus fort !', 'Level %my_level und werde immer stärker!', '%my_level级了,而且我还会变得更强!', '', '', '', ''), + (346, 'broadcast_levelup_max_level', 'Finally at level %my_level! Let’s show off what I can do!', 0, 0, '', 'Enfin, je suis au niveau %my_level ! Il est temps de montrer de quoi je suis capable !', 'Endlich auf Level %my_level! Jetzt zeige ich, was ich kann!', '终于到%my_level级了!让大家看看我的厉害!', '', '', '', ''), + (347, 'broadcast_levelup_max_level', 'Level %my_level is here! I’m ready for the big leagues!', 0, 0, '', 'Le niveau %my_level est arrivé! Je suis prêt pour les grands matchs!', 'Level %my_level ist da! Bereit für die große Liga!', '%my_level级来了!我准备好迎接大挑战了!', '', '', '', ''), + (348, 'broadcast_levelup_max_level', 'Just hit %my_level, get ready for the real challenges!', 0, 0, '', 'Je viens d’atteindre mon niveau %my_level, je suis prêt pour affronter de vrais défis !', 'Gerade Level %my_level erreicht, macht euch bereit für echte Herausforderungen!', '刚到%my_level级,准备好迎接真正的挑战吧!', '', '', '', ''), + (349, 'broadcast_levelup_max_level', 'Level %my_level complete, it’s time for the epic content!', 0, 0, '', 'Niveau %my_level complet, il est temps pour le contenu épique !', 'Level %my_level voll, Zeit für epischen Content!', '%my_level级达成,是时候体验史诗内容了!', '', '', '', ''), + (350, 'broadcast_levelup_max_level', 'Fresh out of %my_level! Ready for endgame dungeons!', 0, 0, '', 'Je viens enfin d’atteindre %my_level ! Prêt pour les donjons de fin de jeu !', 'Frisch auf Level %my_level! Bereit für Endgame-Inis!', '刚到%my_level级!准备好挑战终极地下城了!', '', '', '', ''), + (351, 'broadcast_levelup_max_level', 'Now that I’m level %my_level, nothing is beyond my reach!', 0, 0, '', 'Maintenant que je suis au niveau %my_level, rien ne peut m’arrêter !', 'Jetzt, wo ich Level %my_level bin, ist nichts mehr außerhalb meiner Reichweite!', '现在我是%my_level级,没有什么是我做不到的!', '', '', '', ''), + (352, 'broadcast_levelup_max_level', 'It’s official! Level %my_level, time to dominate!', 0, 0, '', 'C’est officiel ! Niveau %my_level, c’est le moment de tout déchirer !', 'Jetzt ist es offiziell! Level %my_level, Zeit zu dominieren!', '正式宣布!%my_level级,是时候称霸一方了!', '', '', '', ''), + (353, 'broadcast_levelup_max_level', 'Made it to level %my_level! Time to make my mark on the world!', 0, 0, '', 'J’ai atteint le niveau %my_level! Le moment de laisser ma trace dans le monde est enfin arrivé!', 'Level %my_level geschafft! Zeit, der Welt meinen Stempel aufzudrücken!', '达到%my_level级了!是时候在这个世界上留下我的印记了!', '', '', '', ''), + (354, 'broadcast_levelup_max_level', 'Level %my_level unlocked! The real fun begins now!', 0, 0, '', 'Niveau %my_level débloqué! Maintenant, les choses sérieuses commencent!', 'Level %my_level freigeschaltet! Jetzt beginnt der echte Spaß!', '%my_level级解锁!真正的乐趣现在才开始!', '', '', '', ''), + (355, 'broadcast_levelup_max_level', 'Level %my_level, and the world is my playground!', 0, 0, '', 'Niveau %my_level, et le monde est mon terrain de jeu!', 'Level %my_level, und die Welt ist mein Spielplatz!', '%my_level级,这个世界就是我的游乐场!', '', '', '', ''), + (356, 'broadcast_levelup_max_level', 'Level %my_level, ready to show the world my power!', 0, 0, '', 'Niveau %my_level, prêt à montrer au monde ma puissance !', 'Level %my_level, bereit, der Welt meine Power zu zeigen!', '%my_level级,准备好向世界展示我的力量了!', '', '', '', ''), + (357, 'broadcast_levelup_max_level', 'Just hit %my_level, I’m going all in now!', 0, 0, '', 'Je viens d’atteindre %my_level, je me lance à fond maintenant !', 'Gerade %my_level erreicht, jetzt geh ich all in!', '刚到%my_level级,现在我要全力以赴了!', '', '', '', ''), + (358, 'broadcast_levelup_max_level', 'Level %my_level, I can finally take on everything!', 0, 0, '', 'Niveau %my_level, je peux enfin tout affronter !', 'Level %my_level, endlich kann ich alles angehen!', '%my_level级,我终于能挑战一切了!', '', '', '', ''), + (359, 'broadcast_levelup_max_level', 'Level %my_level, I’m feeling unstoppable!', 0, 0, '', 'Niveau %my_level, je me sens imbattable!', 'Level %my_level, fühl mich unaufhaltbar!', '%my_level级,我感觉自己势不可挡!', '', '', '', ''), + (360, 'broadcast_levelup_max_level', 'Just reached level %my_level, can’t wait to dive into the endgame!', 0, 0, '', 'Je viens d’atteindre le niveau %my_level, j’ai hâte de me plonger dans le contenu de fin de jeu!', 'Gerade Level %my_level erreicht, kanns kaum erwarten ins Endgame zu springen!', '刚达到%my_level级,迫不及待要投入终极内容了!', '', '', '', ''), + (361, 'broadcast_guild_promotion', 'Good job %other_name. You deserved this.', 0, 0, '', 'Bien joué %other_name. Tu l’as bien mérité.', 'Gute Arbeit, %other_name. Das hast du dir verdient.', '干得好,%other_name,你当之无愧!', '', '', '', 'Молодец %other_name. Ты это заслужил.'), + (362, 'broadcast_guild_demotion', 'That was awful %other_name. I hate to do this but...', 0, 0, '', 'Désolé %other_name, mais ça a été horrible. Je déteste devoir faire ça, mais...', 'Das war mies, %other_name. Ich mags nicht, aber...', '太糟糕了,%other_name,我也不想这么做,但是...', '', '', '', 'Это было ужасно %other_name. Не хотел этого делать, но...'), + (363, 'broadcast_guild_promotion', 'Congrats %other_name, you’ve earned this promotion!', 0, 0, '', 'Félicitations %other_name, tu as mérité cette promotion !', 'Glückwunsch, %other_name, du hast den Aufstieg verdient!', '恭喜你,%other_name,这晋升是你应得的!', '', '', '', ''), + (364, 'broadcast_guild_promotion', 'Well deserved %other_name, welcome to the next level!', 0, 0, '', 'Bien mérité %other_name, bienvenue au niveau suivant !', 'Verdient, %other_name - willkommen im nächsten Level!', '名副其实,%other_name,欢迎进入下一阶段!', '', '', '', ''), + (365, 'broadcast_guild_promotion', '%other_name is leveling up! Congratulations on the promotion!', 0, 0, '', '%other_name progresse dans sa carrière ! Félicitations pour la promotion !', '%other_name steigt auf! Glückwunsch zur Beförderung!', '%other_name升职了!恭喜晋升!', '', '', '', ''), + (366, 'broadcast_guild_promotion', 'Promotion time! %other_name, you’ve earned this one!', 0, 0, '', 'Promotion time! %other_name, tu l’as bien mérité !', 'Beförderungszeit! %other_name, die hast du dir verdient!', '晋升时刻!%other_name,你当之无愧!', '', '', '', ''), + (367, 'broadcast_guild_promotion', 'A new role for %other_name! Well done, keep it up!', 0, 0, '', 'Une nouvelle fonction pour %other_name ! Bravo, continue comme ça !', 'Neue Rolle für %other_name! Gut gemacht, weiter so!', '%other_name获得了新角色!干得漂亮,继续加油!', '', '', '', ''), + (368, 'broadcast_guild_promotion', 'Huge congrats to %other_name, you’ve earned a well-deserved promotion!', 0, 0, '', '%other_name, félicitations pour ta super promotion, tu la mérites amplement!', 'Dickes Gratz an %other_name, wohlverdiente Beförderung!', '热烈祝贺%other_name,你实至名归地获得了晋升!', '', '', '', ''), + (369, 'broadcast_guild_promotion', 'Let’s all give a round of applause to %other_name, for the promotion!', 0, 0, '', 'Donnons tous un grand round d’applaudissements à %other_name pour sa promotion!', 'Applaus für %other_name zur Beförderung!', '让我们为%other_name的晋升鼓掌庆祝吧!', '', '', '', ''), + (370, 'broadcast_guild_promotion', 'Cheers to %other_name on the promotion! Keep shining!', 0, 0, '', 'Salut à %other_name pour sa promotion! Continue de briller!', 'Gratulation an %other_name zur Beförderung! Weiter so!', '为%other_name的晋升干杯!继续发光发热!', '', '', '', ''), + (371, 'broadcast_guild_promotion', 'Congrats %other_name, you’re moving up! Well done!', 0, 0, '', 'Félicitations %other_name, tu progresses dans ton parcours ! Bravo !', 'Gratz, %other_name, du steigst auf! Gut gemacht!', '恭喜%other_name,你更进一步了!干得好!', '', '', '', ''), + (372, 'broadcast_guild_promotion', 'Another step up for %other_name! Congratulations!', 0, 0, '', 'Encore une promotion pour %other_name ! Félicitations !', 'Noch ein Schritt nach oben für %other_name! Glückwunsch!', '%other_name又迈上新台阶!恭喜!', '', '', '', ''), + (373, 'broadcast_guild_demotion', 'Tough decision, but %other_name, you’ve been demoted. Time to step up!', 0, 0, '', 'Décision difficile, mais %other_name, tu as été rétrogradé. C’est le moment de te ressaisir!', 'Harte Entscheidung, aber %other_name, du wurdest degradiert. Zeit, dich zu beweisen!', '艰难的决定,%other_name,你被降职了。加把劲吧!', '', '', '', ''), + (374, 'broadcast_guild_demotion', 'Sorry %other_name, but it’s time for a demotion. Hope you come back stronger!', 0, 0, '', 'Désolé %other_name, mais c’est l’heure d’une rétrogradation. J’espère que tu reviendras plus fort !', 'Sorry, %other_name, aber es gibt eine Degradierung. Komm stärker zurück!', '抱歉,%other_name,现在得降职了。希望你能更强地回来!', '', '', '', ''), + (375, 'broadcast_guild_demotion', 'Unfortunately, %other_name, it’s a demotion this time. You’ll get \'em next time!', 0, 0, '', '', 'Leider, %other_name, diesmal eine Degradierung. Beim nächsten Mal packst du es!', '遗憾的是,%other_name,这次是降职。下次再努力吧!', '', '', '', ''), + (376, 'broadcast_guild_demotion', 'A hard decision, but %other_name, you’ve been demoted. Time to grind back up!', 0, 0, '', 'Une décision difficile, mais %other_name, tu as été rétrogradé. Il est temps de remonter la pente !', 'Schwere Entscheidung, aber %other_name, du wurdest degradiert. Zeit, dich wieder hochzugrinden!', '艰难的决定,%other_name,你被降职了。是时候努力爬回去了!', '', '', '', ''), + (377, 'broadcast_guild_demotion', 'We hate to see it, but %other_name, it’s a demotion for now.', 0, 0, '', 'C’est toujours difficile à accepter, %other_name, mais pour le moment, c’est une rétrogradation.', 'Unschön, aber %other_name, vorerst eine Degradierung.', '我们也不想看到,%other_name,但现在是降职。', '', '', '', ''), + (378, 'broadcast_guild_demotion', 'Sorry %other_name, your rank has been lowered. Let’s work harder together!', 0, 0, '', 'Désolé %other_name, ton rang a été abaissé. On va devoir redoubler d’efforts ensemble !', 'Sorry, %other_name, dein Rang wurde gesenkt. Lass uns zusammen härter arbeiten!', '对不起,%other_name,你的等级被降低了。咱们一起更努力吧!', '', '', '', ''), + (379, 'broadcast_guild_demotion', 'A demotion for %other_name... Time to focus and rise back up!', 0, 0, '', 'Une rétrogradation pour %other_name... Il est temps de se concentrer et de remonter la pente!', 'Eine Degradierung für %other_name... Zeit, sich zu fokussieren und wieder hochzukommen!', '%other_name被降职了...是时候集中精力再爬上去了!', '', '', '', ''), + (380, 'broadcast_guild_demotion', 'Not what we wanted, but %other_name, you’ve been demoted. Learn and come back stronger!', 0, 0, '', 'Pas ce que nous voulions, mais %other_name, tu as été rétrogradé. Apprends et reviens plus fort !', 'Nicht das, was wir wollten, aber %other_name, du wurdest degradiert. Lern draus und komm stärker zurück!', '这不是我们想要的,%other_name,你被降职了。吸取教训,变得更强吧!', '', '', '', ''), + (381, 'broadcast_guild_demotion', 'A tough moment for %other_name, but we know you can bounce back from this demotion!', 0, 0, '', 'Un moment difficile pour %other_name, mais on sait que tu peux rebondir après cette rétrogradation!', 'Harter Moment für %other_name, aber wir wissen, du kommst von der Degradierung zurück!', '对%other_name来说是艰难的一刻,但我们相信你能从这次降职中恢复!', '', '', '', ''), + (382, 'broadcast_guild_demotion', 'Sorry %other_name, but a demotion has been applied. It’s time for improvement!', 0, 0, '', 'Désolé %other_name, mais une rétrogradation a été appliquée. Il est temps de s’améliorer !', 'Sorry, %other_name, aber du wurdest degradiert. Zeit für Verbesserungen!', '抱歉,%other_name,你被降职了。是时候改进自己了!', '', '', '', ''), + (383, 'broadcast_guild_demotion', 'We didn’t want this for %other_name, but a demotion has occurred. Time to get better!', 0, 0, '', 'On n’a pas voulu cela pour %other_name, mais une rétrogradation a eu lieu. Il est temps de s’améliorer !', 'Wir wollten das nicht für %other_name, aber es gab eine Degradierung. Zeit, besser zu werden!', '我们不希望这样,%other_name,但降职已发生。是时候变得更好了!', '', '', '', ''), + (384, 'broadcast_guild_recruitment', 'Looking for fresh blood! Join the guild, be part of something great!', 0, 0, '', 'À la recherche de sang neuf ! Rejoins la guilde, fais partie de quelque chose de grandiose !', 'Frisches Blut gesucht! Tritt der Gilde bei, sei Teil von was Großem!', '寻找新鲜血液!加入我们的公会,一起成就伟业!', '', '', '', ''), + (385, 'broadcast_guild_recruitment', 'Recruiting members! If you’re ready to join us, the guild door is open!', 0, 0, '', 'Recrutement de membres ! Si tu es prêt à nous rejoindre, la porte de la guilde est ouverte!', 'Wir rekrutieren neue Mitglieder! Wenn du bereit bist, schließ dich an - die Gildentür steht offen!', '招募成员!如果你准备好了,公会的大门随时为你敞开!', '', '', '', ''), + (386, 'broadcast_guild_recruitment', 'The guild is looking for skilled adventurers. Join us and thrive!', 0, 0, '', 'La guilde recherche des aventuriers compétents. Rejoins-nous et prospère!', 'Die Gilde sucht fähige Abenteurer. Schließ dich an und geh mit uns ab!', '公会正在寻找有实力的冒险者。加入我们,一起成长!', '', '', '', ''), + (387, 'broadcast_guild_recruitment', 'Want to join a great guild? We’re looking for new members! Come on in!', 0, 0, '', 'Envie de rejoindre une super guilde ? Nous cherchons de nouveaux membres ! Viens nous rejoindre !', 'Bock auf eine starke Gilde? Wir suchen neue Member! Komm rein!', '想加入一个厉害的公会吗?我们在招新成员!快来加入吧!', '', '', '', ''), + (388, 'broadcast_guild_recruitment', 'The guild is growing! Join us for exciting adventures and rewards!', 0, 0, '', 'La guilde grandit! Rejoignez-nous pour vivre des aventures palpitantes et obtenir des récompenses!', 'Die Gilde wächst! Join uns für fette Abenteuer und Loot!', '公会正在壮大!加入我们,体验刺激的冒险和丰厚的奖励!', '', '', '', ''), + (389, 'suggest_instance', 'Anyone wants %instance_name?', 0, 0, '', 'Qui veut %instance_name?', 'Wer hat Bock auf %instance_name?', '有人想去%instance_name吗?', '', '¿Alguien quiere ir a %instance_name?', '', 'Кому нужен %instance_name?'), + (390, 'suggest_instance', 'Any groups for %instance_name?', 0, 0, '', 'Des groupes pour %instance_name?', 'Gibts Gruppen für %instance_name?', '有组队去%instance_name的吗?', '', '¿Algún grupo para %instance_name?', '', 'Есть группы в %instance_name?'), + (391, 'suggest_instance', 'Need help for %instance_name?', 0, 0, '', 'Besoin d’aide pour %instance_name ?', 'Brauch jemand Hilfe für %instance_name?', '有人需要帮忙打%instance_name吗?', '', '¿Alguien necesita ayuda con %instance_name?', '', 'Нужна помощь в %instance_name?'), + (392, 'suggest_instance', 'LFD: %instance_name.', 0, 0, '', 'LFG: %instance_name.', 'Suche Gruppe: %instance_name.', '寻找队伍:%instance_name。', '', 'Busco grupo para %instance_name.', '', 'ЛФД: %instance_name.'), + (393, 'suggest_instance', 'Anyone needs %my_role for %instance_name?', 0, 0, '', 'Besoin de %my_role pour %instance_name?', 'Braucht wer %my_role für %instance_name?', '有人需要%my_role去%instance_name吗?', '', '¿Alguien necesita un %my_role para %instance_name?', '', 'Кому нужен %my_role для %instance_name?'), + (394, 'suggest_instance', 'Missing %my_role for %instance_name?', 0, 0, '', 'Il manque %my_role pour %instance_name ?', 'Fehlt %my_role für %instance_name?', '缺一个%my_role去%instance_name,有人来吗?', '', '¿Buscando un %my_role para %instance_name?', '', 'Не хватает %my_role для %instance_name?'), + (395, 'suggest_instance', 'Can be a %my_role for %instance_name.', 0, 0, '', 'Je peux être %my_role pour %instance_name.', 'Ich kann %my_role für %instance_name sein.', '我可以当%my_role去%instance_name。', '', 'Puedo ser un %my_role para %instance_name.', '', 'Могу быть %my_role для %instance_name.'), + (396, 'suggest_instance', 'Need help with %instance_name?', 0, 0, '', 'Besoin d’aide avec %instance_name?', 'Braucht jemand Hilfe bei %instance_name?', '有人需要帮忙过%instance_name吗?', '', '¿Alguien necesita ayuda con %instance_name?', '', 'Нужна помощь с %instance_name?'), + (397, 'suggest_instance', 'Need %my_role help with %instance_name?', 0, 0, '', 'Besoin d’aide avec %instance_name? Je peux t’aider avec ça?', 'Braucht ihr %my_role für %instance_name?', '需要一个%my_role帮忙打%instance_name吗?', '', '¿Necesitas ayuda de %my_role con %instance_name?', '', 'Нужна помощь %my_role с %instance_name?'), + (398, 'suggest_instance', 'Anyone needs gear from %instance_name?', 0, 0, '', 'Besoin d’équipement de %instance_name?', 'Braucht wer Gear aus %instance_name?', '有人需要%instance_name的装备吗?', '', '¿Alguien necesita equipo de %instance_name?', '', 'Кому нужен шмот из %instance_name?'), + (399, 'suggest_instance', 'A little grind in %instance_name?', 0, 0, '', 'Un petit farming dans %instance_name ?', 'Kleiner Grind in %instance_name?', '去%instance_name刷一波怎么样?', '', '¿Un poco de farmeo en %instance_name?', '', 'Немного пофармить в %instance_name?'), + (400, 'suggest_instance', 'WTR %instance_name', 0, 0, '', 'WTR %instance_name', 'Lust auf %instance_name', '想跑%instance_name', '', 'WTR %instance_name', '', 'Ищу %instance_name'), + (401, 'suggest_instance', 'Need help for %instance_name.', 0, 0, '', 'Besoin d’aide pour %instance_name.', 'Brauche Hilfe für %instance_name.', '需要帮忙打%instance_name。', '', 'Necesito ayuda para %instance_name.', '', 'Нужна помощь в %instance_name.'), + (402, 'suggest_instance', 'Wanna run %instance_name.', 0, 0, '', 'Tu veux lancer %instance_name.', 'Bock auf %instance_name.', '想去跑一趟%instance_name。', '', 'Quiero hacer %instance_name.', '', 'Хочу пройти %instance_name.'), + (403, 'suggest_instance', '%my_role looks for %instance_name.', 0, 0, '', 'Je recherche %instance_name dans le rôle de %my_role.', '%my_role sucht Gruppe für %instance_name.', '%my_role寻找%instance_name的队伍。', '', '%my_role busca %instance_name.', '', '%my_role ищет %instance_name.'), + (404, 'suggest_instance', 'What about %instance_name?', 0, 0, '', 'Et %instance_name, qu’en est-il?', 'Was ist mit %instance_name?', '%instance_name怎么样,去不去?', '', '¿Qué pasa con %instance_name?', '', 'Как насчет %instance_name?'), + (405, 'suggest_instance', 'Who wants to farm %instance_name?', 0, 0, '', 'Qui veut farm %instance_name ?', 'Wer will %instance_name farmen?', '有谁想去刷%instance_name?', '', '¿Quién quiere farmear %instance_name?', '', 'Кто хочет пофармить %instance_name?'), + (406, 'suggest_instance', 'Go in %instance_name?', 0, 0, '', 'On va dans %instance_name ?', 'Rein in %instance_name?', '去%instance_name怎么样?', '', '¿Vamos a %instance_name?', '', 'Пойдем в %instance_name?'), + (407, 'suggest_instance', 'Looking for %instance_name.', 0, 0, '', 'Je recherche %instance_name.', 'Suche Gruppe für %instance_name.', '寻找%instance_name的队伍。', '', 'Buscando gente para %instance_name.', '', 'Ищу %instance_name.'), + (408, 'suggest_instance', 'Need help with %instance_name quests?', 0, 0, '', 'Besoin d’aide avec les quêtes de %instance_name ?', 'Braucht wer Hilfe bei Quests in %instance_name?', '有人需要帮忙做%instance_name的任务吗?', '', '¿Necesitas ayuda con las misiones en %instance_name?', '', 'Нужна помощь с квестами в %instance_name?'), + (409, 'suggest_instance', 'Wanna quest in %instance_name.', 0, 0, '', 'Tu veux partir en quête dans %instance_name?', 'Bock auf Quests in %instance_name.', '想去%instance_name做任务。', '', 'Quiero hacer una vuelta rapida en %instance_name.', '', 'Хочу квестить в %instance_name.'), + (410, 'suggest_instance', 'Anyone with quests in %instance_name?', 0, 0, '', 'Quelqu’un a des quêtes dans %instance_name?', 'Wer hat Quests in %instance_name?', '有人有%instance_name的任务一起做吗?', '', '¿Alguien con misiones en %instance_name?', '', 'Кто-то с квестами в %instance_name?'), + (411, 'suggest_instance', 'Could help with quests in %instance_name.', 0, 0, '', '%instance_name, vous pouvez m’aider avec les quêtes ?', 'Kann bei Quests in %instance_name helfen.', '我可以帮忙做%instance_name的任务。', '', 'Podría ayudar con las misiones en %instance_name.', '', 'Могу помочь с квестами в %instance_name.'), + (412, 'suggest_instance', '%my_role: any place in group for %instance_name?', 0, 0, '', '%my_role: Est-ce qu’il y a une place dans le groupe pour %instance_name?', '%my_role: Platz in Gruppe für %instance_name?', '%my_role:%instance_name的队伍有位置吗?', '', '%my_role: ¿algún lugar en el grupo para %instance_name?', '', '%my_role: есть место в группе для %instance_name?'), + (413, 'suggest_instance', 'Does anybody still run %instance_name this days?', 0, 0, '', 'Est-ce que quelqu’un utilise encore %instance_name de nos jours?', 'Läuft überhaupt noch jemand %instance_name?', '现在还有人跑%instance_name吗?', '', '¿Alguien todavía hace %instance_name estos días?', '', 'Кто-нибудь еще ходит в %instance_name в наши дни?'), + (414, 'suggest_instance', '%instance_name: anyone wants to take a %my_role?', 0, 0, '', '%instance_name: Est-ce que quelqu’un veut prendre un %my_role?', '%instance_name: nimmt wer einen %my_role mit?', '%instance_name:有人需要一个%my_role吗?', '', '¿Alguien quiere un %my_role para %instance_name?', '', '%instance_name: кто хочет взять %my_role?'), + (415, 'suggest_instance', 'Is there any point being %my_role in %instance_name?', 0, 0, '', 'Est-ce que ça vaut la peine d’être %my_role dans %instance_name?', 'Lohnt es sich als %my_role in %instance_name?', '在%instance_name当%my_role有意义吗?', '', '¿Tiene algún sentido ser %my_role en %instance_name?', '', 'Есть ли смысл быть %my_role в %instance_name?'), + (416, 'suggest_instance', 'It is really worth to go to %instance_name?', 0, 0, '', 'Est-ce que ça vaut vraiment la peine d’aller à %instance_name?', 'Lohnt es sich echt, nach %instance_name zu gehen?', '去%instance_name真的值得吗?', '', '¿Realmente vale la pena ir a %instance_name?', '', 'Стоит ли идти в %instance_name?'), + (417, 'suggest_instance', 'Anybody needs more people for %instance_name?', 0, 0, '', 'Est-ce que quelqu’un a besoin de plus de personnes pour %instance_name?', 'Braucht jemand noch Leute für %instance_name?', '有人需要更多人一起打%instance_name吗?', '', '¿Alguien necesita más personas para %instance_name?', '', 'Кому нужны еще люди для %instance_name?'), + (418, 'suggest_instance', '%instance_name bosses drop good gear. Wanna run?', 0, 0, '', '%instance_name Les boss laissent tomber du bon équipement. Tu veux qu’on y aille?', 'Die Bosse in %instance_name droppen gutes Gear. Bock zu grinden?', '%instance_name的首领掉好装备,想去跑一趟吗?', '', 'Los jefes en %instance_name sueltan buen equipo. ¿Quieres ir?', '', 'В %instance_name боссы дропают хороший шмот. Пойдем?'), + (419, 'suggest_instance', 'What about %instance_name?', 0, 0, '', 'Que dire de %instance_name ?', 'Was ist mit %instance_name?', '%instance_name咋样,去不?', '', '¿Qué pasa con %instance_name?', '', 'Как насчет %instance_name?'), + (420, 'suggest_instance', 'Anybody needs %my_role?', 0, 0, '', 'Est-ce que quelqu’un a besoin de %my_role?', 'Braucht jemand %my_role?', '有人需要一个%my_role吗?', '', '¿Alguien necesita un %my_role?', '', 'Кому нужен %my_role?'), + (421, 'suggest_instance', 'Anyone needs %my_role?', 0, 0, '', 'Besoin de %my_role?', 'Braucht wer %my_role?', '有人缺一个%my_role吗?', '', '¿Alguien hacer un grupo con un %my_role?', '', 'Кому нужен %my_role?'), + (422, 'suggest_instance', 'Who wants %instance_name?', 0, 0, '', 'Qui veut %instance_name ?', 'Wer hat Bock auf %instance_name?', '谁想去%instance_name?', '', '¿Quién quiere %instance_name?', '', 'Кто хочет %instance_name?'), + (423, 'suggest_instance', 'Can anybody summon me at %instance_name?', 0, 0, '', 'Est-ce que quelqu’un peut m’invoquer à %instance_name?', 'Kann mich jemand bei %instance_name beschwören?', '有人能在%instance_name召唤我吗?', '', '¿Alguien puede invocarme en %instance_name?', '', 'Кто может призвать меня в %instance_name?'), + (424, 'suggest_instance', 'Meet me in %instance_name', 0, 0, '', 'Rencontre-moi dans %instance_name', 'Trefft mich in %instance_name', '在%instance_name见我吧。', '', 'Encuéntrame en %instance_name', '', 'Встретимся в %instance_name'), + (425, 'suggest_instance', 'Wanna quick %instance_name run', 0, 0, '', 'Tu veux lancer rapidement %instance_name ?', 'Bock auf schnellen %instance_name Run', '想快速跑一趟%instance_name吗?', '', 'Quiero ir a %instance_name rápido', '', 'Хочу быструю пробежку %instance_name'), + (426, 'suggest_instance', 'Wanna full %instance_name run', 0, 0, '', 'Bien sûr, je veux que l’instance %instance_name fonctionne.', 'Bock auf kompletten %instance_name Run', '想完整跑一遍%instance_name吗?', '', 'Quiero hacer %instance_name completa', '', 'Хочу полный забег %instance_name'), + (427, 'suggest_instance', 'How many times were you in %instance_name?', 0, 0, '', 'Combien de fois ai-je été dans %instance_name?', 'Wie oft wart ihr schon in %instance_name?', '你去过%instance_name几次了?', '', '¿Cuántas veces estuviste en %instance_name?', '', 'Сколько раз ты был в %instance_name?'), + (428, 'suggest_instance', 'Another %instance_name run?', 0, 0, '', 'Une autre éxpédition dans %instance_name ?', 'Noch ein %instance_name Run?', '再跑一趟%instance_name怎么样?', '', '¿Otra vuelta en %instance_name ?', '', 'Еще один забег в %instance_name?'), + (429, 'suggest_instance', 'Wiped in %instance_name? Take me instead!', 0, 0, '', 'Wipe dans %instance_name ? Prends-moi à la place !', 'In %instance_name gewiped? Nehmt lieber mich!', '在%instance_name团灭了?带我吧!', '', '¿Borrado en %instance_name? ¡Tómame a mí en su lugar!', '', 'Вайпнулись в %instance_name? Возьмите меня!'), + (430, 'suggest_instance', 'Take me in %instance_name please.', 0, 0, '', 'Prenez-moi dans %instance_name s’il vous plaît.', 'Nehmt mich bitte mit nach %instance_name.', '请带我去%instance_name。', '', 'Tómame en %instance_name por favor.', '', 'Возьмите меня в %instance_name, пожалуйста.'), + (431, 'suggest_instance', 'Quick %instance_name run?', 0, 0, '', 'Rapidement, %instance_name, on y va ?', 'Schneller %instance_name Run?', '快速跑一趟%instance_name?', '', '¿Partida rápida de %instance_name?', '', 'Быстрый забег в %instance_name?'), + (432, 'suggest_instance', 'Full %instance_name run?', 0, 0, '', 'Complet %instance_name ?', 'Kompletter %instance_name Run?', '完整跑一遍%instance_name?', '', '¿Partida completa de %instance_name?', '', 'Полный забег в %instance_name?'), + (433, 'suggest_instance', 'Who can take %my_role to %instance_name?', 0, 0, '', 'Qui peut prendre %my_role à %instance_name?', 'Wer nimmt %my_role mit nach %instance_name?', '谁能带一个%my_role去%instance_name?', '', '¿Quién puede ir de %my_role para %instance_name?', '', 'Кто может взять %my_role в %instance_name?'), + (434, 'suggest_instance', 'LFG %instance_name, I am %my_role', 0, 0, '', 'LFG %instance_name, je suis %my_role.', 'LFG %instance_name, bin %my_role', '找队%instance_name,我是%my_role', '', '', '', 'ЛФГ %instance_name, я %my_role'), + (435, 'suggest_instance', '%my_role LFG %instance_name', 0, 0, '', '%my_role LFG %instance_name.', '%my_role LFG %instance_name', '%my_role找队%instance_name', '', '', '', '%my_role ЛФГ %instance_name'), + (436, 'suggest_instance', 'Anyone looking for a group for %instance_name?', 0, 0, '', 'Quelqu’un cherche un groupe pour %instance_name?', 'Sucht jemand eine Gruppe für %instance_name?', '有人在找%instance_name的队伍吗?', '', '¿Alguien busca grupo para %instance_name?', '', ''), + (437, 'suggest_instance', 'Need a DPS for %instance_name', 0, 0, '', 'Besoin d’un DPS pour %instance_name.', 'Brauche einen DD für %instance_name', '需要一个DPS去%instance_name', '', 'Necesito un DPS para %instance_name', '', ''), + (438, 'suggest_instance', 'Looking for a healer for %instance_name', 0, 0, '', 'Je suis à la recherche d’un guérisseur pour %instance_name.', 'Suche einen Heiler für %instance_name', '找一个治疗去%instance_name', '', 'Busco un sanador para %instance_name', '', ''), + (439, 'suggest_instance', 'Anyone need a tank for %instance_name?', 0, 0, '', 'Besoin d’un tank pour %instance_name ?', 'Braucht jemand einen Tank für %instance_name?', '有人需要一个坦克去%instance_name吗?', '', '¿Alguien necesita un tanque para %instance_name?', '', ''), + (440, 'suggest_instance', 'Trying to do %instance_name. Need a group!', 0, 0, '', 'J’essaie de faire %instance_name. Il me faut un groupe !', 'Will %instance_name machen. Brauche eine Gruppe!', '想打%instance_name,缺个队伍!', '', 'Intentando hacer %instance_name. ¡Necesito un grupo!', '', ''), + (441, 'suggest_instance', 'Anyone down for %instance_name?', 0, 0, '', 'Qui est partant pour %instance_name?', 'Wer hat Lust auf %instance_name?', '有人想一起去%instance_name吗?', '', '¿Alguien se apunta para %instance_name?', '', ''), + (442, 'suggest_instance', 'Looking for 1 more for %instance_name', 0, 0, '', 'Il nous manque 1 personne pour %instance_name.', 'Suche noch einen für %instance_name', '还差1人去%instance_name', '', 'Busco 1 más para %instance_name', '', ''), + (443, 'suggest_instance', 'Anyone wanna join for %instance_name?', 0, 0, '', 'Quelqu’un veut se joindre pour %instance_name?', 'Will jemand mit nach %instance_name?', '有人想加入%instance_name的队伍吗?', '', '¿Alguien quiere unirse para %instance_name?', '', ''), + (444, 'suggest_instance', 'Anyone interested in %instance_name?', 0, 0, '', 'Quelqu’un intéressé par %instance_name ?', 'Interesse an %instance_name?', '有人对%instance_name感兴趣吗?', '', '¿Alguien interesado en %instance_name?', '', ''), + (445, 'suggest_instance', 'Looking for a quick run through %instance_name', 0, 0, '', 'À la recherche d’un rapide survol de %instance_name', 'Suche einen schnellen Run durch %instance_name', '想快速通关%instance_name', '', 'Buscando una corrida rápida en %instance_name', '', ''), + (446, 'suggest_instance', 'Who’s up for %instance_name?', 0, 0, '', 'Qui est partant pour %instance_name?', 'Wer ist dabei für %instance_name?', '谁愿意去%instance_name?', '', '¿Quién está listo para %instance_name?', '', ''), + (447, 'suggest_instance', 'Need help to clear %instance_name', 0, 0, '', 'Besoin d’aide pour vider %instance_name', 'Brauche Hilfe, um %instance_name zu clearen', '需要帮忙清%instance_name', '', 'Necesito ayuda para limpiar %instance_name', '', ''), + (448, 'suggest_instance', 'Want to farm %instance_name?', 0, 0, '', 'Envie de farmer %instance_name?', 'Wollt ihr %instance_name farmen?', '想去刷%instance_name吗?', '', '¿Quieres farmear %instance_name?', '', ''), + (449, 'suggest_instance', 'Looking to complete %instance_name', 0, 0, '', 'Je cherche à terminer %instance_name.', 'Will %instance_name abschließen', '想完成%instance_name', '', 'Buscando completar %instance_name', '', ''), + (450, 'suggest_instance', 'Anyone needs %instance_name items?', 0, 0, '', 'Besoin d’objets %instance_name ?', 'Braucht jemand Items aus %instance_name?', '有人需要%instance_name的道具吗?', '', '¿Alguien necesita objetos de %instance_name?', '', ''), + (451, 'suggest_instance', 'Looking for a carry through %instance_name', 0, 0, '', 'Je recherche un portage à travers %instance_name.', 'Suche Carry durch %instance_name', '找人带我过%instance_name', '', 'Buscando un carry para %instance_name', '', ''), + (452, 'suggest_instance', 'Anyone want to run %instance_name?', 0, 0, '', 'Quelqu’un veut faire tourner %instance_name?', 'Will jemand %instance_name laufen?', '有人想跑%instance_name吗?', '', '¿Alguien quiere hacer %instance_name?', '', ''), + (453, 'suggest_instance', 'Running %instance_name, looking for more', 0, 0, '', 'En cours d’exécution de %instance_name, je suis à la recherche de plus d’informations.', 'Laufe %instance_name, suche andere', '正在跑%instance_name,还缺人', '', 'Haciendo %instance_name, buscando más personas', '', ''), + (454, 'suggest_instance', 'Anyone available for %instance_name?', 0, 0, '', 'Quelqu’un disponible pour %instance_name ?', 'Hat jemand Zeit für %instance_name?', '有人有空去%instance_name吗?', '', '¿Alguien disponible para %instance_name?', '', ''), + (455, 'suggest_instance', 'Looking for someone to help with %instance_name', 0, 0, '', 'Besoin d’aide pour %instance_name', 'Suche jemanden, der bei %instance_name hilft', '找人帮忙打%instance_name', '', 'Buscando a alguien para ayudar con %instance_name', '', ''), + (456, 'suggest_instance', 'Need %my_role for %instance_name!', 0, 0, '', 'J’ai besoin de %my_role pour %nom_de_l’instance!', 'Brauche %my_role für %instance_name!', '需要一个%my_role去%instance_name!', '', '¡Necesito un %my_role para %instance_name!', '', ''), + (457, 'suggest_instance', 'Want to do a %instance_name run?', 0, 0, '', 'Tu veux faire une séance de %instance_name ?', 'Bock auf einen %instance_name Run?', '有人想一起刷%instance_name吗?', '', '¿Quieres hacer una corrida de %instance_name?', '', ''), + (458, 'suggest_instance', 'Need %my_role for %instance_name run', 0, 0, '', 'J’ai besoin de %my_role pour exécuter %instance_name.', 'Brauche %my_role für %instance_name Run', '刷%instance_name需要一个%my_role!', '', 'Necesito %my_role para la corrida de %instance_name', '', ''), + (459, 'suggest_instance', 'Anyone want to farm %instance_name gear?', 0, 0, '', 'Ça vous dit de farmer les équipements de %instance_name ?', 'Will jemand Gear aus %instance_name farmen?', '有人想一起刷%instance_name的装备吗?', '', '¿Alguien quiere farmear equipo de %instance_name?', '', ''), + (460, 'suggest_instance', 'Looking for a group to speedrun %instance_name', 0, 0, '', 'Je recherche un groupe pour faire une speedrun de %instance_name.', 'Suche Gruppe für %instance_name Speedrun', '找个队伍一起速刷%instance_name!', '', 'Buscando un grupo para speedrun de %instance_name', '', ''), + (461, 'suggest_instance', 'Need a group to clear %instance_name', 0, 0, '', 'Besoin d’un groupe pour nettoyer %instance_name', 'Brauche Gruppe, um %instance_name zu clearen', '需要一个队伍清理%instance_name!', '', 'Necesito un grupo para limpiar %instance_name', '', ''), + (462, 'suggest_instance', 'Anyone interested in farming %instance_name?', 0, 0, '', 'Quelqu’un intéressé par cultiver %instance_name?', 'Interesse, %instance_name zu farmen?', '有人对刷%instance_name感兴趣吗?', '', '¿Alguien interesado en farmear %instance_name?', '', ''), + (463, 'suggest_instance', 'Looking for some gear from %instance_name', 0, 0, '', 'Je cherche des équipements de %instance_name.', 'Suche etwas Gear aus %instance_name', '想从%instance_name弄点装备,有一起的吗?', '', 'Buscando equipo de %instance_name', '', ''), + (464, 'suggest_instance', 'Anyone for a quick %instance_name run?', 0, 0, '', 'Qui est partant pour une petite instance rapide de %instance_name ?', 'Wer hat Lust auf einen schnellen %instance_name Run?', '有人想速刷一波%instance_name吗?', '', '¿Alguien para una corrida rápida de %instance_name?', '', ''), + (465, 'suggest_instance', 'Anyone want to try %instance_name together?', 0, 0, '', 'Qui veut essayer %instance_name ensemble ?', 'Will jemand %instance_name zusammen angehen?', '有人想一起挑战%instance_name吗?', '', '¿Alguien quiere intentar %instance_name juntos?', '', ''), + (466, 'suggest_instance', 'Looking for people to complete %instance_name', 0, 0, '', 'Je recherche des personnes pour compléter %instance_name.', 'Suche Leute, um %instance_name abzuschließen', '找人一起完成%instance_name!', '', 'Buscando personas para completar %instance_name', '', ''), + (467, 'suggest_instance', 'Can we get a group for %instance_name?', 0, 0, '', 'Peut-on former un groupe pour %instance_name?', 'Kriegen wir eine Gruppe für %instance_name zusammen?', '能组个队去%instance_name吗?', '', '¿Podemos formar un grupo para %instance_name?', '', ''), + (468, 'suggest_instance', 'Need a quick run for %instance_name', 0, 0, '', 'Besoin d’une exécution rapide pour %instance_name', 'Brauche einen schnellen Run für %instance_name', '需要快速刷一波%instance_name!', '', 'Necesito una corrida rápida para %instance_name', '', ''), + (469, 'suggest_instance', 'Anyone down to farm %instance_name together?', 0, 0, '', 'Qui est partant pour farmer %instance_name ensemble ?', 'Wer hat Lust, %instance_name zusammen zu farmen?', '有人想一起刷%instance_name吗?', '', '¿Alguien quiere farmear %instance_name juntos?', '', ''), + (470, 'suggest_instance', 'Looking for a few more for %instance_name', 0, 0, '', 'Je cherche encore quelques personnes pour %instance_name.', 'Suche noch ein paar Leute für %instance_name', '还差几个人一起去%instance_name!', '', 'Buscando algunos más para %instance_name', '', ''), + (471, 'suggest_instance', 'Anyone need help with %instance_name mechanics?', 0, 0, '', 'Besoin d’aide avec les mécanismes de %instance_name ?', 'Braucht jemand Hilfe bei den Mechaniken in %instance_name?', '有人需要%instance_name的机制讲解吗?', '', '¿Alguien necesita ayuda con las mecánicas de %instance_name?', '', ''), + (472, 'suggest_instance', 'Need a group for %instance_name fast', 0, 0, '', 'J’ai besoin d’un groupe pour %instance_name rapidement', 'Brauche schnell eine Gruppe für %instance_name', '急需一个队伍去%instance_name!', '', 'Necesito un grupo rápido para %instance_name', '', ''), + (473, 'suggest_instance', 'Anyone want to speedrun %instance_name?', 0, 0, '', 'Quelqu’un veut speedrunner %instance_name?', 'Will jemand %instance_name als Speedrun laufen?', '有人想速刷%instance_name吗?', '', '¿Alguien quiere hacer %instance_name en speedrun?', '', ''), + (474, 'suggest_instance', 'Looking for a quick %instance_name completion', 0, 0, '', 'Je cherche une %instance_name complétion rapide.', 'Suche einen schnellen Abschluss von %instance_name', '找人快速完成%instance_name!', '', 'Buscando completar %instance_name rápido', '', ''), + (475, 'suggest_instance', 'Looking for anyone to join %instance_name', 0, 0, '', 'Je recherche des personnes pour rejoindre %instance_name.', 'Suche jemanden, der bei %instance_name mitkommt', '找人一起加入%instance_name!', '', 'Buscando a alguien para unirse a %instance_name', '', ''), + (476, 'suggest_instance', 'Anyone down to clear %instance_name?', 0, 0, '', 'Quelqu’un est partant pour nettoyer %instance_name?', 'Wer hat Lust, %instance_name zu clearen?', '有人想一起清%instance_name吗?', '', '¿Alguien para limpiar %instance_name?', '', ''), + (477, 'suggest_instance', 'Need help with the final boss of %instance_name', 0, 0, '', 'Besoin d’aide avec le boss final de %instance_name?', 'Brauche Hilfe beim Endboss von %instance_name', '需要帮忙打%instance_name的最终BOSS!', '', 'Necesito ayuda con el último jefe de %instance_name', '', ''), + (478, 'suggest_instance', 'Anyone interested in running %instance_name today?', 0, 0, '', 'Quelqu’un intéressé pour lancer %instance_name aujourd’hui?', 'Wer hat heute Lust, %instance_name zu laufen?', '今天有人想刷%instance_name吗?', '', '¿Alguien interesado en hacer %instance_name hoy?', '', ''), + (479, 'suggest_instance', 'Looking for a quick run of %instance_name to farm some gear', 0, 0, '', 'Je cherche à faire rapidement %instance_name pour récupérer un peu d’équipement.', 'Suche schnellen Run in %instance_name, um etwas Gear zu farmen', '想速刷%instance_name搞点装备,有人一起吗?', '', 'Buscando una corrida rápida de %instance_name para farmear equipo', '', ''), + (480, 'suggest_instance', 'Anyone looking to farm materials from %instance_name?', 0, 0, '', 'Qui veut farmer des matériaux dans %instance_name ?', 'Will jemand Mats aus %instance_name farmen?', '有人想刷%instance_name的材料吗?', '', '¿Alguien quiere farmear materiales de %instance_name?', '', ''), + (481, 'suggest_instance', 'Let’s get a group for %instance_name', 0, 0, '', 'Formons un groupe pour %instance_name.', 'Lass uns eine Gruppe für %instance_name machen', '咱们组个队去%instance_name吧!', '', 'Formemos un grupo para %instance_name', '', ''), + (482, 'suggest_quest', 'Need help with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Braucht wer Hilfe bei %quest_link?', '有人能帮我做%quest_link吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', 'Нужна помощь с %quest_link?'), + (483, 'suggest_quest', 'Anyone wants to share %quest_link?', 0, 0, '', 'Est-ce que quelqu’un veut partager %quest_link ?', 'Will jemand %quest_link teilen?', '有人想一起分享%quest_link吗?', '', '¿Alguien quiere hacer %quest_link?', '', 'Кто хочет поделиться %quest_link?'), + (484, 'suggest_quest', 'Anyone doing %quest_link?', 0, 0, '', 'Quelqu’un fait la %quest_link?', 'Macht jemand %quest_link?', '有人在做%quest_link吗?', '', '¿Alguien está haciendo %quest_link?', '', 'Кто делает %quest_link?'), + (485, 'suggest_quest', 'Wanna do %quest_link.', 0, 0, '', 'Tu veux faire la %quest_link ?', 'Bock auf %quest_link.', '想做%quest_link,有人一起吗?', '', '¿Una ayudita con %quest_link?', '', 'Хочу сделать %quest_link.'), + (486, 'suggest_quest', 'Looking to group for %quest_link.', 0, 0, '', 'À la recherche de personnes pour %quest_link.', 'Suche Gruppe für %quest_link.', '找人组队做%quest_link!', '', 'Buscando un grupo para %quest_link.', '', ''), + (487, 'suggest_quest', 'Who wants to help with %quest_link?', 0, 0, '', 'Qui veut aider avec %quest_link?', 'Wer hilft bei %quest_link?', '谁想帮我做%quest_link?', '', '¿Quién quiere ayudar con %quest_link?', '', ''), + (488, 'suggest_quest', 'Need assistance with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Braucht jemand Unterstützung bei %quest_link?', '需要帮忙做%quest_link吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), + (489, 'suggest_quest', 'Anyone interested in %quest_link?', 0, 0, '', 'Quelqu’un intéressé par %quest_link?', 'Interesse an %quest_link?', '有人对%quest_link感兴趣吗?', '', '¿Alguien interesado en %quest_link?', '', ''), + (490, 'suggest_quest', 'Anyone wants to finish %quest_link?', 0, 0, '', 'Qui veut terminer la %quest_link ?', 'Will jemand %quest_link abschließen?', '有人想完成%quest_link吗?', '', '¿Alguien quiere terminar %quest_link?', '', ''), + (491, 'suggest_quest', 'Need a team for %quest_link?', 0, 0, '', 'Besoin d’une équipe pour %quest_link?', 'Braucht wer ein Team für %quest_link?', '需要一个队伍做%quest_link吗?', '', '¿Necesito un equipo para %quest_link?', '', ''), + (492, 'suggest_quest', 'Any takers for %quest_link?', 0, 0, '', 'Des volontaires pour %quest_link ?', 'Wer hat Bock auf %quest_link?', '有志愿者一起做%quest_link吗?', '', '¿Alguien se apunta a %quest_link?', '', ''), + (493, 'suggest_quest', 'Let’s do %quest_link together.', 0, 0, '', 'Faisons cette %quest_link ensemble.', 'Lass uns %quest_link zusammen machen.', '咱们一起做%quest_link吧!', '', 'Vamos a hacer %quest_link juntos.', '', ''), + (494, 'suggest_quest', 'Need someone for %quest_link.', 0, 0, '', 'Besoin de quelqu’un pour %quest_link.', 'Brauche jemanden für %quest_link.', '需要一个人帮忙做%quest_link。', '', '¿Necesito a alguien para %quest_link?', '', ''), + (495, 'suggest_quest', 'Anyone needs help with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Braucht jemand Hilfe bei %quest_link?', '有人需要帮忙做%quest_link吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), + (496, 'suggest_quest', 'Need help with %quest_link? I got you.', 0, 0, '', 'Besoin d’aide avec %quest_link? Pas de souci, je suis là pour t’épauler.', 'Brauchst du Hilfe bei %quest_link? Ich helfe dir.', '需要帮忙做%quest_link吗?我来帮你!', '', '¿Necesitas ayuda con %quest_link? Yo te ayudo.', '', ''), + (497, 'suggest_quest', 'Looking to do %quest_link. Anyone in?', 0, 0, '', 'Je veux faire %quest_link. Qui veut se joindre à moi ?', 'Will %quest_link machen. Wer ist dabei?', '想做%quest_link,有人加入吗?', '', 'Busco hacer %quest_link. ¿Alguien se une?', '', ''), + (498, 'suggest_quest', 'Help wanted for %quest_link.', 0, 0, '', 'Besoin d’aide pour %quest_link.', 'Hilfe gesucht für %quest_link.', '需要帮忙做%quest_link!', '', 'Se busca ayuda para %quest_link.', '', ''), + (499, 'suggest_quest', 'Who is up for %quest_link?', 0, 0, '', 'Qui est partant pour %quest_link ?', 'Wer ist bereit für %quest_link?', '谁想一起做%quest_link?', '', '¿Quién se apunta a %quest_link?', '', ''), + (500, 'suggest_quest', 'Who wants to join for %quest_link?', 0, 0, '', 'Qui veut se joindre à moi pour %quest_link ?', 'Wer will bei %quest_link mitmachen?', '谁想加入做%quest_link?', '', '¿Quién quiere unirse a %quest_link?', '', ''), + (501, 'suggest_quest', 'Anyone up for %quest_link?', 0, 0, '', 'Qui est partant pour %quest_link ?', 'Wer ist bei %quest_link dabei?', '有人想做%quest_link吗?', '', '¿Alguien quiere hacer %quest_link?', '', ''), + (502, 'suggest_quest', 'Need some help with %quest_link here.', 0, 0, '', 'Besoin d’aide avec %quest_link ici.', 'Brauche hier etwas Hilfe bei %quest_link.', '这里需要帮忙做%quest_link。', '', 'Necesito ayuda con %quest_link.', '', ''), + (503, 'suggest_quest', 'Anyone for %quest_link?', 0, 0, '', 'Quelqu’un pour %quest_link ?', 'Jemand für %quest_link?', '有人一起做%quest_link吗?', '', '¿Alguien para %quest_link?', '', ''), + (504, 'suggest_quest', 'Who is ready to do %quest_link?', 0, 0, '', 'Qui est prêt à faire %quest_link ?', 'Wer ist bereit, %quest_link zu machen?', '谁准备好做%quest_link了?', '', '¿Quién está listo para %quest_link?', '', ''), + (505, 'suggest_quest', 'Looking for some %quest_link help.', 0, 0, '', 'Je cherche de l’aide pour %quest_link.', 'Suche etwas Hilfe für %quest_link.', '找人帮忙做%quest_link。', '', 'Buscando ayuda para %quest_link.', '', ''), + (506, 'suggest_quest', 'Anyone need %quest_link help?', 0, 0, '', 'Besoin d’aide pour la quête %quest_link?', 'Braucht jemand Hilfe bei %quest_link?', '有人需要%quest_link的帮助吗?', '', '¿Alguien necesita ayuda con %quest_link?', '', ''), + (507, 'suggest_quest', 'Who is down to complete %quest_link?', 0, 0, '', 'Qui est partant pour compléter la %quest_link?', 'Wer hat Lust, %quest_link abzuschließen?', '谁想一起完成%quest_link?', '', '¿Quién se apunta a completar %quest_link?', '', ''), + (508, 'suggest_quest', 'Anyone need assistance for %quest_link?', 0, 0, '', 'Besoin d’aide pour %quest_link ?', 'Braucht jemand Unterstützung bei %quest_link?', '有人需要%quest_link的协助吗?', '', '¿Alguien necesita asistencia con %quest_link?', '', ''), + (509, 'suggest_quest', 'Who can help with %quest_link?', 0, 0, '', 'Qui peut m’aider avec %quest_link?', 'Wer kann bei %quest_link helfen?', '谁能帮忙做%quest_link?', '', '¿Quién puede ayudar con %quest_link?', '', ''), + (510, 'suggest_quest', 'Looking for a hand with %quest_link.', 0, 0, '', 'Recherchde l aide pour %quest_link.', 'Suche eine helfende Hand bei %quest_link.', '找人帮一把做%quest_link。', '', 'Buscando ayuda con %quest_link.', '', ''), + (511, 'suggest_quest', 'Anyone ready for %quest_link?', 0, 0, '', 'Qui est prêt pour %quest_link?', 'Ist jemand bereit für %quest_link?', '有人准备好做%quest_link了吗?', '', '¿Alguien listo para %quest_link?', '', ''), + (512, 'suggest_quest', 'Can anyone join me for %quest_link?', 0, 0, '', 'Est-ce que quelqu’un peut se joindre à moi pour %quest_link?', 'Kann jemand bei %quest_link dazukommen?', '有人能和我一起做%quest_link吗?', '', '¿Alguien puede unirse para %quest_link?', '', ''), + (513, 'suggest_quest', 'Looking for some partners for %quest_link.', 0, 0, '', 'Je recherche des partenaires pour %quest_link.', 'Suche ein paar Mitstreiter für %quest_link.', '找几个伙伴一起做%quest_link。', '', 'Buscando compañeros para %quest_link.', '', ''), + (514, 'suggest_quest', 'Anyone want to team up for %quest_link?', 0, 0, '', 'Quelqu’un veut faire équipe pour %quest_link?', 'Will jemand sich für %quest_link zusammentun?', '有人想组队做%quest_link吗?', '', '¿Alguien quiere formar un equipo para %quest_link?', '', ''), + (515, 'suggest_quest', 'Who is willing to do %quest_link?', 0, 0, '', 'Qui est prêt à faire %quest_link ?', 'Wer ist bereit, %quest_link zu machen?', '谁愿意做%quest_link?', '', '¿Quién está dispuesto a hacer %quest_link?', '', ''), + (516, 'suggest_quest', 'Who needs %quest_link to be completed?', 0, 0, '', 'Qui a besoin que %quest_link soit complété ?', 'Wer muss %quest_link noch abschließen?', '谁需要完成%quest_link?', '', '¿Quién necesita completar %quest_link?', '', ''), + (517, 'suggest_quest', 'Looking to complete %quest_link.', 0, 0, '', 'Je cherche à accomplir la %quest_link.', 'Will %quest_link abschließen.', '想完成%quest_link,有人一起吗?', '', 'Buscando completar %quest_link.', '', ''), + (518, 'suggest_quest', 'Who can join me for %quest_link?', 0, 0, '', 'Qui peut se joindre à moi pour %quest_link ?', 'Wer kann bei %quest_link mitkommen?', '谁能和我一起做%quest_link?', '', '¿Quién puede unirse a %quest_link?', '', ''), + (519, 'suggest_quest', 'Anyone interested in completing %quest_link?', 0, 0, '', 'Qui est intéressé pour compléter la %quest_link ?', 'Interesse, %quest_link abzuschließen?', '有人想完成%quest_link吗?', '', '¿Alguien interesado en completar %quest_link?', '', ''), + (520, 'suggest_quest', 'Wanna help with %quest_link?', 0, 0, '', 'Tu veux de l’aide avec %quest_link?', 'Bock, bei %quest_link zu helfen?', '想帮我做%quest_link吗?', '', '¿Quieres ayudar con %quest_link?', '', ''), + (521, 'suggest_quest', 'Anyone willing to help with %quest_link?', 0, 0, '', '%quest_link ? Oh, vous avez besoin d’aide pour cette quête ? Je suis là pour vous aider !', 'Ist jemand bereit, bei %quest_link zu helfen?', '有人愿意帮忙做%quest_link吗?', '', '¿Alguien dispuesto a ayudar con %quest_link?', '', ''), + (522, 'suggest_quest', 'Need someone to finish %quest_link.', 0, 0, '', 'Besoin que quelqu’un termine %quest_link.', 'Brauche jemanden, um %quest_link zu beenden.', '需要有人一起完成%quest_link。', '', 'Necesito a alguien para terminar %quest_link.', '', ''), + (523, 'suggest_quest', 'Anyone want to finish %quest_link?', 0, 0, '', 'Quelqu’un veut finir %quest_link ?', 'Will jemand %quest_link beenden?', '有人想一起完成%quest_link吗?', '', '¿Alguien quiere terminar %quest_link?', '', ''), + (524, 'suggest_quest', 'Any help with %quest_link?', 0, 0, '', 'Besoin d’aide avec %quest_link?', 'Gibt’s Hilfe für %quest_link?', '有谁能帮忙做%quest_link?', '', '¿Necesito ayuda con %quest_link?', '', ''), + (525, 'suggest_quest', 'Anyone up for %quest_link?', 0, 0, '', 'Qui est partant pour %quest_link ?', 'Wer ist dabei für %quest_link?', '有人想去做%quest_link吗?', '', '¿Alguien se apunta a %quest_link?', '', ''), + (526, 'suggest_quest', 'Looking for %quest_link completion partner.', 0, 0, '', 'Je recherche un partenaire pour compléter %quest_link.', 'Suche Partner zum Abschließen von %quest_link.', '找个伙伴一起完成%quest_link。', '', 'Buscando compañero para completar %quest_link.', '', ''), + (527, 'suggest_quest', 'Who is down to finish %quest_link?', 0, 0, '', 'Qui est partant pour finir la %quest_link?', 'Wer hat Lust, %quest_link zu beenden?', '谁愿意一起完成%quest_link?', '', '¿Quién se apunta a terminar %quest_link?', '', ''), + (528, 'suggest_quest', 'Help wanted for %quest_link.', 0, 0, '', 'Besoin d’aide pour %quest_link.', 'Hilfe gesucht für %quest_link.', '求助,需要帮忙做%quest_link。', '', 'Se busca ayuda para %quest_link.', '', ''), + (529, 'suggest_quest', 'Anyone interested in %quest_link today?', 0, 0, '', 'Quelqu’un intéressé par %quest_link aujourd’hui?', 'Heute jemand Interesse an %quest_link?', '今天有人对%quest_link感兴趣吗?', '', '¿Alguien interesado en %quest_link hoy?', '', ''), + (530, 'suggest_quest', 'Anyone doing %quest_link now?', 0, 0, '', 'Est-ce que quelqu’un est en train de faire %quest_link maintenant?', 'Macht gerade jemand %quest_link?', '现在有人在做%quest_link吗?', '', '¿Alguien haciendo %quest_link ahora?', '', ''), + (531, 'suggest_quest', 'Any takers for %quest_link now?', 0, 0, '', 'Des preneurs pour %quest_link maintenant?', 'Wer hat jetzt Bock auf %quest_link?', '现在有人想做%quest_link吗?', '', '¿Alguien se apunta a %quest_link ahora?', '', ''), + (532, 'suggest_quest', 'Anyone wants to start %quest_link?', 0, 0, '', 'Qui veut commencer %quest_link?', 'Will jemand %quest_link starten?', '有人想开始做%quest_link吗?', '', '¿Alguien quiere empezar %quest_link?', '', ''), + (533, 'suggest_quest', 'Need help with %quest_link please.', 0, 0, '', 'Besoin d’aide avec %quest_link s’il vous plaît.', 'Bitte um Hilfe bei %quest_link.', '求帮忙做%quest_link,拜托了。', '', 'Necesito ayuda con %quest_link, por favor.', '', ''), + (534, 'suggest_quest', 'Who can help me with %quest_link?', 0, 0, '', 'Qui peut m’aider avec %quest_link?', 'Wer kann mir bei %quest_link helfen?', '谁能帮我做%quest_link?', '', '¿Quién puede ayudarme con %quest_link?', '', ''), + (535, 'suggest_quest', 'Looking to group for %quest_link.', 0, 0, '', 'Je cherche des compagnons pour %quest_link.', 'Suche Gruppe für %quest_link.', '找队伍一起做%quest_link。', '', 'Buscando un grupo para %quest_link.', '', ''), + (536, 'suggest_trade', 'Anyone to farm %category?', 0, 0, '', 'Quelqu’un pour farmer %category?', 'Jemand bereit zum %category farmen?', '有人一起刷%category吗?', '', '¿Alguien para farmear %category?', '', 'Кто хочет пофармить %category?'), + (537, 'suggest_trade', 'Looking for help farming %category.', 0, 0, '', 'Je cherche de l’aide pour farm %category.', 'Suche Hilfe beim %category farmen.', '找人帮忙一起刷%category。', '', 'Buscando ayuda para farmear %category.', '', 'Ищу помощь для фарма %category.'), + (538, 'suggest_trade', 'Damn %category are so expensive!', 0, 0, '', 'Ah mince, les %category sont tellement chers !', '%category sind verdammt teuer!', '靠,%category也太贵了吧!', '', '¡Malditas %category son tan caras!', '', 'Черт, %category такие дорогие!'), + (539, 'suggest_trade', 'Wanna %category.', 0, 0, '', 'Envie de %category.', 'Will %category.', '想要%category。', '', 'Quiero %category.', '', 'Хочу %category.'), + (540, 'suggest_trade', 'Need help with %category.', 0, 0, '', 'Besoin d’aide avec %category.', 'Brauche Hilfe mit %category.', '需要帮忙搞%category。', '', 'Necesito ayuda con %category.', '', 'Нужна помощь с %category.'), + (541, 'suggest_trade', 'WTB %category.', 0, 0, '', 'Je cherche à acheter %category.', 'Kaufe: %category.', '求购%category。', '', 'Quiero comprar %category.', '', 'Куплю %category.'), + (542, 'suggest_trade', 'Anyone interested in %category?', 0, 0, '', 'Quelqu’un intéressé par %category ?', 'Interesse an %category?', '有人对%category感兴趣吗?', '', '¿Alguien interesado en %category?', '', 'Кому интересно %category?'), + (543, 'suggest_trade', 'WTS %category.', 0, 0, '', 'Je vends %category.', 'Verkaufe: %category.', '出售%category。', '', 'Quiero vender %category.', '', 'Продам %category.'), + (544, 'suggest_trade', 'I am selling %category cheaper than AH.', 0, 0, '', 'Je vends %category moins cher que chez AH.', 'Verkaufe %category günstiger als im AH.', '我卖的%category比拍卖行便宜。', '', 'Estoy vendiendo %category más barato que en las subastas.', '', 'Продаю %category дешевле, чем на Аукционе.'), + (545, 'suggest_trade', 'Who wants to farm %category?', 0, 0, '', 'Qui veut farm %category?', 'Wer will %category farmen?', '谁想一起刷%category?', '', '¿Quién quiere farmear %category?', '', 'Кто хочет пофармить %category?'), + (546, 'suggest_trade', 'Wanna farm %category.', 0, 0, '', 'Envie de farmer %category.', 'Will %category farmen.', '想去刷%category。', '', 'Quiero farmear %category.', '', 'Хочу пофармить %category.'), + (547, 'suggest_trade', 'Looking for party after %category.', 0, 0, '', 'Je cherche un group pour farm %category.', 'Suche Gruppe fürs %category farmen.', '找队伍去搞%category。', '', 'Buscando grupo después de %category.', '', 'Ищу пати после %category.'), + (548, 'suggest_trade', 'Any %category are appreciated.', 0, 0, '', 'Toute %category est appréciée.', '%category sind willkommen.', '任何%category都欢迎。', '', 'Cualquier %category es apreciada.', '', 'Любые %category приветствуются.'), + (549, 'suggest_trade', 'Buying anything of %category.', 0, 0, '', 'Acheter toute votre %category.', 'Kaufe alles von %category.', '收购所有%category。', '', 'Comprando algo de %category.', '', 'Куплю что угодно из %category.'), + (550, 'suggest_trade', 'Wow, anybody is farming %category!', 0, 0, '', 'Oh là là, tout le monde farm le %category !', 'Wow, alle farmen %category!', '哇,有人正在刷%category!', '', '¡Guau, alguien está farmeando %category!', '', 'Вау, кто-то фармит %category!'), + (551, 'suggest_trade', '%category are selling mad in the AH.', 0, 0, '', 'Les %category se vendent comme des petits pains à l’HDV.', '%category gehen im AH weg wie warme Semmeln.', '%category在拍卖行卖疯了。', '', '%category están vendiendo locos en las subastas.', '', '%category отлично продаются на аукционе.'), + (552, 'suggest_trade', 'AH is hot for %category.', 0, 0, '', 'AH c’est fan de %category.', 'AH ist heiß auf %category.', '拍卖行对%category很火爆。', '', 'Las subastas estan caliente para %category.', '', 'Аукцион горячий по %category.'), + (553, 'suggest_trade', '%category are on the market.', 0, 0, '', 'Des %category sont disponibles sur le marché.', '%category sind auf dem Markt.', '%category市场上有了。', '', '%category están en el mercado.', '', '%category на рынке.'), + (554, 'suggest_trade', 'Wanna trade some %category.', 0, 0, '', 'Tu veux échanger des %category ?', 'Will etwas %category tauschen.', '想换点%category吗?', '', 'Quiero intercambiar alguna %category.', '', 'Хочу обменять немного %category.'), + (555, 'suggest_trade', 'Need more %category.', 0, 0, '', 'J’ai besoin de plus de %category.', 'Brauche mehr %category.', '需要更多的%category。', '', 'Necesito más %category.', '', 'Нужно больше %category.'), + (556, 'suggest_trade', 'Anybody can spare some %category?', 0, 0, '', 'Est-ce que quelqu’un pourrait me passer un peu de %category?', 'Kann jemand etwas %category entbehren?', '有人能匀点%category给我吗?', '', '¿Alguien puede ahorrar algo de %category?', '', 'Кто-нибудь может поделиться %category?'), + (557, 'suggest_trade', 'Who wants %category?', 0, 0, '', 'Qui veut la %category ?', 'Wer will %category?', '谁想要%category?', '', '¿Quién quiere %category?', '', 'Кто хочет %category?'), + (558, 'suggest_trade', 'Some %category please?', 0, 0, '', 'Une %category s’il vous plaît ?', 'Etwas %category bitte?', '能给点%category吗?', '', '¿Alguna %category, por favor?', '', 'Немного %category, пожалуйста?'), + (559, 'suggest_trade', 'I should have got skill for %category.', 0, 0, '', 'J’aurais dû avoir des compétences pour cette %category.', 'Hätte wohl den Skill für %category lernen sollen.', '我真该学点关于%category的技能。', '', 'Debería haber adquirido habilidad para %category.', '', 'Надо было прокачать навык для %category.'), + (560, 'suggest_trade', 'I am dying for %category.', 0, 0, '', 'Je meurs d’envie de %category.', 'Ich geiere auf %category.', '我超级想要%category。', '', 'Me muero por %category.', '', 'Очень хочу %category.'), + (561, 'suggest_trade', 'People are killing for %category.', 0, 0, '', 'Les gens se tuent pour cette %category.', 'Für %category würden die Leute töten.', '大家都在抢着要%category。', '', 'La gente está matando por %category.', '', 'Люди убивают ради %category.'), + (562, 'suggest_trade', '%category is a great bargain!', 0, 0, '', 'La catégorie est une super affaire !', '%category ist ein Top-Deal!', '%category真是大便宜!', '', '%category es una gran ganga!', '', '%category — отличная сделка!'), + (563, 'suggest_trade', 'Everybody is mad for %category!', 0, 0, '', 'Tout le monde est fou de %category !', 'Alle sind verrückt nach %category!', '大家都为%category疯狂!', '', '¡Todo el mundo está loco por %category!', '', 'Все сходят с ума по %category!'), + (564, 'suggest_trade', 'Where is the best place to farm for %category?', 0, 0, '', 'Quel est le meilleur endroit pour farm des %category?', 'Wo farmt man am besten %category?', '刷%category最好的地方是哪儿?', '', '¿Dónde está el mejor lugar para farmear para %category?', '', 'Где лучше всего фармить %category?'), + (565, 'suggest_trade', 'I am all set for %category.', 0, 0, '', 'Je suis prêt(e) pour la %category.', 'Bin ready für %category.', '我已经准备好搞%category了。', '', 'Estoy listo para %category.', '', 'Я полностью готов к %category.'), + (566, 'suggest_trade', 'Is it good to sell %category?', 0, 0, '', 'Est-ce une bonne idée de vendre %category?', 'Lohnt es sich, %category zu verkaufen?', '现在卖%category合适吗?', '', '¿Es bueno vender %category?', '', 'Выгодно ли продавать %category?'), + (567, 'suggest_trade', 'I’d probably keep all my %category with me.', 0, 0, '', 'Je garderais probablement toutes mes %category avec moi.', 'Ich behalte meine ganzen %category wohl lieber.', '我大概会把所有%category留着。', '', 'Probablemente mantendría toda mi %category conmigo', '', ''), + (568, 'suggest_trade', 'Need group? Maybe to farm some %category?', 0, 0, '', 'Besoin d’un groupe ? Peut-être pour farmer un peu de %category ?', 'Brauchst du eine Gruppe? Vielleicht zum %category farmen?', '需要组队吗?一起刷点%category怎么样?', '', '¿Necesitas un grupo? ¿Tal vez para farmear alguna %category?', '', 'Нужна группа? Может, пофармить %category?'), + (569, 'suggest_trade', 'I am still thinking about %category.', 0, 0, '', 'Je suis toujours en train de réfléchir à farm %category.', 'Denke immer noch über %category nach.', '我还在考虑%category的事。', '', 'Todavía estoy pensando en %category.', '', 'Все еще думаю о %category.'), + (570, 'suggest_trade', 'I heard about %category already, but my pockets are empty.', 0, 0, '', 'J’ai déjà entendu parler de %category, mais mes poches sont vides.', 'Hab schon von %category gehört, aber meine Taschen sind leer.', '我早就听说过%category,可惜没钱。', '', 'Ya escuché sobre %category, pero mis bolsillos están vacíos.', '', 'Я уже слышал о %category, но карманы пусты.'), + (571, 'suggest_trade', 'LFG for %category', 0, 0, '', 'Je cherche un groupe pour %category.', 'LFG für %category', '找队伍搞%category。', '', 'Busco grupo para %category', '', 'ЛФГ для %category'), + (572, 'suggest_trade', 'Would selling %category make me rich?', 0, 0, '', 'Est-ce que vendre %category me rendrait riche?', 'Würde mich der Verkauf von %category reich machen?', '卖%category能让我发财吗?', '', '¿Vender %category me haría rico?', '', 'Сделает ли продажа %category меня богатым?'), + (573, 'suggest_trade', 'OK. I an farming %category tomorrow.', 0, 0, '', 'D’accord. Demain, je vais grind %category.', 'Okay. Ich farme %category morgen.', '好吧,明天我去刷%category。', '', 'OK. Tengo una sesion de farmeo de %category mañana.', '', 'ОК. Завтра фармлю %category.'), + (574, 'suggest_trade', 'Everyone is talking about %category.', 0, 0, '', 'Tout le monde parle de %category.', 'Alle reden über %category.', '大家都在讨论%category。', '', 'Todos hablan de %category', '', 'Все говорят о %category.'), + (575, 'suggest_trade', 'I saw at least ten people farming for %category.', 0, 0, '', 'J’ai vu au moins dix personnes qui farm %category.', 'Hab mindestens zehn Leute gesehen, die %category farmen.', '我看到至少十个人在刷%category。', '', 'Vi al menos diez personas farmeando para %category.', '', 'Видел как минимум десять человек, фармящих %category.'), + (576, 'suggest_trade', 'I sold all my %category yesterday. I am completely broke!', 0, 0, '', 'J’ai vendu toutes mes %category hier. Je suis complètement fauché !', 'Hab gestern all meine %category verkauft. Bin komplett pleite!', '昨天我把所有%category都卖了,现在彻底破产了!', '', 'Ayer vendí todo mi %category. ¡Estoy completamente arruinado!', '', 'Вчера продал все свои %category. Теперь я полностью на мели!'), + (577, 'suggest_trade', 'Wanna join a guild farming for %category.', 0, 0, '', 'Tu veux rejoindre une guilde qui farme %category.', 'Will einer Gilde joinen, die %category farmt.', '想加入一个刷%category的公会。', '', 'Quiero unirme a una hermandad de farmeo para %category.', '', 'Хочу вступить в гильдию, фармящую %category.'), + (578, 'suggest_trade', 'I am selling %category cheaper than AH.', 0, 0, '', 'Je vends %category moins cher qu’à l’AH.', 'Verkaufe %category günstiger als im AH.', '我卖的%category比拍卖行便宜。', '', 'Estoy vendiendo %category más barato que en las subastas.', '', 'Продаю %category дешевле, чем на Аукционе.'), + (579, 'suggest_trade', 'Who wants to farm %category?', 0, 0, '', 'Qui veut faire du farming pour %category?', 'Wer will %category farmen?', '谁想一起刷%category?', '', '¿Quién quiere farmear %category?', '', 'Кто хочет пофармить %category?'), + (580, 'suggest_trade', 'Selling %category, anyone interested?', 0, 0, '', 'Vente %category, des intéressés ?', 'Verkaufe %category, wer hat Interesse?', '卖%category,有人感兴趣吗?', '', 'Vendo %category, ¿alguien interesado?', '', ''), + (581, 'suggest_trade', 'Anyone needs %category?', 0, 0, '', 'Besoin de %category?', 'Braucht jemand %category?', '有人需要%category吗?', '', '¿Alguien necesita %category?', '', ''), + (582, 'suggest_trade', 'Selling %category, whisper me.', 0, 0, '', 'Vente de %category, wisp me.', 'Verkaufe %category, flüster mir.', '卖%category,私聊我。', '', 'Vendo %category, susúrrame.', '', ''), + (583, 'suggest_trade', 'Looking to trade %category for %category.', 0, 0, '', 'Je cherche à échanger %category contre %category.', 'Suche Tausch %category gegen %category.', '想用%category换%category。', '', 'Busco intercambiar %category por %category.', '', ''), + (584, 'suggest_trade', 'Need %category, anyone selling?', 0, 0, '', 'Besoin de %category, est-ce que quelqu’un en vend?', 'Brauche %category, verkauft das jemand?', '需要%category,有人卖吗?', '', 'Necesito %category, ¿alguien vende?', '', ''), + (585, 'suggest_trade', 'Got %category for sale, hit me up!', 0, 0, '', 'J’ai %category à vendre, fais-moi signe!', 'Hab %category zu verkaufen, meldet euch!', '有%category卖,联系我!', '', '¡Tengo %category en venta, contáctame!', '', ''), + (586, 'suggest_trade', 'Anyone willing to farm %category?', 0, 0, '', '"Quelqu’un prêt à faire du farming %category?"', 'Ist jemand bereit, %category zu farmen?', '有人愿意一起刷%category吗?', '', '¿Alguien dispuesto a farmear %category?', '', ''), + (587, 'suggest_trade', 'Want to trade %category for %category.', 0, 0, '', 'Je veux échanger %category contre %category.', 'Will %category gegen %category tauschen.', '想用 %category 换 %category,有人感兴趣吗?', '', 'Quiero intercambiar %category por %category.', '', ''), + (588, 'suggest_trade', 'Selling %category, best price in the market.', 0, 0, '', 'Je vends %category, au meilleur prix du marché.', 'Verkaufe %category, bester Preis am Markt.', '出售 %category,市场最低价!', '', 'Vendo %category, el mejor precio del mercado.', '', ''), + (589, 'suggest_trade', 'Looking for %category in exchange for %category.', 0, 0, '', 'Je recherche %category en échange de %category.', 'Suche %category im Tausch gegen %category.', '想找 %category,愿意用 %category 交换。', '', 'Busco %category a cambio de %category.', '', ''), + (590, 'suggest_trade', 'Anyone got %category to trade?', 0, 0, '', 'Quelqu’un a %category à échanger ?', 'Hat jemand %category zum Tauschen?', '有人有 %category 能换吗?', '', '¿Alguien tiene %category para intercambiar?', '', ''), + (591, 'suggest_trade', 'Who needs %category? I got some for sale!', 0, 0, '', 'Qui a besoin de %category? J’en ai à vendre!', 'Wer braucht %category? Hab was zu verkaufen!', '谁需要 %category?我有些要卖!', '', '¿Quién necesita %category? ¡Tengo algo para vender!', '', ''), + (592, 'suggest_trade', 'I have %category for sale, whisper for price.', 0, 0, '', 'J’ai %category à vendre, chuchotez-moi pour le prix.', 'Habe %category zu verkaufen, flüster mir für den Preis.', '我有 %category 要卖,私聊问价。', '', 'Tengo %category en venta, susúrrame para el precio.', '', ''), + (593, 'suggest_trade', 'Looking to sell %category, whisper for details.', 0, 0, '', 'Je cherche à vendre des %category, envoyez-moi un message pour plus de détails.', 'Will %category verkaufen, flüster mir für Details.', '想卖 %category,私聊了解详情。', '', 'Busco vender %category, susúrrame para detalles.', '', ''), + (594, 'suggest_trade', 'WTB %category. Anyone selling?', 0, 0, '', 'Je recherche %category. Est-ce que quelqu’un en vend?', 'Kaufe: %category. Verkauft das jemand?', '求购 %category,有人卖吗?', '', 'Busco comprar %category. ¿Alguien vende?', '', ''), + (595, 'suggest_trade', 'I am looking for %category, please contact me.', 0, 0, '', 'Je recherche %category, n’hésitez pas à me contacter.', 'Suche %category, bitte melden.', '我在找 %category,有货请联系我。', '', 'Estoy buscando %category, por favor contáctame.', '', ''), + (596, 'suggest_trade', 'Any takers for %category?', 0, 0, '', 'Des intéressés pour %category ?', 'Wer hat Interesse an %category?', '有人想要 %category 吗?', '', '¿Alguien interesado en %category?', '', ''), + (597, 'suggest_trade', 'Can anyone help farm %category?', 0, 0, '', 'Est-ce que quelqu’un peut aider à farmer %category?', 'Kann jemand beim %category farmen helfen?', '有人能帮我刷 %category 吗?', '', '¿Alguien puede ayudar a farmear %category?', '', ''), + (598, 'suggest_trade', 'Anyone have %category to sell?', 0, 0, '', 'Est-ce que quelqu’un a %category à vendre?', 'Hat jemand %category zu verkaufen?', '有人有 %category 卖吗?', '', '¿Alguien tiene %category para vender?', '', ''), + (599, 'suggest_trade', 'Looking to buy %category, please message me.', 0, 0, '', 'Je cherche à acheter %category, n’hésitez pas à me contacter.', 'Will %category kaufen, flüster mir einfach.', '想买 %category,有货请私我。', '', 'Busco comprar %category, por favor mándame un mensaje.', '', ''), + (600, 'suggest_trade', 'I am looking for %category. Who has it?', 0, 0, '', 'Je cherche %category. Qui l’a en stock?', 'Suche %category. Wer hat was?', '我在找 %category,谁有货?', '', 'Estoy buscando %category. ¿Quién lo tiene?', '', ''), + (601, 'suggest_trade', 'WTB %category at a fair price.', 0, 0, '', 'Je recherche %category à un prix raisonnable.', 'Kaufe: %category zum fairen Preis.', '求购 %category,价格合理就行。', '', 'Busco comprar %category a un precio justo.', '', ''), + (602, 'suggest_trade', 'Need %category urgently, anyone got it?', 0, 0, '', 'Besoin de %category de toute urgence, est-ce que quelqu’un en a ?', 'Brauche dringend %category, hat wer was?', '急需 %category,有人有吗?', '', 'Necesito %category urgente, ¿alguien lo tiene?', '', ''), + (603, 'suggest_trade', 'Got %category, selling at a low price.', 0, 0, '', 'J’ai trouvé %category, vendu à petit prix.', 'Hab %category, verkaufe günstig.', '有 %category,低价出售!', '', 'Tengo %category, vendo a un precio bajo.', '', ''), + (604, 'suggest_trade', 'Selling %category for a good deal.', 0, 0, '', 'Je vends des %category à bon prix.', 'Verkaufe %category zum guten Kurs.', '出售 %category,价格很划算。', '', 'Vendo %category a buen precio.', '', ''), + (605, 'suggest_trade', 'I’m looking to buy %category, anyone selling cheap?', 0, 0, '', 'Je suis à la recherche de %category, est-ce que quelqu’un en vendrait à bon prix ?', 'Suche %category, verkauft wer günstig?', '想买 %category,有人便宜卖吗?', '', 'Busco comprar %category, ¿alguien lo vende barato?', '', ''), + (606, 'suggest_trade', 'Anyone selling %category? I’m interested.', 0, 0, '', 'Quelqu’un vend de %category ? Ça m’intéresse.', 'Verkauft jemand %category? Hätte Interesse.', '有人卖 %category 吗?我感兴趣。', '', '¿Alguien vende %category? Estoy interesado.', '', ''), + (607, 'suggest_trade', 'Need %category, please whisper if selling.', 0, 0, '', 'Besoin de la %category, s’il vous plaît wisp me si vous vendez.', 'Brauche %category, flüstert mir wenn ihr verkauft.', '需要 %category,有卖的请私聊我。', '', 'Necesito %category, susúrrame si lo vendes.', '', ''), + (608, 'suggest_trade', 'Anyone got extra %category for trade?', 0, 0, '', 'Quelqu’un aurait un %category supplémentaire à échanger?', 'Hat wer %category übrig zum Tauschen?', '有人有多余的 %category 能换吗?', '', '¿Alguien tiene %category extra para intercambiar?', '', ''), + (609, 'suggest_trade', 'Looking for %category. Anyone selling?', 0, 0, '', 'À la recherche de %category. Quelqu’un vend-il ?', 'Suche %category. Verkauft jemand?', '找 %category,有人卖吗?', '', 'Busco %category. ¿Alguien lo vende?', '', ''), + (610, 'suggest_trade', 'Anyone interested in trading %category?', 0, 0, '', '%category trading, ça vous tente?', 'Hat wer Interesse, %category zu tauschen?', '有人想换 %category 吗?', '', '¿Alguien interesado en intercambiar %category?', '', ''), + (611, 'suggest_trade', 'Selling %category, hit me up for a trade.', 0, 0, '', 'Je vends des %category, n’hésitez pas à me contacter pour faire un échange.', 'Verkaufe %category, meldet euch für Tausch.', '卖 %category,想换的找我。', '', 'Vendo %category, contáctame para un intercambio.', '', ''), + (612, 'suggest_trade', 'WTB %category, looking for good deals.', 0, 0, '', 'Je recherche des %category à acheter, je souhaite trouver de bonnes affaires.', 'Kaufe: %category, suche guten Kurs.', '求购 %category,找个好价格。', '', 'Busco comprar %category, buscando buenas ofertas.', '', ''), + (613, 'suggest_trade', 'Who needs %category? I have extras.', 0, 0, '', 'Qui a besoin de %category? J’ai des extras.', 'Wer braucht %category? Hab was übrig.', '谁需要 %category?我有多余的。', '', '¿Quién necesita %category? Tengo extras.', '', ''), + (614, 'suggest_trade', 'Anyone wants %category? Selling for cheap.', 0, 0, '', 'Qui veut de la %category? Je vends à bas prix.', 'Will wer %category? Verkaufe billig.', '有人要 %category 吗?便宜卖。', '', '¿Alguien quiere %category? Lo vendo barato.', '', ''), + (615, 'suggest_trade', 'Looking for %category, anyone selling?', 0, 0, '', 'Je recherche des %category, est-ce que quelqu’un en vend?', 'Suche %category, verkauft wer was?', '找 %category,有人卖吗?', '', 'Busco %category, ¿alguien lo vende?', '', ''), + (616, 'suggest_trade', 'Anyone need %category? I have some for trade.', 0, 0, '', 'Besoin de %category? J’en ai quelques-uns à échanger.', 'Braucht jemand %category? Hab was zum Tauschen.', '有人需要 %category 吗?我有些可以换。', '', '¿Alguien necesita %category? Tengo algunos para intercambiar.', '', ''), + (617, 'suggest_trade', 'WTB %category. Anyone selling cheaper than AH?', 0, 0, '', 'Je recherche %category. Est-ce que quelqu’un vend moins cher que à l’HV?', 'Kaufe: %category. Ist jemand günstiger als AH?', '求购 %category,有人比拍卖行便宜卖吗?', '', 'Busco comprar %category. ¿Alguien lo vende más barato que en la subasta?', '', ''), + (618, 'suggest_trade', 'Selling %category cheap, whisper for details.', 0, 0, '', 'Vente de %category à petit prix, chuchotez pour plus de détails.', 'Verkaufe %category billig, flüster mir für Details.', '低价卖 %category,私聊了解详情。', '', 'Vendo %category barato, susúrrame para detalles.', '', ''), + (619, 'suggest_trade', 'Anyone needs %category? I have some for sale.', 0, 0, '', 'Besoin de %category ? J’en ai en vente.', 'Braucht wer %category? Hab was zu verkaufen.', '有人需要 %category 吗?我有些要卖。', '', '¿Alguien necesita %category? Tengo algunos para vender.', '', ''), + (620, 'suggest_trade', 'Looking to trade %category for something else.', 0, 0, '', 'Je cherche à échanger %category contre autre chose.', 'Tausche %category gegen was anderes.', '想用 %category 换点别的东西。', '', 'Busco intercambiar %category por algo más.', '', ''), + (621, 'suggest_trade', 'Got %category, willing to trade it for %category.', 0, 0, '', 'J’ai %category, je serais prêt à l’échanger contre %category.', 'Hab %category, tausche gegen %category.', '有 %category,愿意换成 %category。', '', 'Tengo %category, dispuesto a intercambiarlo por %category.', '', ''), + (622, 'suggest_trade', 'WTB %category, anyone has extras?', 0, 0, '', 'Je recherche %category, est-ce que quelqu’un en a en plus ?', 'Kaufe: %category, hat wer was übrig?', '求购 %category,有人有多余的吗?', '', 'Busco comprar %category, ¿alguien tiene extras?', '', ''), + (623, 'suggest_faction', 'Anyone farming %faction rep?', 0, 0, '', 'Qui ici farm la réputation de la faction %faction ?', 'Farmt wer Ruf bei %faction?', '有人在刷 %faction 的声望吗?', '', '¿Alguien farmea reputación con %faction?', '', 'Кто фармит репутацию %faction?'), + (624, 'suggest_faction', 'Anyone help with %faction?', 0, 0, '', 'Besoin d’aide avec la réputation %faction?', 'Kann wer bei %faction helfen?', '有人能帮我搞定 %faction 的声望吗?', '', '¿Alguien me ayuda con %faction?', '', 'Кто поможет с %faction?'), + (625, 'suggest_faction', 'Wanna quest for %faction.', 0, 0, '', 'Tu veux partir en quête pour la faction %faction?', 'Hab Bock auf Quests für %faction.', '想一起做 %faction 的任务吗?', '', 'Quiero buscar a %faction.', '', 'Хочу квестить ради %faction.'), + (626, 'suggest_faction', '%faction is the best.', 0, 0, '', '%faction est la meilleure.', '%faction ist die beste Fraktion.', '%faction 真是最棒的!', '', '%faction es el mejor.', '', '%faction — лучшая.'), + (627, 'suggest_faction', 'Need just a bit to be %rep_level with %faction.', 0, 0, '', 'J’ai juste besoin d’un peu pour être %rep_level avec %faction.', 'Fehlt nur noch ein bisschen bis %rep_level bei %faction.', '还差一点就能跟 %faction 达到 %rep_level 了。', '', 'Solo necesito un poco para estar %rep_level con %faction.', '', 'Нужно чуть-чуть, чтобы стать %rep_level у %faction.'), + (628, 'suggest_faction', 'Anyone got %rep_level with %faction?', 0, 0, '', 'Est-ce que quelqu’un a le niveau de réputation %rep_level avec %faction ?', 'Hat jemand %rep_level bei %faction?', '有人跟 %faction 达到 %rep_level 了吗?', '', '¿Alguien tiene %rep_level con %faction?', '', 'Кто-нибудь уже %rep_level у %faction?'), + (629, 'suggest_faction', 'Who wants to be %rep_level with %faction?', 0, 0, '', 'Qui veut être %rep_level avec %faction?', 'Wer will %rep_level bei %faction sein?', '谁想跟 %faction 达到 %rep_level?', '', '¿Quién quiere estar en %rep_level con %faction?', '', 'Кто хочет стать %rep_level у %faction?'), + (630, 'suggest_faction', 'I’ll never be %rep_level with %faction.', 0, 0, '', 'Je ne serai jamais %rep_level souhaité avec %faction.', 'Ich werde nie %rep_level bei %faction sein.', '我恐怕永远也跟 %faction 达不到 %rep_level。', '', 'Nunca estaré %rep_level con %faction.', '', ''), + (631, 'suggest_faction', 'Someone missing %faction rep?', 0, 0, '', 'Quelqu’un manque de réputation auprès de %faction ?', 'Fehlt jemandem Ruf bei %faction?', '有人缺 %faction 的声望吗?', '', '¿Alguien falta reputación con %faction?', '', 'Кто-то не хватает репы у %faction?'), + (632, 'suggest_faction', 'Could help farming %faction rep.', 0, 0, '', 'Je peux t’aider à augmenter ta réputation avec %faction.', 'Kann beim Ruf farmen für %faction helfen.', '我能帮忙刷 %faction 的声望。', '', 'Podría ayudar a farmear %faction rep.', '', 'Могу помочь с фармом репы %faction.'), + (633, 'suggest_faction', 'The more rep the better. Especially with %faction.', 0, 0, '', 'Plus il y a de réput, mieux c’est. Surtout avec %faction', 'Je mehr Ruf, desto besser. Besonders bei %faction.', '声望越高越好,特别是跟 %faction 的。', '', 'Cuantas más repeticiones, mejor. Especialmente con %faction.', '', 'Чем больше репы, тем лучше. Особенно у %faction.'), + (634, 'suggest_faction', '%faction: need %rndK for %rep_level.', 0, 0, '', '%faction: J’ai besoin de %rndK pour %rep_level.', '%faction: brauche %rndK bis %rep_level.', '%faction:还差 %rndK 就能到 %rep_level。', '', '%faction: necesita %rndK para %rep_level.', '', '%faction: нужно %rndK для %rep_level.'), + (635, 'suggest_faction', 'Who can share %faction quests?', 0, 0, '', 'Qui peut partager les quêtes de %faction?', 'Wer kann Quests von %faction teilen?', '谁能分享一下 %faction 的任务?', '', '¿Quién puede compartir misiones de %faction?', '', 'Кто может поделиться квестами %faction?'), + (636, 'suggest_faction', 'Any dungeons for %faction?', 0, 0, '', 'Des donjons pour %faction ?', 'Gibts Inis für %faction?', '有给 %faction 刷声望的副本吗?', '', '¿Alguna mazmorra para %faction?', '', 'Есть подземелья для %faction?'), + (637, 'suggest_faction', 'Wanna do %faction rep grind.', 0, 0, '', 'Je veux monter ma réputation avec la %faction.', 'Hab Bock, Ruf bei %faction zu grinden.', '想一起刷 %faction 的声望吗?', '', 'Quiero hacer %faction rep grind.', '', 'Хочу фармить репу %faction.'), + (638, 'suggest_faction', 'Let’s farm %faction rep!', 0, 0, '', 'On va monter la réputation de la %faction !', 'Lasst uns Ruf bei %faction farmen!', '咱们一起刷 %faction 声望吧!', '', '¡Vamos a farmear reputación con %faction!', '', ''), + (639, 'suggest_faction', 'Farming for %faction rep.', 0, 0, '', 'Je fais du farm pour gagner de la réputation auprès de %faction.', 'Farme Ruf bei %faction.', '正在刷 %faction 的声望。', '', 'Farming for %faction rep.', '', 'Фармлю репу %faction.'), + (640, 'suggest_faction', 'Wanna farm for %faction.', 0, 0, '', 'Envie de farmer pour %faction ?', 'Bock, für %faction zu farmen.', '想刷 %faction 的声望吗?', '', 'Quiero farmear para %faction.', '', 'Хочу пофармить %faction.'), + (641, 'suggest_faction', 'Need help with %faction.', 0, 0, '', 'Besoin d’aide avec %faction.', 'Brauche Hilfe bei %faction.', '需要帮忙搞定 %faction 的声望。', '', 'Necesito ayuda con %faction.', '', 'Нужна помощь с %faction.'), + (642, 'suggest_faction', 'Is %faction sells something useful?', 0, 0, '', 'Est-ce que %faction vend quelque chose d’utile ?', 'Verkauft %faction was Nützliches?', '%faction 有卖什么有用的东西吗?', '', '¿%faction vende algo útil?', '', '%faction что-то полезное продает?'), + (643, 'suggest_faction', 'Are there %faction vendors?', 0, 0, '', 'Y a-t-il des vendeurs de %faction ?', 'Gibt es Händler für %faction?', '有 %faction 的商人吗?', '', '¿Hay %proveedores de facciones?', '', 'Есть ли у %faction торговцы?'), + (644, 'suggest_faction', 'Who farms %faction?', 0, 0, '', 'Ah, qui farm %faction?', 'Wer farmt für %faction?', '谁在刷 %faction 的声望?', '', '¿Quién farmea %faction?', '', 'Кто фармит %faction?'), + (645, 'suggest_faction', 'Which is the best way to farm %faction?', 0, 0, '', 'Quelle est la meilleurs façon de farmer %faction ?', 'Wie farmt man %faction am besten?', '刷 %faction 声望最好的方法是什么?', '', '¿Cuál es la mejor manera de farmear %faction?', '', 'Как лучше всего фармить %faction?'), + (646, 'suggest_faction', 'I hate %faction rep grind.', 0, 0, '', 'Je déteste ce fichu grind de réputation pour %faction.', 'Ich hasse den Rufgrind für %faction.', '我真是讨厌刷 %faction 的声望。', '', 'Odio %repetir de facción', '', 'Ненавижу фарм репы %faction.'), + (647, 'suggest_faction', 'I am so tired of %faction.', 0, 0, '', 'Je suis tellement fatigué de cette %faction.', 'Ich hab %faction so satt.', '我真是受够了 %faction。', '', 'Estoy tan cansado de %faction.', '', 'Я так устал от %faction.'), + (648, 'suggest_faction', 'Go for %faction?', 0, 0, '', 'Allez pour la %faction!', 'Gehen wir %faction an?', '去刷 %faction 吗?', '', '¿Quereis ir a por %faction?', '', 'Пойдем за %faction?'), + (649, 'suggest_faction', 'Seems everyone is %rep_level with %faction. Only me is late as usually.', 0, 0, '', 'Il semble que tout le monde soit au niveau %rep_level avec la faction %faction. Et bien sûr, je suis en retard comme d’habitude.', 'Scheint, als wenn alle %rep_level bei %faction sind. Nur ich hänge wie immer hinterher.', '好像大家都跟 %faction 达到 %rep_level 了,就我一如既往地落后。', '', 'Parece que todo el mundo está %rep_level con %faction. Solo yo llego tarde como siempre.', '', 'Кажется, все уже %rep_level у %faction. Только я, как обычно, опаздываю.'), + (650, 'suggest_faction', 'LFG for %faction rep grind?', 0, 0, '', 'Je recherche un groupe pour %faction rep grind !', 'LFG für Rufgrind bei %faction?', '找人一起刷 %faction 的声望吗?', '', 'Busco grupo para subir reputación con %faction', '', 'ЛФГ для фарма репы %faction?'), + (651, 'suggest_faction', 'Can anobody suggest a good spot for %faction rep grind?', 0, 0, '', 'Est-ce que quelqu’un peut me recommander un bon endroit pour gagner de la réputation avec %faction?', 'Kann jemand einen guten Spot für Rufgrind bei %faction empfehlen?', '有谁能推荐个刷 %faction 声望的好地方?', '', '¿Alguien puede sugerir un buen lugar farmear reputacion con %faction?', '', 'Кто-нибудь подскажет хорошее место для фарма репы %faction?'), + (652, 'suggest_faction', 'Would %faction rep benefit me?', 0, 0, '', 'Est-ce que la réputation de %faction me sera bénéfique ?', 'Bringt mir Ruf bei %faction überhaupt was?', '刷 %faction 的声望对我有好处吗?', '', '¿Me beneficiaría subir reputación con %faction?', '', 'Будет ли польза от репы %faction?'), + (653, 'suggest_faction', 'Who would’ve thought that %faction rep will be useful after all...', 0, 0, '', 'Qui aurait pensé que cette réputation %faction serait finalement utile...', 'Wer hätte gedacht, dass Ruf bei %faction doch noch nützlich ist...', '谁能想到 %faction 的声望竟然还真有用……', '', 'Quién hubiera pensado que subir reputacion con %faction sería útil después de todo...', '', ''), + (654, 'suggest_faction', 'I wanna be exalted with all factions, starting with %faction.', 0, 0, '', 'Je veux être honoré par toutes les factions, commençant par %faction.', 'Ich will bei allen Fraktionen ehrfürchtig sein, angefangen mit %faction.', '我想跟所有阵营都达到崇拜,先从 %faction 开始!', '', 'Quiero ser exaltado con todas las facciones, comenzando con %faction.', '', 'Хочу быть превознесенным у всех фракций, начну с %faction.'), + (655, 'suggest_faction', 'Is there any point to improve my rep with %faction?', 0, 0, '', 'Est-ce que ça vaut la peine d’améliorer ma réputation avec la faction %faction ?', 'Lohnt es sich, meinen Ruf bei %faction zu pushen?', '提高 %faction 的声望有意义吗?', '', '¿Hay algún punto para mejorar mi reputación con %faction?', '', 'Есть ли смысл повышать репу с %faction?'), + (656, 'suggest_faction', 'What is better for %faction? Quests or mob grinding?', 0, 0, '', 'Pour %faction, il vaut mieux grind des mobs ou faire des quêtes ?', 'Was ist besser für %faction? Quests oder Mobs grinden?', '刷 %faction 声望,做任务好还是刷怪好?', '', '¿Qué es mejor para %faction? ¿Misiones o mafiosos?', '', 'Что лучше для %faction? Квесты или фарм мобов?'), + (657, 'suggest_faction', 'Will grind %faction rep for you. Just give me some gold.', 0, 0, '', 'Je vais augmenter la réputation de %faction pour toi. Il me suffit de me donner un peu d’or.', 'Grinde euren Ruf bei %faction, gebt mir nur etwas Gold.', '我可以帮你刷 %faction 声望,给点金币就行。', '', 'Ganare reputacion con %faction por ti. Sólo dame un poco de oro.', '', 'Пофармлю репу %faction для тебя. Только дай немного золота.'), + (658, 'suggest_faction', 'I think grinding rep with %faction would take forever.', 0, 0, '', 'Je pense que monter ma réputation avec %faction prendrait une éternité.', 'Ich glaube, Ruf bei %faction zu grinden dauert ewig.', '我觉得刷 %faction 声望得花一辈子时间。', '', 'Creo que subir la reputación con %faction tomaría una eternidad.', '', 'Думаю, фармить репу %faction — это навсегда.'), + (659, 'suggest_faction', 'I am killing for %faction every day now but still far from %rep_level.', 0, 0, '', 'Je fais des missions pour la %faction tous les jours, mais je suis encore loin du niveau de réputation %rep_level.', 'Ich kill jeden Tag für %faction, bin aber noch weit weg von %rep_level.', '我现在每天都在为 %faction 杀怪,但离 %rep_level 还差得远。', '', 'Estoy matando por %faction todos los días ahora, pero aún estoy lejos del %rep_level.', '', 'Я убиваю ради %faction каждый день, но все еще далеко до %rep_level.'), + (660, 'suggest_faction', 'At %my_level AH deposits will decrease, right?', 0, 0, '', 'À mon niveau %my_level, les dépôts AH vont diminuer, n’est-ce pas ?', 'Sinken die AH-Gebühren auf %my_level?', '到 %my_level 拍卖行的手续费会降低,对吧?', '', 'Al %rep_level, los depósitos subastas disminuirán, ¿verdad?', '', 'На %my_level депозиты на аукционе уменьшатся, да?'), + (661, 'suggest_faction', 'How many exalted reps do you have?', 0, 0, '', 'Combien de représentants exaltés avez-vous?', 'Wie viele Fraktionen hast du auf ehrfürchtig?', '你有多少个崇拜的声望?', '', '¿Cuántas reputaciones exaltadas tienes?', '', 'Сколько у тебя превознесенных реп?'), + (662, 'suggest_faction', 'Who wants to be %my_level with %faction?', 0, 0, '', 'Qui veut être %my_level avec %faction?', 'Wer will %my_level bei %faction erreichen?', '有谁想跟我一起把 %faction 刷到 %my_level?', '', '¿Quién quiere estar en %rep_level con %faction?', '', 'Кто хочет быть %my_level у %faction?'), + (663, 'suggest_faction', 'Damn. My guild did a good %faction grind yesterday without me.', 0, 0, '', 'Mince. Mon groupe a bien grindé la réputation %faction hier sans moi.', 'Verdammt. Meine Gilde hat gestern ohne mich gut Ruf bei %faction gegrindet.', '靠!我的公会昨天没带我去刷 %faction 声望,刷得还挺好。', '', 'Maldición. Mi hermandad hizo un buen farmeo de %faction ayer sin mí.', '', 'Черт. Моя гильдия вчера хорошо пофармила %faction без меня.'), + (664, 'suggest_faction', 'Nobody wants to help me because I am %rep_level with %faction.', 0, 0, '', 'Personne ne veut m’aider parce que je suis %rep_level avec %faction.', 'Keiner will mir helfen, weil ich schon %rep_level bei %faction bin.', '没人愿意帮我,就因为我跟 %faction 才 %rep_level。', '', 'Nadie quiere ayudarme porque estoy %rep_level con %faction.', '', 'Никто не хочет мне помогать, потому что я %rep_level у %faction.'), + (665, 'suggest_faction', 'Please stay away from %faction.', 0, 0, '', 'S’il te plaît, garde tes distances avec %faction.', 'Bleib bitte weg von %faction.', '请离 %faction 远点。', '', 'Por favor manténgase alejado de %faction.', '', 'Пожалуйста, держись подальше от %faction.'), + (666, 'suggest_faction', 'Anyone farming %faction rep today?', 0, 0, '', 'Vous êtes en train de farmer la réputation de %faction aujourd’hui ?', 'Farmt heute jemand Ruf bei %faction?', '今天有谁在刷 %faction 声望吗?', '', '¿Alguien farmea reputación de %faction hoy?', '', ''), + (667, 'suggest_faction', 'Help me grind %faction rep!', 0, 0, '', 'Aide-moi à augmenter ma réputation avec la faction %faction!', 'Helft mir, Ruf bei %faction zu grinden!', '帮我刷 %faction 声望吧!', '', '¡Ayúdame a subir mi reputación con %faction!', '', ''), + (668, 'suggest_faction', 'I need %rep_level with %faction so badly!', 0, 0, '', 'J’ai tellement besoin d’atteindre le niveau %rep_level avec la faction %faction !', 'Ich brauch so dringend %rep_level bei %faction!', '我急需把 %faction 刷到 %rep_level!', '', '¡Necesito %rep_level con %faction con urgencia!', '', ''), + (669, 'suggest_faction', 'Is %faction grind worth it?', 0, 0, '', 'Est-ce que le farm de %faction vaut vraiment la peine?', 'Lohnt sich der Grind für %faction?', '刷 %faction 声望值得吗?', '', '¿Vale la pena el farmeo de %faction?', '', ''), + (670, 'suggest_faction', 'Anyone know %faction rep farm spots?', 0, 0, '', 'Je connais des endroits pour %faction pour augmenter ma réputation.', 'Kennt jemand gute Spots für Ruf bei %faction?', '有谁知道刷 %faction 声望的好地方吗?', '', '¿Alguien conoce lugares para farmear reputación con %faction?', '', ''), + (671, 'suggest_faction', 'I love grinding %faction rep!', 0, 0, '', 'J’adore augmenter ma réputation auprès de la faction %!', 'Ich liebe es, Ruf bei %faction zu grinden!', '我超爱刷 %faction 声望!', '', '¡Me encanta hacer grind de reputación con %faction!', '', ''), + (672, 'suggest_faction', 'Need help to finish %faction rep grind!', 0, 0, '', 'Besoin d’aide pour finir ma montée de réputation avec %faction!', 'Brauche Hilfe, um den Rufgrind bei %faction abzuschließen!', '需要帮忙把 %faction 声望刷完!', '', '¡Necesito ayuda para terminar el grind de reputación de %faction!', '', ''), + (673, 'suggest_faction', 'Only a few more %rep_level with %faction to go!', 0, 0, '', 'Il ne me reste que quelques niveaux de réputation à faire avec %faction !', 'Nur noch ein paar Stufen bis %rep_level bei %faction!', '只差一点就能把 %faction 刷到 %rep_level 了!', '', '¡Solo me faltan unos %rep_level para llegar a %faction!', '', ''), + (674, 'suggest_faction', 'Anyone up for %faction grind today?', 0, 0, '', 'Quelqu’un est motivé pour farmer la réputation de la %faction aujourd’hui?', 'Wer hat heute Bock auf Grind für %faction?', '今天有人想一起刷 %faction 声望吗?', '', '¿Alguien se anima al farmeo de %faction hoy?', '', ''), + (675, 'suggest_faction', 'Looking for %faction rep grind buddies!', 0, 0, '', 'À la recherche de compagnons de réputation de %faction! Qui est partant pour monter les réputations ensemble?', 'Suche Mitstreiter für Rufgrind bei %faction!', '找几个一起刷 %faction 声望的小伙伴!', '', 'Busco compañeros para farmear %faction.', '', ''), + (676, 'suggest_faction', 'I’m tired of grinding %faction rep!', 0, 0, '', 'J’en ai marre de faire monter ma réputation auprès de la faction %faction !', 'Ich bin das Rufgrinden für %faction leid!', '我刷 %faction 声望刷到累死了!', '', '¡Estoy cansado de hacer grind de %faction!', '', ''), + (677, 'suggest_faction', 'Anyone on %faction rep grind too?', 0, 0, '', 'Oh, quelqu’un d’autre qui est en train de monter sa réputation avec %faction ?', 'Noch wer auf Rufgrind bei %faction?', '还有人在刷 %faction 声望吗?', '', '¿Alguien más está haciendo el grind de %faction?', '', ''), + (678, 'suggest_faction', 'Time to hit %faction rep grind hard!', 0, 0, '', 'C’est le moment de faire le grind %faction sérieusement !', 'Zeit, den Rufgrind bei %faction richtig anzugehen!', '是时候认真刷 %faction 声望了!', '', '¡Es hora de darle duro al grind de %faction!', '', ''), + (679, 'suggest_faction', 'Help me finish %faction rep!', 0, 0, '', 'Aide-moi à terminer ma réputation de faction !', 'Helft mir, den Ruf bei %faction fertig zu machen!', '帮我把 %faction 声望刷完吧!', '', '¡Ayúdame a completar la reputación de %faction!', '', ''), + (680, 'suggest_faction', 'Need just %rndK more for %faction rep!', 0, 0, '', 'Il ne me manque plus que %rndK pour augmenter ma réputation auprès de la faction!', 'Mir fehlen nur noch %rndK für den %faction Ruf!', '只差 %rndK 就能提升 %faction 声望了!', '', '¡Solo me faltan %rndK para la reputación de %faction!', '', ''), + (681, 'suggest_faction', 'Grinding %faction rep all day!', 0, 0, '', 'À farmer la réputation de la faction toute la journée !', 'Den ganzen Tag Ruf bei %faction am farmen!', '今天一整天都在刷 %faction 声望!', '', '¡Farmearé la reputación de %faction todo el día!', '', ''), + (682, 'suggest_faction', 'Almost at %rep_level with %faction!', 0, 0, '', 'Je suis presque au niveau %rep_level avec la faction %faction !', 'Fast %rep_level bei %faction erreicht!', '快到 %faction 的 %rep_level 了!', '', '¡Casi al %rep_level con %faction!', '', ''), + (683, 'suggest_faction', '%faction rep grind never ends!', 0, 0, '', 'La réputation de faction, c’est jamais fini !', 'Rufgrind bei %faction endet nie!', '%faction 声望刷到天荒地老也刷不完!', '', '¡El grind de reputación de %faction nunca termina!', '', ''), + (684, 'suggest_faction', 'Help with %faction daily quests?', 0, 0, '', 'Besoin d’aide avec les quêtes journalières de %faction ?', 'Hilfe bei den Daily-Quests von %faction?', '有谁能帮我做 %faction 的日常任务?', '', '¿Ayuda con las misiones diarias de %faction?', '', ''), + (685, 'suggest_faction', 'Who’s on %faction grind right now?', 0, 0, '', 'Qui est en train de faire du %faction en ce moment ?', 'Wer ist grad beim Grind für %faction?', '谁现在正在刷 %faction 声望?', '', '¿Quién está farmeando %faction ahora?', '', ''), + (686, 'suggest_faction', 'Let’s do %faction quests together!', 0, 0, '', 'Faisons des quêtes de faction ensemble!', 'Lasst uns Quests von %faction zusammen machen!', '一起去做 %faction 的任务吧!', '', '¡Hagamos las misiones de %faction juntos!', '', ''), + (687, 'suggest_faction', 'Anyone farming %faction rep today?', 0, 0, '', 'Quelqu’un farme la réputation de la faction %faction aujourd’hui?', 'Farmt heute jemand Ruf bei %faction?', '今天有人刷 %faction 声望吗?', '', '¿Alguien está farmeando reputación con %faction hoy?', '', ''), + (688, 'suggest_faction', 'Need %faction rep help for %rep_level!', 0, 0, '', 'Besoin d’aide pour la réputation de %faction %rep_level %rep_level !', 'Brauche Hilfe beim Ruf bei %faction für %rep_level!', '需要帮我把 %faction 声望刷到 %rep_level!', '', '¡Necesito ayuda con la reputación de %faction para %rep_level!', '', ''), + (689, 'suggest_faction', 'Who wants to grind %faction rep with me?', 0, 0, '', 'Qui veut faire monter notre réputation auprès de %faction avec moi?', 'Wer will mit mir Ruf bei %faction grinden?', '有谁想跟我一起刷 %faction 声望?', '', '¿Quién quiere farmeo de %faction conmigo?', '', ''), + (690, 'suggest_faction', 'Come join me for %faction rep grind!', 0, 0, '', 'Viens me rejoindre pour %faction rep grind!', 'Kommt mit zum Rufgrind bei %faction!', '快来跟我一起刷 %faction 声望!', '', '¡Ven a unirte a mí para el grind de reputación de %faction!', '', ''), + (691, 'suggest_faction', 'On a roll with %faction rep grind!', 0, 0, '', 'En plein dans la montée de réputation de la %faction !', 'Läuft bei mir beim Rufgrind für %faction!', '刷 %faction 声望刷得正起劲!', '', '¡Estoy avanzando mucho con el grind de %faction!', '', ''), + (692, 'suggest_faction', 'Anyone want %faction rep grind boosts?', 0, 0, '', 'Qui est intéressé par des boosts pour monter sa réputation auprès de la faction ?', 'Will jemand Boosts für den Rufgrind bei %faction?', '有谁需要我帮忙加速刷 %faction 声望?', '', '¿Alguien quiere ayudas para el grind de %faction?', '', ''), + (693, 'suggest_faction', 'Just hit %rep_level with %faction!', 0, 0, '', 'Je viens d’atteindre le niveau de réputation %rep_level avec la faction %faction !', 'Gerade %rep_level bei %faction erreicht!', '刚把 %faction 刷到 %rep_level!', '', '¡Acabo de alcanzar %rep_level con %faction!', '', ''), + (694, 'suggest_faction', '%faction is so close to %rep_level, help me out!', 0, 0, '', 'La faction est tellement proche du niveau de réputation, aidez-moi !', 'Bei %faction fehlt nicht mehr viel bis %rep_level, helft mir doch mal!', '%faction 声望离 %rep_level 就差一点,帮帮我!', '', '¡%faction está tan cerca de %rep_level, ayúdame!', '', ''), + (695, 'suggest_faction', 'Wanna help me grind %faction rep?', 0, 0, '', 'Tu veux m’aider à monter ma réputation %faction ?', 'Bock, mir beim Rufgrind für %faction zu helfen?', '想帮我刷 %faction 声望吗?', '', '¿Quieres ayudarme a farmear reputación de %faction?', '', ''), + (696, 'suggest_faction', 'Farming %faction rep again!', 0, 0, '', 'Encore en train de gagner de la réputation avec %faction grâce à l’agriculture !', 'Farme wieder Ruf bei %faction!', '又在刷 %faction 声望了!', '', '¡Farmear reputación de %faction otra vez!', '', ''), + (697, 'suggest_faction', 'Anyone know a good spot for %faction rep?', 0, 0, '', 'Est-ce que quelqu’un sait un bon endroit pour augmenter ma réputation avec %faction?', 'Kennt jemand einen guten Spot für Ruf bei %faction?', '有谁知道刷 %faction 声望的好地方?', '', '¿Alguien conoce un buen lugar para farmear %faction?', '', ''), + (698, 'suggest_faction', 'How much more for %faction rep grind?', 0, 0, '', 'Combien de plus pour monter ma réputation avec la faction %faction ?', 'Wie viel fehlt noch beim Rufgrind für %faction?', '刷 %faction 声望还差多少?', '', '¿Cuánto más para terminar el grind de %faction?', '', ''), + (699, 'suggest_faction', 'Anyone doing %faction quests with me?', 0, 0, '', '%Quelqu’un pour faire des quêtes de faction avec moi ?', 'Macht jemand Quests von %faction mit mir?', '有谁跟我一起做 %faction 的任务?', '', '¿Alguien haciendo misiones de %faction conmigo?', '', ''), + (700, 'suggest_faction', 'Looking for %rep_level %faction rep grind!', 0, 0, '', 'Je recherche un moyen d’augmenter ma réputation avec la faction %faction de niveau %rep_level !', 'Suche jemand für Rufgrind bis %rep_level bei %faction!', '找人一起刷 %faction 声望到 %rep_level!', '', 'Busco %rep_level para el grind de %faction!', '', ''), + (701, 'suggest_faction', 'Only need %rndK more %faction rep!', 0, 0, '', 'J’ai seulement besoin de %rndK de plus de réputation avec %faction!', 'Mir fehlen nur noch %rndK Ruf bei %faction!', '只差 %rndK 就能提升 %faction 声望了!', '', '¡Solo necesito %rndK más de reputación con %faction!', '', ''), + (702, 'suggest_faction', '%faction grind is real!', 0, 0, '', 'La course à la réputation est intense!', 'Der Grind für %faction ist brutal!', '刷 %faction 声望真是累死人!', '', '¡El grind de %faction es real!', '', ''), + (703, 'suggest_faction', 'Anyone farming %faction rep for %rep_level?', 0, 0, '', '%Est-ce que quelqu’un fait de l’agriculture de réputation de faction pour atteindre le niveau de réputation %rep_level?', 'Farmt jemand Ruf bei %faction für %rep_level?', '有谁在刷 %faction 声望到 %rep_level?', '', '¿Alguien farmeando %faction para %rep_level?', '', ''), + (704, 'suggest_faction', 'Let’s do %faction rep grind together!', 0, 0, '', 'Hey, on va augmenter notre réputation avec la %faction ensemble !', 'Lasst uns den Rufgrind bei %faction zusammen machen!', '一起刷 %faction 声望吧!', '', '¡Hagamos juntos el grind de reputación de %faction!', '', ''), + (705, 'suggest_faction', 'More %faction rep, more rewards!', 0, 0, '', 'Plus de réputation auprès de la faction, plus de récompenses !', 'Mehr Ruf bei %faction, mehr Belohnungen!', '刷 %faction 声望越多,奖励越多!', '', '¡Más reputación de %faction, más recompensas!', '', ''), + (706, 'suggest_faction', 'Farming %faction rep with %my_class', 0, 0, '', 'Je monte ma réputation avec la faction agricole en jouant avec ma classe.', 'Farme Ruf bei %faction als %my_class', '用我的 %my_class 刷 %faction 声望!', '', 'Farmeando reputación con %faction con %my_class', '', ''), + (707, 'suggest_faction', 'Anyone else stuck at %rep_level with %faction?', 0, 0, '', 'Je suis coincé au niveau %rep_level avec la faction %faction. Vous aussi?', 'Hängt noch jemand bei %faction auf %rep_level fest?', '还有人卡在 %faction 的 %rep_level 吗?', '', '¿Alguien más atascado en %rep_level con %faction?', '', ''), + (708, 'suggest_faction', 'Doing %faction rep grind all night long!', 0, 0, '', 'À enchaîner les quêtes pour ma réputation auprès de la faction toute la nuit !', 'Die ganze Nacht Rufgrind bei %faction!', '通宵刷 %faction 声望!', '', '¡Voy a farmear reputación de %faction toda la noche!', '', ''), + (709, 'suggest_faction', 'Who wants to finish %faction rep grind with me?', 0, 0, '', 'Qui veut finir la réputation de %faction avec moi ?', 'Wer will den Rufgrind bei %faction mit mir abschließen?', '有谁想跟我一起把 %faction 声望刷完?', '', '¿Quién quiere terminar el grind de %faction conmigo?', '', ''), + (710, 'suggest_faction', 'I can help with %faction rep grind!', 0, 0, '', 'Je peux t’aider avec la montée de réputation de %faction!', 'Ich kann beim Rufgrind für %faction helfen!', '我可以帮你刷 %faction 声望!', '', '¡Puedo ayudar con el grind de reputación de %faction!', '', ''), + (711, 'suggest_faction', 'I’m ready for more %faction rep grind!', 0, 0, '', 'Je suis prêt à gagner davantage de réputation auprès de la %faction !', 'Bereit für mehr Rufgrind bei %faction!', '我准备好继续刷 %faction 声望了!', '', '¡Estoy listo para más grind de reputación de %faction!', '', ''), + (712, 'suggest_faction', 'Wanna help with %faction rep? I need it bad!', 0, 0, '', 'Tu veux de l’aide pour %faction rep? J’en ai vraiment besoin!', 'Bock zu helfen beim Ruf für %faction? Ich brauch das dringend!', '能帮我刷 %faction 声望吗?我真的很需要!', '', '¿Quieres ayudarme con %faction rep? ¡Lo necesito mucho!', '', ''), + (713, 'suggest_faction', 'I need %rndK more for %faction rep!', 0, 0, '', 'Il me manque %rndK de plus pour augmenter ma réputation avec %faction !', 'Ich brauche noch %rndK für den Ruf bei %faction!', '我还差 %rndK 就能提升 %faction 声望!', '', '¡Me faltan %rndK para la reputación de %faction!', '', ''), + (714, 'suggest_faction', 'Farm with me for %faction rep!', 0, 0, '', 'Viens faire de l’agriculture avec moi pour gagner de la réputation auprès de ma faction!', 'Farmt mit mir für Ruf bei %faction!', '跟我一起刷 %faction 声望吧!', '', '¡Farmea conmigo para reputación de %faction!', '', ''), + (715, 'suggest_faction', 'I just need %rndK more %faction rep!', 0, 0, '', 'Il ne me manque plus que %rndK de réputation avec %faction!', 'Mir fehlen nur noch %rndK Ruf bei %faction!', '我只差 %rndK 就能提升 %faction 声望了!', '', '¡Solo necesito %rndK más de %faction!', '', ''), + (716, 'suggest_faction', 'Doing %faction rep grind is never-ending...', 0, 0, '', 'Faire le farming de réputation pour la %faction n’en finit jamais...', 'Der Rufgrind bei %faction nimmt kein Ende...', '刷 %faction 声望简直没完没了……', '', 'El grind de %faction nunca termina...', '', ''), + (717, 'suggest_faction', 'Anyone got %faction rep boost items?', 0, 0, '', 'Est-ce que quelqu’un a des objets de boost de réputation de %faction ?', 'Hat jemand Items für Rufboost bei %faction?', '有谁有加速 %faction 声望的道具?', '', '¿Alguien tiene objetos para boostear reputación de %faction?', '', ''), + (718, 'suggest_faction', 'I need %rep_level %faction to unlock cool rewards!', 0, 0, '', 'J’ai besoin d’atteindre le niveau %rep_level auprès de la faction %faction pour débloquer des récompenses géniales!', 'Ich brauche %rep_level bei %faction, um coole Belohnungen freizuschalten!', '我需要把 %faction 刷到 %rep_level 才能解锁超酷的奖励!', '', '¡Necesito %rep_level de %faction para desbloquear buenas recompensas!', '', ''), + (719, 'suggest_faction', 'Anyone doing %faction rep for mounts?', 0, 0, '', 'Qui est en train de monter sa réputation avec %faction pour les montures?', 'Farmt jemand Ruf bei %faction für Mounts?', '有谁在刷 %faction 声望是为了坐骑?', '', '¿Alguien farmeando reputación de %faction para monturas?', '', ''), + (720, 'suggest_faction', 'Any chance to farm %faction rep with me?', 0, 0, '', 'Tu veux farmer ta réputation avec la faction %faction avec moi?', 'Chance, mit mir Ruf bei %faction zu farmen?', '有机会跟我一起刷 %faction 声望吗?', '', '¿Algún chance de farmear reputación de %faction conmigo?', '', ''), + (721, 'suggest_faction', 'I am so close to %rep_level with %faction!', 0, 0, '', 'Je suis tellement proche du niveau %rep_level avec la faction %faction !', 'Ich bin so kurz vor %rep_level bei %faction!', '我离 %faction 的 %rep_level 就差一点了!', '', '¡Estoy tan cerca de %rep_level con %faction!', '', ''), + (722, 'suggest_faction', 'Help me get %rep_level with %faction!', 0, 0, '', 'Aide-moi à atteindre le niveau de réputation %rep_level avec %faction !', 'Helft mir, %rep_level bei %faction zu erreichen!', '帮我把 %faction 声望刷到 %rep_level!', '', '¡Ayúdame a conseguir %rep_level con %faction!', '', ''), + (723, 'suggest_faction', 'Getting %faction rep grind done!', 0, 0, '', 'Je suis en train de grinder ma réputation avec la faction %faction !', 'Ich bin dabei, den %faction Rufgrind abzuschließen!', '正在努力刷完 %faction 声望!', '', '¡Estoy terminando el grind de reputación de %faction!', '', ''), + (724, 'suggest_faction', 'I’m in need of %faction rep grinding help!', 0, 0, '', 'J’ai besoin d’aide pour augmenter ma réputation avec %faction !', 'Brauche Hilfe beim Rufgrind für %faction!', '我需要帮手刷 %faction 声望!', '', '¡Necesito ayuda con el grind de %faction!', '', ''), + (725, 'suggest_faction', 'Need %rep_level %faction for the next rewards!', 0, 0, '', 'J’ai besoin d’atteindre le niveau %rep_level pour %faction afin de débloquer les prochaines récompenses !', 'Brauche %rep_level bei %faction für die nächsten Belohnungen!', '我得把 %faction 的声望刷到 %rep_level 才能拿到下个奖励!', '', '¡Necesito %rep_level de %faction para las siguientes recompensas!', '', ''), + (726, 'suggest_faction', 'Who can farm %faction rep with me?', 0, 0, '', 'Qui peut farmer la réputation de %faction avec moi ?', 'Wer kann mit mir Ruf bei %faction farmen?', '有谁能跟我一起刷 %faction 的声望啊?', '', '¿Quién puede farmear la reputación de %faction conmigo?', '', ''), + (727, 'suggest_faction', 'Grinding %faction rep non-stop!', 0, 0, '', 'En train de farmer la réputation de %faction sans arrêt !', 'Rufgrind bei %faction non-stop!', '一直在拼命刷 %faction 声望,停不下来!', '', '¡Grind de %faction sin parar!', '', ''), + (728, 'suggest_faction', 'Who else is grinding %faction rep right now?', 0, 0, '', 'Je suis en train de bosser le rang de réputation de %faction en ce moment. Qui d’autre est dans le même cas ?', 'Wer grindet gerade noch Ruf bei %faction?', '现在还有谁在刷 %faction 的声望?', '', '¿Quién más está farmeando %faction ahora?', '', ''), + (729, 'suggest_faction', 'Anyone want to farm %faction rep for fun?', 0, 0, '', 'Qui est partant pour monter la réputation de la faction %faction pour le plaisir ?', 'Will jemand zum Spaß Ruf bei %faction farmen?', '有没人想一起刷 %faction 声望,纯属娱乐?', '', '¿Alguien quiere farmear %faction por diversión?', '', ''), + (730, 'suggest_faction', 'Need %faction rep grind to unlock new quests!', 0, 0, '', 'Il me faut améliorer ma réputation auprès de %faction pour débloquer de nouvelles quêtes!', 'Ich muss noch Ruf bei %faction farmen, um neue Quests freizuschalten!', '得刷 %faction 声望才能解锁新任务!', '', '¡Necesito hacer grind de %faction para desbloquear nuevas misiones!', '', ''), + (731, 'suggest_faction', 'Wanna grind %faction rep for rewards!', 0, 0, '', 'Je veux faire monter ma réputation auprès de %faction pour obtenir des récompenses !', 'Bock, Ruf bei %faction für Belohnungen zu grinden!', '想刷 %faction 声望拿奖励,有人一起吗?', '', '¡Vamos a grindear reputación de %faction por recompensas!', '', ''), + (732, 'suggest_faction', 'Grinding %faction rep, one day at a time!', 0, 0, '', 'À faire ma réputation avec %faction, un jour à la fois !', 'Tag für Tag, Rufgrind bei %faction!', '每天刷一点 %faction 声望,慢慢来!', '', '¡Farmeando reputación de %faction, un día a la vez!', '', ''), + (733, 'suggest_faction', 'Anyone want to help with %faction rep grind?', 0, 0, '', 'Est-ce que quelqu’un veut m’aider à augmenter ma réputation auprès de %faction ?', 'Will jemand beim Rufgrind für %faction helfen?', '有人愿意帮我刷 %faction 声望吗?', '', '¿Alguien quiere ayudar con el grind de %faction?', '', ''), + (734, 'suggest_faction', 'Help me unlock rewards with %faction rep!', 0, 0, '', 'Aide-moi à débloquer des récompenses avec ma réputation auprès de la faction %faction !', 'Helft mir, mit Ruf bei %faction Belohnungen freizuschalten!', '帮我刷 %faction 声望,解锁奖励!', '', '¡Ayúdame a desbloquear recompensas con la reputación de %faction!', '', ''), + (735, 'suggest_faction', 'I’m grinding %faction rep for the mounts!', 0, 0, '', 'Je suis en train de farmer la réputation de %faction pour les montures !', 'Ich grinde Ruf bei %faction für Mounts!', '我在刷 %faction 声望,为了那些坐骑!', '', '¡Estoy farmeando %faction para las monturas!', '', ''), + (736, 'suggest_faction', 'Come farm %faction rep with me!', 0, 0, '', 'Viens faire du farm de réputation %faction avec moi!', 'Kommt mit, Ruf bei %faction farmen!', '来跟我一起刷 %faction 声望吧!', '', '¡Ven a farmear %faction conmigo!', '', ''), + (737, 'suggest_faction', 'I’m almost there with %faction rep!', 0, 0, '', 'Je suis presque arrivé à la réputation de %faction !', 'Beim Rufgrind für %faction bin ich fast da!', '我快把 %faction 声望刷到目标了!', '', '¡Casi llego con %faction!', '', ''), + (738, 'suggest_faction', 'Grinding %faction rep with my friends!', 0, 0, '', 'En train de monter la réputation de %faction avec mes potes !', 'Grinde Ruf bei %faction mit meinen Leuten!', '和朋友们一起刷 %faction 声望!', '', '¡Estoy farmeando %faction con mis amigos!', '', ''), + (739, 'suggest_something', 'Wanna party in %zone_name.', 0, 0, '', 'Envie de faire un groupe dans %zone_name.', 'Hab Bock auf Party in %zone_name.', '想在 %zone_name 组队吗?', '', '¡Vamos a perrear a %zone_name!', '', 'Хочу в пати в %zone_name.'), + (740, 'suggest_something', 'Anyone is looking for %my_role?', 0, 0, '', 'Est-ce que quelqu’un recherche %my_role ?', 'Sucht jemand %my_role?', '有人需要 %my_role 吗?', '', '¿Alguien está buscando %my_role?', '', 'Кто ищет %my_role?'), + (741, 'suggest_something', '%my_role is looking for guild.', 0, 0, '', '%my_role Je suis à la recherche d’une guilde.', '%my_role sucht Gilde.', '我是 %my_role,求加入一个公会。', '', '%my_role está buscando hermandad.', '', '%my_role ищет гильдию.'), + (742, 'suggest_something', 'Looking for gold.', 0, 0, '', 'À la recherche d’or.', 'Suche Gold.', '缺金币,求支援!', '', 'Buscando oro.', '', 'Ищу золото.'), + (743, 'suggest_something', '%my_role wants to join a good guild.', 0, 0, '', 'Je veux rejoindre une bonne guilde.', '%my_role will einer guten Gilde beitreten.', '我是 %my_role,想加入一个靠谱的公会。', '', '%my_role quiere unirse a una buen hermandad.', '', '%my_role хочет вступить в хорошую гильдию.'), + (744, 'suggest_something', 'Need a friend.', 0, 0, '', 'Besoin d’un ami.', 'Brauche einen Freund.', '想找个朋友一起玩。', '', 'Necesito un amigo...', '', 'Нужен друг.'), + (745, 'suggest_something', 'Anyone feels alone?', 0, 0, '', 'Vous vous sentez seul ?', 'Fühlt sich noch wer einsam?', '有人觉得孤单吗?', '', '¿Alguien se siente solo?', '', 'Кто-нибудь чувствует себя одиноко?'), + (746, 'suggest_something', 'Boring...', 0, 0, '', 'Je m’ennui', 'Langweilig...', '好无聊啊……', '', 'Aburrido...', '', 'Скучно...'), + (747, 'suggest_something', 'Who wants some?', 0, 0, '', 'Qui en veut?', 'Wer will was abhaben?', '谁想来点刺激?', '', '¿Quién quiere hacer grupo para levear?', '', 'Кто хочет немного?'), + (748, 'suggest_something', 'Go get me!', 0, 0, '', 'Viens me chercher!', 'Kommt her und holt mich!', '来抓我啊!', '', '¡Ven a buscarme!', '', 'Давай, поймай меня!'), + (749, 'suggest_something', 'Maybe a duel in %zone_name?', 0, 0, '', 'Peut-être un duel dans %zone_name?', 'Vielleicht ein Duell in %zone_name?', '在 %zone_name 来场决斗如何?', '', '¿Quizás un duelo en %zone_name?', '', 'Может, дуэль в %zone_name?'), + (750, 'suggest_something', 'Anybody doing something?', 0, 0, '', 'Est-ce que quelqu’un fait quelque chose?', 'Macht hier grade jemand etwas?', '有人在干啥吗?', '', '¿Alguien está haciendo algo?', '', 'Кто-нибудь что-то делает?'), + (751, 'suggest_something', '%zone_name: is anybody here?', 0, 0, '', '%zone_name: Est-ce qu’il y a quelqu’un ici ?', '%zone_name: ist hier jemand?', '%zone_name:有人在吗?', '', '%zone_name: ¿hay alguien aquí?', '', '%zone_name: кто-нибудь здесь есть?'), + (752, 'suggest_something', '%zone_name: where is everyone?', 0, 0, '', '%zone_name: Où est tout le monde ?', '%zone_name: Wo sind alle?', '%zone_name:人都去哪了?', '', '¿Hay alguien en %zone_name?', '', '%zone_name: где все?'), + (753, 'suggest_something', 'Looks like I am alone in %zone_name.', 0, 0, '', 'Il semble que je sois seul dans %zone_name.', 'Scheint als wäre ich allein in %zone_name.', '看起来我在 %zone_name 就一个人。', '', 'Parece que estoy solo en %zone_name.', '', 'Похоже, я один в %zone_name.'), + (754, 'suggest_something', 'Meet me in %zone_name.', 0, 0, '', 'Retrouve-moi dans %zone_name.', 'Trefft mich in %zone_name.', '来 %zone_name 找我吧。', '', 'Encuéntrame en %zone_name.', '', 'Встретимся в %zone_name.'), + (755, 'suggest_something', 'Let’s quest in %zone_name!', 0, 0, '', 'Partons à l’aventure dans %zone_name!', 'Lasst uns in %zone_name questen!', '去 %zone_name 做任务吧!', '', '¡Vamos a hacer un grupo para levear en %zone_name!', '', 'Давайте квестить в %zone_name!'), + (756, 'suggest_something', '%zone_name is the best place to be!', 0, 0, '', '%zone_name est l’endroit idéal où se trouver !', '%zone_name ist der beste Spot!', '%zone_name 是最棒的地方!', '', '%zone_name es el mejor lugar para estar!', '', '%zone_name — лучшее место!'), + (757, 'suggest_something', 'Wanna go to %zone_name. Anybody with me?', 0, 0, '', 'Qui veut aller à la %zone_name? Qui est partant avec moi?', 'Will nach %zone_name. Kommt wer mit?', '想去 %zone_name,有人一起吗?', '', 'Quiero ir a %zone_name. ¿Alguien se apunta?', '', 'Хочу в %zone_name. Кто со мной?'), + (758, 'suggest_something', 'Who wants going to %zone_name?', 0, 0, '', 'Qui veut aller à %zone_name?', 'Wer will nach %zone_name?', '谁想去 %zone_name?', '', '¿Quién quiere ir a %zone_name?', '', 'Кто хочет пойти в %zone_name?'), + (759, 'suggest_something', 'I don’t like %zone_name. Where to go?', 0, 0, '', 'Je n’aime pas %zone_name. Je devrais aller où ?', 'Ich mag %zone_name nicht, aber wohin sonst?', '我不喜欢 %zone_name,换个地方吧?', '', 'No me gusta %zone_name. ¿Dónde podria ir?', '', 'Не нравится %zone_name. Куда идти?'), + (760, 'suggest_something', 'Are there a good quests in %zone_name?', 0, 0, '', 'Y a-t-il de bonnes quêtes dans %zone_name ?', 'Gibt’s gute Quests in %zone_name?', '%zone_name 有啥好任务吗?', '', '¿Hay buenas misiones en %zone_name?', '', 'Есть хорошие квесты в %zone_name?'), + (761, 'suggest_something', 'Where to go after %zone_name?', 0, 0, '', 'Où devrais-je aller après %zone_name ?', 'Wohin soll ich nach %zone_name gehen?', '从 %zone_name 走后去哪?', '', '¿Adónde puedo ir después de %zone_name?', '', 'Куда идти после %zone_name?'), + (762, 'suggest_something', 'Who is in %zone_name?', 0, 0, '', 'Qui est dans %zone_name ?', 'Wer ist in %zone_name?', '谁在 %zone_name?', '', '¿Quién está en %zone_name?', '', 'Кто в %zone_name?'), + (763, 'suggest_something', 'LFG in %zone_name.', 0, 0, '', 'LFG dans %zone_name.', 'LFG in %zone_name.', '在 %zone_name 找队伍。', '', 'Busco grupo en %zone_name.', '', 'ЛФГ в %zone_name.'), + (764, 'suggest_something', '%zone_name is the worst place to be.', 0, 0, '', '%zone_name est le pire endroit où se trouver.', '%zone_name ist der übelste Spot.', '%zone_name 是最烂的地方。', '', '%zone_name es el peor lugar para estar', '', '%zone_name — худшее место.'), + (765, 'suggest_something', 'Catch me in %zone_name!', 0, 0, '', 'Retrouve-moi dans %zone_name !', 'Schnappt mich in %zone_name!', '在 %zone_name 抓我啊!', '', '¡Atrápame en %zone_name!', '', 'Лови меня в %zone_name!'), + (766, 'suggest_something', 'Go for %zone_name!', 0, 0, '', 'Allons-y pour %zone_name!', 'Ab nach %zone_name!', '冲向 %zone_name!', '', 'Me voy a %zone_name!', '', 'Пойдем в %zone_name!'), + (767, 'suggest_something', 'Wanna quest in %zone_name', 0, 0, '', 'Tu veux partir en quête dans %zone_name?', 'Bock zu questen in %zone_name', '想在 %zone_name 做任务吗?', '', 'Quiero hacer una búsqueda en %zone_name', '', 'Хочу квестить в %zone_name'), + (768, 'suggest_something', 'Anyone has quests in %zone_name?', 0, 0, '', 'Est-ce que quelqu’un a des quêtes dans %zone_name?', 'Hat wer Quests in %zone_name?', '有人在 %zone_name 有任务吗?', '', '¿Alguien tiene misiones en %zone_name?', '', 'У кого есть квесты в %zone_name?'), + (769, 'suggest_something', 'Come here to %zone_name!', 0, 0, '', 'Viens ici dans %zone_name!', 'Kommt her nach %zone_name!', '来 %zone_name 这儿!', '', '¡Ven aquí a %zone_name!', '', 'Приходите сюда, в %zone_name!'), + (770, 'suggest_something', 'Seems there is no Horde in %zone_name', 0, 0, '', 'Il semblerait qu’il n’y ait pas de Hordeux dans %zone_name.', 'Sieht so aus, als wär keine Horde in %zone_name', '看来 %zone_name 没有部落的人。', '', 'Parece que no hay Horda en %zone_name', '', 'Похоже, в %zone_name нет Орды'), + (771, 'suggest_something', 'Seems there is no Alliance in %zone_name', 0, 0, '', 'Il semble qu’il n’y ait pas d’Allianceux dans %zone_name.', 'Sieht so aus, als wär keine Allianz in %zone_name', '看来 %zone_name 没有联盟的人。', '', 'Parece que no hay Alianza en %zone_name', '', 'Похоже, в %zone_name нет Альянса'), + (772, 'suggest_something', 'I am really tired of %zone_name. Maybe go somewhere else?', 0, 0, '', 'Je suis vraiment fatigué de %zone_name. Pourquoi n’irions-nous pas ailleurs ?', 'Ich bin echt müde von %zone_name. Vielleicht sollte ich woanders hin?', '我真受够了 %zone_name,要不要换个地方?', '', 'Estoy realmente cansado de %zone_name. Quizás ire a otro lugar', '', 'Я очень устал от %zone_name. Может, пойти куда-нибудь еще?'), + (773, 'suggest_something', 'GL', 0, 0, '', 'Bonne chance', 'Viel Glück', '祝好运!', '', '', '', 'Удачи'), + (774, 'suggest_something', 'I want to go home, and then edge', 0, 0, '', 'Je veux rentrer chez moi, puis me détendre.', 'Ich will nach Hause und dann in die Kiste.', '我想回家,然后放松一下。', '', '', '', 'Хочу домой, а потом на край'), + (775, 'suggest_something', 'does anyone know what you need to dual wield?', 0, 0, '', 'Est-ce que quelqu’un peux m’aider avec la double spé ?', 'Weiß wer, was man fürs Beidhändig-Kämpfen braucht?', '有人知道双持武器需要啥条件吗?', '', '', '', 'Кто-нибудь знает, что нужно для двуручного боя?'), + (776, 'suggest_something', 'hi everyone!', 0, 0, '', 'Salut tout le monde!', 'hi zusammen!', '大家好!', '', '', '', 'Всем привет!'), + (777, 'suggest_something', '%zone_name is comfy', 0, 0, '', '%zone_name est vraiment agréable.', '%zone_name ist gemütlich', '%zone_name 感觉还挺舒服的。', '', '', '', '%zone_name уютно'), + (778, 'suggest_something', 'i feel great', 0, 0, '', 'Je me sens super bien.', 'Ich fühl mich super', '我感觉棒极了!', '', '', '', 'Я отлично себя чувствую'), + (779, 'suggest_something', 'i don’t ignore people i troll them until they ignore me', 0, 0, '', 'Je n’ignore pas les gens, je les trolle jusqu’à ce qu’ils m’ignorent.', 'Ich ignoriere Leute nicht, ich trolle sie, bis sie mich ignorieren', '我不无视别人,我会逗他们直到他们无视我。', '', '', '', ''), + (780, 'suggest_something', 'What do you guys think about my build? %my_role', 0, 0, '', 'Qu’est-ce que vous pensez de mon build? %mon_rolôle', 'Was haltet ihr von meinem Build? %my_role', '大家觉得我的装备咋样?我是 %my_role', '', '', '', 'Что думаете о моей сборке? %my_role'), + (781, 'suggest_something', 'good to see chat still remembers', 0, 0, '', 'Content de voir que le chat se souvient toujours', 'schön zu sehen, dass der Chat sich noch erinnert', '很高兴聊天频道里还有人记得我。', '', '', '', 'Рад видеть, что чат еще помнит'), + (782, 'suggest_something', 'like all weapons its hunter BIS', 0, 0, '', 'Comme toutes les armes, c’est le meilleur équipement pour les chasseurs.', 'Wie jede Waffe ist es Jäger-BiS', '跟所有武器一样,这对猎人来说是极品。', '', '', '', 'Как и все оружие — это лучший для охотника'), + (783, 'suggest_something', 'whole point to the game for me is soloing and finding new ways to solo stuff', 0, 0, '', 'Pour moi, le cœur du jeu réside dans le fait d’évoluer en solo et de découvrir de nouvelles façons de vaincre mes adversaires en solo.', 'für mich geht es im Spiel nur ums Solo-Spielen und darum, neue Wege zu finden, Dinge solo zu machen', '对我来说,游戏的乐趣就是单刷,找到新的单刷方法。', '', '', '', 'Вся суть игры для меня — соло и поиск новых способов соло'), + (784, 'suggest_something', 'i’ve NEVER ripped off anyone', 0, 0, '', 'Je n’ai JAMAIS arnaqué qui que ce soit.', 'ich habe NIEMANDEN jemals abgezockt', '我从没坑过任何人!', '', '', '', ''), + (785, 'suggest_something', 'ah yes the world of warcraft, where i come for life advice', 0, 0, '', 'Ah yes, le monde de Warcraft, là où je viens chercher des conseils de vie.', 'Ah ja, die Welt von Warcraft, wo ich Ratschläge fürs Lebensrat herbekomme.', '啊对,魔兽世界,我来这里寻求人生建议的地方。', '', '', '', 'Ах да, мир варкрафта, где я ищу жизненные советы'), + (786, 'suggest_something', 'HELLO?', 0, 0, '', 'Salut! Comment ça va?', 'HALLO?', '喂,有人吗?', '', '', '', 'АЛЛО?'), + (787, 'suggest_something', 'Time to fight my way into %zone_name', 0, 0, '', 'Il est temps que je me batte pour rentrer dans %zone_name.', 'Zeit, mir den Weg nach %zone_name freizukämpfen', '是时候杀进 %zone_name 了!', '', '', '', 'Пора пробиваться в %zone_name'), + (788, 'suggest_something', '%zone_name', 0, 0, '', '%zone_name', '%zone_name', '%zone_name', '', '', '', '%zone_name'), + (789, 'suggest_something', 'gotta poop', 0, 0, '', 'je dois aller aux toilettes.', 'ich muss kacken', '得去解决一下个人问题,等等我!', '', '', '', 'надо в туалет'), + (790, 'suggest_something', 'if you don’t loot your skinnable kills your pp loses 1mm permanently', 0, 0, '', 'Si vous ne pillez pas les ennemis que je peux dépecer, ton zgegue perd 1mm de façon permanente.', 'Wenn du deine häutbaren Kills nicht lootest, schrumpft dein Schniedel jedes mal dauerhaft um 1mm', '不拾取能剥皮的战利品,你的小兄弟可要永久缩水1毫米啊!', '', '', '', ''), + (791, 'suggest_something', 'NOOOOOOOOOOOOO', 0, 0, '', 'Oh nonnnnnnnnnn!', 'NEEEEEEEEEEEEIN', '不啊啊啊啊啊啊啊!', '', '', '', 'НЕТТТТТТТТТТ'), + (792, 'suggest_something', 'I LIKE POTATO', 0, 0, '', 'J’AIME LA POMME DE TERRE.', 'ICH MAG ZÜGE', '我超爱土豆!', '', '', '', 'Я ЛЮБЛЮ КАРТОШКУ'), + (793, 'suggest_something', 'w chat', 0, 0, '', 'w chat', '/w mir im chat', '聊天真棒!', '', '', '', 'w чат'), + (794, 'suggest_something', 'hi how are you guys', 0, 0, '', 'Salut, comment vous allez ?', 'hi, wie geht es euch', '嗨,大家好吗?', '', '', '', 'привет, как дела'), + (795, 'suggest_something', 'i just logged out and logged back in', 0, 0, '', 'Je viens de me déconnecter et de me reconnecter.', 'hab mich grad ausgeloggt und wieder eingeloggt', '我刚掉线又重新登录了。', '', '', '', 'только что вышел и снова зашел'), + (796, 'suggest_something', 'can you guys keep it down a bit, im lost in %zone_name', 0, 0, '', 'Est-ce que vous pouvez baisser un peu le volume, je suis perdu dans %zone_name.', 'könnt ihr mal leiser sein, ich hab mich in %zone_name verlaufen', '能小声点吗?我在%zone_name迷路了!', '', '', '', 'можете потише, я заблудился в %zone_name'), + (797, 'suggest_something', 'anyone want to have a drink with me at %zone_name ...hic!', 0, 0, '', 'Quelqu’un veut boire un verre avec moi à %zone_name ...hic!', 'wer will mit mir was trinken in %zone_name ...hicks!', '有谁想在%zone_name和我喝一杯吗……嗝!', '', '', '', 'кто хочет выпить со мной в %zone_name ... ик!'), + (798, 'suggest_something', 'hahahahaheeeeeeee dirin diring ingggggg hahahahaheeeeeeeeeeeeee', 0, 0, '', 'hahahahaheeeeeeeeee dirin diring ingggggg hahahahaheeeeeeeeeeeeee', 'hahahahaheeeeeeee dirin diring ingggggg hahahahaheeeeeeeeeeeeee', '哈哈哈哈嘿嘿嘿叮叮叮哈哈哈哈嘿嘿嘿!', '', '', '', 'хахахахахииииииии дирин диринг инггггг хахахахахииииииииииииии'), + (799, 'suggest_something', 'bait used to be believeable', 0, 0, '', 'Le leurre était autrefois crédible.', 'Früher war der Bait glaubwürdiger.', '以前的诱饵还能让人信服。', '', '', '', 'раньше приманка была правдоподобной'), + (800, 'suggest_something', 'maybe you just lost your innocence', 0, 0, '', 'Peut-être que tu viens juste de perdre ton innocence.', 'vielleicht hast du einfach deine Unschuld verloren', '可能你只是丢掉了天真。', '', '', '', 'может, ты просто потерял невинность'), + (801, 'suggest_something', 'any guilds willing to carry %my_role?', 0, 0, '', 'Y a-t-il des guildes prêtes à me prendre %my_role?', 'Gibts hier Gilden, die %my_role mitziehen?', '有公会愿意带带我这个%my_role吗?', '', '', '', 'есть гильдии, готовые тащить %my_role?'), + (802, 'suggest_something', 'once you start getting higher, gold is so easy to get', 0, 0, '', 'Une fois que tu commences à monter en niveau, l’or devient si facile à obtenir.', 'Sobald man höher kommt, ist Gold easy zu machen', '等级高了之后,金币真是手到擒来。', '', '', '', 'чем выше уровень, тем легче золото'), + (803, 'suggest_something', 'morning', 0, 0, '', 'Bonjour, comment ça va ce matin ?', 'moin', '早上好啊!', '', '', '', 'утро'), + (804, 'suggest_something', 'why does my ass hurt?', 0, 0, '', 'Pourquoi j’ai mal aux fesses?', 'Warum tut mir der Arsch weh?', '为啥我的屁股这么疼?', '', '', '', 'почему у меня болит задница?'), + (805, 'suggest_something', 'I feel like spirit is bis for leveling', 0, 0, '', 'Je pense que l’esprit est bis pour monter de niveau.', 'Ich hab das Gefühl, Willenskraft ist BiS zum Leveln', '我觉得精神属性对升级来说是极品。', '', '', '', 'Мне кажется, дух — лучший для прокачки'), + (806, 'suggest_something', 'Even more for troll', 0, 0, '', 'Encore plus pour les trolls', 'bei Trollen erst recht', '对巨魔来说更是如此。', '', '', '', 'Особенно для тролля'), + (807, 'suggest_something', 'CAN SOMEONE INVITE ME', 0, 0, '', 'ON PEUT M INVITER', 'KANN MICH JEMAND INVITEN', '谁能邀请我一下啊!', '', '', '', 'КТО-НИБУДЬ ПРИГЛАСИТЕ МЕНЯ'), + (808, 'suggest_something', 'need a lot of drinks', 0, 0, '', 'J’ai besoin de beaucoup de boissons.', 'brauche viele Drinks', '需要一大堆饮料!', '', '', '', 'нужно много выпивки'), + (809, 'suggest_something', 'damn gnomes', 0, 0, '', 'Ces satanés gnomes!', 'verdammte Gnome', '该死的侏儒!', '', '', '', 'чертовы гномы'), + (810, 'suggest_something', 'no one likes gnomes', 0, 0, '', 'Personne n’aime les gnomes.', 'niemand mag Gnome', '没人喜欢侏儒。', '', '', '', 'никто не любит гномов'), + (811, 'suggest_something', 'gnomes are only good for one thing', 0, 0, '', 'Les gnomes ne sont bons que pour une seule chose', 'Gnome taugen nur für eins', '侏儒就只有一件事干得不错。', '', '', '', 'гномы годятся только для одного'), + (812, 'suggest_something', 'Well', 0, 0, '', 'Bien', 'nun ja', '好吧', '', '', '', 'Ну'), + (813, 'suggest_something', 'mushrooms', 0, 0, '', 'champignons', 'Pilze', '蘑菇', '', '', '', 'грибы'), + (814, 'suggest_something', 'automatic thoughts are a scary thing', 0, 0, '', 'Les pensées automatiques sont effrayantes, elles surgissent sans prévenir et peuvent vraiment perturber ma tranquillité d’esprit.', 'selbstständige Gedanken sind schon gruselig', '自动冒出来的想法真挺吓人的。', '', '', '', 'автоматические мысли — страшная вещь'), + (815, 'suggest_something', 'the mind is more pliable than we like to believe', 0, 0, '', '', 'der Geist ist formbarer, als man glaubt', '思想比我们想象中更容易被塑造。', '', '', '', ''), + (816, 'suggest_something', 'any leveling guilds out there?', 0, 0, '', 'Des guildes de leveling par ici ?', 'gibts hier Levelgilden?', '有升级公会吗?', '', '', '', 'есть гильдии для прокачки?'), + (817, 'suggest_something', 'brb', 0, 0, '', 'brb.', 'brb', '马上回来!', '', '', '', 'брб'), + (818, 'suggest_something', 'Why is snow white but Ice is clear ? made of the same thing', 0, 0, '', 'Oh, la neige est blanche parce que ses cristaux reflètent la lumière du soleil, ce qui donne cette couleur. Tandis que la glace est claire car elle compacte les cristaux de glace, ce qui la rend transparente. C’est fou de penser que ce sont tous les deux faits de la même chose, n’est-ce pas ?', 'warum ist Schnee weiß, aber Eis klar? ist doch das Gleiche', '为啥雪是白的而冰是透明的?明明是同一种东西啊!', '', '', '', 'Почему снег белый, а лед прозрачный? Ведь это одно и то же'), + (819, 'suggest_something', 'why is whipped cream fluffy and regular not', 0, 0, '', 'Oh, whipped cream is all light and airy because we are beating some heavy cream until it traps air in. Regular cream just sits there all heavy and dense because it hasn’t been whipped yet. ', 'warum ist Schlagsahne fluffig und normale nicht', '为啥打发的奶油这么蓬松,而普通的就不行呢?', '', '', '', 'почему взбитые сливки пышные, а обычные нет'), + (820, 'suggest_something', 'why do feet smell if they have no nose', 0, 0, '', 'Pourquoi les pieds sentent alors qu’ils n’ont pas de nez !?', 'warum riechen Füße, wenn sie keine Nase haben', '脚上又没长鼻子,咋还能臭呢?', '', '', '', 'почему ноги пахнут, если у них нет носа'), + (821, 'suggest_something', 'seems a can of newbies arrived', 0, 0, '', 'Il semble qu’une brochette de noob soit arrivée.', 'scheint als hätte jemand ne Dose Neulinge aufgemacht', '看来来了一群菜鸟啊。', '', '', '', 'похоже, прибыла банка новичков'), + (822, 'suggest_something', 'stop trolling new players with BS answers', 0, 0, '', 'Arrête de troll les nouveaux joueurs avec des réponses bidon.', 'hört auf, Neulinge mit Bullshit-Antworten zu trollen', '别再用瞎话忽悠新手了!', '', '', '', 'хватит троллить новичков бредовыми ответами'), + (823, 'suggest_something', 'Is there PvP on this server?', 0, 0, '', 'Il y’a du pvp sur ce serveur ?', 'gibts hier auf dem Server PvP?', '这个服务器有PVP吗?', '', '', '', 'На этом сервере есть PvP?'), + (824, 'suggest_something', 'duh', 0, 0, '', 'duh', 'eh klar', '废话!', '', '', '', 'угу'), + (825, 'suggest_something', 'phew... :)', 0, 0, '', 'Phew... :)', 'puh... :)', '呼……还好 :)', '', '', '', 'фух... :)'), + (826, 'suggest_something', 'did you guys know that', 0, 0, '', 'Savez-vous que...', 'wusstet ihr schon, dass...', '你们知道吗?', '', '', '', 'вы знали что'), + (827, 'suggest_something', 'i don’t try to imagine what other creatures feel like', 0, 0, '', 'Je ne cherche pas à imaginer ce que d’autres créatures peuvent ressentir.', 'Ich stell mir lieber nicht vor, wie sich die Viecher fühlen, während ich sie umboxe.', '我可不去猜其他生物心里咋想的。', '', '', '', ''), + (828, 'suggest_something', 'oh whoops, wrong chat', 0, 0, '', 'Oh mince, mauvais chat', 'ups, falscher Chat', '哎呀,发错频道了!', '', '', '', 'ой, не тот чат'), + (829, 'suggest_something', 'bruh you guys are wildin out today', 0, 0, '', 'Oh là là, vous les gars, vous déconnez aujourd’hui!', 'bruh, ihr dreht heute völlig durch', '兄弟们,今天你们也太嗨了吧!', '', '', '', 'бро, вы сегодня отжигаете'), + (830, 'suggest_something', 'just let it be known that my text was here', 0, 0, '', 'Je vais juste faire en sorte que mon texte soit là.', 'Nett hier. Aber waren Sie schon mal in Darnassus?', '就让大家知道,我的话在这儿呢!', '', '', '', 'пусть все знают, что мой текст был здесь'), + (831, 'suggest_something', 'grrr angyy', 0, 0, '', 'Grrrrrr colère.', 'grrr wütend', '咕噜咕噜,气炸了!', '', '', '', 'гррр злой'), + (832, 'suggest_something', 'the grind is fun', 0, 0, '', 'Le grind est amusant', 'der Grind macht Spaß', '刷怪升级真有意思!', '', '', '', 'гринд — это весело'), + (833, 'suggest_something', 'Wow keeps me sharp', 0, 0, '', 'Wow me garde alerte', 'WoW hält mich wachsam', '魔兽让我保持敏锐!', '', '', '', 'Wow держит меня в тонусе'), + (834, 'suggest_something', 'hey have a question where can i take the roll for more xp? im in %zone_name', 0, 0, '', 'Hey, j’ai une question. Où est-ce que je peux trouver le rouleau pour gagner plus d’XP? Je suis dans %zone_name.', 'kurze Frage: wo krieg ich den Buff für mehr XP? bin in %zone_name', '嘿,有个问题,哪里能接任务多拿经验?我在%zone_name。', '', '', '', 'у меня вопрос: где можно взять бросок на больше опыта? я в %zone_name'), + (835, 'suggest_something', 'do you guys like sausages?', 0, 0, '', 'Hmm, est-ce que vous aimez les saucisses?', 'mögt ihr Würstchen?', '你们喜欢香肠吗?', '', '', '', 'вы любите сосиски?'), + (836, 'suggest_something', 'Invite me. I’ll help', 0, 0, '', 'Invite-moi. Je vais t’aider.', 'inv mich. ich helfe', '组我吧,我能帮忙!', '', '', '', ''), + (837, 'suggest_something', 'which class do you think is better for pvp?', 0, 0, '', 'Quelle classe est la meilleurs pour PvP !?', 'welche Klasse ist gut für PvP?', '你们觉得哪个职业在PVP里更强?', '', '', '', 'какой класс лучше для пвп?'), + (838, 'suggest_something', 'where the fuck is the cooking trainer in %zone_name', 0, 0, '', '%zone_name? Ah, putain, c’est toujours la galère pour trouver les PNJ dans cette zone-là. Attends, laisse-moi voir sur ma carte... Ah, je crois que le formateur en cuisine est près de la taverne, à droite de l’aubergiste. Tu devrais le trouver là-bas!', 'wo zum Teufel ist der Kochlehrer in %zone_name', '靠,%zone_name的烹饪训练师到底在哪?', '', '', '', 'где, черт возьми, тренер кулинарии в %zone_name'), + (839, 'suggest_something', 'you know what happens in %zone_name?', 0, 0, '', 'Tu sais ce qu’il se passe dans la %zone_name ?', 'wisst ihr, was in %zone_name abgeht?', '你知道%zone_name发生了啥事吗?', '', '', '', 'знаете, что происходит в %zone_name?'), + (840, 'suggest_something', 'I need to craft something', 0, 0, '', 'Je dois fabriquer quelque chose.', 'muss was craften', '我得做点东西。', '', '', '', 'Мне нужно что-то скрафтить'), + (841, 'suggest_something', 'Anyone up for some nonsense in %zone_name?', 0, 0, '', 'Quelqu’un pour un peu de folie dans la %zone_name?', 'Bock auf Blödsinn in %zone_name?', '有谁想在%zone_name搞点乱子吗?', '', '¿Alguien para tonterías en %zone_name?', '', 'Кто-нибудь для глупости в %zone_name?'), + (842, 'suggest_something', 'Let’s make %zone_name our playground.', 0, 0, '', 'Faisons de %zone_name notre terrain de jeu.', 'machen wir %zone_name zu unserem Spielplatz', '咱们把%zone_name变成我们的游乐场吧!', '', 'Hagamos de %zone_name nuestro patio de recreo.', '', 'Давайте сделаем %zone_name нашей игровой площадкой.'), + (843, 'suggest_something', 'I’m gonna get lost in %zone_name again.', 0, 0, '', 'Je vais me perdre dans %zone_name encore une fois.', 'ich verirr mich gleich wieder in %zone_name', '我又要在%zone_name迷路了。', '', 'Voy a perderme otra vez en %zone_name.', '', 'Снова потеряюсь в %zone_name.'), + (844, 'suggest_something', 'Can’t believe I’m still here in %zone_name.', 0, 0, '', 'Je n’arrive pas à croire que je suis toujours dans %zone_name.', 'kaum zu glauben, dass ich immer noch in %zone_name hocke', '真不敢相信我还在%zone_name。', '', 'No puedo creer que todavía esté aquí en %zone_name.', '', 'Не верю, что я до сих пор здесь в %zone_name.'), + (845, 'suggest_something', 'Wanna team up for %zone_name madness?', 0, 0, '', 'Tu veux faire équipe pour une folie dans la zone %zone_name?', 'Willst du dich für Chaos in %zone_name zusammentun?', '想组队在%zone_name大闹一场吗?', '', '¿Quieres unirte para la locura en %zone_name?', '', 'Хочешь составить команду для сумасшествия в %zone_name?'), + (846, 'suggest_something', 'Let’s raid the mailbox in %zone_name!', 0, 0, '', 'Allons piller la boîte aux lettres dans %zone_name!', 'lasst uns die Mailbox in %zone_name raiden!', '咱们去%zone_name抢邮箱吧!', '', '¡Vamos a asaltar el buzón en %zone_name!', '', 'Давайте захватим почтовый ящик в %zone_name!'), + (847, 'suggest_something', 'I swear, %zone_name is full of trolls.', 0, 0, '', 'Je te jure, %zone_name est rempli de trolls.', 'ich schwöre, in %zone_name wimmelt es von Trollen', '我发誓,%zone_name全是巨魔。', '', 'Juro que %zone_name está lleno de trolls.', '', 'Клянусь, %zone_name полно троллей.'), + (848, 'suggest_something', 'Let’s create chaos in %zone_name together.', 0, 0, '', 'Créons le chaos dans %zone_name ensemble.', 'lasst uns zusammen Chaos in %zone_name anrichten', '咱们一起在%zone_name制造混乱吧!', '', 'Creamos caos juntos en %zone_name.', '', 'Давайте устроим хаос в %zone_name вместе.'), + (849, 'suggest_something', 'I feel like doing absolutely nothing in %zone_name.', 0, 0, '', 'J’ai juste envie de ne rien faire du tout dans %zone_name.', 'hab Bock, in %zone_name absolut gar nix zu machen', '我只想在%zone_name啥也不干。', '', 'Tengo ganas de no hacer nada en %zone_name.', '', 'Хочу просто ничего не делать в %zone_name.'), + (850, 'suggest_something', 'No one ever listens in %zone_name.', 0, 0, '', 'Personne n’écoute jamais dans %zone_name.', 'in %zone_name hört eh keiner zu', '在%zone_name没人听我说话。', '', 'Nadie escucha nunca en %zone_name.', '', 'В %zone_name никто никогда не слушает.'), + (851, 'suggest_something', 'Is %zone_name even real?', 0, 0, '', '%zone_name est vraiment réel?', 'ist %zone_name überhaupt real oder wie Bielefeld?', '%zone_name到底是不是真的?', '', '¿Es %zone_name siquiera real?', '', 'Существует ли вообще %zone_name?'), + (852, 'suggest_something', 'Someone send help to %zone_name! It’s wild!', 0, 0, '', 'Envoyez de l’aide dans %zone_name ! C’est la folie !', 'schickt mal wer Hilfe nach %zone_name! ist wild hier!', '谁来%zone_name救救我!这里乱成一团!', '', '¡Alguien envíe ayuda a %zone_name! ¡Es una locura!', '', 'Кто-нибудь, отправьте помощь в %zone_name! Тут творится безумие!'), + (853, 'suggest_something', 'I am sure %zone_name is haunted.', 0, 0, '', 'Je suis persuadé que %zone_name est hanté.', 'ich bin sicher, in %zone_name spukt es', '我敢肯定%zone_name闹鬼。', '', 'Estoy seguro de que %zone_name está embrujado.', '', 'Уверен, что в %zone_name есть призраки.'), + (854, 'suggest_something', 'If %zone_name could talk, it would probably scream.', 0, 0, '', 'Si %zone_name pouvait parler, je pense qu’il crierait à s’en briser la voix.', 'wenn %zone_name reden könnte, würde es wahrscheinlich schreien', '如果%zone_name会说话,估计得尖叫起来。', '', 'Si %zone_name pudiera hablar, probablemente gritaría.', '', 'Если бы %zone_name мог говорить, он, наверное, кричал бы.'), + (855, 'suggest_something', 'There’s something fishy about %zone_name.', 0, 0, '', 'Il y a quelque chose de louche avec %zone_name.', 'irgendwas ist faul an %zone_name', '%zone_name有点不对劲。', '', 'Hay algo sospechoso acerca de %zone_name.', '', 'В %zone_name что-то подозрительное.'), + (856, 'suggest_something', 'I’m about to break %zone_name with my awesomeness.', 0, 0, '', 'Je m’apprête à conquérir %zone_name avec ma grandeur.', 'gleich zerleg ich %zone_name mit meiner Awesomeness', '我要用我的牛逼把%zone_name给炸了!', '', 'Estoy a punto de romper %zone_name con mi grandeza.', '', 'Я собираюсь разрушить %zone_name своей крутостью.'), + (857, 'suggest_something', 'Someone tell %zone_name that I am here.', 0, 0, '', 'Quelqu’un dit à %zone_name que je suis ici.', 'sagt %zone_name, dass ich da bin', '谁告诉%zone_name,我来了!', '', 'Alguien dígale a %zone_name que estoy aquí.', '', 'Кто-нибудь скажите %zone_name, что я здесь.'), + (858, 'suggest_something', 'I’m lost in %zone_name, send help.', 0, 0, '', 'Je suis perdu dans %zone_name, envoyez de l’aide.', 'ich hab mich in %zone_name verlaufen, sendet Hilfe', '我在%zone_name迷路了,快来救我!', '', 'Estoy perdido en %zone_name, envíen ayuda.', '', 'Я заблудился в %zone_name, отправьте помощь.'), + (859, 'suggest_something', 'I’m starting a revolution in %zone_name.', 0, 0, '', 'Je lance une révolution dans %zone_name.', 'ich starte ne Revolution in %zone_name', '我在%zone_name发起一场革命!', '', 'Estoy comenzando una revolución en %zone_name.', '', 'Я начинаю революцию в %zone_name.'), + (860, 'suggest_something', 'Anyone else find %zone_name suspiciously quiet?', 0, 0, '', 'Est-ce que vous trouvez que %zone_name est étrangement calme, non?', 'findet noch wer %zone_name verdächtig ruhig?', '还有人觉得%zone_name安静得诡异吗?', '', '¿Alguien más encuentra %zone_name sospechosamente tranquila?', '', 'Кто-нибудь еще находит %zone_name подозрительно тихой?'), + (861, 'suggest_something', 'I bet %zone_name has secrets we don’t know about.', 0, 0, '', 'Je parie que %zone_name a des secrets que nous ne connaissons pas.', 'ich wette, %zone_name hat Geheimnisse, von denen wir nix wissen', '我打赌%zone_name藏着我们不知道的秘密。', '', 'Apuesto a que %zone_name tiene secretos que no conocemos.', '', 'Держу пари, что в %zone_name есть секреты, о которых мы не знаем.'), + (862, 'suggest_something', 'You can never have enough %my_race in %zone_name.', 0, 0, '', 'On ne peut jamais avoir assez de %my_race dans %zone_name.', '%my_race kann man in %zone_name nie genug haben', '在%zone_name,%my_race永远不够多。', '', 'Nunca se tiene suficiente %my_race en %zone_name.', '', 'Никогда не бывает слишком много %my_race в %zone_name.'), + (863, 'suggest_something', 'I’m beginning to think %zone_name is just a myth.', 0, 0, '', 'Je commence à penser que %zone_name n’est qu’un mythe.', 'langsam glaube ich, %zone_name ist nur ein Mythos', '我开始怀疑%zone_name是不是只是个传说。', '', 'Empiezo a pensar que %zone_name es solo un mito.', '', 'Я начинаю думать, что %zone_name — это миф.'), + (864, 'suggest_something', 'Who knew %zone_name would be this crazy?', 0, 0, '', 'Qui aurait pensé que la %zone_name serait aussi folle ?', 'wer hätte gedacht, dass %zone_name so abgeht?', '谁能想到%zone_name会这么疯狂啊?', '', '¿Quién sabía que %zone_name sería tan loco?', '', 'Кто знал, что %zone_name будет таким безумным?'), + (865, 'suggest_something', 'It’s like the game broke in %zone_name.', 0, 0, '', 'C’est comme si le jeu avait buggé dans %zone_name.', 'als wäre das Spiel in %zone_name kaputt', '感觉在%zone_name游戏都崩了!', '', 'Es como si el juego se rompiera en %zone_name.', '', 'Похоже, игра сломалась в %zone_name.'), + (866, 'suggest_something', 'Is %zone_name actually a desert or am I just lost?', 0, 0, '', 'Est-ce que %zone_name est vraiment un désert ou suis-je simplement perdu(e) ?', 'ist %zone_name eigentlich ne Wüste, oder bin ich einfach nur lost?', '%zone_name到底是沙漠,还是我迷路了?', '', '¿%zone_name es realmente un desierto o simplemente estoy perdido?', '', '%zone_name реально пустыня или я просто заблудился?'), + (867, 'suggest_something', 'This place is starting to feel like %zone_name in a blender.', 0, 0, '', 'Cet endroit commence à ressembler à une véritable tornade de %zone_name.', 'fühlt sich langsam an wie %zone_name im Mixer', '这地方感觉就像把%zone_name扔进搅拌机里!', '', 'Este lugar empieza a sentirse como %zone_name en una licuadora.', '', 'Это место начинает напоминать %zone_name в блендере.'), + (868, 'suggest_something', 'Anyone got a map for %zone_name?', 0, 0, '', 'Est-ce que quelqu’un a une carte pour %zone_name?', 'hat jemand ne Karte für %zone_name?', '谁有%zone_name的地图啊?', '', '¿Alguien tiene un mapa para %zone_name?', '', 'Кто-нибудь имеет карту для %zone_name?'), + (869, 'suggest_something', 'Who else thinks %zone_name is trying to kill us?', 0, 0, '', 'Qui d’autre pense que %zone_name essaie de nous tuer ?', 'Noch wer, der denkt das %zone_name uns umbringen will?', '还有谁觉得%zone_name是想弄死咱们?', '', '¿Quién más cree que %zone_name está tratando de matarnos?', '', 'Кто еще думает, что %zone_name пытается нас убить?'), + (870, 'suggest_something', 'I’m on a wild ride through %zone_name!', 0, 0, '', 'Je suis en train de vivre une aventure folle à travers %zone_name !', 'Ich bin auf wilder Fahrt durch %zone_name!', '我在%zone_name经历了一场狂野冒险!', '', '¡Estoy en una aventura salvaje a través de %zone_name!', '', 'Я на дикой поездке через %zone_name!'), + (871, 'suggest_something', 'Is it just me or is %zone_name getting weirder?', 0, 0, '', 'Est-ce que c’est moi ou %zone_name devient de plus en plus étrange ?', 'Bin das nur ich, oder wird %zone_name immer weirder?', '是我一个人觉得%zone_name越来越诡异了吗?', '', '¿Soy solo yo o %zone_name se está volviendo más raro?', '', 'Только мне кажется, что %zone_name становится все страннее?'), + (872, 'suggest_something', 'I’m convinced %zone_name is secretly an amusement park.', 0, 0, '', 'Je suis convaincu que %zone_name est secrètement un parc d’attractions.', 'Ich bin davon überzeugt, das %zone_name heimlich ein Freizeitpark ist.', '我敢肯定%zone_name其实是个隐秘的游乐园!', '', 'Estoy convencido de que %zone_name es secretamente un parque de atracciones.', '', 'Я убежден, что %zone_name — это тайный парк аттракционов.'), + (873, 'suggest_something_toxic', 'why is my keyboard not working, is it because I don’t have enough friends', 0, 0, '', 'Pourquoi mon clavier ne fonctionne pas ? Ce doit être parceque je n’ai pas d’amis ....', 'Tastatur kaputt... wahrscheinlich wieder zu wenig Freunde.', '我的键盘怎么不好使了,是因为我朋友不够多吗?', '', '', '', ''), + (874, 'suggest_something_toxic', 'do you think the trees in this game have feelings', 0, 0, '', 'Vous pensez que les arbres ont des sentiments ?', 'Glaubt ihr, die Bäume in dem Spiel haben Gefühle?', '你们觉得游戏里的树有感情吗?', '', '', '', ''), + (875, 'suggest_something_toxic', 'I keep pressing the same button, but i’m still not winning', 0, 0, '', 'Je continue d’appuyer sur le même bouton, mais je ne gagne toujours pas.', 'Ich drücke immer dieselbe Taste, aber ich gewinne trotzdem nicht', '我一直在按同一个键,但怎么还是赢不了?', '', '', '', ''), + (876, 'suggest_something_toxic', 'I bet i could win a raid if i just yelled loot', 0, 0, '', 'Je parie que je pourrais gagner un raid si je hurlais LOOT !', 'ich wette, ich könnte nen Raid gewinnen, in dem ich einfach nur LOOT schreie', '我敢打赌,只要我大喊“战利品”,就能赢下团队副本!', '', '', '', ''), + (877, 'suggest_something_toxic', 'I’ve been trying to mount my mount for an hour, nothing’s happening', 0, 0, '', 'Ça fait une heure que j’essaie de monterma monture, mais rien ne se passe.', 'versuche seit einer Stunde, auf mein Mount zu steigen, nix passiert', '我试了一个小时想上坐骑,啥反应都没有!', '', '', '', ''), + (878, 'suggest_something_toxic', 'Is there a way to become invincible without actually playing the game', 0, 0, '', 'Il y’a un moyen de devenir invincible sans même jouer au jeu ?', 'gibt es nen Weg, unverwundbar zu werden, ohne wirklich zu spielen?', '有没有办法不玩游戏也能变无敌啊?', '', '', '', ''), + (879, 'suggest_something_toxic', 'can i just press random keys and still win, asking for a friend', 0, 0, '', 'Est-ce que je peux juste appuyer sur des touches au hasard et quand même gagner, c’est pour un ami.', 'kann ich einfach random Tasten smashen und trotzdem gewinnen? frage für nen Freund', '我随便按键也能赢吗?替朋友问的。', '', '', '', ''), + (880, 'suggest_something_toxic', 'If i stand still, does that mean i’m a better player than you', 0, 0, '', 'Si je reste immobile, est-ce que cela signifie que je suis un meilleur joueur que toi ?', 'Wenn ich einfach stehen bleibe, dann bin ich immer noch besser als du!', '如果我站着不动,是不是就比你厉害了?', '', '', '', ''), + (881, 'suggest_something_toxic', 'why doesn’t my pet heal me when i stand in fire', 0, 0, '', 'Pourquoi mon pet ne me soigne pas quand je reste dans le feu ?', 'warum heilt mich mein Pet nicht wenn ich im Feuer stehe', '为什么我站在火里,宠物不给我加血?', '', '', '', ''), + (882, 'suggest_something_toxic', 'so wait, do i need armor to be a true hero or is it optional', 0, 0, '', 'Alors, attends, est-ce que j’ai besoin d’une armure pour être un vrai héros ou est-ce facultatif ?', 'brauch ich Rüstung, um ein echter wahrer Held zu sein, oder ist das optional?', '等等,成为真正的英雄得穿铠甲吗,还是可选的?', '', '', '', ''), + (883, 'suggest_something_toxic', 'I need to collect 10000 Lucky Charm’s to level up, right?', 0, 0, '', 'Pour monter de niveau, j’ai besoin de collecter 10000 Lucky Charm’s, c’est ça ?', 'Muss ich erst 10.000 Koboldkerzen klauen, um zu leveln, oder was?', '我得收集10000个幸运符才能升级,对吧?', '', '', '', ''), + (884, 'suggest_something_toxic', 'my weapon is broken, but can I still just slap enemies with my huge Di.. err fist', 0, 0, '', 'Oh non, mon arme est cassée, mais je peux toujours juste gifler les ennemis avec mon énorme Bi.. euh poing, non?', 'meine Waffe ist kaputt, kann ich Gegner noch mit meinem riesigen Schw... äh, meiner Faust klatschen?', '我的武器坏了,但还能用我巨大的……呃,拳头打敌人吗?', '', '', '', ''), + (885, 'suggest_something_toxic', 'Can we just rename this game to “World of DieCraft a Player keeps killing me”', 0, 0, '', 'Pouvons-nous simplement renommer le jeu en “World of DieCraft, un Joueur me farm la gueule“ ?', 'können wir das Spiel bitte in World of DieCraft umbenennen, ein Spieler killt mich dauernd', '能不能把这游戏改名叫“死亡世界”,有个玩家老是杀我!', '', '', '', ''), + (886, 'suggest_something_toxic', 'why doesn’t my horse fly? can’t it just go up', 0, 0, '', 'Pourquoi mon cheval ne vol pas ? Pourquoi il ne peut juste pas aller vers le ciel ?', 'Warum fliegt mein Pferd nicht? kann es nicht einfach nach oben gehen?', '为什么我的马不会飞?就不能直接飞起来吗?', '', '', '', ''), + (887, 'suggest_something_toxic', 'Is there an option to turn off the graphics so it looks like my old calculator', 0, 0, '', 'Il n’y a pas une option pour désactiver les graphismes calculette ? Le jeu est vraiment moche.', 'Gibts ne Option, die Grafik auszumachen, damit es aussieht wie mein alter Taschenrechner?', '有没选项能关掉画面,让它看起来像我老计算器一样?', '', '', '', ''), + (888, 'suggest_something_toxic', 'I was supposed to kill something but i fell asleep, is that normal', 0, 0, '', 'Je devais tuer un truc mais je me suis endormi, c’est normal ?', 'ich sollte was killen, bin aber eingepennt, ist das normal?', '我本来要去杀怪,结果睡着了,这正常吗?', '', '', '', ''), + (889, 'suggest_something_toxic', 'does anyone else just kill random NPCs for fun or is it just me', 0, 0, '', 'Est-ce que je suis le seul à tuer des PNJ au hasard juste pour le plaisir, ou est-ce qu’il y a d’autres dans le coin?', 'killt noch wer random NPCs zum Spaß, oder bin das nur ich?', '还有谁跟我一样随便杀NPC玩吗?', '', '', '', ''), + (890, 'suggest_something_toxic', 'I’m %my_level and i already know all the secrets of the game', 0, 0, '', 'Je suis %my_level et je connais déjà tous les secrets du jeu.', 'ich bin %my_level und kenn schon alle Geheimnisse des Spiels', '我才%my_level就知道游戏里所有秘密了!', '', '', '', ''), + (891, 'suggest_something_toxic', 'can i just talk to the NPCs until they give me all the loot?', 0, 0, '', 'Je peux juste parler aux PNJ jusqu’à ce qu’ils me donnent tout le butin?', 'kann ich einfach so lange mit NPCs reden, bis sie mir ihren ganzen Loot einfach geben?', '我能一直跟NPC聊天,直到他们把所有装备给我吗?', '', '', '', ''), + (892, 'suggest_something_toxic', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', 0, 0, '', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', 'WWWWWWwwwwwwwww ssssSSsssss dDDddddd aaaAA', '', '', '', ''), + (893, 'suggest_something_toxic', 'why am i losing, i’ve been playing for 15 minutes', 0, 0, '', 'Pourquoi est-ce que je perds, je joue depuis 15 minutes.', 'warum verliere ich, ich spiel doch schon 15 Minuten', '我玩了15分钟了,怎么还在输啊?', '', '', '', ''), + (894, 'suggest_something_toxic', 'I don’t need a strategy, i just need to spam this ability and hope for the best', 0, 0, '', 'Je n’ai pas besoin d’une stratégie, je vais juste spammer cette compétence et espérer le meilleur.', 'Ich brauch keine Strategie, ich spam einfach diese Fähigkeit und hoffe aufs Beste', '我不需要啥策略,就狂按这个技能,祈祷好运吧!', '', '', '', ''), + (895, 'suggest_something_toxic', 'If i equip a bunch of Lucky Charm’s, does that make me get more loot?', 0, 0, '', 'Équiper plusieurs Lucky Charm’s ne me donne pas un avantage supplémentaire pour obtenir plus de butin. Ces amulettes n’affectent pas directement la quantité de butin que je reçois.', 'wenn ich haufenweise Glücksbringer anlege, krieg ich dann mehr Loot?', '如果我装备一堆幸运符,能多爆装备吗?', '', '', '', ''), + (896, 'suggest_something_toxic', 'Is the secret to the game just screaming at the screen', 0, 0, '', 'Le secret du jeu est juste d’hurler sur l’écran', 'Ist das Geheimnis dieses Spiels einfach, den Bildschirm anzuschreien?', '游戏的秘诀是不是就是对着屏幕大喊大叫?', '', '', '', ''), + (897, 'suggest_something_toxic', 'can i just put on every item i find and hope for the best', 0, 0, '', 'Est-ce que je peux juste equipper n’importe quel loot et yolo ?', 'Kann ich einfach jedes Item anziehen, das ich finde, und aufs Beste hoffen?', '我能把找到的所有东西都穿上,然后祈祷好运吗?', '', '', '', ''), + (898, 'suggest_something_toxic', 'do i really need to do quests or can i just explore and yell at people', 0, 0, '', 'Ah, est-ce que je dois vraiment faire des quêtes ou est-ce que je peux juste explorer et crier sur les gens?', 'Muss ich echt Quests machen oder kann ich einfach erkunden und Leute anbrüllen?', '我真得做任务吗,还是能随便逛逛然后对人乱吼?', '', '', '', ''), + (899, 'suggest_something_toxic', 'I’m at work playing this game, they say I should be working >.> but it’s WOW!', 0, 0, '', 'Je suis au travail en train de jouer à ce jeu, ils disent que je devrais travailler >.> mais c’est WOW!', 'ich zocke auf Arbeit, die sagen ich soll arbeiten >.> aber es ist WoW!', '我在上班玩游戏,他们说我该工作>.>但这是魔兽啊!', '', '', '', ''), + (900, 'suggest_something_toxic', 'If i eat all the food in the game, will it make me invincible', 0, 0, '', 'Si je mange toute la nourriture dans le jeu, est-ce que cela va me rendre invincible ?', 'wenn ich alles Essen im Spiel esse, werd ich dann unbesiegbar', '如果我把游戏里所有食物都吃了,会不会变无敌?', '', '', '', ''), + (901, 'suggest_something_toxic', 'so what if i just throw my pet into every boss fight and need all time, it’s hunters rules', 0, 0, '', 'Donc je balance mon pet sur tout les boss du jeu et je clique sur need, c’est la règle de base des chasseurs."', 'Also, ich werfe mein Pet einfach in jeden Bosskampf und drücke beim würfeln immer auf Gier, das sind die Hunter-Regeln.', '要是每次打BOSS我都把宠物扔上去,然后一直点需求,这是猎人的规矩吧?', '', '', '', ''), + (902, 'suggest_something_toxic', 'can you die if you’re already dead, asking for a ghost', 0, 0, '', 'On peut mourir si on est déjà mort ? Je demande pour un fantome...', 'kann man sterben, wenn man schon tot ist? Ich frage für nen Geist...', '如果你已经死了,还能再死一次吗?替一个幽灵问的。', '', '', '', ''), + (903, 'suggest_something_toxic', 'does anyone know where the legendary sword is, i’ve only looked for 5 minutes', 0, 0, '', 'Est-ce que quelqu’un sait où se trouve l’épée légendaire? Je n’ai cherché que pendant 5 minutes.', 'weiß jemand, wo das legendäre Schwert ist? hab schon 5 Minuten gesucht', '有人知道传说之剑在哪吗?我才找了5分钟。', '', '', '', ''), + (904, 'suggest_something_toxic', 'I was told if I come here I get 100000 Gold where do I go?', 0, 0, '', 'On m’a dit que si je viens ici, je reçois 100 000 pièces d’or. Où dois-je aller?', 'Man sagte mir, hier gibts 100.000 Gold, wo muss ich hin?', '听说来这里能拿10万金币,我该去哪?', '', '', '', ''), + (905, 'suggest_something_toxic', 'I play this game like I had your mum lastnight, in a screaming glory!', 0, 0, '', 'Je joue à ce jeu comme si j’avais ta mère la nuit dernière, dans un vacarme triomphant!', 'ich spiel das hier wie ich deine Mom letzte Nacht genommen hab - mit Geschrei und einem ruhmreichem Ende!', '我玩这游戏就像昨晚跟你妈一样,喊得震天响!', '', '', '', ''), + (906, 'suggest_something_toxic', 'so when does the game start getting fun, or is it all just grinding forever', 0, 0, '', 'Il devient fun quand le jeu ? Ou c’est juste du grind pour toujours ?', 'ab wann wirds Spaß, oder ist alles nur für immer Grind', '这游戏啥时候开始好玩啊,还是就一直刷刷刷?', '', '', '', ''), + (907, 'suggest_something_toxic', 'I keep asking for directions, but they keep sending me to random places, is this normal', 0, 0, '', 'J’arrête pas de demander mon chemin mais ils m’envoient constament à des endroits aléatoires, c’est normal ?', 'ich frage ständig nach dem Weg, aber sie schicken mich immer irgendwo hin, ist das normal?', '我一直问路,但他们老把我指到乱七八糟的地方,这正常吗?', '', '', '', ''), + (908, 'suggest_something_toxic', 'I found out if your press ALF+f4 you get 100000 Gold, trick is you need to attact a red NPC 1st', 0, 0, '', 'WOOOW Si tu presses ALF + F4, tu te fais 100,000 Gold. Le truc c’est que tu dois attaquer un mob rouge juste avant !!!', 'hab gehört, wenn du ALT+F4 drückst, kriegst du 100.000 Gold - Der Trick ist, vorher nen roten NPC anzugreifen', '我发现按ALT+F4能得10万金币,窍门是你得先攻击一个红名NPC!', '', '', '', ''), + (909, 'suggest_something_toxic', 'why does my character always look so confused, is that my fault', 0, 0, '', 'Pourquoi mon personnage a l’air complètement con ? C’est de ma faute ?', 'warum sieht mein Char immer so verwirrt aus, bin ich schuld?', '为什么我的角色老是一脸懵逼,是我的错吗?', '', '', '', ''), + (910, 'suggest_something_toxic', 'Hey your mum said goto bed!', 0, 0, '', 'Hey, ta mère a dit d’aller te coucher!', 'hey, deine Mom meinte, du sollst jetzt ins Bett!', '嘿,你妈说该上床睡觉了!', '', '', '', ''), + (911, 'suggest_something_toxic', 'Warcraft is a grind just like your mum was other day, she even has monthly subscription!', 0, 0, '', 'Warcraft, c’est une vraie corvée, tout comme ta mère l’était l’autre jour, elle a même un abonnement mensuel!', 'Warcraft ist ein Grind - genau wie deine Mom neulich, hat sogar Abo!', '魔兽就是个刷刷刷的游戏,就像前几天你妈一样,她还有月卡呢!', '', '', '', ''), + (912, 'suggest_something_toxic', 'Is it true that Chuck Norris once leveled up without even pressing a button', 0, 0, '', 'Oh là là! Il parait que Chuck Norris a déjà monté de niveau sans même appuyer sur un bouton! C’est ça la légende de Chuck Norris, incroyable non?', 'stimmt es, dass Chuck Norris mal gelevelt hat, ohne nen Knopf zu drücken', '听说查克·诺里斯不用按键就能升级,这是真的吗?', '', '', '', ''), + (913, 'suggest_something_toxic', 'Chuck Norris doesn’t need a mount, the ground just moves under him', 0, 0, '', 'Chuck Norris n’a pas besoin d’une monture, le sol se déplace juste sous lui', 'Chuck Norris braucht kein Mount, der Boden bewegt freiwillig sich unter ihm', '查克·诺里斯不需要坐骑,地面会自己在他脚下移动。', '', '', '', ''), + (914, 'suggest_something_toxic', 'when Chuck Norris enters a raid, the boss just surrenders', 0, 0, '', 'Quand Chuck Norris entre dans un raid, le boss se rend simplement', 'wenn Chuck Norris nen Raid betritt, gibt der Boss einfach auf', '查克·诺里斯一进团队副本,BOSS直接投降。', '', '', '', ''), + (915, 'suggest_something_toxic', 'Chuck Norris doesn’t use abilities, he just looks at the enemies and they die', 0, 0, '', 'Chuck Norris ne se sert pas de ses capacités, il regarde juste les ennemis et ils meurent.', 'Chuck Norris nutzt keine Fähigkeiten, er guckt nur - und die Gegner sterben', '查克·诺里斯不用技能,瞪一眼敌人就死了。', '', '', '', ''), + (916, 'suggest_something_toxic', 'Chuck Norris doesn’t need healing, his enemies just heal him out of respect', 0, 0, '', 'Chuck Norris n’a pas besoin de guérison, ses ennemis le soignent juste par respect.', 'Chuck Norris braucht keine Heilung, seine Feinde heilen ihn aus Respekt', '查克·诺里斯不需要治疗,敌人出于尊敬会主动给他加血。', '', '', '', ''), + (917, 'suggest_something_toxic', 'Chuck Norris can solo a raid... with no gear... and while AFK', 0, 0, '', 'Chuck Norris peut faire une raid tout seul... sans équipement... et en étant AFK', 'Chuck Norris kann nen Raid solo... ohne Gear... und AFK', '查克·诺里斯能单刷团队副本……不穿装备……还挂机!', '', '', '', ''), + (918, 'suggest_something_toxic', 'Chuck Norris doesn’t need to dodge, the game dodges him', 0, 0, '', 'Chuck Norris n’a pas besoin d’esquiver, c’est le jeu qui l’évite', 'Chuck Norris weicht nicht aus, das Spiel weicht ihm aus', '查克·诺里斯不需要躲闪,游戏会主动避开他。', '', '', '', ''), + (919, 'suggest_something_toxic', 'Chuck Norris once used a low-level weapon, and the game gave him the best loot', 0, 0, '', 'Chuck Norris a déjà utilisé une arme de bas niveau, et le jeu lui a donné le meilleur butin', 'Chuck Norris nutzte mal ne Lowlevel-Waffe - das Spiel gab ihm den besten Loot', '查克·诺里斯用一把低级武器,游戏直接送他顶级装备。', '', '', '', ''), + (920, 'suggest_something_toxic', 'When Chuck Norris enters the instance, the loot automatically drops into his inventory', 0, 0, '', 'Quand Chuck Norris entre dans l’instance, le butin tombe automatiquement dans son inventaire.', 'wenn Chuck Norris eine Ini betritt, landet der Loot automatisch in seinem Inventar', '查克·诺里斯一进副本,战利品自动掉进他背包。', '', '', '', ''), + (921, 'suggest_something_toxic', 'what is ligma', 0, 0, '', 'C’est quoi ligma', 'was ist Wilma', '啥是ligma?', '', '', '', 'что такое лигма'), + (922, 'suggest_something_toxic', 'what is sugma', 0, 0, '', 'C’est quoi sugma', 'Bist du aus Bayern?', '啥是sugma?', '', '', '', 'что такое сугма'), + (923, 'suggest_something_toxic', 'ligma balls', 0, 0, '', 'ligma balls', 'Wilma lecken', 'ligma balls', '', '', '', 'лима болс'), + (924, 'suggest_something_toxic', 'sugma balls', 0, 0, '', 'sugma balls', 'Dann leck mir an den Eiern.', 'sugma balls', '', '', '', 'сугма болс'), + (925, 'suggest_something_toxic', 'I EAT ASS', 0, 0, '', 'JE MANGE DES CULS', 'ICH FRESS NEN ARSCH', '我吃屁股!', '', '', '', 'Я ЕМ ЗАДНИЦУ'), + (926, 'suggest_something_toxic', 'I want to shove %random_inventory_item_link up my ass', 0, 0, '', 'Je veux enfoncer %random_inventory_item_link dans mon cul.', 'ich will mir %random_inventory_item_link in den Arsch schieben', '我想把%random_inventory_item_link塞进我屁股里!', '', '', '', 'Я хочу засунуть %random_inventory_item_link себе в задницу'), + (927, 'suggest_something_toxic', 'I want to shove %random_inventory_item_link up your ass', 0, 0, '', 'Je veux te fourrer %random_inventory_item_link dans le cul.', 'ich will dir %random_inventory_item_link in den Arsch schieben', '我想把%random_inventory_item_link塞进你屁股里!', '', '', '', 'Я хочу засунуть %random_inventory_item_link тебе в задницу'), + (928, 'suggest_something_toxic', 'Darnasses', 0, 0, '', 'Darnasses', 'Darnarschloch', '达纳苏斯的蠢货', '', '', '', 'Дарнасс'), + (929, 'suggest_something_toxic', 'seems like your suffering from sugma', 0, 0, '', 'Il semble que tu souffres de sugma.', 'Du leidest an mehr Bugs als das Spiel selbst!', '看起来你得了“傻瓜病”啊', '', '', '', 'похоже, у тебя сугма'), + (930, 'suggest_something_toxic', 'deez nutz in ur mouth', 0, 0, '', 'deez nutz dans ta bouche', 'Du hast Chefkochs salzige Schokobällchen im Mund!', '嘴里塞满了我的“坚果”吧', '', '', '', 'дииз натс в твой рот'), + (931, 'suggest_something_toxic', 'cool boner, bro', 0, 0, '', 'Belle bite bro', 'cooler Ständer, Bro', '老兄,你的“装备”真不错啊', '', '', '', 'крутой стояк, бро'), + (932, 'suggest_something_toxic', 'ERP?', 0, 0, '', 'ERP?', 'ERP?', '来一场角色扮演吗?', '', '', '', 'ERP?'), + (933, 'suggest_something_toxic', 'i tried everything, but at the end ERP did the trick', 0, 0, '', 'J’ai tout essayé, mais finalement l’ERP a fait l’affaire.', 'hab alles probiert, am Ende hats ERP gerichtet', '我啥都试过了,最后还是角色扮演最管用', '', '', '', 'я перепробовал все, но в итоге ERP помог'), + (934, 'suggest_something_toxic', 'I want to bonk in %zone_name', 0, 0, '', 'Je veux ken dans %zone_name.', 'ich will in %zone_name bumsen', '我想在%zone_name来一发', '', '', '', 'Хочу заняться этим в %zone_name'), + (935, 'suggest_something_toxic', 'looking for female gnome with gorilla pet for erp in %zone_name', 0, 0, '', 'Je recherche une gnome femelle avec un gorille de compagnie pour du jeu de rôle érotique dans %zone_name.', 'suche weiblichen Gnom mit Gorilla-Pet für ERP in %zone_name', '在%zone_name找个带猩猩宠物的女性侏儒一起角色扮演', '', '', '', 'ищу гнома-девушку с гориллой для ERP в %zone_name'), + (936, 'suggest_something_toxic', 'i may understand an asshole, but a pervert?', 0, 0, '', 'Je peux comprendre un imbécile, mais un pervers ?', 'ein Arschloch versteh ich, aber nen Perversen?', '我能理解一个混蛋,但一个变态?', '', '', '', 'я могу понять засранца, но извращенца?'), + (937, 'suggest_something_toxic', 'there is no gyat in %zone_name', 0, 0, '', 'Il n’y a pas de gyat dans %zone_name.', 'Kein Gyat in %zone_name, nur Gesichtseintopf so weit das Auge reicht.', '%zone_name里啥也没有啊', '', '', '', 'в %zone_name нет гят'), + (938, 'suggest_something_toxic', 'I’m killing all the animals in %zone_name. Fuck the animals!!!', 0, 0, '', 'Je tue tous les animaux dans %zone_name. NIQUE les animaux enculé de vegan !!!', 'ich kill alle Tiere in %zone_name. Scheiß auf Tiere und die PETA!!!', '我在%zone_name把所有动物都杀了!去他娘的动物!', '', '', '', ''), + (939, 'suggest_something_toxic', 'good think i got 3 legs', 0, 0, '', 'Je suis doté de 3 jambes', 'Nur gut, dass ich 3 Beine hab', '还好我有三条“腿”', '', '', '', 'хорошо, что у меня три ноги'), + (940, 'suggest_something_toxic', 'dont be mad im goonin like a sigma', 0, 0, '', 'Hey, ne sois pas fâché, je vais y aller comme un sigma.', 'Chill, ich bin einfach grad komplett in meinem Film.', '别生气,我可是像头领狼一样在行动', '', '', '', 'не злись, я гоиню как сигма'), + (941, 'suggest_something_toxic', 'try finger, but hole', 0, 0, '', 'Essai ton doigt mais dans le cul ?', 'Finger im Po Mexiko', '试试用手指,不过是哪个洞呢?', '', '', '', 'попробуй палец, но дырку'), + (942, 'suggest_something_toxic', 'You somehow managed to mess that up, impressive', 0, 0, '', 'Tu as réussi à tout gâcher, c’est impressionnant.', 'Das hast du irgendwie vergeigt, beeindruckend', '你居然能把这事搞砸,真是厉害', '', '', '', ''), + (943, 'suggest_something_toxic', 'I could do this blindfolded, but I don’t need to', 0, 0, '', 'Je pourrais le faire les yeux bandés, mais ce n’est pas nécessaire.', 'das könnte ich blind machen, muss ich aber nicht', '我闭着眼睛都能搞定,根本不用睁眼', '', '', '', ''), + (944, 'suggest_something_toxic', 'You sure you know how to play this game?', 0, 0, '', 'Est-ce que tu es sûr de savoir comment jouer à ce jeu?', 'sicher, dass du weißt, wie man das Spiel spielt?', '你确定你会玩这个游戏吗?', '', '', '', ''), + (945, 'suggest_something_toxic', 'It’s like you’re trying to lose on purpose', 0, 0, '', 'Tu sembles vouloir perdre délibérément.', 'als würdest du absichtlich verlieren', '你这操作像是故意想输啊', '', '', '', ''), + (946, 'suggest_something_toxic', 'I’ve seen better plays from level 1 characters', 0, 0, '', 'J’ai vu des meilleures performances de la part de personnages de niveau 1.', 'hab schon bessere Plays von Level-1-Chars gesehen', '我见过1级的菜鸟都比你玩得好', '', '', '', ''), + (947, 'suggest_something_toxic', 'You’re like a speed bump in this game', 0, 0, '', 'Tu es comme un dos d’âne dans ce jeu.', 'du bist hier wie ein Speedbump', '你在这游戏里就是个绊脚石', '', '', '', ''), + (948, 'suggest_something_toxic', 'Maybe you should try a different hobby', 0, 0, '', 'Peut-être que tu devrais essayer une autre passion.', 'vielleicht suchst du dir lieber ein anderes Hobby', '也许你该换个兴趣,游戏不适合你', '', '', '', ''), + (949, 'suggest_something_toxic', 'Wow, your gameplay is like watching paint dry', 0, 0, '', 'Wow, ton gameplay c’est comme regarder de la peinture sécher', 'dein Gameplay ist wie Farbe beim Trocknen zusehen', '哇,你的玩法跟看油漆干一样无聊', '', '', '', ''), + (950, 'suggest_something_toxic', 'I think my alt could carry you through this', 0, 0, '', 'Je pense que mon ALT pourrait te carry', 'mein Twink könnte dich da durch carrien', '我觉得我的小号都能带你飞过这关', '', '', '', ''), + (951, 'suggest_something_toxic', 'Are you sure you’re not trolling us?', 0, 0, '', 'T’est sure que tu nous troll pas ?', 'trollst du uns, oder bist du ernsthaft so?', '你确定你不是在逗我们玩?', '', '', '', ''), + (952, 'suggest_something_toxic', 'I think the enemy team is actually doing better with you on our side', 0, 0, '', 'Je crois que l’équipe adverse se débrouille mieux avec toi dans notre équipe.', 'Ich glaub, das Gegnerteam läuft besser, wenn du bei uns bist', '我觉得有你在我们这边,敌方队伍反而更强', '', '', '', ''), + (953, 'suggest_something_toxic', 'Maybe play the game instead of watching the scenery', 0, 0, '', 'Peut-être vaut-il mieux jouer au jeu au lieu de regarder le paysage.', 'spiel vielleicht mal das Spiel statt nur die Landschaft zu bestaunen', '别老盯着风景看,认真玩游戏吧', '', '', '', ''), + (954, 'suggest_something_toxic', 'I think you missed every single ability there', 0, 0, '', 'Je pense que vous avez manqué chaque capacité là.', 'ich glaube, du hast da jede einzelne Fähigkeit verfehlt', '我觉得你一个技能都没打中', '', '', '', ''), + (955, 'suggest_something_toxic', 'Are you lost or just pretending to be bad?', 0, 0, '', 'T’es perdu ou tu fais simplement semblant d’être mauvais?', 'bist du lost oder stellst du dich nur dumm?', '你是迷路了,还是故意装菜?', '', '', '', ''), + (956, 'suggest_something_toxic', 'I’ve seen level 1 characters take less damage than that', 0, 0, '', 'J’ai vu des personnages de niveau 1 prendre moins de dégâts que ça.', 'Level-1-Chars nehmen weniger Schaden als das', '我见过1级的小号都比你挨的打少', '', '', '', ''), + (957, 'suggest_something_toxic', 'That was embarrassing for both of us', 0, 0, '', 'C’était gênant pour nous deux.', 'das war für uns beide peinlich', '这操作真让我们俩都丢脸', '', '', '', ''), + (958, 'suggest_something_toxic', 'I’ve seen better teamwork in a solo game', 0, 0, '', 'J’ai déjà vu une meilleure coopération dans un jeu solo.', 'Hab schon besseres Teamplay in nem Solo-Game gesehen', '我单人游戏时看到的配合都比这强', '', '', '', ''), + (959, 'suggest_something_toxic', 'I’m just going to stand back and watch you fail', 0, 0, '', 'Je vais juste reculer et te regarder creuver.', 'ich stell mich mal hinten hin und schau dir beim Failen zu', '我就站这儿看你怎么失败', '', '', '', ''), + (960, 'suggest_something_toxic', 'Did you really think that was a good idea?', 0, 0, '', 'Penses-tu vraiment que c’était une bonne idée?', 'dachtest du wirklich, das wäre ne gute Idee?', '你真觉得那是个好主意?', '', '', '', ''), + (961, 'suggest_something_toxic', 'I can’t believe you managed to fail that simple task', 0, 0, '', 'Je n’arrive pas à croire que tu aies échoué à cette tâche si simple.', 'kaum zu glauben, dass du diese simple Aufgabe verkackt hast', '我真不敢相信你连这么简单的任务都能搞砸', '', '', '', ''), + (962, 'suggest_something_toxic', 'Are you here to help, or just to waste time?', 0, 0, '', 'Êtes-vous là pour aider, ou simplement perdre du temps ?', 'bist du hier, um zu helfen, oder nur um Zeit zu verschwenden?', '你是来帮忙的,还是来浪费时间的?', '', '', '', ''), + (963, 'suggest_something_toxic', 'It’s a good thing I’m here to carry this team', 0, 0, '', 'Heureusement que je suis là pour porter cette équipe', 'gut, dass ich da bin, um das Team zu schützen', '还好有我在,不然这队伍早就完了', '', '', '', ''), + (964, 'suggest_something_toxic', 'I swear you make this harder than it needs to be', 0, 0, '', 'Tu sais, parfois tu rends ça plus difficile que nécessaire.', 'ich schwöre, du machst es schwerer als nötig', '我发誓,你把事情搞得比实际难多了', '', '', '', ''), + (965, 'suggest_something_toxic', 'How do you keep failing the simplest things?', 0, 0, '', 'Comment est-ce que je continue à échouer dans les tâches les plus simples?', 'wie schaffst du es, die simpelsten Sachen zu failen?', '你怎么连最简单的事都能搞砸?', '', '', '', ''), + (966, 'suggest_something_toxic', 'Maybe try watching the game, instead of just playing', 0, 0, '', 'Peut-être que tu devrais essayer de regarder le match, au lieu de simplement y jouer.', 'vielleicht mal lieber nur zuschauen, statt das zu tun was du spielen nennst', '也许你该先看看别人怎么玩,而不是瞎搞', '', '', '', ''), + (967, 'suggest_something_toxic', 'You could at least try to pretend you’re good', 0, 0, '', 'Tu pourrais au moins essayer de faire semblant d’être compétent.', 'du könntest dich wenigstens bemühen, so zu tun, als wärst du gut', '你至少可以假装自己很厉害吧', '', '', '', ''), + (968, 'suggest_something_toxic', 'If you keep this up, we’ll be here all day', 0, 0, '', 'Si tu continues comme ça, on va y passer la journée entière.', 'mach so weiter und wir sitzen hier den ganzen Tag', '你再这样下去,我们得在这耗一整天', '', '', '', ''), + (969, 'suggest_something_toxic', 'Honestly, that was the worst decision ever', 0, 0, '', 'Franchement, c’était la pire décision que j’aie jamais prise.', 'ehrlich, das war die schlechteste Entscheidung ever', '老实说,这是我见过的最烂决定', '', '', '', ''), + (970, 'suggest_something_toxic', 'I don’t think you’re cut out for this game', 0, 0, '', 'Je ne pense pas que tu sois fait pour ce jeu.', 'ich glaub, dieses Spiel ist nix für dich', '我觉得你真不适合玩这游戏', '', '', '', ''), + (971, 'suggest_something_toxic', 'What was the point of that move?', 0, 0, '', 'Quel était l’intérêt de ce coup ?', 'was sollte dieser Move?', '你那操作到底想干嘛?', '', '', '', ''), + (972, 'suggest_something_toxic', 'You’re like a burden on this team', 0, 0, '', 'Tu es comme un fardeau pour cette équipe.', 'du bist ne Last fürs Team', '你就是这队伍的累赘', '', '', '', ''), + (973, 'suggest_something_toxic', 'You really think you’re a pro, huh?', 0, 0, '', 'Tu te prends vraiment pour un pro, hein ?', 'du denkst echt, du bist ein Pro, was?', '你真觉得自己是大神啊?', '', '', '', ''), + (974, 'suggest_something_toxic', 'Maybe it’s time to just quit and save us all the trouble', 0, 0, '', 'Il serait temps de déco et nous épargner tes problèmes.', 'vielleicht einfach quitten und uns allen den Stress sparen', '也许你该退出,省得给我们添麻烦', '', '', '', ''), + (975, 'suggest_toxic_links', '%prefix %random_taken_quest_or_item_link', 0, 0, '', '.', '%prefix %random_taken_quest_or_item_link', '', '', '', '', '%prefix %random_taken_quest_or_item_link'), + (976, 'suggest_toxic_links', '%prefix %random_inventory_item_link', 0, 0, '', '', '%prefix %random_inventory_item_link', '', '', '', '', '%prefix %random_inventory_item_link'), + (977, 'thunderfury_spam', '%thunderfury_link', 0, 0, '', '.', '%thunderfury_link', '', '', '', '', '%thunderfury_link'), + (978, 'thunderfury_spam', '%thunderfury_link%thunderfury_link', 0, 0, '', '', '%thunderfury_link%thunderfury_link', '', '', '', '', '%thunderfury_link%thunderfury_link'), + (979, 'thunderfury_spam', '%thunderfury_link%thunderfury_link%thunderfury_link', 0, 0, '', '', '%thunderfury_link%thunderfury_link%thunderfury_link', '', '', '', '', '%thunderfury_link%thunderfury_link%thunderfury_link'), + (980, 'thunderfury_spam', 'I think I just heard %thunderfury_link', 0, 0, '', 'J’ai entendu %thunderfury_link', 'ich glaube, ich hab gerade %thunderfury_link gehört', '我好像刚听到了%thunderfury_link', '', '', '', 'Кажется, я только что услышал %thunderfury_link'), + (981, 'thunderfury_spam', 'I think I heard %thunderfury_link', 0, 0, '', 'Je viens juste d’entendre %thunderfury_link', 'ich glaub, ich habe %thunderfury_link gehört', '我觉得我听到了%thunderfury_link', '', '', '', 'Кажется, я слышал %thunderfury_link'), + (982, 'thunderfury_spam', 'I definitely heard %thunderfury_link', 0, 0, '', 'Je suis sûr d’avoir entendu parler de %thunderfury_link.', 'ich hab definitiv %thunderfury_link gehört', '我确定听到了%thunderfury_link', '', '', '', 'Я точно слышал %thunderfury_link'), + (983, 'thunderfury_spam', 'I dunno but I’m pretty sure I heard %thunderfury_link', 0, 0, '', 'Je ne sais pas, mais je suis presque certain d’avoir entendu %thunderfury_link.', 'kein Plan, aber ich bin ziemlich sicher, das ich %thunderfury_link gehört habe', '我不确定,但我几乎肯定听到了%thunderfury_link', '', '', '', ''), + (984, 'thunderfury_spam', 'Did you just say %thunderfury_link', 0, 0, '', 'Qui vient de parler de %thunderfury_link?', 'hast du gerade %thunderfury_link gesagt', '你刚刚是不是说了%thunderfury_link?', '', '', '', 'Ты только что сказал %thunderfury_link'), + (985, 'thunderfury_spam', 'did someone say %thunderfury_link', 0, 0, '', 'Oh, vous avez mentionné %thunderfury_link ?', 'hat jemand %thunderfury_link gesagt', '有人提到%thunderfury_link了吗?', '', '', '', 'кто-то сказал %thunderfury_link'), + (986, 'thunderfury_spam', 'Did someone say %thunderfury_link ?', 0, 0, '', 'Qui a mentionné %thunderfury_link ?', 'hat jemand %thunderfury_link gesagt?', '有人说了%thunderfury_link吗?', '', '', '', 'Кто-то сказал %thunderfury_link?'), + (987, 'thunderfury_spam', 'Someone said %thunderfury_link', 0, 0, '', 'Quelqu’un a mentionné %thunderfury_link.', 'jemand hat %thunderfury_link gesagt', '有人提到%thunderfury_link了!', '', '', '', 'Кто-то сказал %thunderfury_link'), + (988, 'thunderfury_spam', '%thunderfury_link is coming out of the closet', 0, 0, '', '%thunderfury_link sort du placard', '%thunderfury_link kommt aus dem Schrank', '%thunderfury_link终于现身了!', '', '', '', '%thunderfury_link выходит из шкафа'), + (989, 'thunderfury_spam', 'could swear it was a %thunderfury_link, might have been %thunderfury_link tho', 0, 0, '', 'Je pourrais jurer que c’était un %thunderfury_link, mais ça aurait aussi pu être %thunderfury_link en fait.', 'ich schwöre, es war %thunderfury_link, könnte aber auch %thunderfury_link gewesen sein', '我敢发誓那是%thunderfury_link,不过也可能是%thunderfury_link吧。', '', '', '', 'мог бы поклясться, что это был %thunderfury_link, хотя, может, и %thunderfury_link'), + (990, 'thunderfury_spam', 'Why use %thunderfury_link when %thunderfury_link is clearly way more OP', 0, 0, '', 'Pourquoi utiliser %thunderfury_link quand %thunderfury_link est clairement beaucoup plus puissant.', 'warum %thunderfury_link nutzen, wenn %thunderfury_link offensichtlich viel stärker ist', '为什么用%thunderfury_link,%thunderfury_link明显更强啊!', '', '', '', 'Зачем использовать %thunderfury_link, если %thunderfury_link явно круче'), + (991, 'thunderfury_spam', 'Did I just hear %thunderfury_link?', 0, 0, '', 'Ai-je juste entendu %thunderfury_link?', 'hab ich gerade %thunderfury_link gehört?', '我刚才是听到%thunderfury_link了吗?', '', '', '', ''), + (992, 'thunderfury_spam', 'No way... is that a %thunderfury_link I hear?', 0, 0, '', 'Pas possible... Est-ce que j’entends %thunderfury_link ?', 'niemals... hör ich da echt %thunderfury_link?', '不会吧……我听到了%thunderfury_link?', '', '', '', ''), + (993, 'thunderfury_spam', '%thunderfury_link? Sounds like something only legends wield!', 0, 0, '', '%thunderfury_link? Ça a l’air d’être quelque chose que seuls les légendes manient!', '%thunderfury_link? klingt nach was, das nur Legenden führen!', '%thunderfury_link?听起来只有传说中的英雄才能驾驭!', '', '', '', ''), + (994, 'thunderfury_spam', 'I can feel the power of %thunderfury_link in the air!', 0, 0, '', 'Je peux sentir la puissance de %thunderfury_link dans l’air !', 'ich spüre die Macht von %thunderfury_link in der Luft!', '我能感受到空气中%thunderfury_link的力量!', '', '', '', ''), + (995, 'thunderfury_spam', '%thunderfury_link is the *real* MVP', 0, 0, '', '%thunderfury_link est le *vrai* MVP.', '%thunderfury_link ist der *echte* MVP', '%thunderfury_link才是真正的MVP!', '', '', '', ''), + (996, 'thunderfury_spam', 'Can someone get me a %thunderfury_link? Asking for a friend... maybe even two!', 0, 0, '', 'Est-ce que quelqu’un peut me trouver un %thunderfury_link ? Je demande pour un ami... peut-être même deux !', 'kann mir jemand ne %thunderfury_link besorgen? frage für einen Freund... vielleicht zwei!', '谁能帮我搞把%thunderfury_link?替朋友问的……可能还得两把!', '', '', '', ''), + (997, 'thunderfury_spam', 'Hey, did %thunderfury_link just *smite* something? I swear I felt the ground shake.', 0, 0, '', 'Hey, est-ce que %thunderfury_link vient de *foudroyer* quelque chose ? Je te jure que j’ai senti la terre trembler.', 'hat %thunderfury_link da gerade was *gesmitet*? ich schwöre, der Boden hat gewackelt', '嘿,%thunderfury_link刚是不是劈了什么?我发誓感觉地面都在抖!', '', '', '', ''), + (998, 'thunderfury_spam', '%thunderfury_link on the battlefield... It’s like an instant flex.', 0, 0, '', '%thunderfury_link sur le champ de bataille... C’est comme montrer sa puissance instantanément.', '%thunderfury_link auf dem Schlachtfeld... Das ist purer Flex', '%thunderfury_link在战场上……简直就是瞬间秀肌肉!', '', '', '', ''), + (999, 'thunderfury_spam', 'I *think* I heard %thunderfury_link... Or was that just a myth?', 0, 0, '', '*Je crois* avoir entendu %thunderfury_link... Ou peut-être que c’était juste un mythe?', 'ich *glaube*, ich hab %thunderfury_link gehört... oder war das nur ein Mythos?', '我*好像*听到了%thunderfury_link……还是只是个传说?', '', '', '', ''), + (1000, 'thunderfury_spam', 'Wait, did I just see someone wielding %thunderfury_link?!', 0, 0, '', 'Attends, est-ce que je viens de voir quelqu’un manier %thunderfury_link ?!', 'hab ich gerade jemanden mit %thunderfury_link gesehen?!', '等等,我刚是不是看到有人拿着%thunderfury_link?!', '', '', '', ''), + (1001, 'thunderfury_spam', 'The power of %thunderfury_link is *undeniable*', 0, 0, '', 'Le pouvoir de %thunderfury_link est *indéniable*', 'Die Macht von %thunderfury_link ist *unbestreitbar*', '%thunderfury_link的力量*无可否认*!', '', '', '', ''), + (1002, 'thunderfury_spam', 'I’m 99% sure that was %thunderfury_link. 1% might have been a random NPC, tho.', 0, 0, '', 'Je suis à 99% certain que c’était %thunderfury_link. Le 1% restant aurait pu être un PNJ aléatoire, cependant.', 'Bin zu 99% sicher, das war %thunderfury_link. Zu 1% wars vielleicht nur ein random NPC.', '我99%确定那是%thunderfury_link。不过1%可能是随机NPC吧。', '', '', '', ''), + (1003, 'thunderfury_spam', '%thunderfury_link in action! Who needs an army when you’ve got that kind of power?', 0, 0, '', '%thunderfury_link en action ! Qui a besoin d’une armée lorsqu’on a ce genre de puissance ?', '%thunderfury_link in Aktion! Wer braucht ne Armee, mit so einer Power?', '%thunderfury_link出手了!有这种力量谁还需要军队?', '', '', '', ''), + (1004, 'thunderfury_spam', 'I heard %thunderfury_link is what really brings all the boys to the yard.', 0, 0, '', 'J’ai entendu dire que %thunderfury_link, c’est ce qui attire vraiment tous les garçons dans la cour.', 'Hab gehört, %thunderfury_link lockt alle Jungs an.', '听说%thunderfury_link才是真正吸引大家的东西!', '', '', '', ''), + (1005, 'thunderfury_spam', 'Why even use %thunderfury_link when %thunderfury_link is clearly the superior option?', 0, 0, '', 'Pourquoi même utiliser %thunderfury_link quand %thunderfury_link est clairement l’option supérieure ?', 'Warum überhaupt %thunderfury_link nutzen, wenn %thunderfury_link klar die bessere Option ist?', '干嘛还用%thunderfury_link,%thunderfury_link明显是更好的选择啊!', '', '', '', ''), + (1006, 'thunderfury_spam', 'Is it me or does %thunderfury_link have a “smack talk” passive effect?', 0, 0, '', 'Est-ce moi ou est-ce que %thunderfury_link a un effet passif de provocation ?', 'Bin das nur ich, oder hat %thunderfury_link nen passiven „Trash-Talk“-Effekt?', '是我感觉错了吗,%thunderfury_link是不是自带嘲讽被动效果?', '', '', '', ''), + (1007, 'thunderfury_spam', 'I thought I heard %thunderfury_link, but it was just the sound of sheer awesome.', 0, 0, '', 'J’ai cru entendre %thunderfury_link, mais en fait c’était juste le son de la pure puissance.', 'Dachte, ich hätte %thunderfury_link gehört, war aber nur der Klang reiner Awesomeness.', '我以为听到了%thunderfury_link,结果只是纯粹的震撼之声。', '', '', '', ''), + (1008, 'thunderfury_spam', 'Oh, %thunderfury_link... Where *have* you been all my life?', 0, 0, '', 'Oh, %thunderfury_link ... Où as-tu été tout ce temps, ma belle épée ?', 'Oh, %thunderfury_link... Wo *warst* du mein ganzes Leben?', '哦,%thunderfury_link……我这辈子怎么才遇到你?', '', '', '', ''), + (1009, 'thunderfury_spam', 'Did someone say %thunderfury_link? I think I need to go pick mine up.', 0, 0, '', '%thunderfury_link? Oh là là, j’entends toujours parler de cet incroyable objet! Je crois bien qu’il est temps pour moi d’aller le chercher.', 'Hat jemand %thunderfury_link gesagt? Ich glaub, ich hol mir mal meine.', '有人提到%thunderfury_link了?我觉得我也得去弄一把。', '', '', '', ''), + (1010, 'thunderfury_spam', 'That was definitely the sound of %thunderfury_link! I can feel the epicness.', 0, 0, '', 'C’était certainement le son de %thunderfury_link ! Je peux sentir toute son épopée.', 'Das war definitiv der Sound von %thunderfury_link! Ich spür die Epicness.', '那绝对是%thunderfury_link的声音!我能感受到那份史诗感!', '', '', '', ''), + (1011, 'thunderfury_spam', 'Is it just me, or does %thunderfury_link have a tendency to make everyone else *look* weak?', 0, 0, '', 'C’est moi ou bien %thunderfury_link a tendance à faire paraître les autres faibles?', 'Geht das nur mir so, oder lässt %thunderfury_link alle anderen *schwach aussehen*?', '是我一个人觉得吗,%thunderfury_link让其他人都显得*好弱*?', '', '', '', ''), + (1012, 'thunderfury_spam', 'You know what they say... the louder the %thunderfury_link, the bigger the bragging rights!', 0, 0, '', 'Tu sais ce qu’on dit... plus le %thunderfury_link rugit, plus grand sont les droits de vantardise!', 'Wie man so sagt... je lauter %thunderfury_link, desto größer das Recht zu prahlen!', '都知道……%thunderfury_link声音越大,吹牛的资本就越足!', '', '', '', ''), + (1013, 'suggest_sell', 'WTS %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold.', 'VK: %item_formatted_link für %cost_gold.', '卖%item_formatted_link,只要%cost_gold!', '', 'Quiero vender %item_formatted_link por %cost_gold.', '', 'Продаю %item_formatted_link за %cost_gold.'), + (1014, 'suggest_sell', 'Who wants %item_formatted_link for %cost_gold?', 0, 0, '', 'Qui veut %item_formatted_link pour %cost_gold?', 'Wer will %item_formatted_link für %cost_gold?', '谁想要%item_formatted_link?只要%cost_gold!', '', '¿Quién quiere %item_formatted_link por %cost_gold?', '', 'Кто хочет %item_formatted_link за %cost_gold?'), + (1015, 'suggest_sell', 'Anyone wants %item_formatted_link? Only %cost_gold.', 0, 0, '', 'Est-ce que quelqu’un veut %item_formatted_link ? Seulement %cost_gold.', 'Will wer %item_formatted_link? Nur %cost_gold.', '有人要%item_formatted_link吗?只要%cost_gold。', '', '¿Alguien quiere %item_formatted_link? Solo %cost_gold.', '', 'Кому нужен %item_formatted_link? Всего %cost_gold.'), + (1016, 'suggest_sell', 'Just %cost_gold for %item_formatted_link!', 0, 0, '', 'Juste %cost_gold pour %item_formatted_link!', 'Nur %cost_gold für %item_formatted_link!', '只要%cost_gold就能拿走%item_formatted_link!', '', '¡Solo %cost_gold para %item_formatted_link!', '', 'Всего %cost_gold за %item_formatted_link!'), + (1017, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold.', 'Verkaufe %item_formatted_link für %cost_gold.', '出售%item_formatted_link,价格%cost_gold。', '', 'Vendo %item_formatted_link por %cost_gold.', '', 'Продаю %item_formatted_link за %cost_gold.'), + (1018, 'suggest_sell', '%item_formatted_link is yours just for %cost_gold!', 0, 0, '', '%item_formatted_link est à toi pour seulement %cost_gold !', '%item_formatted_link gehört dir für nur %cost_gold!', '%item_formatted_link归你了,只要%cost_gold!', '', '%item_formatted_link es tuyo solo por %cost_gold!', '', '%item_formatted_link твой всего за %cost_gold!'), + (1019, 'suggest_sell', 'Ridiculus price of %cost_gold for %item_formatted_link!', 0, 0, '', 'Le prix ridicule de %cost_gold pour %item_formatted_link !', 'Lächerlicher Preis: %cost_gold für %item_formatted_link!', '白菜价%cost_gold卖%item_formatted_link!', '', '¡Precio ridículo de %cost_gold para %item_formatted_link_link!', '', 'Смешная цена %cost_gold за %item_formatted_link!'), + (1020, 'suggest_sell', 'Wanna sell %item_formatted_link for %cost_gold.', 0, 0, '', 'Je veux vendre %item_formatted_link pour %cost_gold.', 'Will %item_formatted_link für %cost_gold verkaufen.', '想卖掉%item_formatted_link,价格%cost_gold。', '', 'Quiero vender %item_formatted_link por %cost_gold.', '', 'Хочу продать %item_formatted_link за %cost_gold.'), + (1021, 'suggest_sell', 'Who needs %item_formatted_link? Only %cost_gold.', 0, 0, '', 'Qui a besoin de %item_formatted_link? Seulement %cost_gold.', 'Wer braucht %item_formatted_link? Nur %cost_gold.', '谁需要%item_formatted_link?只要%cost_gold!', '', '¿Quién necesita %item_formatted_link? Solo %cost_gold.', '', 'Кому нужен %item_formatted_link? Всего %cost_gold.'), + (1022, 'suggest_sell', 'Anyone needs %item_formatted_link for %cost_gold?', 0, 0, '', 'Besoin du %item_formatted_link pour %cost_gold?', 'Braucht jemand %item_formatted_link für %cost_gold?', '有人需要%item_formatted_link吗?价格%cost_gold。', '', '¿Alguien necesita %item_formatted_link_link por %cost_gold?', '', 'Кому нужен %item_formatted_link за %cost_gold?'), + (1023, 'suggest_sell', '%cost_gold for %item_formatted_link. Less than AH!', 0, 0, '', '%cost_gold pour %item_formatted_link. Moins cher que l’HD !', '%cost_gold für %item_formatted_link. Günstiger als im AH!', '%item_formatted_link只要%cost_gold,比拍卖行还便宜!', '', '%cost_gold para %item_formatted_link. ¡Menos que en las subastas!', '', '%cost_gold за %item_formatted_link. Дешевле, чем на аукционе!'), + (1024, 'suggest_sell', '%item_formatted_link is expensive, but I’d sell it for %cost_gold.', 0, 0, '', '%item_formatted_link est cher, mais je le vendrais pour %cost_gold.', '%item_formatted_link ist Wertvoll, aber ich würd es für %cost_gold verkaufen.', '%item_formatted_link挺贵的,但我愿意%cost_gold卖给你。', '', '%item_formatted_link es caro, pero lo vendería por %cost_gold.', '', ''), + (1025, 'suggest_sell', 'You’ll never find %item_formatted_link cheaper than %cost_gold!', 0, 0, '', 'Tu ne trouveras jamais %item_formatted_link moins cher que %cost_gold !', 'Billiger als %cost_gold kriegst du %item_formatted_link nicht!', '你绝对找不到比%cost_gold更便宜的%item_formatted_link!', '', '¡Nunca encontrarás %item_formatted_link más barato que %cost_gold!', '', ''), + (1026, 'suggest_sell', 'Need more than %item_formatted_link!', 0, 0, '', 'J’ai besoin de plus de %item_formatted_link !', 'Brauche mehr als %item_formatted_link!', '我需要更多的%item_formatted_link!', '', '¡Necesito más %item_formatted_link!', '', 'Нужно больше, чем %item_formatted_link!'), + (1027, 'suggest_sell', 'I have %item_formatted_link and need more.', 0, 0, '', 'J’ai %item_formatted_link et j’en ai besoin de plus.', 'Habe %item_formatted_link und brauche mehr.', '我有%item_formatted_link,但还需要更多。', '', 'Tengo %item_formatted_link y necesito más. ¿Alguien me vende alguno?', '', 'У меня есть %item_formatted_link и нужно еще.'), + (1028, 'suggest_sell', 'Have %item_formatted_link. Who wants to buy for %cost_gold?', 0, 0, '', 'J’ai %item_formatted_link. Qui veut acheter pour %cost_gold?', 'Habe %item_formatted_link. Wer kauft für %cost_gold?', '我有%item_formatted_link,谁想买?只要%cost_gold!', '', 'Tengo %item_formatted_link. ¿Quién quiere comprarlo por %cost_gold?', '', 'Есть %item_formatted_link. Кто хочет купить за %cost_gold?'), + (1029, 'suggest_sell', 'Anyone WTB %item_formatted_link for %cost_gold?', 0, 0, '', 'Quelqu’un veut acheter %item_formatted_link pour %cost_gold?', 'Will jemand %item_formatted_link für %cost_gold kaufen?', '有人想买%item_formatted_link吗?价格%cost_gold!', '', '¿Alguien compra %item_formatted_link por %cost_gold?', '', 'Кто хочет купить %item_formatted_link за %cost_gold?'), + (1030, 'suggest_sell', 'What about %item_formatted_link? For %cost_gold.', 0, 0, '', 'Qu’en est-il de %item_formatted_link ? Pour %cost_gold.', 'Wie wärs mit %item_formatted_link? Für %cost_gold.', '%item_formatted_link怎么样?只要%cost_gold。', '', '¿Qué pasa con %item_formatted_link? Por el oro.', '', 'Как насчет %item_formatted_link? За %cost_gold.'), + (1031, 'suggest_sell', 'Who said I am a bastard? %item_formatted_link for %cost_gold is a good price.', 0, 0, '', 'Qui a dit que je suis un bâtard ? %item_formatted_link pour %cost_gold, c’est un bon prix.', 'Wer meinte, ich bin ein Bastard? %item_formatted_link für %cost_gold ist ein guter Preis!', '谁说我是坑货?%item_formatted_link卖%cost_gold已经很公道了!', '', '¿Quién ha dicho que soy un rata? %item_formatted_link por %cost_gold es una ganga.', '', 'Кто сказал, что я ублюдок? %item_formatted_link за %cost_gold — хорошая цена.'), + (1032, 'suggest_sell', 'I am selling %item_formatted_link? Just %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link à seulement %cost_gold.', 'Verkaufe %item_formatted_link? Nur %cost_gold.', '我在卖%item_formatted_link,只要%cost_gold!', '', '¿Vendo %item_formatted_link? Solo %cost_gold.', '', 'Продаю %item_formatted_link? Всего %cost_gold.'), + (1033, 'suggest_sell', 'LFG for farming. You can still buy %item_formatted_link I have for %cost_gold.', 0, 0, '', 'Je cherche des gens pour farmer. Tu peux toujours acheter %item_formatted_link que j’ai pour %cost_gold.', 'LFG zum Farmen. %item_formatted_link gibts von mir noch für %cost_gold.', '组队刷怪,有兴趣的可以买我的%item_formatted_link,只要%cost_gold。', '', 'LFG para farmear. Todavía puedes comprar %item_formatted_link que tengo por %cost_gold.', '', 'ЛФГ для фарма. Все еще можешь купить %item_formatted_link у меня за %cost_gold.'), + (1034, 'suggest_sell', 'Sold almost everything today. Still have %item_formatted_link for %cost_gold.', 0, 0, '', 'J’ai presque tout vendu aujourd’hui. Il me reste seulement %item_formatted_link pour %cost_gold.', 'Heute fast alles verkauft. %item_formatted_link hab ich noch für %cost_gold.', '今天几乎都卖光了,还剩%item_formatted_link,卖%cost_gold。', '', 'Se vendió casi todo hoy. Todavía tengo %item_formatted_link por %cost_gold.', '', 'Сегодня почти все продал. Еще есть %item_formatted_link за %cost_gold.'), + (1035, 'suggest_sell', 'What use for trade chat? Of course to sell %item_formatted_link for %cost_gold.', 0, 0, '', 'Pourquoi utiliser le chat de commerce ? Eh bien, évidemment pour vendre mon %item_formatted_link au prix de %cost_gold.', 'Wozu Handelschat? Klar, um %item_formatted_link für %cost_gold zu verkaufen.', '交易频道有什么用?当然是卖我的%item_formatted_link,价格%cost_gold!', '', '¿De qué sirve el chat comercial? Por supuesto, para vender %item_formatted_link por %cost_gold.', '', 'Для чего нужен торговый чат? Конечно, чтобы продавать %item_formatted_link за %cost_gold.'), + (1036, 'suggest_sell', 'Can anyone beat the price of %cost_gold for %item_formatted_link?', 0, 0, '', 'Est-ce que quelqu’un peut battre le prix de %cost_gold pour %item_formatted_link ?', 'Kann jemand den Preis %cost_gold für %item_formatted_link unterbieten?', '有人能比%cost_gold更便宜卖%item_formatted_link吗?', '', '¿Alguien puede superar el precio de %cost_gold por %item_formatted_link?', '', 'Кто перебьет цену %cost_gold за %item_formatted_link?'), + (1037, 'suggest_sell', 'Wanna stop trade chat? Just buy %item_formatted_link? For %cost_gold!', 0, 0, '', 'Tu veux arrêter le chat commercial ? Il suffit d’acheter %item_formatted_link ! Pour seulement %cost_gold !', 'Wollt ihr den Handelschat stoppen? Kauft einfach %item_formatted_link! Für %cost_gold!', '想让交易频道安静?买我的%item_formatted_link吧,只要%cost_gold!', '', '¿Quieres detener el chat comercial? ¿Solo compra %item_formatted_link? ¡Por el oro!', '', 'Хочешь прекратить торговый чат? Просто купи %item_formatted_link за %cost_gold!'), + (1038, 'suggest_sell', 'Everybody spams in trade chat. Me too - %cost_gold for %item_formatted_link!', 0, 0, '', 'Tout le monde spamme dans le chat commercial. Moi aussi - %cost_gold pour %item_formatted_link !', 'Alle spammen im Handelschat. Ich auch - %cost_gold für %item_formatted_link!', '交易频道都在刷屏,我也来——%item_formatted_link卖%cost_gold!', '', 'Todo el mundo envía spam en el chat y yo también! ¡%item_formatted_link por %cost_gold!', '', 'Все спамят в торговом чате. Я тоже — %cost_gold за %item_formatted_link!'), + (1039, 'suggest_sell', 'Is %item_formatted_link any use? Just selling it for %cost_gold.', 0, 0, '', 'Est-ce que %item_formatted_link est utile ? Je le vends juste pour %cost_gold.', 'Taugt %item_formatted_link was? Verkaufe es für %cost_gold.', '%item_formatted_link有用吗?随便卖了,价格%cost_gold。', '', '¿Es %item_formatted_link útil? Solo lo vendo por %cost_gold.', '', 'Есть ли польза от %item_formatted_link? Просто продаю за %cost_gold.'), + (1040, 'suggest_sell', 'I have %item_formatted_link ready to sell you for %cost_gold.', 0, 0, '', 'J’ai %item_formatted_link prêt à te vendre pour %cost_gold.', 'Hab %item_formatted_link, verkaufe an dich für %cost_gold.', '我有%item_formatted_link,可以卖给你,只要%cost_gold。', '', 'Tengo %item_formatted_link listo a la venta por %cost_gold.', '', 'У меня есть %item_formatted_link, готов продать за %cost_gold.'), + (1041, 'suggest_sell', 'Did nothing yesterday but have got %item_formatted_link. Selling it for %cost_gold.', 0, 0, '', 'Je n’ai rien fait hier mais j’ai %item_formatted_link Je le vends pour %cost_gold', 'Gestern nix geschafft, aber %item_formatted_link bekommen. Verkaufe für %cost_gold.', '昨天啥也没干,但搞到了%item_formatted_link,卖%cost_gold。', '', 'No hice nada ayer pero consegu’i %item_formatted_link. Lo vendo por %cost_gold.', '', 'Вчера ничего не делал, но получил %item_formatted_link. Продаю за %cost_gold.'), + (1042, 'suggest_sell', 'Farmed yesterday and got %item_formatted_link. Anyone wtb for %cost_gold?', 0, 0, '', 'J’ai farmé hier et j’ai obtenu %item_formatted_link. Quelqu’un est intéressé pour %cost_gold ?', 'Gestern gefarmt und %item_formatted_link gezogen. Will jemand für %cost_gold kaufen?', '昨天刷怪拿到了%item_formatted_link,有人想买吗?价格%cost_gold。', '', 'Ayer farmee y conseguí mucho %item_formatted_link. ¿Alguien lo quiere comprar por %cost_gold?', '', 'Вчера фармил и получил %item_formatted_link. Кто купит за %cost_gold?'), + (1043, 'suggest_sell', 'Bought %item_formatted_link yesterday. Anyone needs it for %cost_gold?', 0, 0, '', 'J’ai acheté %item_formatted_link hier. Est-ce que quelqu’un en a besoin pour %cost_gold?', 'Gestern %item_formatted_link gekauft. Brauchts wer für %cost_gold?', '昨天买了%item_formatted_link,有人需要吗?只要%cost_gold。', '', 'Ayer compré %item_formatted_link. ¿Alguien lo quiere por %cost_gold?', '', 'Вчера купил %item_formatted_link. Кому нужно за %cost_gold?'), + (1044, 'suggest_sell', 'Who asked for %item_formatted_link? The price is the same - %cost_gold.', 0, 0, '', 'Qui a demandé %item_formatted_link? Le prix est le même - %cost_gold.', 'Wer hatte %item_formatted_link gesucht? Preis bleibt - %cost_gold.', '谁问%item_formatted_link的?价格没变,还是%cost_gold。', '', '¿Quién pidió %item_formatted_link? El precio es el mismo: %cost_gold.', '', 'Кто спрашивал про %item_formatted_link? Цена та же — %cost_gold.'), + (1045, 'suggest_sell', 'I sill have %item_formatted_link. WTB for %cost_gold?', 0, 0, '', 'J’ai toujours %item_formatted_link. Je cherche à l’acheter pour %cost_gold.', 'Hab immer noch %item_formatted_link. Kauft wer für %cost_gold?', '我还有%item_formatted_link,有人想买吗?价格%cost_gold。', '', 'Todavía tengo %item_formatted_link. ¿Alguien lo compra por %cost_gold?', '', 'У меня еще есть %item_formatted_link. Купишь за %cost_gold?'), + (1046, 'suggest_sell', 'I used to have more than %item_formatted_link. Now needs to sell it for %cost_gold.', 0, 0, '', 'J’avais plus de %item_formatted_link avant. Maintenant, je dois le vendre pour %cost_gold.', 'Früher war ich besser bei Kasse. Jetzt verkauf ich %item_formatted_link für %cost_gold.', '我以前有好多%item_formatted_link,现在得卖了,价格%cost_gold。', '', 'Solía tener más de %item_formatted_link. Ahora necesito venderlo por %cost_gold.', '', 'Раньше было больше, чем %item_formatted_link. Теперь нужно продать за %cost_gold.'), + (1047, 'suggest_sell', 'I wish I have more than %item_formatted_link. You could buy it for %cost_gold anyways.', 0, 0, '', 'J’aimerais avoir plus que %item_formatted_link. De toute façon, tu pourrais l’acheter pour %cost_gold.', 'Ich Wünschte, ich hätte mehr als %item_formatted_link. Kaufen könnt ihrs trotzdem für %cost_gold.', '我希望能有更多%item_formatted_link,反正你可以先买这个,价格%cost_gold。', '', 'Desearía tener más de %item_formatted_link. Podrías comprarlo por %cost_gold de todos modos.', '', 'Хотел бы иметь больше, чем %item_formatted_link. Но можешь купить за %cost_gold.'), + (1048, 'suggest_sell', 'What use for your gold? To buy my %item_formatted_link for %cost_gold.', 0, 0, '', 'Que vais-je faire de ton or ? Acheter mon %item_formatted_link pour %cost_gold.', 'Wozu ist euer Gold gut? Um mein %item_formatted_link für %cost_gold zu kaufen.', '你的金币有啥用?当然是买我的%item_formatted_link啦,价格%cost_gold。', '', '¿De qué sirve tu oro? Para comprar mi %item_formatted_link por %cost_gold.', '', 'Для чего твое золото? Чтобы купить мой %item_formatted_link за %cost_gold.'), + (1049, 'suggest_sell', 'Please spare some gold for me. You can buy %item_formatted_link for %cost_gold.', 0, 0, '', 'S’il vous plaît, épargnez-moi de l’or. Vous pouvez acheter %item_formatted_link pour %cost_gold', 'Gönnt mir etwas Gold. Kauft %item_formatted_link für %cost_gold.', '行行好,给我点金币吧。你可以花 %cost_gold 买下 %item_formatted_link。', '', 'Por favor, dame algo de oro. Puedes comprar %item_formatted_link por %cost_gold.', '', 'Пожалуйста, подкинь немного золота. Можешь купить %item_formatted_link за %cost_gold.'), + (1050, 'suggest_sell', 'Is %cost_gold is a good price for %item_formatted_link?', 0, 0, '', 'Est-ce que %cost_gold, c’est un bon prix pour %item_formatted_link?', 'Sind %cost_gold ein guter Preis für %item_formatted_link?', '%cost_gold 买 %item_formatted_link,这个价格咋样?', '', '¿%cost_gold es un buen precio para %item_formatted_link?', '', '%cost_gold — хорошая цена за %item_formatted_link?'), + (1051, 'suggest_sell', 'Just bought yesterday %item_formatted_links, but do not need it anymore. Anyone wants for %cost_gold?', 0, 0, '', 'Hier, j’ai acheté %item_formatted_links, mais je n’en ai plus besoin. Est-ce que quelqu’un le veut pour %cost_gold?', 'Hab gestern erst %item_formatted_link gekauft, brauch es aber nicht mehr. Will wer für %cost_gold?', '昨天刚买的 %item_formatted_links,现在用不上了,有人要吗?只要 %cost_gold!', '', 'Ayer compré %item_formatted_link, pero ya no los necesito. ¿Alguien lo quiere por %cost_gold?', '', 'Вчера купил %item_formatted_links, но больше не нужно. Кому за %cost_gold?'), + (1052, 'suggest_sell', 'I am going to post %item_formatted_link on the AH but you can buy it now cheaper just for %cost_gold.', 0, 0, '', 'Je vais mettre en vente %item_formatted_link sur l’HV, mais tu peux l’acheter maintenant à un prix réduit, pour seulement %cost_gold.', 'Stell %item_formatted_link gleich ins AH, aber ihr könnts jetzt günstiger haben: nur %cost_gold.', '我正打算把 %item_formatted_link 挂拍卖行,但你现在就可以便宜买走,只要 %cost_gold!', '', 'Voy a poner %item_formatted_link en la casa de subastas pero puedes comprarlo ahora más barato solo por %cost_gold.', '', 'Собирался выставить %item_formatted_link на аукцион, но можешь купить дешевле сейчас за %cost_gold.'), + (1053, 'suggest_sell', 'Why the #!@ have I bought %item_formatted_link? Anyone needs it for %cost_gold?', 0, 0, '', 'Pourquoi diable ai-je acheté %item_formatted_link? Est-ce que quelqu’un en a besoin pour %cost_gold?', 'Warum zum #!@ hab ich %item_formatted_link gekauft? Brauchts wer für %cost_gold?', '我咋就脑抽买了 %item_formatted_link 呢?有人需要吗?只要 %cost_gold!', '', '¿Por qué #!@ he comprado %item_formatted_link? ¿Alguien lo necesita por %cost_gold?', '', 'Зачем, черт возьми, я купил %item_formatted_link? Кому нужно за %cost_gold?'), + (1054, 'suggest_sell', 'WTS %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold.', 'VK: %item_formatted_link für %cost_gold.', '出售 %item_formatted_link,只要 %cost_gold!', '', 'Quiero vender %item_formatted_link por %cost_gold.', '', 'Продаю %item_formatted_link за %cost_gold.'), + (1055, 'suggest_sell', 'Who wants %item_formatted_link for %cost_gold?', 0, 0, '', 'Qui veut %item_formatted_link pour %cost_gold ?', 'Wer will %item_formatted_link für %cost_gold?', '谁想要 %item_formatted_link?只要 %cost_gold!', '', '¿Quién quiere %item_formatted_link por %cost_gold?', '', 'Кто хочет %item_formatted_link за %cost_gold?'), + (1056, 'suggest_sell', 'Anyone wants %item_formatted_link? Only %cost_gold.', 0, 0, '', 'Qui veut %item_formatted_link ? Seulement %cost_gold.', 'Will wer %item_formatted_link? Nur %cost_gold.', '有人要 %item_formatted_link 吗?只卖 %cost_gold!', '', '¿Alguien quiere %item_formatted_link? Solo %cost_gold.', '', 'Кому нужен %item_formatted_link? Всего %cost_gold.'), + (1057, 'suggest_sell', 'Just %cost_gold for %item_formatted_link!', 0, 0, '', 'Juste %cost_gold pour %item_formatted_link !', 'Nur %cost_gold für %item_formatted_link!', '只要 %cost_gold 就能拿下 %item_formatted_link!', '', '¡Solo %cost_gold para %item_formatted_link!', '', 'Всего %cost_gold за %item_formatted_link!'), + (1058, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold.', 0, 0, '', 'Je vends cet %item_formatted_link pour %cost_gold.', 'Verkaufe %item_formatted_link für %cost_gold.', '卖 %item_formatted_link,只需 %cost_gold!', '', 'Vendo %item_formatted_link por %cost_gold.', '', 'Продаю %item_formatted_link за %cost_gold.'), + (1059, 'suggest_sell', '%item_formatted_link is yours just for %cost_gold!', 0, 0, '', '%item_formatted_link est à toi pour seulement %cost_gold!', '%item_formatted_link gehört dir für nur %cost_gold!', '%item_formatted_link 只要 %cost_gold 就是你的了!', '', '%item_formatted_link es tuyo solo por %cost_gold!', '', '%item_formatted_link твой всего за %cost_gold!'), + (1060, 'suggest_sell', 'WTS %item_formatted_link for %cost_gold. DM if interested.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold en or. Envoyez-moi un message si vous êtes intéressé(e).', 'VK: %item_formatted_link für %cost_gold. flüster mir bei Interesse.', '卖 %item_formatted_link,价格 %cost_gold,有意私聊!', '', 'Quiero vender %item_formatted_link por %cost_gold. Envíame un mensaje si te interesa.', '', ''), + (1061, 'suggest_sell', 'Need to sell %item_formatted_link, %cost_gold or best offer.', 0, 0, '', 'Je dois vendre %item_formatted_link, pour %cost_gold ou meilleure offre.', 'Muss %item_formatted_link loswerden, %cost_gold oder bestes Angebot.', '急卖 %item_formatted_link,标价 %cost_gold,可议价!', '', 'Necesito vender %item_formatted_link, %cost_gold o la mejor oferta.', '', ''), + (1062, 'suggest_sell', '%item_formatted_link available for %cost_gold. Get it now!', 0, 0, '', '%item_formatted_link disponible pour %cost_gold. Tu devrais le prendre maintenant!', '%item_formatted_link für %cost_gold verfügbar. Schlagt zu!', '%item_formatted_link 只要 %cost_gold,赶紧入手吧!', '', '%item_formatted_link disponible por %cost_gold. ¡Consíguelo ahora!', '', ''), + (1063, 'suggest_sell', 'Got %item_formatted_link, asking %cost_gold.', 0, 0, '', 'J’ai trouvé %item_formatted_link, et on me demande %cost_gold.', 'Habe %item_formatted_link, will %cost_gold.', '有 %item_formatted_link,卖 %cost_gold。', '', 'Tengo %item_formatted_link, pido %cost_gold.', '', ''), + (1064, 'suggest_sell', 'Selling %item_formatted_link, only %cost_gold left.', 0, 0, '', 'Je vends %item_formatted_link, il ne reste que %cost_gold.', 'Verkaufe %item_formatted_link, nur %cost_gold.', '卖 %item_formatted_link,剩最后一件,只要 %cost_gold!', '', 'Vendo %item_formatted_link, solo queda %cost_gold.', '', ''), + (1065, 'suggest_sell', 'Anyone want %item_formatted_link for %cost_gold? DM me!', 0, 0, '', '%Item_formatted_link à %cost_gold, ça vous tente ? Envoyez-moi un message en privé !', 'Will jemand %item_formatted_link für %cost_gold? flüster mir!', '有人要 %item_formatted_link 吗?只要 %cost_gold,私聊我!', '', '¿Alguien quiere %item_formatted_link por %cost_gold? ¡Envíame un mensaje!', '', ''), + (1066, 'suggest_sell', 'Selling %item_formatted_link at %cost_gold, grab it while you can!', 0, 0, '', 'Je vends %item_formatted_link à %cost_gold, dépêchez-vous de le saisir avant qu’il ne soit trop tard!', 'Verkaufe %item_formatted_link für %cost_gold, greift zu solange es geht!', '卖 %item_formatted_link,只要 %cost_gold,欲购从速!', '', 'Vendo %item_formatted_link a %cost_gold, ¡agárralo mientras puedas!', '', ''), + (1067, 'suggest_sell', '%item_formatted_link for %cost_gold, hurry up before it’s gone!', 0, 0, '', '%item_formatted_link pour %cost_gold, dépêchez-vous avant qu’il ne soit trop tard!', '%item_formatted_link für %cost_gold, schnell sein bevor es weg ist!', '%item_formatted_link 只要 %cost_gold,快来买,不然就没了!', '', '%item_formatted_link por %cost_gold, ¡apúrate antes de que se vaya!', '', ''), + (1068, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, who’s in?', 0, 0, '', 'Qui veut acheter %item_formatted_link pour %cost_gold ?', 'Verkaufe %item_formatted_link für %cost_gold, wer ist dabei?', '卖 %item_formatted_link,只要 %cost_gold,谁来?', '', 'Vendo %item_formatted_link por %cost_gold, ¿quién está dentro?', '', ''), + (1069, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, first come, first serve!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, premier arrivé, premier servi !', 'Verkaufe %item_formatted_link für %cost_gold, wer zuerst kommt wird auch zuerst bedient!', '卖 %item_formatted_link,价格 %cost_gold,先到先得!', '', 'Vendo %item_formatted_link por %cost_gold, ¡el primero que llega se lo lleva!', '', ''), + (1070, 'suggest_sell', 'Only %cost_gold for %item_formatted_link, limited time offer!', 0, 0, '', 'Seulement %cost_gold pour %item_formatted_link, une offre à durée limitée!', 'Nur %cost_gold für %item_formatted_link, nur für kurze Zeit!', '仅需 %cost_gold 就能买到 %item_formatted_link,限时优惠!', '', 'Solo %cost_gold para %item_formatted_link, ¡oferta por tiempo limitado!', '', ''), + (1071, 'suggest_sell', '%item_formatted_link available for %cost_gold, hit me up if you need it!', 0, 0, '', 'Lien pour %item_formatted_link disponible pour %cost_gold en or, n’hésite pas à me contacter si tu en as besoin ! ', '%item_formatted_link für %cost_gold verfügbar, meldet euch wenn ihrs braucht!', '%item_formatted_link 卖 %cost_gold,需要的联系我!', '', '%item_formatted_link disponible por %cost_gold, ¡contáctame si lo necesitas!', '', ''), + (1072, 'suggest_sell', 'Anyone interested in %item_formatted_link for %cost_gold?', 0, 0, '', 'Quelqu’un est intéressé par %item_formatted_link à %cost_gold ?', 'Interesse an %item_formatted_link für %cost_gold?', '有人对 %item_formatted_link 感兴趣吗?只要 %cost_gold!', '', '¿Alguien interesado en %item_formatted_link por %cost_gold?', '', ''), + (1073, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, need to clear inventory.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, je dois vider mon inventaire.', 'Verkaufe %item_formatted_link für %cost_gold, muss meine Taschen leeren.', '卖 %item_formatted_link,价格 %cost_gold,急清背包!', '', 'Vendo %item_formatted_link por %cost_gold, necesito vaciar mi inventario.', '', ''), + (1074, 'suggest_sell', 'Get %item_formatted_link for %cost_gold, it’s a great deal!', 0, 0, '', 'Procurez-vous %item_formatted_link pour %cost_gold, c’est une bonne affaire !', 'Hol dir %item_formatted_link für %cost_gold, Top-Deal!', '买 %item_formatted_link 只要 %cost_gold,超值啊!', '', 'Consigue %item_formatted_link por %cost_gold, ¡es una gran oferta!', '', ''), + (1075, 'suggest_sell', '%item_formatted_link for %cost_gold, get it while it’s hot!', 0, 0, '', 'Fais-toi plaisir avec %item_formatted_link pour %cost_gold, c’est le moment ou jamais!', '%item_formatted_link für %cost_gold, greift zu solange der Vorrat reicht!', '%item_formatted_link 只要 %cost_gold,趁热快买!', '', '%item_formatted_link por %cost_gold, ¡cómpralo mientras esté caliente!', '', ''), + (1076, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, don’t miss out!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, ne ratez pas cette occasion!', 'Verkaufe %item_formatted_link für %cost_gold, nicht verpassen!', '卖 %item_formatted_link,价格 %cost_gold,别错过!', '', 'Vendo %item_formatted_link por %cost_gold, ¡no te lo pierdas!', '', ''), + (1077, 'suggest_sell', 'Only %cost_gold for %item_formatted_link, hit me up for details!', 0, 0, '', 'Seulement %cost_gold pour %item_formatted_link, n’hésitez pas à me contacter pour plus de détails !', 'Nur %cost_gold für %item_formatted_link, flüster mir für Details!', '仅 %cost_gold 就能买 %item_formatted_link,详情私聊!', '', 'Solo %cost_gold para %item_formatted_link, ¡contáctame para más detalles!', '', ''), + (1078, 'suggest_sell', '%item_formatted_link is available for %cost_gold, let’s trade!', 0, 0, '', '%item_formatted_link est disponible pour %cost_gold, ça te dirait de faire un échange?', '%item_formatted_link gibts für %cost_gold, lasst uns handeln!', '%item_formatted_link 卖 %cost_gold,来交易吧!', '', '%item_formatted_link está disponible por %cost_gold, ¡hagamos un intercambio!', '', ''), + (1079, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, message me for more info.', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, n’hésitez pas à me contacter pour plus d’informations.', 'Verkaufe %item_formatted_link für %cost_gold, flüster mir für Infos.', '卖 %item_formatted_link,价格 %cost_gold,更多信息私聊。', '', 'Vendo %item_formatted_link por %cost_gold, envíame un mensaje para más información.', '', ''), + (1080, 'suggest_sell', 'Looking to sell %item_formatted_link, %cost_gold takes it!', 0, 0, '', 'Je cherche à vendre %item_formatted_link, et je le laisse pour seulement %cost_gold !', 'Will %item_formatted_link verkaufen, für %cost_gold gehört es dir!', '想卖 %item_formatted_link,%cost_gold 就能拿走!', '', 'Busco vender %item_formatted_link, ¡%cost_gold lo consigue!', '', ''), + (1081, 'suggest_sell', 'I’m selling %item_formatted_link, %cost_gold only.', 0, 0, '', 'Je vends %item_formatted_link, seulement %cost_gold.', 'Verkaufe %item_formatted_link, nur %cost_gold.', '我在卖 %item_formatted_link,只要 %cost_gold。', '', 'Estoy vendiendo %item_formatted_link, solo %cost_gold.', '', ''), + (1082, 'suggest_sell', 'Got %item_formatted_link for %cost_gold, let’s make a deal!', 0, 0, '', 'J’ai mis la main sur %item_formatted_link pour %cost_gold pièces d’or, ça t’intéresse de conclure un marché avec moi ?', 'Hab %item_formatted_link für %cost_gold, machen wir nen Deal!', '有 %item_formatted_link,卖 %cost_gold,来谈个价吧!', '', 'Tengo %item_formatted_link por %cost_gold, ¡hagamos un trato!', '', ''), + (1083, 'suggest_sell', 'Selling %item_formatted_link, %cost_gold takes it home!', 0, 0, '', 'Je vends %item_formatted_link, %cost_gold et c’est à toi de le ramener chez toi !', 'Verkaufe %item_formatted_link, für %cost_gold nimmst du es mit!', '卖 %item_formatted_link,%cost_gold 就能带回家!', '', 'Vendo %item_formatted_link, ¡%cost_gold lo lleva a casa!', '', ''), + (1084, 'suggest_sell', 'WTS %item_formatted_link for %cost_gold, anyone interested?', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, ça intéresse quelqu’un ?', 'VK: %item_formatted_link für %cost_gold, Interesse?', '出售 %item_formatted_link,价格 %cost_gold,有人感兴趣吗?', '', 'Vendo %item_formatted_link por %cost_gold, ¿alguien interesado?', '', ''), + (1085, 'suggest_sell', 'Need to sell %item_formatted_link fast, %cost_gold.', 0, 0, '', 'Besoin de vendre %item_formatted_link rapidement, %cost_gold.', 'Muss %item_formatted_link schnell loswerden, %cost_gold.', '急卖 %item_formatted_link,只要 %cost_gold!', '', 'Necesito vender %item_formatted_link rápido, %cost_gold.', '', ''), + (1086, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, best price around!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, c’est le meilleur prix du coin !', 'Verkaufe %item_formatted_link für %cost_gold, bester Preis weit und breit!', '卖 %item_formatted_link,价格 %cost_gold,绝对是最低价!', '', 'Vendo %item_formatted_link por %cost_gold, ¡el mejor precio!', '', ''), + (1087, 'suggest_sell', '%item_formatted_link for %cost_gold, who’s buying?', 0, 0, '', '%item_formatted_link pour %cost_gold, qui est intéressé(e) ?', '%item_formatted_link für %cost_gold, wer kauft?', '%item_formatted_link 卖 %cost_gold,谁来买?', '', '%item_formatted_link por %cost_gold, ¿quién compra?', '', ''), + (1088, 'suggest_sell', '%item_formatted_link for %cost_gold, let’s make it happen!', 0, 0, '', '%item_formatted_link à %cost_gold, on se lance !', '%item_formatted_link für %cost_gold, machen wirs fix!', '%item_formatted_link 只要 %cost_gold,成交吧!', '', '%item_formatted_link por %cost_gold, ¡hagámoslo realidad!', '', ''), + (1089, 'suggest_sell', 'Selling %item_formatted_link for %cost_gold, grab it before it’s gone!', 0, 0, '', 'Je vends %item_formatted_link pour %cost_gold, dépêchez-vous avant qu’il ne soit trop tard !', 'Verkaufe %item_formatted_link für %cost_gold, schnappt zu bevor alles weg ist!', '卖 %item_formatted_link,价格 %cost_gold,赶紧买,不然就没了!', '', 'Vendo %item_formatted_link por %cost_gold, ¡agárralo antes de que se vaya!', '', ''), + (1090, 'suggest_sell', 'Want %item_formatted_link? %cost_gold gets you one!', 0, 0, '', 'Tu veux %item_formatted_link? Ça te coûte %cost_gold pour en obtenir un !', 'Willst %item_formatted_link? Für %cost_gold kriegst du eins!', '想要 %item_formatted_link 吗?%cost_gold 就能买到!', '', '¿Quieres %item_formatted_link? ¡%cost_gold te consigue uno!', '', ''), + (1091, 'suggest_sell', '%item_formatted_link for %cost_gold, quick sale!', 0, 0, '', '%item_formatted_link en vente à %cost_gold, une super affaire à saisir rapidement !', '%item_formatted_link für %cost_gold, schneller Verkauf!', '%item_formatted_link 卖 %cost_gold,速卖!', '', '%item_formatted_link por %cost_gold, ¡venta rápida!', '', ''), + (1092, 'response_lfg_quests_channel', 'I have %quest_links', 0, 0, '', 'J’ai %quest_links', 'Ich habe %quest_links', '我有 %quest_links。', '', '', '', 'У меня есть %quest_links'), + (1093, 'response_lfg_quests_channel', 'I also have %quest_links', 0, 0, '', 'J’ai aussi des %quest_links.', 'Ich habe auch %quest_links', '我也有 %quest_links。', '', '', '', 'У меня тоже есть %quest_links'), + (1094, 'response_lfg_quests_channel', 'I also have %quest_links, I am in %zone_name right now', 0, 0, '', 'J’ai aussi des %quest_links, je suis actuellement dans la zone %zone_name', 'Ich habe auch %quest_links, bin gerade in %zone_name', '我也有 %quest_links,现在在 %zone_name。', '', '', '', 'У меня тоже есть %quest_links, сейчас я в %zone_name'), + (1095, 'response_lfg_quests_channel', '%other_name, I also have %quest_links', 0, 0, '', 'Oh, j’ai aussi %quest_links', '%other_name, ich habe auch %quest_links', '%other_name,我也有 %quest_links。', '', '', '', '%other_name, у меня тоже есть %quest_links'), + (1096, 'response_lfg_quests_channel', '%other_name, I also have %quest_links, I am in %zone_name right now', 0, 0, '', '%other_name, je possède également %quest_links, je me trouve actuellement dans la zone %zone_name.', '%other_name, ich habe auch %quest_links, bin gerade in %zone_name', '%other_name,我也有 %quest_links,现在在 %zone_name。', '', '', '', '%other_name, у меня тоже есть %quest_links, сейчас я в %zone_name'), + (1097, 'response_lfg_quests_channel', 'I am up for %quest_links, I am in %zone_name right now', 0, 0, '', 'Je suis partant pour %quest_links, je suis actuellement dans la zone de %zone_name.', 'Bin bereit für %quest_links, bin gerade in %zone_name', '我可以做 %quest_links,现在在 %zone_name。', '', '', '', 'Я готов к %quest_links, сейчас я в %zone_name'), + (1098, 'response_lfg_quests_channel', 'I am up for %quest_links, I am %my_role', 0, 0, '', 'Je suis prêt pour %quest_links, je suis un %my_role.', 'Bin bereit für %quest_links, bin %my_role', '我可以做 %quest_links,我是 %my_role。', '', '', '', 'Я готов к %quest_links, я %my_role'), + (1099, 'response_lfg_quests_channel', '%other_name, I am up for %quest_links, I am in %zone_name right now', 0, 0, '', '%other_name, je suis prêt pour %liens_quête, je suis actuellement à %nom_zone', '%other_name, bin bereit für %quest_links, bin gerade in %zone_name', '%other_name,我可以做 %quest_links,现在在 %zone_name。', '', '', '', '%other_name, я готов к %quest_links, сейчас я в %zone_name'), + (1100, 'response_lfg_quests_channel', '%other_name, I am up for %quest_links, I am %my_role', 0, 0, '', '%other_name, je suis partant pour %quest_links, je suis %mon rôle', '%other_name, bin bereit für %quest_links, bin %my_role', '%other_name,我可以做 %quest_links,我是 %my_role。', '', '', '', '%other_name, я готов к %quest_links, я %my_role'), + (1101, 'response_lfg_quests_channel', 'Anyone need %quest_links? I’m in %zone_name.', 0, 0, '', 'Besoin de %quest_links? Je me trouve dans %zone_name.', 'Braucht wer %quest_links? Bin in %zone_name.', '有人需要做 %quest_links 吗?我在 %zone_name。', '', '', '', ''), + (1102, 'response_lfg_quests_channel', 'Looking for more for %quest_links in %zone_name.', 0, 0, '', 'À la recherche de plus de lieux pour %quest_links dans %zone_name.', 'Suche mehr %quest_links in %zone_name.', '找人一起在 %zone_name 做 %quest_links。', '', '', '', ''), + (1103, 'response_lfg_quests_channel', 'Need some help with %quest_links in %zone_name.', 0, 0, '', 'Besoin d’aide avec les %quest_links dans %zone_name.', 'Brauche Hilfe bei %quest_links in %zone_name.', '在 %zone_name 做 %quest_links 需要帮忙。', '', '', '', ''), + (1104, 'response_lfg_quests_channel', 'Anyone want to do %quest_links with me in %zone_name?', 0, 0, '', 'Qui veut faire des %quest_links avec moi dans la zone %zone_name?', 'Will jemand %quest_links mit mir in %zone_name machen?', '有人想和我一起在 %zone_name 做 %quest_links 吗?', '', '', '', ''), + (1105, 'response_lfg_quests_channel', 'I have %quest_links, looking for group in %zone_name.', 0, 0, '', 'J’ai %quest_links, je cherche un groupe dans la zone %zone_name.', 'Habe %quest_links, suche Gruppe in %zone_name.', '我有 %quest_links,在 %zone_name 找队伍。', '', '', '', ''), + (1106, 'response_lfg_quests_channel', 'Who needs %quest_links in %zone_name?', 0, 0, '', 'Qui a besoin de %quest_links dans %zone_name ?', 'Wer braucht %quest_links in %zone_name?', '谁在 %zone_name 需要做 %quest_links?', '', '', '', ''), + (1107, 'response_lfg_quests_channel', 'Up for %quest_links in %zone_name, who’s in?', 0, 0, '', 'Qui est partant pour des %quest_links à %zone_name ?', 'Bereit für %quest_links in %zone_name, wer ist dabei?', '准备在 %zone_name 做 %quest_links,谁要一起?', '', '', '', ''), + (1108, 'response_lfg_quests_channel', '%my_role looking for group to complete %quest_links in %zone_name.', 0, 0, '', 'Je cherche un groupe pour terminer %quest_links dans la zone %zone_name. Qui est partant?', '%my_role sucht Gruppe, um %quest_links in %zone_name zu erledigen.', '我是 %my_role,找队伍在 %zone_name 完成 %quest_links。', '', '', '', ''), + (1109, 'response_lfg_quests_channel', 'I need %quest_links, anyone available in %zone_name?', 0, 0, '', 'J’ai besoin de %quest_links, est-ce qu’il y a quelqu’un disponible dans la région de %zone_name?', 'Ich brauche %quest_links, ist jemand in %zone_name frei?', '我需要做 %quest_links,有人在 %zone_name 能帮忙吗?', '', '', '', ''), + (1110, 'response_lfg_quests_channel', 'Looking for more people for %quest_links in %zone_name.', 0, 0, '', 'Je recherche plus de personnes pour %quest_links dans la région de %zone_name.', 'Suche mehr Leute für %quest_links in %zone_name.', '在 %zone_name 找更多人一起做 %quest_links。', '', '', '', ''), + (1111, 'response_lfg_quests_channel', 'I’m doing %quest_links in %zone_name, need more help.', 0, 0, '', 'Je suis en train de faire des %quest_links dans %zone_name, j’aurais besoin de davantage d’aide.', 'Mach %quest_links in %zone_name, brauche mehr Hilfe.', '我正在 %zone_name 做 %quest_links,还需要人手帮忙。', '', '', '', ''), + (1112, 'response_lfg_quests_channel', 'Any %my_role interested in %quest_links in %zone_name?', 0, 0, '', 'Des %my_role intéressés par des %quest_links dans la zone %zone_name?', 'Gibt’s %my_role mit Interesse an %quest_links in %zone_name?', '有 %my_role 对 %zone_name 的 %quest_links 感兴趣吗?', '', '', '', ''), + (1113, 'response_lfg_quests_channel', 'Help needed for %quest_links in %zone_name, looking for more.', 0, 0, '', 'J’ai besoin d’aide pour %quest_links dans %zone_name, j’en cherche davantage.', 'Hilfe benötigt für %quest_links in %zone_name, mir ist langweilig.', '在 %zone_name 做 %quest_links 需要帮忙,找更多人一起!', '', '', '', ''), + (1114, 'response_lfg_quests_channel', 'I have %quest_links, %my_role in %zone_name, who’s in?', 0, 0, '', 'J’ai %quest_links, en tant que %my_role dans %zone_name, qui est partant?', 'Habe %quest_links, %my_role in %zone_name, wer ist dabei?', '我在 %zone_name 有 %quest_links,我是 %my_role,谁想加入?', '', '', '', ''), + (1115, 'response_lfg_quests_channel', 'Who wants to do %quest_links in %zone_name? I am %my_role.', 0, 0, '', 'Qui veut faire des %quest_links dans la zone %zone_name? Je suis %my_role.', 'Wer will %quest_links in %zone_name machen? Ich bin %my_role.', '谁想在 %zone_name 做 %quest_links?我是 %my_role。', '', '', '', ''), + (1116, 'response_lfg_quests_channel', 'Anyone doing %quest_links right now? I’m in %zone_name.', 0, 0, '', 'Est-ce que quelqu’un est en train de faire des %quest_links en ce moment ? Je suis dans la zone %zone_name.', 'Macht grad jemand %quest_links? Bin in %zone_name.', '有人现在在做 %quest_links 吗?我在 %zone_name。', '', '', '', ''), + (1117, 'response_lfg_quests_channel', '%quest_links needed in %zone_name, any takers?', 0, 0, '', ' %quest_links est nécessaire dans la zone %zone_name, des volontaires ?', '%quest_links offen in %zone_name, wer möchte?', '在 %zone_name 需要完成 %quest_links,有人愿意加入吗?', '', '', '', ''), + (1118, 'response_lfg_quests_channel', 'Up for %quest_links in %zone_name, looking for group.', 0, 0, '', 'À la recherche de %quest_links dans %zone_name, je suis à la recherche d’un groupe.', 'Bereit für %quest_links in %zone_name, suche Gruppe.', '想在 %zone_name 做 %quest_links,找队伍一起!', '', '', '', ''), + (1119, 'response_lfg_quests_channel', 'I need %quest_links and some company, %zone_name anyone?', 0, 0, '', 'J’ai besoin de %quest_links et de compagnie, %zone_name quelqu’un ?', 'Brauche %quest_links und Gesellschaft, jemand in %zone_name?', '我需要完成 %quest_links,想找人一起,%zone_name 有没有人?', '', '', '', ''), + (1120, 'response_lfg_quests_channel', 'Who wants to join me in %zone_name for %quest_links?', 0, 0, '', 'Qui veut me rejoindre dans la %zone_name pour %quest_links ?', 'Wer kommt mit mir nach %zone_name für %quest_links?', '谁想在 %zone_name 跟我一起做 %quest_links?', '', '', '', ''), + (1121, 'response_lfg_quests_channel', 'Any %my_role available for %quest_links in %zone_name?', 0, 0, '', 'Il y a des postes de %my_role disponibles pour les %quest_links dans la zone %zone_name?', 'Irgend ein %my_role frei für %quest_links in %zone_name?', '有 %my_role 能来 %zone_name 帮忙做 %quest_links 吗?', '', '', '', ''), + (1122, 'response_lfg_quests_channel', 'I have %quest_links and I am %my_role, need more people in %zone_name.', 0, 0, '', 'J’ai %quest_links et je suis %my_role, j’ai besoin de plus de monde dans la zone %zone_name.', 'Habe %quest_links und bin %my_role, brauche mehr Leute in %zone_name.', '我有 %quest_links,我是 %my_role,在 %zone_name 需要更多人手。', '', '', '', ''), + (1123, 'response_lfg_quests_channel', 'Looking for people to do %quest_links in %zone_name.', 0, 0, '', 'Je recherche des personnes pour faire des %quest_links dans la zone %zone_name.', 'Suche Leute für %quest_links in %zone_name.', '在 %zone_name 找人一起做 %quest_links。', '', '', '', ''), + (1124, 'response_lfg_quests_channel', 'I am %my_role and have %quest_links, looking for group in %zone_name.', 0, 0, '', 'Je suis %my_role et j’ai %quest_links, je cherche un groupe dans la zone %zone_name.', 'Bin %my_role und habe %quest_links, suche Gruppe in %zone_name.', '我是 %my_role,有 %quest_links,在 %zone_name 找队伍。', '', '', '', ''), + (1125, 'response_lfg_quests_channel', 'Need help with %quest_links in %zone_name, any %my_role available?', 0, 0, '', 'Besoin d’aide avec les %quest_links dans la zone de %zone_name, est-ce qu’il y a un %my_role disponible pour m’aider?', 'Brauche Hilfe bei %quest_links in %zone_name, ist ein %my_role frei?', '在 %zone_name 做 %quest_links 需要帮助,有 %my_role 能来吗?', '', '', '', ''), + (1126, 'response_lfg_quests_channel', 'Any %my_role interested in completing %quest_links with me in %zone_name?', 0, 0, '', 'Des %my_role intéressées pour compléter des %quest_links avec moi dans la zone %zone_name?', 'Hat ein %my_role Lust, %quest_links mit mir in %zone_name zu machen?', '有 %my_role 想跟我一起在 %zone_name 完成 %quest_links 吗?', '', '', '', ''), + (1127, 'response_lfg_quests_channel', 'Let’s complete %quest_links together in %zone_name.', 0, 0, '', 'Complétons ensemble les %quest_links dans %zone_name.', 'Lasst uns %quest_links zusammen in %zone_name abschließen.', '咱们一起在 %zone_name 完成 %quest_links 吧!', '', '', '', ''), + (1128, 'response_lfg_quests_channel', 'I am in %zone_name and have %quest_links, who wants to join?', 0, 0, '', 'Je suis actuellement dans %zone_name et j’ai %quest_links. Qui vient se joindre à moi ?', 'Bin in %zone_name und habe %quest_links, wer will mit?', '我在 %zone_name,有 %quest_links,谁想加入?', '', '', '', ''), + (1129, 'response_lfg_quests_channel', 'Looking for more %my_role to complete %quest_links in %zone_name.', 0, 0, '', 'Je recherche plus de personnes pour compléter les %my_role nécessaires dans les %quest_links de la zone %zone_name.', 'Suche mehr %my_role, um %quest_links in %zone_name abzuschließen.', '在 %zone_name 找更多 %my_role 一起完成 %quest_links。', '', '', '', ''), + (1130, 'response_lfg_quests_channel', 'I have %quest_links and I need %my_role in %zone_name.', 0, 0, '', 'J’ai %quest_links et j’ai besoin de %my_role dans %zone_name.', 'Habe %quest_links und brauche %my_role in %zone_name.', '我有 %quest_links,在 %zone_name 需要 %my_role。', '', '', '', ''), + (1131, 'response_lfg_quests_channel', 'Anyone up for %quest_links? I am %my_role in %zone_name.', 0, 0, '', 'Qui est partant pour %quest_links? Je suis %my_role dans %zone_name.', 'Wer hat Bock auf %quest_links? Ich bin %my_role in %zone_name.', '有人想做 %quest_links 吗?我是 %my_role,在 %zone_name。', '', '', '', ''), + (1132, 'response_lfg_quests_channel', 'Looking for a group to do %quest_links in %zone_name, %my_role needed.', 0, 0, '', 'Je recherche un groupe pour faire des %quest_links dans la région de %zone_name, j’ai besoin de jouer en tant que %my_role.', 'Suche Gruppe für %quest_links in %zone_name, %my_role gebraucht.', '在 %zone_name 找队伍做 %quest_links,需要 %my_role。', '', '', '', ''), + (1133, 'response_lfg_quests_channel', 'Anyone need help with %quest_links in %zone_name? I’m %my_role.', 0, 0, '', 'Besoin d’aide avec les %quest_links dans la %zone_name? Je suis là pour aider en tant que %my_role.', 'Braucht wer Hilfe bei %quest_links in %zone_name? Ich bin %my_role.', '有人在 %zone_name 需要帮忙做 %quest_links 吗?我是 %my_role。', '', '', '', ''), + (1134, 'response_lfg_quests_channel', 'I have %quest_links to complete, need %my_role in %zone_name.', 0, 0, '', 'J’ai %quest_links à terminer, j’ai besoin de %my_role dans la zone %zone_name.', 'Habe %quest_links offen, brauche %my_role in %zone_name.', '我有 %quest_links 要完成,在 %zone_name 需要 %my_role。', '', '', '', ''), + (1135, 'response_lfg_quests_channel', 'I’m in %zone_name looking for %quest_links help, who’s in?', 0, 0, '', 'Je suis dans %zone_name à la recherche d’aide pour %quest_links, qui est partant?', 'Bin in %zone_name und suche Hilfe für %quest_links, wer ist dabei?', '我在 %zone_name 找人帮忙做 %quest_links,谁愿意加入?', '', '', '', ''), + (1136, 'response_lfg_quests_channel', 'Looking for a group to finish %quest_links in %zone_name.', 0, 0, '', 'Je cherche un groupe pour terminer les %quest_links dans la zone %zone_name.', 'Suche Gruppe, um %quest_links in %zone_name fertig zu machen.', '在 %zone_name 找队伍完成 %quest_links。', '', '', '', ''), + (1137, 'response_lfg_quests_channel', 'Anyone looking for %quest_links in %zone_name? I’m %my_role.', 0, 0, '', 'Quelqu’un recherche %quest_links dans la zone de %zone_name? Je suis %my_role.', 'Sucht jemand %quest_links in %zone_name? Ich bin %my_role.', '有人在 %zone_name 找 %quest_links 吗?我是 %my_role。', '', '', '', ''), + (1138, 'response_lfg_quests_channel', 'Let’s do %quest_links together! %zone_name is waiting.', 0, 0, '', 'Allons-y ensemble pour %quest_links ! %zone_name nous attend.', 'Machen wir %quest_links zusammen! %zone_name wartet.', '咱们一起做 %quest_links 吧!%zone_name 正等着我们!', '', '', '', ''), + (1139, 'response_lfg_quests_channel', 'Need a group for %quest_links, %zone_name, anyone interested?', 0, 0, '', 'Besoin d’un groupe pour %quest_links dans la zone %zone_name, des intéressés ?', 'Brauche Gruppe für %quest_links, %zone_name - wer hat Interesse?', '在 %zone_name 需要队伍做 %quest_links,有人感兴趣吗?', '', '', '', ''), + (1140, 'response_lfg_quests_channel', 'Who wants to join for %quest_links? I’m in %zone_name.', 0, 0, '', 'Qui veut se joindre pour %quest_links? Je suis actuellement dans la zone %zone_name.', 'Wer will für %quest_links dazukommen? Bin in %zone_name.', '谁想加入做 %quest_links?我在 %zone_name。', '', '', '', ''), + (1141, 'response_lfg_quests_channel', 'I’m in %zone_name, looking for people to complete %quest_links with.', 0, 0, '', 'Je suis dans %zone_name, à la recherche de joueurs pour compléter les missions %quest_links avec moi.', 'Bin in %zone_name, suche Leute zum Abschließen von %quest_links.', '我在 %zone_name,找人一起完成 %quest_links。', '', '', '', ''), + (1142, 'response_lfg_quests_channel', 'Need a hand with %quest_links in %zone_name, any %my_role?', 0, 0, '', 'Besoin d’aide avec %quest_links dans %zone_name, quelqu’un de mon rôle?', 'Brauche Hilfe bei %quest_links in %zone_name, ein %my_role da?', '在 %zone_name 做 %quest_links 需要帮忙,有 %my_role 吗?', '', '', '', ''), + (1143, 'response_lfg_quests_channel', 'Looking for group to finish %quest_links, %zone_name is the place to be.', 0, 0, '', 'Je cherche un groupe pour finir %quest_links, %zone_name est l’endroit où il faut être.', 'Suche Gruppe, um %quest_links zu beenden, %zone_name da steppt der Bär.', '找队伍完成 %quest_links,%zone_name 就是好地方。', '', '', '', ''), + (1144, 'response_lfg_quests_channel', 'Anyone want to help with %quest_links in %zone_name? I’m %my_role.', 0, 0, '', 'Est-ce que quelqu’un veut aider avec les %quest_links dans la région de %zone_name? Je suis %my_role.', 'Will jemand bei %quest_links in %zone_name helfen? Ich bin %my_role.', '有人想在 %zone_name 帮忙做 %quest_links 吗?我是 %my_role。', '', '', '', ''), + (1145, 'response_lfg_quests_channel', 'Looking for %my_role to join me for %quest_links in %zone_name.', 0, 0, '', 'Je recherche %my_role pour me rejoindre pour des %quest_links dans la %zone_name.', 'Suche %my_role, um %quest_links in %zone_name mitzumachen.', '在 %zone_name 找 %my_role 一起做 %quest_links。', '', '', '', ''), + (1146, 'response_lfg_quests_channel', 'I’ve got %quest_links, %my_role needed for %zone_name.', 0, 0, '', 'J’ai %quest_links, %my_role requis pour %zone_name.', 'Hab %quest_links, %my_role für %zone_name gesucht.', '我有 %quest_links,在 %zone_name 需要 %my_role。', '', '', '', ''), + (1147, 'response_lfg_quests_channel', 'Looking to finish %quest_links in %zone_name, need more people.', 0, 0, '', 'Je cherche à finir les %quest_links dans %zone_name, j’ai besoin de plus de personnes.', 'Will %quest_links in %zone_name fertig bekommen, brauche mehr Leute.', '想在 %zone_name 完成 %quest_links,需要更多人手。', '', '', '', ''), + (1148, 'response_lfg_quests_channel', 'I need help with %quest_links, who’s around in %zone_name?', 0, 0, '', 'J’ai besoin d’aide avec %quest_links, qui est autour dans %zone_name?', 'Brauche Hilfe bei %quest_links, wer ist in %zone_name unterwegs?', '我需要帮忙做 %quest_links,%zone_name 附近有谁在?', '', '', '', ''), + (1149, 'response_lfg_quests_channel', 'Need more people to complete %quest_links, %zone_name.', 0, 0, '', 'J’ai besoin de plus de personnes pour compléter %quest_links dans la zone %zone_name.', 'Brauche mehr Leute für %quest_links, %zone_name.', '需要更多人完成 %quest_links,在 %zone_name。', '', '', '', ''), + (1150, 'response_lfg_quests_channel', 'Looking for group in %zone_name to finish %quest_links.', 0, 0, '', 'Je cherche un groupe dans la %zone_name pour terminer les %quest_links.', 'Suche Gruppe in %zone_name, um %quest_links abzuschließen.', '在 %zone_name 找队伍完成 %quest_links。', '', '', '', ''), + (1151, 'response_lfg_quests_channel', 'Who wants to do %quest_links in %zone_name? I’m ready to go.', 0, 0, '', 'Qui veut faire des %quest_links dans la zone de %zone_name? Je suis prêt à y aller.', 'Wer will %quest_links in %zone_name machen? Bin startklar.', '谁想在 %zone_name 做 %quest_links?我已经准备好了。', '', '', '', ''), + (1152, 'response_lfg_quests_channel', 'Anyone want to join me for %quest_links? I’m %my_role in %zone_name.', 0, 0, '', 'Qui veut se joindre à moi pour %quest_links? Je suis %my_role dans la zone %zone_name.', 'Will wer für %quest_links dazustoßen? Ich bin %my_role in %zone_name.', '有人想跟我一起做 %quest_links 吗?我是 %my_role,在 %zone_name。', '', '', '', ''), + (1153, 'response_lfg_quests_channel', 'Looking for %my_role to do %quest_links with in %zone_name.', 0, 0, '', 'Je recherche %my_role pour faire %quest_links avec dans la zone %zone_name.', 'Suche %my_role für %quest_links in %zone_name.', '在 %zone_name 找 %my_role 一起做 %quest_links。', '', '', '', ''), + (1154, 'response_lfg_quests_channel', 'I’m in %zone_name, anyone need help with %quest_links?', 0, 0, '', 'Je suis dans %zone_name, est-ce que quelqu’un a besoin d’aide avec %quest_links?', 'Bin in %zone_name, braucht wer Hilfe bei %quest_links?', '我在 %zone_name,有人需要帮忙做 %quest_links 吗?', '', '', '', ''), + (1155, 'response_lfg_quests_channel', 'Need more for %quest_links in %zone_name, any takers?', 0, 0, '', 'Besoin de renfort pour les %quest_links dans %zone_name, des volontaires ?', 'Brauche mehr für %quest_links in %zone_name, bin allein nicht stark genug?', '在 %zone_name 做 %quest_links 需要更多人,有人愿意吗?', '', '', '', ''), + (1156, 'response_lfg_quests_channel', 'I need help with %quest_links in %zone_name, who’s up for it?', 0, 0, '', 'J’ai besoin d’aide avec les %quest_links dans la zone %zone_name, qui est partant pour m’accompagner?', 'Brauche Hilfe bei %quest_links in %zone_name, wer hat Bock?', '在 %zone_name 做 %quest_links 需要帮忙,谁愿意一起?', '', '', '', ''), + (1157, 'response_lfg_quests_channel', 'Looking for more to finish %quest_links, %zone_name.', 0, 0, '', 'Recherche compagnions pour %quest_links dans la zone %zone_name.', 'Suche mehr Leute, um %quest_links zu beenden, %zone_name.', '找更多人完成 %quest_links,在 %zone_name。', '', '', '', ''), + (1158, 'response_lfg_quests_whisper', 'Hey, I’m up for %quest_links', 0, 0, '', 'Salut, je suis partant pour %quest_links', 'Hey, bin bei %quest_links dabei', '嘿,我想做 %quest_links。', '', '', '', ''), + (1159, 'response_lfg_quests_whisper', 'Hey, I could do %quest_links with you', 0, 0, '', 'Salut, je pourrais faire %quest_links avec toi', 'Hey, ich könnte %quest_links mit dir machen', '嘿,我可以跟你一起做 %quest_links。', '', '', '', 'Привет, могу сделать %quest_links с тобой'), + (1160, 'response_lfg_quests_whisper', 'Hey, I also have %quest_links', 0, 0, '', 'Salut, j’ai aussi %quest_links.', 'Hey, ich habe auch %quest_links', '嘿,我也有 %quest_links。', '', '', '', 'Привет, у меня тоже есть %quest_links'), + (1161, 'response_lfg_quests_whisper', 'Hey %other_name, I’m up for %quest_links', 0, 0, '', 'Salut %other_name, je suis prêt pour %quest_links', 'Hey %other_name, bin bei %quest_links dabei', '嘿,%other_name,我想做 %quest_links。', '', '', '', ''), + (1162, 'response_lfg_quests_whisper', 'Hey %other_name, I could do %quest_links with you', 0, 0, '', 'Salut %other_name, je pourrais faire %quest_links avec toi.', 'Hey %other_name, ich könnte %quest_links mit dir machen', '嘿,%other_name,我可以跟你一起做 %quest_links。', '', '', '', 'Привет %other_name, могу сделать %quest_links с тобой'), + (1163, 'response_lfg_quests_whisper', 'Hey %other_name, I also have %quest_links', 0, 0, '', 'Salut %other_name, j’ai aussi des %quest_links', 'Hey %other_name, ich habe auch %quest_links', '嘿,%other_name,我也有 %quest_links。', '', '', '', 'Привет %other_name, у меня тоже есть %quest_links'), + (1164, 'response_lfg_quests_whisper', 'wanna group up for %quest_links ?', 0, 0, '', 'Tu veux qu’on se groupe pour faire les %quest_links ensemble ?', 'Bock, für %quest_links eine Gruppe zu bilden?', '想组队一起做 %quest_links 吗?', '', '', '', 'Хочешь сгруппироваться для %quest_links?'), + (1165, 'response_lfg_quests_whisper', 'I am up for %quest_links, I am in %zone_name right now', 0, 0, '', 'Je suis partant pour des %quest_links, je me trouve en ce moment dans %zone_name.', 'Bin bereit für %quest_links, bin gerade in %zone_name', '我想做 %quest_links,我现在在 %zone_name。', '', '', '', 'Я готов к %quest_links, сейчас я в %zone_name'), + (1166, 'response_lfg_quests_whisper', 'I am up for %quest_links, I am %my_role', 0, 0, '', 'Je suis prêt pour des %quest_links, je suis %my_role.', 'Bin bereit für %quest_links, bin %my_role', '我想做 %quest_links,我是 %my_role。', '', '', '', 'Я готов к %quest_links, я %my_role'), + (1167, 'response_lfg_quests_whisper', 'Hey, I’m down for %quest_links', 0, 0, '', 'Hey, je suis partant pour %quest_links', 'Hey, ich hab Bock auf %quest_links', '嘿,我愿意做 %quest_links。', '', '', '', ''), + (1168, 'response_lfg_quests_whisper', 'I can help with %quest_links if you need', 0, 0, '', 'Je peux t’aider avec les %quest_links si tu en as besoin.', 'Ich kann bei %quest_links helfen, wenn ihr wollt', '如果你需要,我可以帮你做 %quest_links。', '', '', '', ''), + (1169, 'response_lfg_quests_whisper', 'Let’s do %quest_links together!', 0, 0, '', 'Faisons %quest_links ensemble!', 'Lasst uns %quest_links zusammen machen!', '咱们一起做 %quest_links 吧!', '', '', '', ''), + (1170, 'response_lfg_quests_whisper', 'I’ve got %quest_links to do too', 0, 0, '', 'J’ai aussi %quest_links à faire.', 'Ich hab auch %quest_links offen', '我也有 %quest_links 要做。', '', '', '', ''), + (1171, 'response_lfg_quests_whisper', 'I’m available for %quest_links if you want', 0, 0, '', 'Je suis disponible pour %quest_links si tu veux', 'Bin für %quest_links verfügbar, wenn ihr wollt', '如果你想做 %quest_links,我有空。', '', '', '', ''), + (1172, 'response_lfg_quests_whisper', 'I’m down for %quest_links, just let me know', 0, 0, '', 'Je suis partant pour %quest_links, il me suffit de me dire', 'Bin bei %quest_links dabei, sagt Bescheid', '我愿意做 %quest_links,告诉我一声就行。', '', '', '', ''), + (1173, 'response_lfg_quests_whisper', 'Looking for %quest_links? I’m in %zone_name', 0, 0, '', 'À la recherche de %quest_links? Je me trouve à %zone_name.', 'Sucht wer %quest_links? Bin in %zone_name', '在找 %quest_links 吗?我在 %zone_name。', '', '', '', ''), + (1174, 'response_lfg_quests_whisper', 'Hey, I need %quest_links too, let’s go', 0, 0, '', 'Salut, j’ai besoin de %quest_links aussi, allons-y', 'Hey, ich brauche auch %quest_links, lets go', '嘿,我也需要 %quest_links,一起走吧!', '', '', '', ''), + (1175, 'response_lfg_quests_whisper', 'I’ve got %quest_links, want to join me?', 0, 0, '', 'J’ai %quest_links, tu veux te joindre à moi?', 'Hab %quest_links, will wer mit?', '我有%quest_links,要不要一起来?', '', '', '', ''), + (1176, 'response_lfg_quests_whisper', 'I could use %quest_links, let’s do this', 0, 0, '', 'Je peux faire %quest_links, allons-y', 'Könnte %quest_links brauchen, machen wirs', '我正好需要做%quest_links,一起搞定吧!', '', '', '', ''), + (1177, 'response_lfg_quests_whisper', 'Hey, want to join for %quest_links?', 0, 0, '', 'Salut, tu veux te joindre à moi pour %quest_links?', 'Hey, willst du für %quest_links dazukommen?', '嘿,想一起做%quest_links吗?', '', '', '', ''), + (1178, 'response_lfg_quests_whisper', 'I can help with %quest_links if you need', 0, 0, '', 'Je peux t’aider avec %quest_links si besoin', 'Ich kann bei %quest_links helfen, wenn ihr wollt', '如果需要帮忙,我可以助你一臂之力,%quest_links没问题!', '', '', '', ''), + (1179, 'response_lfg_quests_whisper', 'Hey %other_name, up for %quest_links?', 0, 0, '', 'Salut %other_name, partant pour %quest_links?', 'Hey %other_name, Bock auf %quest_links?', '嘿,%other_name,有兴趣做%quest_links吗?', '', '', '', ''), + (1180, 'response_lfg_quests_whisper', 'Want to group up for %quest_links %other_name?', 0, 0, '', 'Tu veux faire équipe pour %quest_links %other_name?', 'Willst du für %quest_links gruppen, %other_name?', '想组队做%quest_links吗,%other_name?', '', '', '', ''), + (1181, 'response_lfg_quests_whisper', 'Let’s do %quest_links together, %other_name', 0, 0, '', 'Faisons les %quest_links ensemble, %other_name.', 'Lass %quest_links zusammen machen, %other_name', '咱们一起把%quest_links搞定吧,%other_name!', '', '', '', ''), + (1182, 'response_lfg_quests_whisper', 'I’m %my_role and ready for %quest_links', 0, 0, '', 'Je suis %my_role, prêt pour %quest_links.', 'Ich bin %my_role und ready für %quest_links', '我是%my_role,已经准备好做%quest_links了!', '', '', '', ''), + (1183, 'response_lfg_quests_whisper', 'I can join for %quest_links, %my_role here', 0, 0, '', 'Je peux vous rejoindre pour %quest_links, %my_role ici.', 'Kann für %quest_links joinen, %my_role hier', '我可以加入做%quest_links,我是%my_role!', '', '', '', ''), + (1184, 'response_lfg_quests_whisper', 'I’m up for %quest_links, %my_role!', 0, 0, '', 'Je suis prêt pour %quest_links, %my_role!', 'Bin bereit für %quest_links, %my_role!', '我准备好做%quest_links了,我是%my_role!', '', '', '', ''), + (1185, 'response_lfg_quests_whisper', 'Looking to do %quest_links with a %my_role', 0, 0, '', 'Je cherche à faire des %quest_links avec mon rôle.', 'Suche %my_role für %quest_links', '想找人一起做%quest_links,我是%my_role。', '', '', '', ''), + (1186, 'response_lfg_quests_whisper', 'Anyone need help with %quest_links?', 0, 0, '', 'Besoin d’aide avec %quest_links?', 'Braucht wer Hilfe bei %quest_links?', '有人需要帮忙做%quest_links吗?', '', '', '', ''), + (1187, 'response_lfg_quests_whisper', 'Who else is up for %quest_links?', 0, 0, '', 'Qui d’autre est partant pour %quest_links?', 'Wer ist noch bei %quest_links dabei?', '还有谁想做%quest_links?', '', '', '', ''), + (1188, 'response_lfg_quests_whisper', 'Anyone down for %quest_links in %zone_name?', 0, 0, '', 'Qui est partant pour des %quest_links dans la région %zone_name ?', 'Jemand bereit für %quest_links in %zone_name?', '有人想在%zone_name做%quest_links吗?', '', '', '', ''), + (1189, 'response_lfg_quests_whisper', 'I’ve got %quest_links, %my_role on the way', 0, 0, '', 'J’ai %quest_links, %my_role en route.', 'Hab %quest_links, %my_role unterwegs', '我有%quest_links,我是%my_role,正在赶来!', '', '', '', ''), + (1190, 'response_lfg_quests_whisper', 'I need %quest_links too, who’s in?', 0, 0, '', 'J’ai besoin de %quest_links aussi, qui est partant?', 'Ich brauch %quest_links auch, wer ist dabei?', '我也需要做%quest_links,谁一起?', '', '', '', ''), + (1191, 'response_lfg_quests_whisper', 'I’ve been grinding %quest_links, need a hand?', 0, 0, '', 'J’ai bossé dur sur %quest_links, tu as besoin d’aide ?', 'Grinde %quest_links, wer hilft?', '我一直在刷%quest_links,需要帮忙吗?', '', '', '', ''), + (1192, 'response_lfg_quests_whisper', 'I’m ready for %quest_links, anyone else?', 0, 0, '', 'Je suis prêt pour les %quest_links, quelqu’un d’autre est prêt aussi ?', 'Bin bereit für %quest_links, noch wer?', '我准备好做%quest_links了,还有人吗?', '', '', '', ''), + (1193, 'response_lfg_quests_whisper', 'Can anyone join me for %quest_links?', 0, 0, '', 'Est-ce que quelqu’un veut se joindre à moi pour %quest_links?', 'Kann jemand für %quest_links dazukommen?', '有人能和我一起做%quest_links吗?', '', '', '', ''), + (1194, 'response_lfg_quests_whisper', 'Looking for help with %quest_links, %my_role here', 0, 0, '', 'Je recherche de l’aide avec %quest_links, je suis %my_role ici. En quoi puis-je vous aider?', 'Suche Hilfe bei %quest_links, %my_role hier', '找人帮忙做%quest_links,我是%my_role!', '', '', '', ''), + (1195, 'response_lfg_quests_whisper', 'I can help out with %quest_links, anyone need me?', 0, 0, '', 'Je peux aider avec %quest_links, quelqu’un a besoin de moi ?', 'Ich kann bei %quest_links aushelfen, braucht mich wer?', '我可以帮忙做%quest_links,有人需要我吗?', '', '', '', ''), + (1196, 'response_lfg_quests_whisper', 'Anyone else doing %quest_links? Let’s go', 0, 0, '', 'Quelqu’un d’autre fait des %quest_links? Allons-y', 'Macht noch wer %quest_links? Los gehts', '还有人在做%quest_links吗?一起上吧!', '', '', '', ''), + (1197, 'response_lfg_quests_whisper', 'Anyone need %quest_links help? %my_role here', 0, 0, '', 'Besoin d’aide pour %quest_links? C’est %my_role qui s’en charge par ici.', 'Braucht wer Hilfe bei %quest_links? %my_role hier', '有人需要帮忙做%quest_links吗?我这里是%my_role!', '', '', '', ''), + (1198, 'response_lfg_quests_whisper', 'Who’s up for some %quest_links in %zone_name?', 0, 0, '', 'Qui est partant pour quelques %quest_links dans la %zone_name ?', 'Wer hat Bock auf %quest_links in %zone_name?', '谁想在%zone_name做点%quest_links?', '', '', '', ''), + (1199, 'response_lfg_quests_whisper', 'I can join for %quest_links if you want', 0, 0, '', 'Je peux me joindre pour %quest_links si tu veux.', 'Kann für %quest_links joinen, wenn ihr wollt', '如果你愿意,我可以加入做%quest_links。', '', '', '', ''), + (1200, 'response_lfg_quests_whisper', 'Looking for a %my_role to do %quest_links', 0, 0, '', 'Je suis à la recherche d’un %my_role pour réaliser les %quest_links.', 'Suche einen %my_role für %quest_links', '找一个%my_role一起做%quest_links。', '', '', '', ''), + (1201, 'response_lfg_quests_whisper', 'Any %my_role up for %quest_links?', 0, 0, '', 'Des volontaires pour %my_role pour les %quest_links?', 'Ist ein %my_role für %quest_links am Start?', '有%my_role想做%quest_links的吗?', '', '', '', ''), + (1202, 'response_lfg_quests_whisper', 'I’ve got %quest_links, anyone want to join me?', 0, 0, '', 'J’ai %quest_links, quelqu’un veut se joindre à moi?', 'Ich habe %quest_links, will wer mit?', '我有%quest_links,有人想一起吗?', '', '', '', ''), + (1203, 'response_lfg_quests_whisper', 'Hey %other_name, up for some %quest_links?', 0, 0, '', 'Salut %other_name, partant pour quelques %quest_links?', 'Hey %other_name, Bock auf %quest_links?', '嘿,%other_name,有兴趣做点%quest_links吗?', '', '', '', ''), + (1204, 'response_lfg_quests_whisper', 'Let’s team up for %quest_links, %other_name', 0, 0, '', 'Partons à l’aventure pour %quest_links, %other_name!', 'Lass uns für %quest_links gruppen, %other_name', '咱们组队做%quest_links吧,%other_name!', '', '', '', ''), + (1205, 'response_lfg_quests_whisper', 'I need %quest_links, anyone else want to do it?', 0, 0, '', 'J’ai besoin de %quest_links, est-ce que quelqu’un d’autre veut le faire avec moi ?', 'Ich brauche %quest_links, wer macht mit?', '我需要做%quest_links,还有人想一起吗?', '', '', '', ''), + (1206, 'response_lfg_quests_whisper', 'I need %quest_links too, anyone else around?', 0, 0, '', 'J’ai besoin de %quest_links aussi, est-ce qu’il y a d’autres personnes ici?', 'Ich brauche auch %quest_links, ist noch jemand da?', '我也需要做%quest_links,附近有人吗?', '', '', '', ''), + (1207, 'response_lfg_quests_whisper', 'Looking for %quest_links in %zone_name, who’s with me?', 0, 0, '', 'À la recherche de %quest_links dans %zone_name, qui est partant avec moi ?', 'Suche %quest_links in %zone_name, wer ist dabei?', '在%zone_name找人做%quest_links,谁跟我一起?', '', '', '', ''), + (1208, 'response_lfg_quests_whisper', 'Anyone available for %quest_links?', 0, 0, '', 'Quelqu’un de disponible pour %quest_links ?', 'Ist jemand frei für %quest_links?', '有人有空做%quest_links吗?', '', '', '', ''), + (1209, 'response_lfg_quests_whisper', 'Hey, I could join you for %quest_links!', 0, 0, '', 'Hey, je pourrais me joindre à vous pour les %quest_links!', 'Hey, ich könnte bei %quest_links dazustoßen!', '嘿,我可以加入你们一起做%quest_links!', '', '', '', ''), + (1210, 'response_lfg_quests_whisper', 'I’ve got %quest_links too, let’s team up', 0, 0, '', 'J’ai aussi %quest_links, unissons nos forces!', 'Ich habe auch %quest_links, lass uns gruppen', '我也有%quest_links,咱们组队吧!', '', '', '', ''), + (1211, 'response_lfg_quests_whisper', 'I’ve got %quest_links to do, want to join?', 0, 0, '', 'J’ai %quest_links à accomplir, ça te dit de m’accompagner ?', 'Ich habe %quest_links zu erledigen, willst du mit?', '我有%quest_links要做,要不要一起?', '', '', '', ''), + (1212, 'response_lfg_quests_whisper', 'Any %my_role up for %quest_links in %zone_name?', 0, 0, '', 'Y a-t-il des %my_role partants pour des %quest_links dans %zone_name?', 'Ist ein %my_role für %quest_links in %zone_name frei?', '有%my_role想在%zone_name做%quest_links吗?', '', '', '', ''), + (1213, 'response_lfg_quests_whisper', 'Let’s finish %quest_links together, %other_name', 0, 0, '', 'Terminons ensemble %quest_links, %other_name.', 'Lass uns %quest_links zusammen abschließen, %other_name', '咱们一起把%quest_links做完吧,%other_name!', '', '', '', ''), + (1214, 'response_lfg_quests_whisper', 'I’m up for %quest_links, anyone else want to join?', 0, 0, '', 'Je suis partant pour %quest_links, est-ce qu’il y a d’autres personnes qui veulent se joindre ?', 'Ich bin bei %quest_links dabei, will noch wer mit?', '我准备好做%quest_links了,还有人想加入吗?', '', '', '', ''), + (1215, 'response_lfg_quests_whisper', 'Who else is doing %quest_links? I’m in', 0, 0, '', 'Qui d’autre fait les %quest_links ? J’y participe également!', 'Wer macht noch %quest_links? Ich bin dabei', '还有谁在做%quest_links?我也参加!', '', '', '', ''), + (1216, 'response_lfg_quests_whisper', 'I’ve got %quest_links in %zone_name, need help?', 0, 0, '', 'J’ai %quest_links dans %zone_name, besoin d’aide ?', 'Ich habe %quest_links in %zone_name, braucht jemand Hilfe?', '我在%zone_name有%quest_links,需要帮忙吗?', '', '', '', ''), + (1217, 'response_wtb_items_channel', '%other_name, I could sell you %formatted_item_links', 0, 0, '', 'Ah, %formatted_item_links, je pourrais vous les vendre.', '%other_name, könnte dir %formatted_item_links verkaufen', '%other_name,我可以卖给你%formatted_item_links。', '', '', '', '%other_name, могу продать тебе %formatted_item_links'), + (1218, 'response_wtb_items_channel', 'I could potentially sell %formatted_item_links', 0, 0, '', 'Je pourrais potentiellement vendre %formatted_item_links.', 'Könnte eventuell %formatted_item_links verkaufen', '我可能有%formatted_item_links可以卖。', '', '', '', 'Могу, возможно, продать %formatted_item_links'), + (1219, 'response_wtb_items_channel', 'Think I could sell %formatted_item_links', 0, 0, '', 'Penses-tu que je pourrais vendre les %formatted_item_links ?', 'Denke, ich könnte %formatted_item_links verkaufen', '我觉得我可以卖%formatted_item_links。', '', '', '', 'Думаю, могу продать %formatted_item_links'), + (1220, 'response_wtb_items_channel', '%other_name, I could potentially sell %formatted_item_links', 0, 0, '', 'Hé toi, je pourrais potentiellement te vendre %formatted_item_links', '%other_name, könnte eventuell %formatted_item_links verkaufen', '嘿,%other_name,我可能有%formatted_item_links可以卖给你。', '', '', '', '%other_name, возможно, могу продать %formatted_item_links'), + (1221, 'response_wtb_items_channel', '%other_name, Think I could sell %formatted_item_links', 0, 0, '', '%other_name, tu crois que je pourrais vendre %formatted_item_links?', '%other_name, ich denke, ich könnte %formatted_item_links verkaufen', '%other_name,你觉得我能卖%formatted_item_links吗?', '', '', '', '%other_name, думаю, могу продать %formatted_item_links'), + (1222, 'response_wtb_items_whisper', 'I could sell you %formatted_item_links', 0, 0, '', 'Je pourrais te vendre des %formatted_item_links.', 'Ich könnte dir %formatted_item_links verkaufen', '我可以卖给你%formatted_item_links。', '', '', '', 'Могу продать тебе %formatted_item_links'), + (1223, 'response_wtb_items_whisper', 'Hey, I got %formatted_item_links for sale', 0, 0, '', 'Salut, j’ai %formatted_item_links à vendre.', 'Hey, habe %formatted_item_links zu verkaufen', '嘿,我有%formatted_item_links要卖!', '', '', '', 'Привет, у меня есть %formatted_item_links на продажу'), + (1224, 'response_wtb_items_whisper', 'Hey, I could potentially sell %formatted_item_links', 0, 0, '', 'Salut, je pourrais potentiellement vendre %formatted_item_links', 'Hey, könnte eventuell %formatted_item_links verkaufen', '嘿,我可能有%formatted_item_links可以卖给你。', '', '', '', 'Привет, возможно, могу продать %formatted_item_links'), + (1225, 'response_wtb_items_whisper', 'I could sell you %formatted_item_links', 0, 0, '', 'Je pourrais te vendre %formatted_item_links.', 'Ich könnte dir %formatted_item_links verkaufen', '我可以卖给你%formatted_item_links。', '', '', '', 'Могу продать тебе %formatted_item_links'), + (1226, 'response_wtb_items_whisper', 'Hey, I got %formatted_item_links for sale', 0, 0, '', 'Salut, j’ai %formatted_item_links à vendre.', 'Hey, habe %formatted_item_links zu verkaufen', '嘿,我有%formatted_item_links要卖!', '', '', '', 'Привет, у меня есть %formatted_item_links на продажу'), + (1227, 'response_wtb_items_whisper', 'Hey, I could potentially sell %formatted_item_links', 0, 0, '', 'Hey, je pourrais potentiellement vendre ces %formatted_item_links.', 'Hey, ich könnte eventuell %formatted_item_links verkaufen', '嘿,我可能能卖给你%formatted_item_links。', '', '', '', 'Привет, возможно, могу продать %formatted_item_links'), + (1228, 'response_wtb_items_whisper', 'I have %formatted_item_links, are you interested?', 0, 0, '', 'J’ai des %formatted_item_links, ça t’intéresse ?', 'Ich habe %formatted_item_links, Interesse?', '我有%formatted_item_links,你感兴趣吗?', '', '', '', ''), + (1229, 'response_wtb_items_whisper', 'Looking to sell %formatted_item_links, what’s your offer?', 0, 0, '', 'Je cherche à vendre %formatted_item_links, quelle est votre offre ?', 'Will %formatted_item_links verkaufen, was bietest du?', '我想卖掉%formatted_item_links,你能开个什么价?', '', '', '', ''), + (1230, 'response_wtb_items_whisper', 'I can sell you %formatted_item_links for a fair price', 0, 0, '', 'Je peux vous vendre des %formatted_item_links à un prix raisonnable.', 'Ich kann dir %formatted_item_links zu einem fairem Preis verkaufen', '我可以以公道价格卖给你%formatted_item_links。', '', '', '', ''), + (1231, 'response_wtb_items_whisper', 'I have %formatted_item_links in stock, how much are you offering?', 0, 0, '', 'J’ai %formatted_item_links en stock, combien proposez-vous ?', 'Ich habe %formatted_item_links auf Lager, was zahlst du?', '我有%formatted_item_links库存,你能出多少钱?', '', '', '', ''), + (1232, 'response_wtb_items_whisper', 'I’m selling %formatted_item_links for %cost_gold, let me know if you’re interested', 0, 0, '', 'Je vends des %formatted_item_links pour %cost_gold, dis-moi si ça t’intéresse.', 'Verkaufe %formatted_item_links für %cost_gold, sag Bescheid bei Interesse', '我在卖%formatted_item_links,价格%cost_gold,感兴趣就告诉我。', '', '', '', ''), + (1233, 'response_wtb_items_whisper', 'Got %formatted_item_links for sale, what are you willing to pay?', 0, 0, '', 'J’ai %formatted_item_links à vendre, combien es-tu prêt à payer?', 'Habe %formatted_item_links zu verkaufen, was zahlst du?', '我有%formatted_item_links要卖,你愿意出多少钱?', '', '', '', ''), + (1234, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links, how does %cost_gold sound for you?', 0, 0, '', 'J’ai %formatted_item_links, est-ce que %cost_gold te semble raisonnable ?', 'Ich habe %formatted_item_links, wie klingt %cost_gold für dich?', '我有%formatted_item_links,%cost_gold这个价格你觉得咋样?', '', '', '', ''), + (1235, 'response_wtb_items_whisper', 'Selling %formatted_item_links for %cost_gold, message me if you want it', 0, 0, '', 'Je vends des %formatted_item_links pour %cost_gold, contactez-moi si vous êtes intéressé.', 'Verkaufe %formatted_item_links für %cost_gold, flüster mir bei Interesse', '卖%formatted_item_links,价格%cost_gold,想买就私聊我。', '', '', '', ''), + (1236, 'response_wtb_items_whisper', 'I have %formatted_item_links, hit me up if you’re interested', 0, 0, '', 'J’ai des %formatted_item_links, contactez-moi si ça vous intéresse', 'Ich habe %formatted_item_links, meld dich bei Interesse', '我有%formatted_item_links,感兴趣就联系我。', '', '', '', ''), + (1237, 'response_wtb_items_whisper', 'I can part with %formatted_item_links if you make the right offer', 0, 0, '', 'Je peux me séparer de %formatted_item_links si ton offre est intéressante.', 'Gebe %formatted_item_links ab, wenn das Angebot passt', '如果你出价合适,我可以把%formatted_item_links卖给你。', '', '', '', ''), + (1238, 'response_wtb_items_whisper', 'Looking to get rid of %formatted_item_links, hit me up', 0, 0, '', 'Je cherche à me débarrasser de %formatted_item_links, contactez-moi', 'Will %formatted_item_links loswerden, flüster mir', '我想处理掉%formatted_item_links,有意就找我。', '', '', '', ''), + (1239, 'response_wtb_items_whisper', 'Got %formatted_item_links for %cost_gold, are you buying?', 0, 0, '', 'J’ai %formatted_item_links pour %cost_gold, tu veux acheter?', 'Habe %formatted_item_links für %cost_gold, kaufst du?', '我有%formatted_item_links,价格%cost_gold,要买吗?', '', '', '', ''), + (1240, 'response_wtb_items_whisper', 'Selling %formatted_item_links, let’s negotiate', 0, 0, '', 'Je vends %formatted_item_links, on peut discuter du prix ensemble.', 'Verkaufe %formatted_item_links, lass uns handeln', '卖%formatted_item_links,咱们可以谈谈价格。', '', '', '', ''), + (1241, 'response_wtb_items_whisper', 'Hey, I have %formatted_item_links for %cost_gold', 0, 0, '', 'Salut, j’ai des %formatted_item_links pour un coût en or de %cost_gold.', 'Hey, habe %formatted_item_links für %cost_gold', '嘿,我有%formatted_item_links,价格%cost_gold。', '', '', '', ''), + (1242, 'response_wtb_items_whisper', 'I can hook you up with %formatted_item_links, just name your price', 0, 0, '', 'Je peux vous procurer des liens d’articles %formatted_item_links, vous n’avez qu’à me dire votre prix.', 'Kann dich mit %formatted_item_links versorgen, nenn deinen Preis', '我能给你弄到%formatted_item_links,你开个价吧。', '', '', '', ''), + (1243, 'response_wtb_items_whisper', 'I have %formatted_item_links available, let me know if you’re interested', 0, 0, '', 'J’ai des %formatted_item_links disponibles, fais-moi savoir si cela t’intéresse.', 'Ich habe %formatted_item_links verfügbar, sag Bescheid bei Interesse', '我有%formatted_item_links可供出售,感兴趣就告诉我。', '', '', '', ''), + (1244, 'response_wtb_items_whisper', 'Selling %formatted_item_links, let’s do business', 0, 0, '', 'Je vends %formatted_item_links, on pourrait faire affaire ensemble.', 'Verkaufe %formatted_item_links, machen wir Business', '卖%formatted_item_links,咱们做笔生意吧。', '', '', '', ''), + (1245, 'response_wtb_items_whisper', 'I have %formatted_item_links, feel free to make an offer', 0, 0, '', 'J’ai %formatted_item_links, n’hésitez pas à faire une offre.', 'Ich habe %formatted_item_links, macht mir ein Angebot', '我有%formatted_item_links,欢迎出价。', '', '', '', ''), + (1246, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links, send me a message if you want to buy', 0, 0, '', 'J’ai des %formatted_item_links, envoie-moi un message si tu veux acheter.', 'Ich habe %formatted_item_links, schreib mir, wenn du kaufen willst', '我有%formatted_item_links,想买就给我发消息。', '', '', '', ''), + (1247, 'response_wtb_items_whisper', 'I can offer you %formatted_item_links for %cost_gold', 0, 0, '', 'Je peux vous proposer des %formatted_item_links pour %cost_gold.', 'Kann dir %formatted_item_links für %cost_gold anbieten', '我可以给你%formatted_item_links,价格%cost_gold。', '', '', '', ''), + (1248, 'response_wtb_items_whisper', 'I have %formatted_item_links for sale, message me for the price', 0, 0, '', 'J’ai %formatted_item_links à vendre, envoyez-moi un message pour connaître le prix.', 'Ich habe %formatted_item_links zu verkaufen, schreib mir für den Preis', '我有%formatted_item_links要卖,私聊我问价格。', '', '', '', ''), + (1249, 'response_wtb_items_whisper', 'I’m selling %formatted_item_links, get it while it’s hot', 0, 0, '', 'Je vends %formatted_item_links, profitez-en pendant que c’est chaud !', 'Verkaufe %formatted_item_links, greif zu solange es heiß ist', '我在卖%formatted_item_links,手快有手慢无!', '', '', '', ''), + (1250, 'response_wtb_items_whisper', 'Selling %formatted_item_links, you won’t find a better deal', 0, 0, '', 'Je vends des %formatted_item_links, tu ne trouveras pas meilleure affaire que celle-ci.', 'Verkaufe %formatted_item_links, nen besseren Deal findet ihr nicht', '卖%formatted_item_links,绝对找不到比这更划算的了!', '', '', '', ''), + (1251, 'response_wtb_items_whisper', 'I have %formatted_item_links for sale, how much are you willing to pay?', 0, 0, '', 'J’ai des %formatted_item_links à vendre, combien es-tu prêt à payer?', 'Ich habe %formatted_item_links zu verkaufen, was zahlst du?', '我有%formatted_item_links要卖,你能出多少钱?', '', '', '', ''), + (1252, 'response_wtb_items_whisper', 'Selling %formatted_item_links, feel free to message me for the price', 0, 0, '', 'Je vends des %formatted_item_links, n’hésitez pas à me contacter pour connaître les prix.', 'Verkaufe %formatted_item_links, Preis auf Anfrage per flüstern', '卖%formatted_item_links,想知道价格就私聊我。', '', '', '', ''), + (1253, 'response_wtb_items_whisper', 'Interested in %formatted_item_links? I’ve got them for sale', 0, 0, '', 'Intéressé par %formatted_item_links? J’en ai en vente.', 'Interesse an %formatted_item_links? Ich verkaufe welche', '对%formatted_item_links感兴趣吗?我有卖的。', '', '', '', ''), + (1254, 'response_wtb_items_whisper', 'I have %formatted_item_links, PM me if you want them', 0, 0, '', 'J’ai %formatted_item_links, envoie-moi un message si tu les veux.', 'Ich habe %formatted_item_links, PN für Preis', '我有%formatted_item_links,想要就私聊我。', '', '', '', ''), + (1255, 'response_wtb_items_whisper', 'Selling %formatted_item_links at %cost_gold, hit me up', 0, 0, '', 'Je vends des %formatted_item_links à %cost_gold, n’hésitez pas à me contacter', 'Verkaufe %formatted_item_links für %cost_gold, meldet euch', '卖%formatted_item_links,价格%cost_gold,有意就找我。', '', '', '', ''), + (1256, 'response_wtb_items_whisper', 'I’m selling %formatted_item_links, make me an offer', 0, 0, '', 'Je vends des %formatted_item_links, faites-moi une offre.', 'Verkaufe %formatted_item_links, macht mir ein Angebot', '我在卖%formatted_item_links,给我开个价吧。', '', '', '', ''), + (1257, 'response_wtb_items_whisper', 'Got %formatted_item_links, message me for the details', 0, 0, '', 'J’ai des %formatted_item_links, envoie-moi un message pour obtenir les détails', 'Habe %formatted_item_links, schreib mir für Details', '我有%formatted_item_links,想了解详情就私聊我。', '', '', '', ''), + (1258, 'response_wtb_items_whisper', 'Hey, I’ve got %formatted_item_links, hit me up if you’re interested', 0, 0, '', 'Salut, j’ai des %formatted_item_links, fais-moi signe si cela t’intéresse.', 'Hey, habe %formatted_item_links, meldet euch bei Interesse', '嘿,我有%formatted_item_links,感兴趣就联系我。', '', '', '', ''), + (1259, 'response_wtb_items_whisper', 'Selling %formatted_item_links, let’s make a deal', 0, 0, '', 'Je vends des %formatted_item_links, on peut s’arranger sur le prix.', 'Verkaufe %formatted_item_links, machen wir nen Deal?', '卖%formatted_item_links,咱们谈个交易吧。', '', '', '', ''), + (1260, 'response_wtb_items_whisper', 'Selling %formatted_item_links, let me know if you want them', 0, 0, '', 'Je vends des %formatted_item_links, fais-moi savoir si tu en veux.', 'Verkaufe %formatted_item_links, sag Bescheid wenn du willst', '卖%formatted_item_links,想要就告诉我。', '', '', '', ''), + (1261, 'response_wtb_items_whisper', 'I have %formatted_item_links, I’ll make you a good deal', 0, 0, '', 'J’ai des %formatted_item_links, je vais te faire une bonne affaire', 'Ich habe %formatted_item_links, ich mach dir nen guten Preis', '我有%formatted_item_links,给你个好价格。', '', '', '', ''), + (1262, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links, make me an offer and it’s yours', 0, 0, '', 'J’ai %formatted_item_links, fais-moi une offre et il est à toi.', 'Ich habe %formatted_item_links, mit gutem Angebot gehörts dir', '我有%formatted_item_links,开个价,合适就归你了。', '', '', '', ''), + (1263, 'response_wtb_items_whisper', 'Selling %formatted_item_links, I’m open to offers', 0, 0, '', 'Je vends des %formatted_item_links, je suis ouvert aux offres.', 'Verkaufe %formatted_item_links, bin offen für Angebote', '卖%formatted_item_links,价格好商量。', '', '', '', ''), + (1264, 'response_wtb_items_whisper', 'I have %formatted_item_links, message me if you’re interested', 0, 0, '', 'J’ai %formatted_item_links, n’hésitez pas à me contacter si vous êtes intéressé(e).', 'Ich habe %formatted_item_links, melde dich bei Interesse', '我有%formatted_item_links,感兴趣就私聊我。', '', '', '', ''), + (1265, 'response_wtb_items_whisper', 'I can sell you %formatted_item_links for a good price', 0, 0, '', 'Je peux te vendre des %formatted_item_links à un bon prix.', 'Ich kann dir %formatted_item_links zu einem guten Preis verkaufen', '我可以便宜点卖给你%formatted_item_links。', '', '', '', ''), + (1266, 'response_wtb_items_whisper', 'I have %formatted_item_links for sale, I’ll consider any offers', 0, 0, '', 'J’ai des %formatted_item_links à vendre, je considérerai toutes les offres.', 'Ich habe %formatted_item_links zu verkaufen, nehme alle Angebote an', '我有%formatted_item_links要卖,任何报价我都会考虑。', '', '', '', ''), + (1267, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links for sale, message me for details', 0, 0, '', 'J’ai %formatted_item_links à vendre, envoyez-moi un message pour plus de détails.', 'Habe %formatted_item_links im Verkauf, flüster mir für Details', '我有%formatted_item_links要卖,详情私聊我。', '', '', '', ''), + (1268, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links for sale, make me an offer', 0, 0, '', 'J’ai des %formatted_item_links à vendre, faites-moi une offre.', 'Habe %formatted_item_links im Verkauf, macht mir ein Angebot', '我有%formatted_item_links要卖,给我开个价。', '', '', '', ''), + (1269, 'response_wtb_items_whisper', 'I have %formatted_item_links, how much are you willing to pay?', 0, 0, '', 'J’ai des %formatted_item_links, combien es-tu prêt à payer?', 'Ich habe %formatted_item_links, was zahlt ihr?', '我有%formatted_item_links,你愿意出多少钱?', '', '', '', ''), + (1270, 'response_wtb_items_whisper', 'I’m selling %formatted_item_links, message me for more info', 0, 0, '', 'Je vends %formatted_item_links, n’hésitez pas à me contacter pour plus d’informations.', 'Verkaufe %formatted_item_links, schreib mir für mehr Infos', '我在卖%formatted_item_links,想了解更多就私聊我。', '', '', '', ''), + (1271, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links, let me know if you’re interested', 0, 0, '', 'J’ai %formatted_item_links, fais-moi savoir si cela t’intéresse.', 'Ich habe %formatted_item_links, sagt Bescheid bei Interesse', '我有%formatted_item_links,感兴趣就告诉我。', '', '', '', ''), + (1272, 'response_wtb_items_whisper', 'Selling %formatted_item_links, let’s talk numbers', 0, 0, '', 'Je vends des %formatted_item_links, parlons chiffres.', 'Verkaufe %formatted_item_links, lass uns über Zahlen reden', '卖%formatted_item_links,咱们聊聊价格吧。', '', '', '', ''), + (1273, 'response_wtb_items_whisper', 'I can sell you %formatted_item_links, just let me know what you’re offering', 0, 0, '', 'Je peux vous vendre %formatted_item_links, il suffit de me dire ce que vous proposez.', 'Ich kann dir %formatted_item_links verkaufen, was bietest du?', '我可以卖给你%formatted_item_links,告诉我你能出啥价。', '', '', '', ''), + (1274, 'response_wtb_items_whisper', 'Hey, I’ve got %formatted_item_links for sale, hit me up if you’re interested', 0, 0, '', 'Salut, j’ai des %formatted_item_links à vendre, fais-moi signe si ça t’intéresse', 'Hey, habe %formatted_item_links zu verkaufen, meldet euch bei Interesse', '嘿,我有%formatted_item_links要卖,感兴趣就找我。', '', '', '', ''), + (1275, 'response_wtb_items_whisper', 'I have %formatted_item_links, PM me for the price', 0, 0, '', 'J’ai %formatted_item_links, contactez-moi en privé pour connaître le prix.', 'Ich habe %formatted_item_links, PN für den Preis', '我有%formatted_item_links,私聊我问价格。', '', '', '', ''), + (1276, 'response_wtb_items_whisper', 'Selling %formatted_item_links, I’m open to offers', 0, 0, '', 'Je vends %formatted_item_links, je suis ouvert aux offres.', 'Verkaufe %formatted_item_links, bin offen für Angebote', '卖%formatted_item_links,价格可以商量。', '', '', '', ''), + (1277, 'response_wtb_items_whisper', 'I have %formatted_item_links, feel free to message me', 0, 0, '', 'J’ai des %formatted_item_links, n’hésite pas à me contacter.', 'Ich habe %formatted_item_links, schreibt mir einfach', '我有%formatted_item_links,随时可以联系我。', '', '', '', ''), + (1278, 'response_wtb_items_whisper', 'I can offer %formatted_item_links, just let me know if you’re interested', 0, 0, '', 'Je peux vous proposer %formatted_item_links, dites-moi si cela vous intéresse.', 'Kann %formatted_item_links anbieten, sagt Bescheid bei Interesse', '我可以提供%formatted_item_links,感兴趣就告诉我。', '', '', '', ''), + (1279, 'response_wtb_items_whisper', 'Selling %formatted_item_links, I can work with any reasonable offer', 0, 0, '', 'Je vends des %formatted_item_links, je peux m’adapter à toute offre raisonnable.', 'Verkaufe %formatted_item_links, bitte vernünftige Angebote', '卖%formatted_item_links,合理报价我都能接受。', '', '', '', ''), + (1280, 'response_wtb_items_whisper', 'I have %formatted_item_links, hit me up for more info', 0, 0, '', 'J’ai des %formatted_item_links, n’hésitez pas à me contacter pour plus d’informations.', 'Ich habe %formatted_item_links, meldet euch für mehr Infos', '我有%formatted_item_links,想知道更多就找我。', '', '', '', ''), + (1281, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links, make me an offer', 0, 0, '', 'J’ai %formatted_item_links, proposez-moi quelque chose.', 'Ich habe %formatted_item_links, macht mir ein Angebot', '我有%formatted_item_links,给我开个价吧。', '', '', '', ''), + (1282, 'response_wtb_items_whisper', 'Selling %formatted_item_links, shoot me a message if you’re interested', 0, 0, '', 'Je vends des %formatted_item_links, envoie-moi un message si ça t’intéresse', 'Verkaufe %formatted_item_links, schreibt mir bei Interesse', '卖%formatted_item_links,感兴趣就给我发消息。', '', '', '', ''), + (1283, 'response_wtb_items_whisper', 'I’ve got %formatted_item_links for sale, let’s talk business', 0, 0, '', 'J’ai des %formatted_item_links à vendre, parlons affaires.', 'Habe %formatted_item_links zu verkaufen, lass uns handeln', '我有%formatted_item_links要卖,咱们谈谈生意吧。', '', '', '', ''), + (1284, 'response_wtb_items_whisper', 'I have %formatted_item_links, let me know if you want them', 0, 0, '', 'J’ai des %formatted_item_links, tu veux que je te les envoie ?', 'Ich habe %formatted_item_links, sag Bescheid wenn du willst', '我有%formatted_item_links,想要就告诉我。', '', '', '', ''), + (1285, 'quest_accept', 'Quest accepted', 0, 0, '', 'Quête accepté!', 'Quest angenommen', '任务已接受。', '', 'Misión aceptada', '', 'Квест принят'), + (1286, 'quest_remove', 'Quest removed', 0, 0, '', 'Quête supprimée', 'Quest entfernt', '任务已删除。', '', 'Misión eliminada', '', 'Квест удален'), + (1287, 'quest_cant_take', 'I can’t take this quest', 0, 0, '', 'Je ne peux pas prendre cette quête.', 'Ich kann diese Quest nicht annehmen', '我无法接受这个任务。', '', 'No puedo tomar esta búsqueda', '', 'Я не могу взять этот квест'), + (1288, 'quest_error_talk', 'I can’t talk with the quest giver', 0, 0, '', 'Je ne peux pas parler à ce donneur de quête', 'Ich kann nicht mit dem Questgeber sprechen', '我无法与任务发布者交谈。', '', 'No puedo hablar con el propietario de la mision', '', ''), + (1289, 'quest_error_completed', 'I have already completed %quest', 0, 0, '', 'J’ai déjà accompli %quête', 'Ich habe %quest bereits abgeschlossen', '我已经完成了%quest。', '', 'Ya he completado la mision %quest', '', 'Я уже выполнил %quest'), + (1290, 'quest_error_have_quest', 'I already have %quest', 0, 0, '', 'Je possède déjà %quest', 'Ich habe %quest bereits', '我已经拥有%quest。', '', 'Ya tengo la mision %quest', '', 'У меня уже есть %quest'), + (1291, 'quest_error_cant_take', 'I can’t take %quest', 0, 0, '', 'Je ne peux pas accomplir cette %quest.', 'Ich kann %quest nicht annehmen', '我无法接受%quest。', '', 'No puedo aceptar la mision %quest', '', ''), + (1292, 'quest_error_log_full', 'I can’t take %quest because my quest log is full', 0, 0, '', 'Je ne peux pas prendre %quest, car mon journal de quêtes est plein.', 'Ich kann %quest nicht annehmen, mein Questlog ist voll', '我没法接%quest,任务日志已经满了!', '', 'No puedo aceptar la mision %quest porque mi registro de misiones esta completo', '', ''), + (1293, 'quest_error_bag_full', 'I can’t take %quest because my bag is full', 0, 0, '', 'Je ne peux pas prendre %quest car mon sac est plein.', 'Ich kann %quest nicht annehmen, meine Tasche ist voll', '我没法接%quest,背包已经塞满了!', '', 'No puedo aceptar la mision %quest porque mi inventario esta lleno', '', ''), + (1294, 'quest_accepted', 'I have accepted %quest', 0, 0, '', 'J’ai accepté la %quête.', 'Ich habe %quest angenommen', '我已经接下了%quest任务!', '', 'He aceptado la mision %quest', '', 'Я принял %quest'), + (1295, 'quest_status_incomplete', 'I have not completed the quest %quest', 0, 0, '', 'Je n’ai pas encore terminé la quête %quest.', 'Ich habe die Quest %quest nicht abgeschlossen', '我还没完成%quest任务呢。', '', 'No he completado la mision %quest', '', 'Я не выполнил квест %quest'), + (1296, 'quest_status_available', 'Quest %quest available', 0, 0, '', 'Il y a une %quête disponible.', 'Quest %quest verfügbar', '%quest任务可以接了!', '', 'Mision %quest disponible', '', 'Квест %quest доступен'), + (1297, 'quest_status_failed', 'I have failed the quest %quest', 0, 0, '', 'J’ai échoué à la quête %quest.', 'Ich habe die Quest %quest vergeigt', '我失败了%quest任务。', '', 'He fallado la mision %quest', '', 'Я провалил квест %quest'), + (1298, 'quest_status_unable_to_complete', 'I am unable to turn in the quest %quest', 0, 0, '', 'Je ne peux pas terminer la quête %quest.', 'Ich kann die Quest %quest nicht abgeben', '我没法交%quest任务。', '', 'No puedo entregar la mision %quest', '', 'Я не могу сдать квест %quest'), + (1299, 'quest_status_completed', 'I have completed the quest %quest', 0, 0, '', 'J’ai terminé la quête %quest', 'Ich habe die Quest %quest abgeschlossen', '我已经完成了%quest任务!', '', 'He completado la mision %quest', '', 'Я выполнил квест %quest'), + (1300, 'quest_status_complete_single_reward', 'I have completed the quest %quest and received %item', 0, 0, '', 'J’ai terminé la quête %quest et j’ai reçu %item.', 'Ich habe die Quest %quest abgeschlossen und %item erhalten', '我完成了%quest任务,拿到了%item!', '', 'He completado la mision %quest y he recibido %item', '', 'Я выполнил квест %quest и получил %item'), + (1301, 'quest_status_complete_pick_reward', 'Which reward should I pick for completing the quest %quest?%rewards', 0, 0, '', 'Quelle récompense devrais-je choisir pour avoir complété la quête %quest? %rewards', 'Welche Belohnung soll ich für die Quest %quest nehmen? %rewards', '完成%quest任务后,我该选哪个奖励呢?%rewards', '', '¿Que recompensa deberia escoger por completar la mision %quest?%rewards', '', 'Какую награду выбрать за выполнение квеста %quest?%rewards'), + (1302, 'quest_choose_reward', 'Okay, I will pick %item as a reward', 0, 0, '', 'D’accord, je vais choisir %item comme récompense.', 'Okay, ich nehme %item als Belohnung', '好吧,我就选%item作为奖励吧。', '', 'De acuerdo, voy a escoger %item de recompensa', '', 'Хорошо, выберу %item в качестве награды'), + (1303, 'hello', 'Hello', 0, 0, '', 'Salut!', 'Hallo', '你好!', '', 'Hola', '', 'Привет'), + (1304, 'hello', 'Hello!', 0, 0, '', 'Salut!', 'Hallo!', '你好啊!', '', '¡Hola!', '', 'Привет!'), + (1305, 'hello', 'Hi', 0, 0, '', 'Salut! Comment ça va?', 'Hi', '嗨!', '', 'Hola', '', 'Здравствуй'), + (1306, 'hello', 'Hi!', 0, 0, '', 'Salut!', 'Hi!', '嗨!', '', '¡Hola!', '', 'Здравствуй!'), + (1307, 'hello', 'Hello there!', 0, 0, '', 'Salut toi!', 'Hallo zusammen!', '嘿,你好!', '', '¡Hola!', '', 'Приветствую!'), + (1308, 'hello_follow', 'Hello, I follow you!', 0, 0, '', 'Bonjour, je te suis !', 'Hallo, ich folge dir!', '你好,我跟着你走!', '', '¡Hola, te sigo!', '', 'Привет, я следую за тобой!'), + (1309, 'hello_follow', 'Hello, lead the way!', 0, 0, '', 'Bonjour, je vais vous montrer le chemin !', 'Hallo, geh vor!', '你好,带路吧!', '', '¡Hola, guía el camino!', '', 'Привет, веди меня!'), + (1310, 'hello_follow', 'Hi, lead the way!', 0, 0, '', 'Salut, montre le chemin!', 'Hi, geh vor!', '嗨,带路吧!', '', '¡Hola, guía el camino!', '', 'Привет, веди меня!'), + (1311, 'join_group', 'Hey %player, do you want join my group?', 0, 0, '', 'Salut %player, est-ce que tu veux rejoindre mon groupe ?', 'Hey %player, willst du in meine Gruppe?', '嘿,%player,要不要加入我的队伍?', '', 'Oye %player, ¿Quieres unirte a mi grupo?', '', 'Эй %player, хочешь в мою группу?'), + (1312, 'join_raid', 'Hey %player, do you want join my group?', 0, 0, '', 'Salut %player, est-ce que tu veux rejoindre mon groupe?', 'Hey %player, kommst du in meine Gruppe?', '嘿,%player,要不要加入我的团队?', '', 'Oye %player, ¿Quieres unirte a mi banda?', '', 'Эй %player, хочешь в мою группу?'), + (1313, 'logout_cancel', 'Logout cancelled!', 0, 0, '', 'Annulation de la déconnexion !', 'Logout abgebrochen!', '退出取消了!', '', 'Cerrar sesión cancelado!', '', 'Выход отменен!'), + (1314, 'logout_start', 'I’m logging out!', 0, 0, '', 'Je me déconnecte !', 'Ich logge mich aus!', '我要下线了!', '', '¡Me estoy desconectando!', '', 'Я выхожу из игры!'), + (1315, 'goodbye', 'Goodbye!', 0, 0, '', 'Au revoir!', 'Auf Wiedersehen!', '再见!', '', '¡Adiós!', '', 'До свидания!'), + (1316, 'goodbye', 'Bye bye!', 0, 0, '', 'Au revoir!', 'Tschüss!', '拜拜!', '', '¡Adiós!', '', 'Пока!'), + (1317, 'goodbye', 'See you later!', 0, 0, '', 'À plus tard !', 'Bis später!', '回头见!', '', '¡Hasta luego!', '', 'Увидимся!'), + (1318, 'reply', 'what was that %s?', 0, 0, '', 'c’était quoi ce %s?', 'Was war das, %s?', '那是什么,%s?', '', '¿qué has dicho %s?', '', 'что это было, %s?'), + (1319, 'reply', 'not sure I understand %s?', 0, 0, '', 'Je ne suis pas sûre de comprendre %s.', 'Bin nicht sicher, ob ich dich verstehe, %s?', '我不确定我懂了%s的意思?', '', '¿no estoy seguro de entenderte %s?', '', 'не уверен, что понял %s?'), + (1320, 'reply', 'uh... no clue what yer talkin bout', 0, 0, '', 'Oh, je ne sais vraiment pas de quoi vous parlez.', 'äh... keine Ahnung, wovon du redest', '呃……完全不知道你在说啥。', '', 'uh... ni idea de lo que estás hablando', '', 'эээ... не понимаю, о чем ты'), + (1321, 'reply', 'you talkin to me %s?', 0, 0, '', 'Tu me parles, %s ?', 'Redest du mit mir, %s?', '你在跟我说话吗,%s?', '', '¿Me hablas a mí %s?', '', 'ты ко мне обращаешься, %s?'), + (1322, 'reply', 'whaaaa?', 0, 0, '', 'Oh là là, qu’est-ce qui se passe?', 'whaaaa?', '啥???', '', '¿quéaaa?', '', 'чт-что?'), + (1323, 'reply', 'huh?', 0, 0, '', 'heein ?', 'hä?', '哈?', '', '¿eh?', '', 'а?'), + (1324, 'reply', 'what?', 0, 0, '', 'Quoi ?', 'was?', '啥?', '', '¿qué?', '', 'что?'), + (1325, 'reply', 'are you talking?', 0, 0, '', 'Tu es en train de parler ?', 'redest du grade?', '你在说话吗?', '', '¿estás hablando?', '', 'ты говоришь?'), + (1326, 'reply', 'whatever dude', 0, 0, '', 'on s’en fou mec', 'mir egal, Digga', '随便吧,兄弟。', '', 'Menudo tio...', '', 'как хочешь'), + (1327, 'reply', 'you lost me', 0, 0, '', 'tu m’as perdu', 'da bin ich raus', '你把我整懵了。', '', 'No entiendo ni papa', '', 'ты меня запутал'), + (1328, 'reply', 'Bla bla bla...', 0, 0, '', 'bla bla bla ...', 'Bla bla bla...', '叽里呱啦……', '', 'Bla bla bla...', '', 'Бла бла бла...'), + (1329, 'reply', 'What did you say, %s?', 0, 0, '', 'Qu’est-ce que tu as dit, %s?', 'Was hast du gesagt, %s?', '你说了啥,%s?', '', '¿Qué me estas contando %s?', '', 'Что ты сказал, %s?'), + (1330, 'reply', 'Concentrate on the game, %s!', 0, 0, '', 'Concentre-toi sur le jeu, %s!', 'Konzentrier dich aufs Spiel, %s!', '专心点,%s!', '', '¡Concéntrate en el juego, %s!', '', 'Сконцентрируйся на игре, %s!'), + (1331, 'reply', 'Chatting with you %s is so great! I always wanted to meet you', 0, 0, '', 'Discuter avec toi %s est tellement génial! J’ai toujours voulu te rencontrer.', 'Mit dir zu chatten, %s, ist so großartig! Wollte dich immer schon treffen', '跟%s你聊天真是太棒了!我早就想认识你了!', '', '¡%s, chatear contigo es genial! Siempre quise conocerte', '', 'Общаться с тобой, %s, так здорово! Всегда хотел встретиться'), + (1332, 'reply', 'These chat-messages are freaking me out! I feel like I know you all!', 0, 0, '', 'Ces messages de chat me rendent dingue! J’ai l’impression de tous vous connaître!', 'Diese Chatnachrichten machen mich fertig! Fühlt sich an, als würde ich euch alle kennen!', '这些聊天信息让我有点慌!感觉好像都认识你们!', '', '¡Estos mensajes de chat me están asustando! ¿De que cojones estais hablando?', '', 'Эти сообщения в чате сводят меня с ума! Такое чувство, что я всех вас знаю!'), + (1333, 'reply', 'YEAH RIGHT! HAHA SURE!!!', 0, 0, '', 'Bien sûr! Haha, ouais, bien sûr!', 'JA SICHER! HAHA KLAR!!!', '对对对!哈哈,肯定的!!!', '', '¡SÍ, CORRECTO! ¡¡¡JAJA SEGURO!!!', '', 'ДА ЛАДНО! ХАХА КОНЕЧНО!!!'), + (1334, 'reply', 'I believe you!!!', 0, 0, '', 'Je te crois !!!', 'Ich glaub dir!!!', '我信你!!!', '', '¡Si, claro!', '', 'Я тебе верю!!!'), + (1335, 'reply', 'OK, uhuh LOL', 0, 0, '', 'D’accord, euhuh MDR', 'Okay, ähm, lol', '好吧,嗯嗯,哈哈', '', 'OK, uhuh LOL', '', 'ОК, ага, ЛОЛ'), + (1336, 'reply', 'Why is everybody always saying the same things???', 0, 0, '', 'Pourquoi est-ce que tout le monde répète toujours les mêmes choses ???', 'Warum sagt eigentlich jeder immer dasselbe???', '为什么大家老是说一样的话???', '', '¿Por qué todo el mundo siempre dice lo mismo?', '', 'Почему все всегда говорят одно и то же???'), + (1337, 'reply', 'Hey %s....oh nevermind!', 0, 0, '', 'Hey %s....oh, laisse tomber!', 'Hey %s... ach egal!', '嘿,%s……算了,不说了!', '', 'Oye %s... ¡oh, no importa!', '', 'Эй %s... а, неважно!'), + (1338, 'reply', 'What are you talking about %s', 0, 0, '', 'De quoi parles-tu %s', 'Wovon redest du, %s', '你在说啥啊,%s?', '', 'De qué estás hablando %s', '', 'О чем ты, %s'), + (1339, 'reply', 'Who said that? I resemble that remark', 0, 0, '', 'Qui a dit ça ? Je ressemble à ce commentaire', 'Wer hat das gesagt? Ich fühl mich angesprochen', '谁说的?这话咋听着像说我呢?', '', '¿Quién ha dicho eso? ¿Estais hablando de mi?', '', 'Кто это сказал? Я похож на это замечание'), + (1340, 'reply', 'wtf are you all talking about', 0, 0, '', 'Qu’est-ce que vous racontez tous en fait?', 'wtf, worüber redet ihr alle', '你们都在扯啥啊?', '', '', '', 'чего вы все несете'), + (1341, 'reply', 'fr fr no cap on a stack', 0, 0, '', '', 'ehrlich jetzt, kein Witz', '真没骗你,兄弟,绝不撒谎!', '', '', '', 'фр фр, без шуток'), + (1342, 'reply', 'your not getting jack shit', 0, 0, '', 'Tu ne comprends que dalle.', 'du bekommst gar nix', '你啥也得不到!', '', '', '', 'ты ничего не получишь'), + (1343, 'reply', 'swag', 0, 0, '', '', 'Plus Aura!!!', '真帅!', '', '', '', 'сваг'), + (1344, 'reply', 'ty!', 0, 0, '', 'De rien !', 'thx!', '谢谢!', '', '', '', 'спасибо!'), + (1345, 'reply', 'no', 0, 0, '', 'non', 'nein', '不', '', '', '', 'нет'), + (1346, 'reply', 'Yep', 0, 0, '', 'ouai', 'jup', '嗯', '', '', '', 'Да'), + (1347, 'reply', 'f', 0, 0, '', '', 'f', '唉', '', '', '', 'ф'), + (1348, 'reply', '%s no shit xD', 0, 0, '', '%s sans déconner xD', '%s, kein Scheiß xD', '%s,真的假的xD', '', '', '', '%s, без шуток xD'), + (1349, 'reply', 'why is that', 0, 0, '', 'Pourquoi cela?', 'warum ist das so', '为啥啊?', '', '', '', 'почему так'), + (1350, 'reply', 'lmao', 0, 0, '', 'Je me marre à en pleurer.', 'ROFL', '笑死我了!', '', '', '', 'лмао'), + (1351, 'reply', 'thought i should remain silent, was getting confused from chat again', 0, 0, '', 'Je pensais que je devrais rester silencieux, je commençais à me perdre dans la conversation encore une fois.', 'dachte, ich halt besser die Klappe, der Chat hat mich wieder verwirrt', '我觉得我还是闭嘴吧,聊天又把我整糊涂了。', '', '', '', 'решил помолчать, снова запутался в чате'), + (1352, 'reply', 'i can get realy jealous', 0, 0, '', 'Je peux devenir vraiment jaloux.', 'ich kann richtig eifersüchtig werden', '我可会吃醋了!', '', '', '', 'я могу по-настоящему завидовать'), + (1353, 'reply', '%s you can’t hear the dripping sarcasm in my text', 0, 0, '', '%s, tu ne peux pas entendre le sarcasme qui goutte de mon texte?', '%s, du hörst den triefenden Sarkasmus in meinem text_loc3 nicht', '%s,你听不出我文字里满满的讽刺吗?', '', '', '', ''), + (1354, 'reply', 'he said no homo, it’s fine', 0, 0, '', 'Il a dit no homo, tout va bien.', 'er meinte „kein Ding“, passt schon', '他说没别的意思,那就没事。', '', '', '', ''), + (1355, 'reply', 'dwarf moment', 0, 0, '', '', 'Zwergen-Moment', '矮人时刻!', '', '', '', 'момент дворфа'), + (1356, 'reply', 'Yes %s', 0, 0, '', 'Oui %s', 'Ja, %s', '好的,%s', '', '', '', 'Да, %s'), + (1357, 'reply', 'interesting...', 0, 0, '', 'Intéressant...', 'interessant...', '有意思……', '', '', '', 'интересно...'), + (1358, 'reply', 'lol', 0, 0, '', 'Ah ah!', 'lol', '哈哈!', '', '', '', 'лол'), + (1359, 'reply', '%s fuck you man :D', 0, 0, '', '%s va te faire enculer :D', '%s, fick dich, Mann :D', '%s,去你的吧 :D', '', '', '', '%s, иди ты :D'), + (1360, 'reply', ':^)', 0, 0, '', '', ':^)', ':^)', '', '', '', ':^)'), + (1361, 'reply', 'ty', 0, 0, '', 'Merci à toi!', 'thx', '谢谢!', '', '', '', 'спс'), + (1362, 'reply', '%s well said', 0, 0, '', '%s Bien dit.', '%s, gut gesagt', '%s,说得好!', '', '', '', '%s, хорошо сказано'), + (1363, 'reply', 'yay', 0, 0, '', '', 'yay', '耶!', '', '', '', 'ура'), + (1364, 'reply', 'yeah', 0, 0, '', '', 'ja', '对对对!', '', '', '', 'да'), + (1365, 'reply', 'ooooooh', 0, 0, '', '', 'ooooooh', '哦哦哦!', '', '', '', 'ооооо'), + (1366, 'reply', 'hmm', 0, 0, '', '', 'hmm', '嗯……', '', '', '', 'хмм'), + (1367, 'reply', 'yeah right', 0, 0, '', 'Oui, c’est ça.', 'ja klar', '得了吧,真的?', '', '', '', 'ага, конечно'), + (1368, 'reply', 'you made me gag wtf', 0, 0, '', 'Tu m’as vraiment dégouté, c’est quoi ce délire ?', 'mir wird schlecht, wtf', '你让我恶心死了,搞什么啊!', '', '', '', 'ты заставил меня блевануть, что за'), + (1369, 'reply', 'hot', 0, 0, '', ' !', 'heiß', '真火爆!', '', '', '', 'жарко'), + (1370, 'reply', 'hoes mad', 0, 0, '', 'Les pétasses sont folles', 'die Weiber sind sauer', '一群人气炸了!', '', '', '', 'злятся'), + (1371, 'reply', 'what have you been eating %s', 0, 0, '', 'Qu’est-ce que tu as mangé %s', 'was hast du gegessen, %s', '%s,你到底吃了啥?', '', '', '', 'что ты ел, %s'), + (1372, 'reply', 'wtf', 0, 0, '', '', 'wtf', '啥情况?!', '', '', '', 'что за'), + (1373, 'reply', 'i will attempt to understand that comment', 0, 0, '', 'Je vais essayer de comprendre ce commentaire', 'ich versuche, diesen Kommentar zu verstehen', '我试着理解你这话啥意思……', '', '', '', 'попробую понять этот комментарий'), + (1374, 'reply', '*confused*', 0, 0, '', '*Je suis un peu perdu.*', '*verwirrt*', '*一脸懵逼*', '', '', '', '*в замешательстве*'), + (1375, 'reply', 'fuck yea', 0, 0, '', 'D’accord !', 'fuck ja', '太棒了!', '', '', '', 'да, черт возьми'), + (1376, 'reply', '0/10 would not read again', 0, 0, '', 'Hennetement, 0/10 je ne te relirais pas.', '0/10 würde ich nicht wieder lesen', '0/10,完全不想再看一次。', '', '', '', '0/10 не стал бы читать снова'), + (1377, 'reply', '10/10 would read again', 0, 0, '', '10/10 Absolument, je te relirais sans hésiter!', '10/10 würde ich wieder lesen', '10/10,绝对会再看一遍!', '', '', '', '10/10 прочитал бы снова'), + (1378, 'reply', '6/10 would read', 0, 0, '', 'Je dirais que c’est un 6/10 en termes de lecture.', '6/10 würde es lesen', '6/10,还行吧,能看。', '', '', '', '6/10 прочитал бы'), + (1379, 'reply', '7/10 would', 0, 0, '', '7/10 serait mon verdict.', '7/10 vielleicht', '7/10,不错啦。', '', '', '', '7/10 прочитал бы'), + (1380, 'reply', 'based', 0, 0, '', '', 'Respekt!', '真有道理!', '', '', '', 'бейсд'), + (1381, 'reply', 'oh yeah maybe', 0, 0, '', 'Oh oui, peut-être que oui.', 'oh ja, vielleicht', '哦,对,可能吧。', '', '', '', 'о да, может быть'), + (1382, 'reply', 'yeah so what', 0, 0, '', 'Ouais, donc quoi ?', 'ja und?', '对啊,那又咋样?', '', '', '', 'да, и что'), + (1383, 'reply', 'hey %s i havent forgotten you', 0, 1, '', 'salut %s je ne t’ai pas oublié', 'hey %s, ich habe dich nicht vergessen', '嘿,%s,我可没忘了你!', '', 'hey %s no te he olvidado', '', 'эй %s, я тебя не забыл'), + (1384, 'reply', 'you piss me off %s', 0, 1, '', 'Tu me saoules %s', 'du gehst mir hart auf den Sack, %s', '%s,你真让我火大!', '', 'me cabreas %s', '', 'ты меня бесишь, %s'), + (1385, 'reply', 'im gunna get you this time %s', 0, 1, '', 'je vais t’avoir cette fois %s', 'diesmal krieg ich dich, %s', '这次我非抓住你不可,%s!', '', 'voy a atraparte esta vez %s', '', 'я достану тебя в этот раз, %s'), + (1386, 'reply', 'better watch your back %s', 0, 1, '', 'Fais mieux de surveiller tes arrières %s', 'Pass besser auf, %s', '小心你的背后,%s!', '', 'mejor cuida tu espalda %s', '', 'лучше берегись, %s'), + (1387, 'reply', 'i did not like last round so much', 0, 1, '', 'Je n’ai pas aimé notre dernier round.', 'Die letzte Runde mochte ich nicht so', '上一轮真不咋样。', '', 'no me gustó tanto la última ronda', '', 'мне не понравился прошлый раунд'), + (1388, 'reply', 'i sucked last round thanks to %s', 0, 1, '', 'J’ai été nul lors de la dernière manche à cause de %s.', 'Letzte Runde war ich mies, danke, %s', '上一轮我表现烂透了,都怪%s!', '', 'chupé la última ronda gracias a %s', '', 'я был плох в прошлом раунде из-за %s'), + (1389, 'reply', 'prepare to die %s', 0, 1, '', 'Prépare-toi à mourir %s', 'Mach dich bereit zu sterben, %s', '准备受死吧,%s!', '', 'prepárate para morir %s', '', 'готовься умереть, %s'), + (1390, 'reply', 'dont appreciate you killin me %s', 0, 1, '', 'Je n’apprécie pas que tu me tues %s.', 'Fands nicht geil, dass du mich gekillt hast, %s', '%s,杀我可不厚道啊!', '', 'no aprecio que me mates %s', '', 'не нравится, что ты меня убил, %s'), + (1391, 'reply', '%s, i hate you', 0, 1, '', 'Oh, %s je te déteste.', '%s, ich hasse dich', '%s,我恨你!', '', '%s, te odio', '', '%s, я тебя ненавижу'), + (1392, 'reply', 'grrrrrr, ill get you this time %s', 0, 1, '', 'grrrrrr, je vais t’avoir cette fois %s', 'grrrrrr, diesmal krieg ich dich, %s', '嗷嗷,这次我非得干掉你,%s!', '', 'grrrrrr, te libraste esta vez %s', '', 'грррр, в этот раз я тебя достану, %s'), + (1393, 'reply', 'well fuck you', 0, 1, '', 'Eh bien va te faire mettre', 'FICK DICH! NEIN, FICK MICH!', '去你的吧!', '', 'vamos a la mierda', '', 'ну и пошел ты'), + (1394, 'reply', '%s i’ll vomit into your fkin mouth', 0, 1, '', '%s je vais te gerber dans la bouche', '%s, ich kotze dir in dein verficktes Maul', '%s,我要吐你一脸!', '', '', '', ''), + (1395, 'reply', 'dont fucking judge me', 0, 1, '', 'Ne me juge pas putain.', 'verurteil mich nicht, verdammt nochmal', '别他妈评头论足!', '', '', '', 'не суди меня'), + (1396, 'reply', 'Yo momma so fat, she can’t even fit through the Dark Portal', 0, 1, '', 'Ta mère est tellement grosse qu’elle ne peut même pas passer à travers le Portail des Ténèbres.', 'Deine Mom ist so fett, sie passt nicht mal durchs Dunkle Portal', '你妈胖得连黑暗之门都挤不进去!', '', '', '', ''), + (1397, 'reply', 'wtf', 0, 2, '', 'wtf', 'wtf', '啥情况?!', '', 'wtf', '', 'что за'), + (1398, 'reply', 'wtf??', 0, 2, '', 'wtf??', 'wtf??', '啥情况??', '', 'wtf??', '', 'что за??'), + (1399, 'reply', 'low life', 0, 2, '', 'low life', 'armselige Gestalt', '卑微的家伙!', '', 'low life', '', 'ничтожество'), + (1400, 'reply', 'wth', 0, 2, '', 'wth', 'was zum Teufel', '搞啥呢?', '', 'wth', '', 'что за'), + (1401, 'reply', 'sucked', 0, 2, '', 'nul à chier', 'das war scheiße', '太烂了!', '', 'chupate esa', '', 'отстой'), + (1402, 'reply', 'REMATCH!!! im taking him down', 0, 2, '', 'REVANCHE!!! Je vais le terrasser', 'REMATCH!!! Ich mach ihn platt', '再来一局!!我要把他干掉!', '', '¡¡REVANCHA!!! lo estoy derribando', '', 'РЕВАНШ!!! я его уделаю'), + (1403, 'reply', 'pathetic, i got killed by %s', 0, 2, '', 'Pathétique, je me suis fait tuer par %s.', 'peinlich, ich wurde von %s gekillt', '真丢人,被%s干掉了!', '', 'patético, me mató %s', '', 'жалко, меня убил %s'), + (1404, 'reply', 'ok I’m done', 0, 2, '', 'Ok j’en fini.', 'ok, ich bin raus', '好了,我受够了!', '', '', '', ''), + (1405, 'reply', 'hehe, i nailed %s?', 0, 3, '', 'Héhé, j’ai cloué %s?', 'hehe, hab %s weggeklatscht?', '嘿嘿,我搞定了%s?', '', 'jeje, ¿acerté a %s?', '', 'хе-хе, я уделал %s?'), + (1406, 'reply', 'that was too easy, killin %s', 0, 3, '', 'c’était trop facile, de tuer %s', 'viel zu easy, %s zu killen', '太简单了,干掉%s跟玩儿似的!', '', 'fue demasiado fácil matar a %s', '', 'это было слишком просто, убил %s'), + (1407, 'reply', 'gotcha mofo', 0, 3, '', 'je t’ai eu enfoiré', 'hab dich, du Bastard', '抓到你了,混蛋!', '', 'te pille hijo de perra', '', 'попался, дружок'), + (1408, 'reply', 'ha ha', 0, 3, '', 'Ha ha', 'haha', '哈哈!', '', 'ja, ja', '', 'ха-ха'), + (1409, 'reply', 'loser', 0, 3, '', 'loser', 'Loser', '输家!', '', 'loser', '', 'лузер'), + (1410, 'reply', 'i killed %s and yer all next dudes', 0, 3, '', 'J’ai éliminé %s et vous êtes tous les prochains, les gars.', 'ich hab %s gekillt und ihr seid als nächste dran, Jungs', '我干掉了%s,你们下一个,伙计们!', '', 'yo maté a %s y a todos los siguientes tipos', '', 'я убил %s, вы все следующие'), + (1411, 'reply', 'oh yeah i owned him', 0, 3, '', 'Oh ouais, je l’ai complètement battu', 'oh ja, hab ihn geownt', '哦耶,我完全碾压了他!', '', 'oh, sí, lo tenía', '', 'о да, я его уделал'), + (1412, 'reply', 'im a killin machine', 0, 3, '', 'Je suis une machine à tuer.', 'ich bin ne Killmaschine', '我就是杀戮机器!', '', 'soy una máquina de matar', '', 'я машина для убийств'), + (1413, 'reply', '%s, this reminds me of a Slayer song...all this bloodshed', 0, 3, '', 'Oh ça, ça me rappelle une chanson de Slayer... tout ce carnage', '%s, erinnert mich an einen Slayer-Song... dieses ganze Gemetzel', '%s,这让我想起一首杀手乐队的歌……全是血腥杀戮!', '', '%s, esto me recuerda a una canción de Slayer... todo este derramamiento de sangre', '', '%s, это напоминает мне песню Slayer... столько крови'), + (1414, 'reply', 'sorry, %s. can we do the scene again?', 0, 3, '', 'Désolé, %s. On peut refaire la scène?', 'sorry, %s. Können wir die Szene nochmal machen?', '抱歉,%s。咱们能再来一次吗?', '', 'lo siento, %s. ¿Podemos hacer la escena de nuevo?', '', 'извини, %s. можем повторить сцену?'), + (1415, 'reply', 'so....how do you like being worm food %s???', 0, 3, '', 'Alors, comment aimes-tu être de la bouffe pour vers de terre %s ??', 'na, wie gefällt es dir Wurmfutter zu sein, %s???', '嘿……%s,变成虫子粮食的感觉咋样啊???', '', 'así que... ¿cómo te gusta ser comida de gusanos %s???', '', 'ну как тебе быть кормом для червей, %s???'), + (1416, 'reply', 'yer supposed to be dead, %s its part of the game!!!!!', 0, 3, '', 'Je suis mort, %s, c’est une partie du jeu !!!!', 'du solltest tot sein, %s, das gehört zum Spiel!!!!!', '你应该早就死了,%s,这是游戏规则!!!!', '', '¡¡Se supone que estás muerto, %s es parte del juego!!!!!', '', 'ты должен быть мёртв, %s, это часть игры!!!!!'), + (1417, 'reply', 'sorry, %s. that looked as good as an Andy Worhol painting!', 0, 3, '', 'Désolé, %s. Cela ressemblait aussi bien qu’une peinture d’Andy Warhol!', 'sorry, %s. das sah so gut aus wie ein Kunstwerk von der Kirmes!', '抱歉,%s。这场景美得像安迪·沃霍尔的画作!', '', 'lo siento, %s. ¡Eso se veía tan bien como una pintura de Andy Worhol!', '', 'извини, %s. выглядело так же хорошо, как картина Энди Уорхола!'), + (1418, 'reply', '%s, ill use the rubber bullets next time !', 0, 3, '', 'D’accord, %s la prochaine fois j’utiliserai les balles en caoutchouc !', '%s, ich nehme nächstes Mal Gummigeschosse!', '%s,下次我用橡皮弹好了!', '', '%s, ¡usaré las balas de goma la próxima vez!', '', '%s, в следующий раз использую резиновые пули!'), + (1419, 'reply', 'whatsamatter, %s?? lose your head? hahaha gotta keep cool!!', 0, 3, '', 'Qu’est-ce qu’il se passe, %s ? Tu perds la tête ? Haha, faut garder son calme !', 'was ist los, %s?? Kopf verloren? hahaha, ruhig bleiben!!', '咋了,%s??掉了脑袋?哈哈,保持冷静!!', '', '¿qué pasa, %s?? ¿has perdido la cabeza? jajaja tengo que mantener la calma!!', '', 'что случилось, %s?? голову потерял? хахаха, надо сохранять хладнокровие!!'), + (1420, 'reply', 'i had to do it, %s. You understand. The Director said so!!', 0, 3, '', 'Il fallait que je le fasse, %s. Tu comprends. Le Directeur l’a dit !!', 'musste das tun, %s. Du verstehst schon. Der Regisseur hat es so gesagt!!', '我不得不这么做,%s。你懂的,是导演要求的!!', '', 'Tenía que hacerlo, %s. Entiendelo... ¡¡Dios me lo ordenó!!', '', 'я должен был это сделать, %s. Ты понимаешь. Режиссёр так сказал!!'), + (1421, 'reply', 'hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', 0, 3, '', 'Hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', 'hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', '嘿,%s.......哇哈哈哈哈哈哈哈哈哈', '', 'hey %s.......MUAHAHAHAHAHAHAHAHAHAHA', '', 'эй, %s.......МУАХАХАХАХАХАХА'), + (1422, 'reply', '%s, i enjoyed that one!! Lets play it again Sam', 0, 3, '', '%s, j’ai adoré celui-là!! On rejoue Sam', '%s, das hab ich genossen!! Wiederholung bitte!', '%s,这局我玩得爽!!再来一次吧,山姆!', '', '%s, ¡disfruté ese! Juguemos de nuevo Sam', '', '%s, мне это понравилось!! Давай сыграем ещё раз, Сэм'), + (1423, 'reply', 'hey, %s! ju can start callin me scarface.. ju piece of CHIT!!!!', 0, 3, '', 'Hé, %s ! tu peux commencer à m’appeler Scarface .. sac à merde !!!!', 'hey, %s! Kannst mich ab jetzt "Scarface" nennen.. du Stück SCHEI**!!!!', '嘿,%s!以后你可以叫我刀疤脸……你个小废物!!!!', '', '¡oye, %s! ¡Puedes empezar a llamarme caracortada... ¡¡¡un trozo de CHIT!!!', '', 'эй, %s! можешь звать меня ликом... ты кусок ЧЕРТА!!!!'), + (1424, 'reply', 'are you talking to me %s??', 0, 3, '', 'Tu me parles, %s?', 'redest du mit mir, %s??', '你是在跟我说话吗,%s??', '', '¿me estás hablando a mí %s?', '', 'ты со мной разговариваешь, %s??'), + (1425, 'reply', '%s get it right this time, dont stand in front of my bullets.', 0, 3, '', 'Désolé %s, ne te mets pas devant mes balles cette fois-ci.', '%s, mach es diesmal richtig, stell dich nicht vor meine Kugeln.', '%s,这次别搞错了,别站在我的子弹前面。', '', '%s hazlo bien esta vez, no te pares frente a mis balas.', '', '%s, в этот раз не стой перед моими пулями.'), + (1426, 'reply', '%s, what are you laying around for??? hehe', 0, 3, '', 'Oups, %s qu’est-ce que tu fais à trainer là??? hihi', '%s, warum liegst du da rum??? hehe', '%s,你躺在这儿干嘛呢???嘿嘿', '', '%s, ¿para qué estás tirado? jeje', '', '%s, чего ты валяешься??? хехе'), + (1427, 'reply', 'laughed real hard', 0, 3, '', 'J ai vraiment bien rigoler.', 'hab mich totgelacht', '笑得我肚子都疼了!', '', '', '', 'сильно смеялся'), + (1428, 'reply', 'hi %s', 0, 4, '', 'Salut %s', 'hi %s', '嗨,%s', '', 'hola %s', '', 'привет %s'), + (1429, 'reply', 'oh, hi %s', 0, 4, '', 'oh, salut %s', 'oh, hi %s', '哦,嗨,%s', '', 'oh, hola %s', '', 'о, привет %s'), + (1430, 'reply', 'wazzup %s!!!', 0, 4, '', 'Salut %s, ça roule !!!', 'was geht, %s!!!', '咋样啊,%s!!!', '', 'wazzup %s!!!', '', 'как дела, %s!!!'), + (1431, 'reply', 'hi', 0, 4, '', 'Salut! Comment ça va?', 'hi', '嗨!', '', 'hi', '', 'Здравствуй'), + (1432, 'reply', 'wazzup', 0, 4, '', 'Salut! Comment ça va?', 'was geht', '最近咋样?', '', 'wazzup', '', 'как дела'), + (1433, 'reply', 'hello %s', 0, 4, '', 'Salut %s', 'hallo %s', '你好,%s', '', 'hola %s', '', 'здравствуй %s'), + (1434, 'reply', 'hi %s, do i know you?', 0, 4, '', 'Salut %s, est-ce qu’on se connait ?', 'hi %s, kennen wir uns?', '嗨,%s,我们认识吗?', '', 'hola %s, ¿te conozco?', '', 'привет %s, мы знакомы?'), + (1435, 'reply', 'hey %s', 0, 4, '', 'Salut %s', 'hey %s', '嘿,%s', '', 'hey %s', '', 'эй %s'), + (1436, 'reply', 'hai %s', 0, 4, '', 'Salut %s', 'hai %s', '嗨,%s', '', 'hai %s', '', 'хай %s'), + (1437, 'reply', 'wth', 0, 6, '', 'wth', 'was zur Hölle', '搞什么鬼?', '', 'wth', '', 'что за'), + (1438, 'reply', 'wtf', 0, 6, '', 'wtf', 'WTF', '什么情况?!', '', 'que cojones!', '', 'что за'), + (1439, 'reply', 'this is bs', 0, 6, '', 'c’est vraiment de la merde', 'das ist Bullshit', '这简直是胡扯!', '', 'esto es una mierda', '', 'это бред'), + (1440, 'reply', 'admin', 0, 6, '', 'admin', 'admin', '管理员', '', 'admin', '', 'админ'), + (1441, 'reply', 'hey %s quit abusing your admin', 0, 6, '', 'Hey %s, arrête d’abuser de tes droits d’admin.', 'hey %s, hör auf, deine Admin-Rechte zu missbrauchen', '嘿,%s,别滥用你的管理员权限!', '', 'hey %s deja de abusar de tu poder de administrador', '', 'эй %s, хватит злоупотреблять своими правами админа'), + (1442, 'reply', 'leave me alone admin!', 0, 6, '', 'Laisse-moi tranquille, admin !', 'lass mich in Ruhe, Admin!', '管理员,别烦我!', '', '¡déjame en paz administrador!', '', 'оставь меня в покое, админ!'), + (1443, 'reply', 'you suck admin', 0, 6, '', 't’es une merde l’admin.', 'du bist scheisse, Admin', '管理员,你真烂!', '', 'apestas administrador', '', 'ты отстой, админ'), + (1444, 'reply', 'thats my name, what you want %s', 0, 5, '', 'c’est mon nom, qu’est-ce que tu veux %s', 'das ist mein Name, was willst du, %s', '那是我的名字,你想干嘛,%s?', '', 'ese es mi nombre, ¿que quieres %s?', '', 'это моё имя, что тебе нужно %s'), + (1445, 'reply', 'yes???', 0, 5, '', 'Oui???', 'ja???', '啥事???', '', 'sí???', '', 'да???'), + (1446, 'reply', 'uh... what', 0, 5, '', 'Heein ... quoi ???', 'äh... was', '呃……啥?', '', 'uh... qué', '', 'эээ... что'), + (1447, 'reply', 'you talkin to me %s?', 0, 5, '', 'Tu me parles, %s?', 'redest du mit mir, %s?', '你跟我说话呢,%s?', '', '¿Me hablas a mí %s?', '', 'ты ко мне обращаешься, %s?'), + (1448, 'taunt', 'I have puppies under my armor!', 0, 0, '', 'J’ai des chiots sous mon armure!', 'Ich habe Welpen unter meiner Rüstung!', '我的盔甲底下藏着小狗崽呢!', '', '¡Tengo cachorros bajo mi armadura!', '', 'У меня под бронёй щенки!'), + (1449, 'taunt', 'Bite me, !', 0, 0, '', 'Va te faire voir, !', 'Beiß mich, !', '来咬我啊,!', '', '¡Muérdeme, !', '', 'Укуси меня, !'), + (1450, 'taunt', 'Hey ! Guess what your mom said last night!', 0, 0, '', 'Hey ! Devine ce que ta mère a dit hier soir!', 'Hey ! Rate mal, was deine Mom letzte Nacht gesagt hat!', '嘿,!猜猜你妈昨晚说了啥!', '', '¡Oye, ! ¡Adivina lo que me dijo tu madre anoche!', '', 'Эй, ! Угадай, что твоя мама сказала прошлой ночью!'), + (1451, 'taunt', ', you’re so ugly you couldn’t score in a monkey whorehouse with a bag of bananas!', 0, 0, '', ', tu es tellement moche que tu n’arriverais même pas à marquer dans une maison close pour singes avec un sac de bananes!', ', du bist so hässlich, du würdest nicht mal im Affenbordell mit einem Sack Bananen landen!', ',你丑得连猴子妓院里带一袋香蕉都捞不到妹子!', '', ', ¡eres tan feo que no podrías anotar en un burdel de monos con una bolsa de plátanos!', '', ''), + (1452, 'taunt', 'Shut up , you’ll never be the man your mother is!!', 0, 0, '', ', tais-toi, tu ne seras jamais l’homme que ta mère est !!', 'Halt die Klappe, , du wirst so ein Mann sein, wie deine Mutter!!', '闭嘴,,你永远也比不上你妈那股子爷们儿气!', '', 'Cállate , ¡nunca serás el hombre que es tu madre!', '', ''), + (1453, 'taunt', 'Your mother was a hampster and your father smelt of elderberries!!!!', 0, 0, '', 'Ta mère était un hamster et ton père sentait la myrtille!!!!', 'Deine Mutter ist so fett, dass man eine Pokéflöte braucht, um sie aufzuwecken und dein Vater ist der Trainer!!!!', '你妈是只仓鼠,你爸身上一股接骨木浆果味儿!!!!', '', 'Tu madre era un hámster y tu padre olía a bayas de saúco!!!!', '', 'Твоя мать была хомяком, а отец пах одуванчиками!!!!'), + (1454, 'taunt', 'I don’t want to talk to you no more, you empty headed animal food trough wiper!!!', 0, 0, '', 'Je ne veux plus te parler, espèce de chiffon mangeur de nourriture pour animaux!!!', 'Dich als dumm zu bezeichnen, wäre eine Beleidigung für dumme Leute!', '我不想再跟你废话了,你个脑子空空的喂牲口槽的废物!!!', '', '¡¡No quiero hablar más contigo, limpiaparabrisas de cabeza vacía!', '', ''), + (1455, 'taunt', 'I fart in your general direction!!!', 0, 0, '', 'Je te pète à la figure !!!', 'Ich furze in deine allgemeine Richtung!!!', '我朝你大方向放了个屁!!!', '', '¡¡Me tiro un pedo en tu dirección general!!!', '', 'Я пускаю в твою сторону газы!!!'), + (1456, 'taunt', 'Go and boil your bottom, you son of a silly person!!!', 0, 0, '', 'Va faire bouillir ton derrière, espèce de fils d’une personne stupide !!!', 'Jeder Atemzug, den ich ohne deine Erlaubnis mache, steigert mein Selbstwertgefühl!', '去把你屁股煮熟吧,你个傻子的崽!!!', '', '¡Ve y hierve tu trasero, hijo de una persona tonta!', '', 'Иди и вскипяти свою задницу, сын смешного человека!!!'), + (1457, 'taunt', 'What are you going to do , bleed on me? HAVE AT YOU!', 0, 0, '', 'Que comptes-tu faire, , me saigner dessus ? VIENS TE BATTRE !', 'Was willst du tun , mich vollbluten? NIMM DAS!', '你能拿我咋样,,往我身上淌血吗?来干一架吧!', '', '¿Qué vas a hacer , llorar?', '', 'Что ты собираешься делать, , заставить меня кровоточить? ВПЕРЁД!'), + (1458, 'taunt', 'M-O-O-N! That spells aggro!', 0, 0, '', 'M-O-O-N! Ça, ça annonce des ennuis!', 'A-G-G-R-O! Das heißt Bosszeit!', 'M-O-O-N!这就意味着开打!', '', '¡LUNA! ¡Eso deletrea agresividad!', '', 'М-О-О-Н! Это значит агр!'), + (1459, 'taunt', 'You’re about as useful as a one-legged man in an ass kicking contest.', 0, 0, '', 'You know, tu es aussi utile qu’un homme à une jambe dans un concours de coups de pied aux fesses.', 'Wir hatten grausame und hatten irre Begleiter, aber mit dir sind wir mit einem grausamen Irren gestraft!', '你这人跟独腿男参加踢屁股比赛一样没用。', '', 'Eres tan útil como un hombre con una sola pierna en un concurso de patadas en el culo.', '', ''), + (1460, 'taunt', 'Hey ! Stop hitting on them, they’re not your type. They aren’t inflatable.', 0, 0, '', 'Hé ! Arrête d’essayer de les draguer, Elles ne sont pas de ton style. Elles ne sont pas gonflables.', 'Hey ! Hör auf, sie anzubaggern, sie ist nicht dein Typ. Die sind nicht aufblasbar.', '嘿,!别再勾搭了,人家不是你那款,不是充气娃娃。', '', '¡Oye, ! Deja de coquetear con ellos, no son tu tipo. No son inflables.', '', ''), + (1461, 'taunt', ' you’re so far outta your league, you’re playing a different sport.', 0, 0, '', ' tu es tellement dépassé, tu joues à un autre sport.', ', du bist so weit außerhalb deiner Liga, du spielst schon eine andere Sportart.', ',你完全不是一个量级的,简直在玩不同的运动。', '', ' estás tan lejos de tu liga, estás jugando un deporte diferente.', '', ''), + (1462, 'taunt', 'You made a big mistake today , you got out of bed.', 0, 0, '', ', tu as commis une grosse erreur aujourd’hui en sortant du lit.', 'Großer Fehler heute Morgen, : Du bist aufgestanden!', '你今天犯了大错,,你不该起床的。', '', 'Cometiste un gran error hoy , te levantaste de la cama.', '', 'Ты сегодня совершил большую ошибку, , ты встал с кровати.'), + (1463, 'taunt', 'I wanna try turning into a horse, but I need help. I’ll be the front, you be yourself.', 0, 0, '', 'Je veux essayer de me transformer en cheval, mais j’ai besoin d’aide. Tu seras à l’avant, et je serai juste moi-même.', 'Ich will mal versuchen, mich in ein Pferd zu verwandeln, brauche aber Hilfe. Ich mach die Vorderseite, du bleibst du.', '我想试试变身成马,但需要帮忙。我来当前面,你就做你自己吧。', '', 'Quiero intentar convertirme en un caballo, pero necesito ayuda. Yo seré el frente, tú mismo.', '', ''), + (1464, 'taunt', 'Can I borrow your face for a few days? My ass is going on holiday....', 0, 0, '', 'Je peux emprunter ton visage pendant quelques jours ? Mon derrière part en vacances...', 'Du kommst mir vor, als wäre das Beste von dir damals am Arsch deiner Mutter runtergelaufen und als braune Soße auf der Matratze geblieben!', '能借你的脸用几天吗?我的屁股要去度假……', '', '¿Me prestas tu cara por unos días? Mi culo se va de vacaciones...', '', 'Можно я одолжу твоё лицо на пару дней? Моя задница уходит в отпуск....'), + (1465, 'taunt', 'I’d like to give you a going away present... First you do your part.', 0, 0, '', 'Je voudrais te donner un cadeau d’au revoir... Tout d’abord, toi tu fais ta part.', 'Ich würd dir gern ein Abschiedsgeschenk machen... Aber zuerst machst du deinen Teil.', '我想送你个临别礼物……先干好你的份内事吧。', '', 'Me gustaría darte un regalo de despedida... Primero haz tu parte.', '', ''), + (1466, 'taunt', 'Before you came along we were hungry, Now we’re just fed up.', 0, 0, '', 'Avant ton arrivée, on avait faim. Maintenant, on en a juste assez.', 'Bevor du kamst, waren wir motiviert, jetzt sind wir einfach nur noch genervt.', '你来之前我们还饿着肚子,现在都快被你气饱了。', '', 'Antes de que llegaras teníamos hambre, ahora estamos hartos', '', ''), + (1467, 'taunt', 'I like you. People say I have no taste, but I like you.', 0, 0, '', 'Je t’aime bien. Les gens disent que je n’ai pas de goût, mais je t’aime bien.', 'Ich mag dich. Man sagt, ich hätte keinen Geschmack, aber ich mag dich.', '我挺喜欢你的。别人都说我没品,但我就是喜欢你。', '', 'Me gustas. La gente dice que no tengo gusto, pero me gustas.', '', 'Ты мне нравишься. Говорят, у меня нет вкуса, но ты мне нравишься.'), + (1468, 'taunt', 'I think you have an inferiority complex, but that’s okay, it’s justified.', 0, 0, '', 'Je pense que tu as un complexe d’infériorité, mais il n’y a pas de problème, c’est justifié.', 'Ich glaube, du hast Minderwertigkeitskomplexe, aber ist okay, ist gerechtfertigt.', '我觉得你有自卑情结,不过没关系,这情结挺合理。', '', 'Creo que tienes complejo de inferioridad, pero está bien, está justificado', '', ''), + (1469, 'taunt', 'Hence rotten thing! Or I shall shake thy bones out of thy garments.', 0, 0, '', 'Miserable vermine! Sinon, je vais te secouer jusqu’à ce que tes os sortent de tes vêtements.', 'Verschwinde, sonst passiert was.', '滚吧,臭东西!不然我把你骨头从衣服里抖出来!', '', '¡Por lo tanto, cosa podrida! o te sacudiré los huesos de las vestiduras.', '', 'Прочь, гнилое создание! Или я вытрясу твои кости из одежды.'), + (1470, 'taunt', 'I can’t believe I’m wasting my time with you!', 0, 0, '', 'Je n’arrive pas à croire que je perds mon temps avec toi!', 'Kaum zu fassen, dass ich meine Zeit mit dir verschwende!', '我真不敢相信我在你身上浪费时间!', '', '¡No puedo creer que estoy perdiendo el tiempo contigo!', '', ''), + (1471, 'taunt', 'I love it when someone insults me, it means I don’t have to be nice anymore.', 0, 0, '', 'j’adore quand quelqu’un m’insulte, ça veut dire que je n’ai plus besoin d’être gentil.', 'Ich liebe es, wenn mich jemand beleidigt, dann muss ich nicht mehr nett sein.', '我喜欢被人骂,这意味着我再也不用装客气了。', '', 'Me encanta cuando alguien me insulta, significa que ya no tengo que ser amable', '', ''), + (1472, 'taunt', 'Thou leathern-jerkin, crystal-button, knot-pated, agatering, puke-stocking, caddis-garter, smooth-tongue, Spanish pouch!', 0, 0, '', 'Whoo, ça va swinguer, toi en ta peau de cuir, tes boutons de cristal, ton crâne noueux, tes chausses de vomi, tes jarretières en galon, ta langue de miel, et ta bourse espagnole !', 'Ich werde dich auch auslöschen, du Mistkerl! Ich werde dir die Eingeweide rausreißen und dir ins Gesicht schmieren!', '你个皮夹克、水晶扣、脑壳疙瘩、玛瑙戒、呕吐袜、粗布吊带、油嘴滑舌、西班牙钱袋!', '', '¡Tú, jubón de cuero, botón de cristal, anudado, ágata, medias de vómito, liga de caddis, lengua suave, bolsa española!', '', 'Ты кожаный-жилет, хрустальная пуговица, узловатый, агатовый, рвотный чулок, ленточный подвязочник, гладкоязычный, испанский кошелёк!'), + (1473, 'taunt', 'Thou qualling bat-fowling malt-worm!', 0, 0, '', 'Espèce de vermine chauve-souris qui infeste l’orge maltée!', 'Du abscheuliche Bestie, nicht mal ein Hund würde dich fressen.', '你个令人作呕的蝙蝠猎手麦芽虫!', '', '¡Tú, gusano de malta cazador de murciélagos!', '', 'Ты дрожащий ловец летучих мышей, пивной червь!'), + (1474, 'taunt', 'Thou art truely an idol of idiot-worshippers!', 0, 0, '', 'Tu es vraiment l’idole des adorateurs d’idiots!', 'Du schlägst wie ein Vegetarier.', '你真是白痴崇拜者的偶像!', '', '¡Eres verdaderamente un ídolo de los adoradores de idiotas!', '', 'Ты действительно идол поклонников идиотов!'), + (1475, 'taunt', 'Thou misbegotten knotty-pated wagtail!', 0, 0, '', 'Espèce de galopin au cerveau embrouillé!', 'Wer hat dich ausgebuddelt, du wandelnde Seuche?', '你个天生脑壳疙瘩的摇尾小鸟!', '', '¡Maldito lavandero nudoso!', '', 'Ты ублюдочный узловатый хвостун!'), + (1476, 'taunt', 'Thou whoreson mandrake, thou art fitter to be worn in my cap than to wait at my heels!', 0, 0, '', '', 'Ich schau dir nicht in die Augen, ich will heute noch schlafen können.', '你个曼陀罗贱种,戴在我帽子上都比跟在我脚后跟合适!', '', '¡Hijo de puta mandrágora, eres mejor para que te lleve en mi gorra que para que me pise los talones!', '', 'Ты, ублюдочный мандрагор, тебе больше подходит быть у меня на шапке, чем ждать у моих пяток!'), + (1477, 'taunt', 'You! You scullion! You rampallian! You fustilarian! I’ll tickle your catastrophe!', 0, 0, '', 'Toi! Toi l’écuyer! Toi le gredin! Toi le vaurien! Je vais titiller ta catastrophe!', 'Boah, bei dir quillt ja noch das Mittagessen raus! Ich hab dich gerochen, bevor ich dich gesehen hab.', '你!下贱厨子!无赖!废物!我来挠挠你的倒霉事!', '', '¡Tú! ¡Scullion! ¡Ramaliano! ¡Fustilarista! ¡Te haré cosquillas en tu catástrofe!', '', ''), + (1478, 'taunt', 'Oh ! Thou infectious ill-nurtured flax-wench!', 0, 0, '', 'Oh ! Toi, vilaine lavandière malveillante et infecte!', 'Oh du bist so hässlich, selbst der Respawn will dich nicht!', '哦,!你个恶毒又传染的亚麻贱女!', '', '¡Oh, ! ¡Tú, contagiosa moza del lino mal educada!', '', 'О, ! Ты заразная, плохо воспитанная льняная девка!'), + (1479, 'taunt', 'We leak in your chimney, !', 0, 0, '', 'On fuit dans ta cheminée, !', 'Ich muss mal einen riesigen machen. ist mein neues Wort für Scheiße.', '我们在你烟囱里漏水了,!', '', '¡Tenemos una fuga en tu chimenea, !', '', 'Мы протекаем в твоей трубе, !'), + (1480, 'taunt', 'Oh thou bootless fen-sucked canker-blossom!', 0, 0, '', 'Oh mon impuissant aspiré par les marais, fleur de chancre!', 'Oh du nutzlose, moorsaufende Krebsgeschwulst!', '哦,你个无用的沼泽吸血疮花!', '', '¡Oh tú, flor de cancro chupada por el pantano sin botas!', '', 'О, ты бесполезный болотный цветок!'), + (1481, 'taunt', 'Were I like thee I’d throw away myself!', 0, 0, '', 'Si j’étais comme toi, je me jetterais en l’air !', 'Wäre ich wie du, würd ich mich selbst löschen!', '如果我像你一样,我早就把自己扔了!', '', '¡Si yo fuera como tú, me tiraría a la basura!', '', ''), + (1482, 'taunt', 'O teach me , how I should forget to think!', 0, 0, '', 'Oh, enseigne-moi, , comment oublier de trop réfléchir!', 'Oh lehre mich, wie ich das Denken vergesse!', '哦,教教我吧,,怎样才能忘记思考!', '', '¡Oh, enséñame , cómo debo olvidarme de pensar!', '', 'Научи меня, , как забыть думать!'), + (1483, 'taunt', 'Truly thou art damned, like an ill-roasted egg, all on one side!', 0, 0, '', 'Franchement, tu es foutu, comme un œuf mal cuit, tout d’un côté!', 'Ich hab schon Toiletten gesehen, die sauberer kämpfen als du.', '你真该死,就像个烤坏的蛋,全都焦在一边!', '', '¡Verdaderamente estás condenado, como un huevo mal asado, todo de un lado!', '', 'Истинно, ты проклят, как плохо прожаренное яйцо, с одной стороны!'), + (1484, 'taunt', 'You starvelling, you eel-skin, you dried neat’s-tongue, you bull’s-pizzle, you stock-fish- O for breath to utter what is like thee!! -you tailor’s-yard, you sheath, you bow-case, you vile standing tuck!', 0, 0, '', '', 'Du verdammter Scheißkerl. Wo hast du dein Handwerk gelernt? Du dumme, verdammte Schlampe. Du Idiot. Wer hat dir gesagt, dass du mit Männern arbeiten kannst?', '你个饿殍、鳗鱼皮、干瘪牛舌、公牛鞭、腌鱼——哦,我要是有气力说出跟你有多像就好了!——你个裁缝尺、剑鞘、弓套、卑劣的站立家伙!', '', 'Estás muerto de hambre, piel de anguila, te secaste la lengua limpiamente, toro-pizzle, pescado-O para respirar pronunciar lo que es como tú !! ¡Tú, sastrería, vaina, estuche de arco, vil flaco de pie!', '', ''), + (1485, 'taunt', 'Fie! Drop thee into the rotten mouth of Death!', 0, 0, '', 'Que le diable t’emporte dans la gueule pourrie de la Mort!', 'Wenn Unfähigkeit ’ne Klasse wäre, hättest du sie gemaxt.', '呸!掉进死亡的腐烂大嘴去吧!', '', '¡Fie! ¡Déjate caer en la podrida boca de la Muerte!', '', 'Фу! Брось себя в гнилую пасть Смерти!'), + (1486, 'taunt', ', you are a fishmonger!', 0, 0, '', ', tu es poissonnier!', ' du dummes Arschfurz-Teppichladen-Arschloch', ',你就是个卖鱼的!', '', ', ¡eres un pescadero!', '', ', ты торговец рыбой!'), + (1487, 'taunt', 'I shall live to knock thy brains out!', 0, 0, '', 'Je vivrai pour te cogner le cerveau!', 'Ich lebe lang genug und dann nehm ich dir das Hirn raus!', '我会活着把你的脑浆打出来!', '', '¡Viviré para romperte los sesos!', '', 'Я доживу, чтобы выбить тебе мозги!'), + (1488, 'taunt', 'Most shallow are you, !! Thou art worms-meat in respect of a good piece of flesh, indeed!!', 0, 0, '', 'Comme tu es superficiel !! Tu n’es que de la chair à vers comparé à un bon morceau de viande, en effet!!', 'Wenn einen intelligenten Gedanken im Kopf hätte, würde er an Einsamkeit sterben.', '你真肤浅,!!跟一块好肉比,你就是个虫子啃的货,的确如此!!', '', '¡¡Eres el más superficial, !! ¡Eres carne de gusano con respecto a un buen trozo de carne, de hecho!', '', 'Ты очень поверхностен, !! Ты корм для червей по сравнению с хорошим куском мяса!!'), + (1489, 'taunt', 'Vile wretch! O , thou odiferous hell-hated pignut!', 0, 0, '', 'Misérable vermine! Ô , toi l’infâme pignouf puant des enfers!', 'Hör auf zu weinen, du winselnder Arsch, hör auf mit deinem Unsinn. Du bist nur die Nachgeburt, .', '卑劣的混蛋!哦,,你个地狱厌弃的臭猪头!', '', '¡Miserable vil! ¡Oh, , fétido cerdo odiado por el infierno!', '', 'Подлый негодяй! О, , ты зловонный, ненавистный к свиньям орех!'), + (1490, 'taunt', '! Thy kiss is as comfortless as frozen water to a starved snake!', 0, 0, '', '! Ton baiser est aussi réconfortant que de l’eau gelée pour un serpent affamé!', '! Wenn Gehirn Vogelkacke wäre, hättest du einen sauberen Käfig', '!你的吻就像给一条饿蛇喂冰水一样毫无安慰!', '', '! ¡Tu beso es tan desconsolador como el agua helada para una serpiente hambrienta!', '', '! Твой поцелуй так же утешителен, как замёрзшая вода для голодной змеи!'), + (1491, 'taunt', 'I scorn you, scurvy companion. What, you poor, base, rascally, cheating, lack-linen mate! Away, you moldy rogue, away!', 0, 0, '', 'Je vous méprise, misérable compagnon. Quoi, toi, pauvre, vil, scélérat, tricheur, compagnon usé de toile! Déguerpisse, espèce de coquin moisissure, déguerpisse!', 'Hör mal zu, du Vokuhila... warum zündest du nicht einfach dein Tampon an und sprengst deine Box in die Luft? Denn das ist der einzige Knall, den du kriegen wirst.', '我鄙视你,卑劣的家伙。什么,你个穷酸、下贱、无赖、骗子、破布般的同伴!滚吧,你个发霉的混账,滚!', '', 'Te desprecio, compañero escorbuto. ¡Qué, pobre, vil, pícaro, tramposo, compañero falto de lino! ¡Fuera, mohoso granuja, fuera!', '', 'Я презираю тебя, паршивый спутник. Что, ты бедный, низкий, мошеннический, безрубашечный приятель! Прочь, ты плесневелый негодяй, прочь!'), + (1492, 'taunt', 'Out of my sight! Thou dost infect my eyes !', 0, 0, '', 'Hors de ma vue! Tu infectes mes yeux !', 'Verschwinde aus meiner Sicht ! Du verseuchst meine Augen!', '从我眼前消失!你污染了我的眼睛,!', '', '¡Fuera de mi vista! ¡Me infectas los ojos !', '', 'Прочь с глаз моих! Ты заражаешь мои глаза, !'), + (1493, 'taunt', 'PLAY TIME!!!!', 0, 0, '', 'L’HEURE DE LA MORT !', 'SPIELZEIT!!!!', '开战时间!!!!', '', '¡¡¡HORA DE JUGAR!!!!', '', 'ВРЕМЯ ИГРЫ!!!!'), + (1494, 'taunt', 'None shall pass!', 0, 0, '', 'Vous ne passerez pas !', 'Niemand kommt vorbei!', '谁也别想通过!', '', '¡Ninguno pasará!', '', 'Никто не пройдёт!'), + (1495, 'taunt', 'We’re under attack! A vast, ye swabs! Repel the invaders!', 0, 0, '', 'Nous sommes attaqués! Visez bien, bande de moussaillons! Repoussez les envahisseurs!', 'Wir werden angegriffen! Klar zum Gefecht, ihr Deckschrubber! Wehrt die Eindringlinge ab!', '我们遭到攻击!一大波敌人,蠢货们!击退入侵者!', '', '¡Estamos bajo ataque! ¡Un vasto, hisopos! ¡Repeler a los invasores!', '', ''), + (1496, 'taunt', 'None may challenge the Brotherhood!', 0, 0, '', 'Personne ne peut défier la Confrérie!', 'Niemand darf die Bruderschaft herausfordern!', '没人能挑战兄弟会!', '', '¡Nadie puede desafiar a la Hermandad!', '', 'Никто не может бросить вызов Братству!'), + (1497, 'taunt', 'Foolsss...Kill the one in the dress!', 0, 0, '', 'Imbéciles... Tuez celui en robe!', 'Narren... Tötet den im Kleid!', '蠢货们……干掉那个穿长袍的!', '', 'Tontosss... ¡Matad al del vestido!', '', 'Дураки... Убейте того, кто в платье!'), + (1498, 'taunt', 'I’ll feed your soul to Hakkar himself! ', 0, 0, '', 'Je vais nourrir ton âme à Hakkar lui-même!', 'Ich werde deine Seele Hakkar selbst zum Fraß vorwerfen!', '我会把你的灵魂喂给哈卡本人!', '', '¡Le daré de comer con tu alma al mismísimo Hakkar! ', '', ''), + (1499, 'taunt', 'Pride heralds the end of your world! Come, mortals! Face the wrath of the !', 0, 0, '', ' m’apporte une grande fierté! Venez, mortels ! Affrontez la colère de !', 'Stolz kündigt das Ende eurer Welt an! Kommt, Sterbliche! Stellt euch dem Zorn der !', '骄傲预示着你们世界的终结!来吧,凡人!面对的怒火!', '', '¡El orgullo anuncia el fin de tu mundo! ¡Venid, mortales! ¡Enfréntate a la ira de la !', '', 'Гордость предвещает конец вашего мира! Идите, смертные! Столкнитесь с гневом !'), + (1500, 'taunt', 'All my plans have led to this!', 0, 0, '', 'Tous mes projets ont mené à cela!', 'All meine Pläne haben zu diesem Moment geführt!', '我所有的计划都指向这一刻!', '', '¡Todos mis planes me han llevado a esto!', '', 'Все мои планы привели к этому!'), + (1501, 'taunt', 'Ahh! More lambs to the slaughter!', 0, 0, '', 'Oh non! Encore des agneaux pour l’abattoir!', 'Ahh! Noch mehr Lämmer zur Schlachtbank!', '啊哈!又有羔羊来送死了!', '', '¡Ahh! ¡Más corderos al matadero!', '', 'Ах! Еще ягнята на заклание!'), + (1502, 'taunt', 'Another day, another glorious battle!', 0, 0, '', 'Une autre journée, une autre bataille glorieuse !', 'Neuer Tag, neue glorreiche Schlacht!', '又是新的一天,又是一场光荣的战斗!', '', '¡Otro día, otra batalla gloriosa!', '', 'Еще один день, еще одна славная битва!'), + (1503, 'taunt', 'So, business... or pleasure?', 0, 0, '', 'Alors, affaires... ou plaisir ?', 'Also, Geschäft... oder Vergnügen?', '那么,是公事……还是私事?', '', 'Entonces, ¿negocios... o placer?', '', 'Итак, дело... или удовольствие?'), + (1504, 'taunt', 'You are not prepared!', 0, 0, '', 'Tu n’es pas prêt!', 'Ihr seid nicht vorbereitet!', '你们还没准备好!', '', '¡No estás preparado!', '', 'Вы не готовы!'), + (1505, 'taunt', 'The ’s final conquest has begun! Once again the subjugation of this world is within our grasp. Let none survive! ', 0, 0, '', 'La dernière conquête de a commencé! Encore une fois, la soumission de ce monde est à portée de main. Que personne ne survive!', 'Die letzte Eroberung der hat begonnen! Wieder liegt die Unterwerfung dieser Welt in unseren Händen. Lasst keinen überleben!', '的最终征服开始了!这个世界的臣服再次近在咫尺,一个也别想活!', '', '¡La conquista final de ha comenzado! Una vez más, la subyugación de este mundo está a nuestro alcance. ¡Que nadie sobreviva! ', '', ''), + (1506, 'taunt', 'Your death will be a painful one. ', 0, 0, '', 'Ta mort sera une douloureuse.', 'Euer Tod wird ein schmerzhafter sein.', '你的死会非常痛苦。', '', 'Tu muerte será dolorosa. ', '', 'Ваша смерть будет болезненной.'), + (1507, 'taunt', 'Cry for mercy! Your meaningless lives will soon be forfeit. ', 0, 0, '', 'Pleurez miséricorde! Vos vies insignifiantes seront bientôt perdues.', 'Fleht um Gnade! Eure bedeutungslosen Leben sind bald verwirkt.', '求饶吧!你们无意义的生命马上就要终结了。', '', '¡Llora por piedad! Sus vidas sin sentido pronto se perderán. ', '', 'Просите о милости! Ваши бессмысленные жизни скоро будут потеряны.'), + (1508, 'taunt', 'Abandon all hope! The has returned to finish what was begun so many years ago. This time there will be no escape! ', 0, 0, '', 'Abandonne tout espoir! La est de retour pour finir ce qui a été commencé il y a tant d’années. Cette fois, il n’y aura pas d’échappatoire!', 'Gebt alle Hoffnung auf! Die ist zurückgekehrt, um zu beenden, was vor so vielen Jahren begann. Diesmal gibt es kein Entkommen!', '放弃所有希望吧!回来了,要完成多年前未竟的事业。这次,你们无处可逃!', '', '¡Abandona toda esperanza! La ha regresado para terminar lo que comenzó hace tantos años. ¡Esta vez no habrá escapatoria! ', '', 'Оставьте все надежды! вернулся, чтобы завершить то, что было начато много лет назад. На этот раз не будет побега!'), + (1509, 'taunt', 'Alert! You are marked for Extermination! ', 0, 0, '', 'Alerte ! Vous êtes désigné pour extermination !', 'Alarm! Ihr seid zur Auslöschung markiert!', '警报!你们被标记为清除目标!', '', '¡Alerta! ¡Estás marcado para el exterminio! ', '', 'Внимание! Вы помечены для уничтожения!'), + (1510, 'taunt', 'The is for guests only...', 0, 0, '', 'Le est réservé aux invités uniquement...', 'Die ist nur für Gäste...', '仅对访客开放……', '', 'La es solo para invitados...', '', ' предназначена только для гостей...'), + (1511, 'taunt', 'Ha ha ha! You are hopelessly outmatched!', 0, 0, '', 'Ha ha ha! Tu es totalement dépassé!', 'Hahaha! Ihr seid hoffnungslos unterlegen!', '哈哈哈!你们完全不是对手!', '', '¡Ja, ja, ja! ¡Estás irremediablemente superado!', '', 'Ха-ха-ха! Вы безнадежно не на уровне!'), + (1512, 'taunt', 'I will crush your delusions of grandeur! ', 0, 0, '', 'Je vais briser tes illusions de grandeur!', 'Ich werde euren Größenwahn zerschmettern!', '我会粉碎你自大的幻想!', '', '¡Aplastaré tus delirios de grandeza! ', '', 'Я сокрушу ваши иллюзии величия!'), + (1513, 'taunt', 'Forgive me, for you are about to lose the game.', 0, 0, '', 'Pardonnez-moi, mais vous êtes sur le point de perdre la partie.', 'Verzeiht mir, denn ihr werdet gleich das Spiel verlieren.', '原谅我,因为你们马上就要输了。', '', 'Perdóname, porque estás a punto de perder el juego.', '', 'Простите меня, ведь вы собираетесь проиграть игру.'), + (1514, 'taunt', 'Struggling only makes it worse.', 0, 0, '', 'Se battre ne fait qu’empirer les choses.', 'Wehren macht es nur schlimmer.', '挣扎只会让事情变得更糟。', '', 'La lucha solo lo empeora.', '', 'Борьба только усугубляет ситуацию.'), + (1515, 'taunt', 'Vermin! Leeches! Take my blood and choke on it!', 0, 0, '', 'Créatures immondes ! Sangsues ! Prenez mon sang et étouffez-y !', 'Ungeziefer! Blutegel! Saugt mein Blut und erstickt daran!', '害虫!吸血鬼!喝我的血,噎死你们!', '', '¡Alimañas! ¡Sanguijuelas! ¡Toma mi sangre y atragantate con ella!', '', 'Паразиты! Пиявки! Берите мою кровь и подавитесь ею!'), + (1516, 'taunt', 'Not again... NOT AGAIN!', 0, 0, '', 'Pas encore... PAS ENCORE !', 'Nicht schon wieder... NICHT SCHON WIEDER!', '别再来了……别再来了!', '', 'No otra vez... ¡NO OTRA VEZ!', '', 'Опять... ЕЩЕ РАЗ!'), + (1517, 'taunt', 'My blood will be the end of you!', 0, 0, '', 'Mon sang sera ta fin!', 'Mein Blut wird euer Ende sein!', '我的鲜血将是你的终结!', '', '¡Mi sangre será tu fin!', '', 'Моя кровь станет вашим концом!'), + (1518, 'taunt', 'Good, now you fight me!', 0, 0, '', 'Bien, maintenant combat moi !', 'Gut, jetzt kämpfst du gegen mich!', '好极了,现在来跟我打吧!', '', 'Bien, ¡ahora pelea conmigo!', '', 'Хорошо, теперь ты сразишься со мной!'), + (1519, 'taunt', 'Get da move on, guards! It be killin’ time!', 0, 0, '', 'Allez, les gardes, on se presse! C’est l’heure de tuer!', 'Macht hinne, Wachen! Es ist Zeit zum Töten!', '快点动起来,卫兵们!是时候大开杀戒了!', '', '¡Muévanse, guardias! ¡Será para matar el tiempo!', '', ''), + (1520, 'taunt', 'Don’t be delayin’ your fate. Come to me now. I make your sacrifice quick.', 0, 0, '', 'Ne retarde pas ton destin. Viens à moi maintenant. Je rends ton sacrifice rapide.', 'Zögert euer Schicksal nicht hinaus. Kommt jetzt zu mir. Ich mache euer Opfer kurz.', '别拖延你的命运。现在过来,我会让你的牺牲快一点。', '', 'No demores tu destino. Ven a mí ahora. Hago que tu sacrificio sea rápido.', '', ''), + (1521, 'taunt', 'You be dead soon enough!', 0, 0, '', 'Tu seras bientôt mort !', 'Bald seid ihr tot!', '你很快就会死翘翘!', '', '¡Estarás muerto lo suficientemente pronto!', '', 'Ты скоро будешь мертв!'), + (1522, 'taunt', 'Mua-ha-ha!', 0, 0, '', 'Mua-ha-ha!', 'Mu-ha-ha!', '哇哈哈哈!', '', '¡Mua-ja-ja!', '', 'Муа-ха-ха!'), + (1523, 'taunt', 'I be da predator! You da prey...', 0, 0, '', 'Je suis le prédateur! Tu es la proie...', 'Ich bin der Jäger! Ihr seid die Beute...', '我是猎手!你是猎物……', '', '¡Soy un depredador! Eres presa...', '', 'Я хищник! Ты жертва...'), + (1524, 'taunt', 'You gonna leave in pieces!', 0, 0, '', 'Tu vas finir en morceaux !', 'Ihr werdet diesen Ort in Stücken verlassen!', '你会碎成渣离开这里!', '', '¡Te vas a ir en pedazos!', '', 'Ты уйдешь в кусках!'), + (1525, 'taunt', 'Death comes. Will your conscience be clear? ', 0, 0, '', 'La mort arrive. Est-ce que ta conscience sera claire ?', 'Der Tod naht. Wird euer Gewissen rein sein?', '死亡降临了。你的良心能安宁吗?', '', 'La muerte llega. ¿Tu conciencia estará limpia? ', '', 'Смерть приходит. Будет ли твоя совесть чиста?'), + (1526, 'taunt', 'Your behavior will not be tolerated.', 0, 0, '', 'Votre comportement ne sera pas toléré.', 'Euer Verhalten wird nicht toleriert.', '你的行为不可容忍。', '', 'Tu comportamiento no será tolerado', '', 'Ваше поведение не будет терпимо.'), + (1527, 'taunt', 'The Menagerie is for guests only.', 0, 0, '', 'La Ménagerie est réservée aux invités seulement.', 'Die Menagerie ist nur für Gäste.', '展览馆仅对访客开放。', '', 'The Menagerie es solo para invitados', '', 'Менажерия предназначена только для гостей.'), + (1528, 'taunt', 'Hmm, unannounced visitors, Preparations must be made... ', 0, 0, '', 'Des visiteurs non annoncés, il me faut faire des préparatifs...', 'Hmm, unangekündigte Besucher. Vorbereitungen müssen getroffen werden...', '嗯,未经通报的访客,必须做好准备……', '', 'Hmm, visitantes inesperados, se deben hacer preparativos... ', '', 'Хмм, незваные гости, нужно подготовиться...'), + (1529, 'taunt', 'Hostile entities detected. Threat assessment protocol active. Primary target engaged. Time minus thirty seconds to re-evaluation.', 0, 0, '', 'Entités hostiles détectées. Protocole d’évaluation des menaces activé. Cible principale engagée. Temps moins trente secondes avant réévaluation.', 'Feindliche Einheiten entdeckt. Bedrohungsanalyse-Protokoll aktiv. Primärziel erfasst. Zeit minus dreißig Sekunden bis zur Neubewertung.', '侦测到敌对实体。威胁评估协议已激活。主要目标锁定。距离重新评估还有三十秒。', '', 'Entidades hostiles detectadas. Protocolo de evaluación de amenazas activo. Objetivo principal comprometido. Tiempo menos treinta segundos para la reevaluación.', '', 'Обнаружены враждебные сущности. Протокол оценки угрозы активирован. Основная цель захвачена. Время до повторной оценки - тридцать секунд.'), + (1530, 'taunt', 'New toys? For me? I promise I won’t break them this time!', 0, 0, '', 'De nouveaux jouets ? Pour moi ? Je te promets que cette fois, je ne les casserai pas !', 'Neue Spielzeuge? Für mich? Ich verspreche, ich mach sie diesmal nicht kaputt!', '新玩具?给我玩的?我保证这次不会弄坏它们!', '', '¿Juguetes nuevos? ¿Para mi? ¡Te prometo que no los romperé esta vez!', '', ''), + (1531, 'taunt', 'I’m ready to play!', 0, 0, '', 'Je suis prêt à jouer !', 'Ich bin bereit zu spielen!', '我准备好玩一玩了!', '', '¡Estoy listo para jugar!', '', ''), + (1532, 'taunt', 'Shhh... it will all be over soon.', 0, 0, '', 'Chut... tout sera bientôt fini.', 'Pssst... gleich ist alles vorbei.', '嘘……这一切很快就会结束。', '', 'Shhh... todo terminará pronto.', '', 'Шшш... скоро все закончится.'), + (1533, 'taunt', 'Aaaaaughibbrgubugbugrguburgle!', 0, 0, '', 'Aaaaaughibbrgubugbugrguburgle!', 'Aaaaaughibbrgubugbugrguburgle!', '啊啊啊啊咕噜咕噜咕噜咕!', '', '¡Aaaaaughibbrgubugbugrguburgle!', '', 'Ааааагхиббргубугбугрубгл!'), + (1534, 'taunt', 'RwlRwlRwlRwl!', 0, 0, '', 'RwlRwlRwlRwl!', 'RwlRwlRwlRwl!', '嗷嗷嗷嗷!', '', 'RwlRwlRwlRwl!', '', 'РвлРвлРвлРвл!'), + (1535, 'taunt', 'You too, shall serve!', 0, 0, '', 'Toi aussi, tu serviras !', 'Auch du wirst dienen!', '你也一样,必须效忠!', '', '¡Tú también, debes servir!', '', 'Ты тоже будешь служить!'), + (1536, 'taunt', 'Tell me... tell me everything! Naughty secrets! I’ll rip the secrets from your flesh!', 0, 0, '', 'Dis-moi... dis-moi tout ! Les petits secrets coquins ! Je vais arracher les secrets de ta chair !', 'Sag es mir... sag mir alles, Geheimnisse und Privatsachen! Ich reiße dir die Infos aus dem Fleisch!', '告诉我……把一切都告诉我!那些小秘密!我会从你的血肉中撕出真相!', '', 'Cuéntame... ¡cuéntame todo! ¡Secretos traviesos! ¡Arrancaré los secretos de tu carne!', '', ''), + (1537, 'taunt', 'Prepare yourselves, the bells have tolled! Shelter your weak, your young and your old! Each of you shall pay the final sum! Cry for mercy, the reckoning has come!', 0, 0, '', 'Préparez-vous, les cloches ont sonné ! Protégez vos faibles, vos jeunes et vos anciens ! Chacun d’entre vous paiera le prix final ! Pleurez miséricorde, l’heure du jugement est arrivée!', 'Macht euch bereit, die Glocken haben geläutet! Bringt die Schwachen, die Jungen und die Alten in Sicherheit! Jeder von euch zahlt den letzten Preis! Fleht um Gnade, die Abrechnung ist gekommen!', '准备好吧,丧钟已敲响!保护你们的弱者、孩子和老人!你们每个人都将付出最终代价!哭求怜悯吧,清算时刻到了!', '', '¡Prepárense, las campanas han doblado! ¡Abriga a tus débiles, a tus jóvenes ya tus viejos! ¡Cada uno de ustedes pagará la suma final! Clama por misericordia, el ajuste de cuentas ha llegado!', '', 'Приготовьтесь, колокола прозвучали! Укройте своих слабых, молодых и старых! Каждый из вас заплатит окончательную цену! Просите о милости, расплата пришла!'), + (1538, 'taunt', 'Where in Bonzo’s brass buttons am I?', 0, 0, '', 'Où diable suis-je?', 'Wo, bei Bonzos Messingknöpfen, bin ich?', '我到底在邦佐的黄铜扣子哪里?', '', '¿Dónde estoy en los botones de latón de Bonzo?', '', ''), + (1539, 'taunt', 'I can bear it no longer! Goblin King! Goblin King! Wherever you may be! Take this far away from me!', 0, 0, '', 'Je n’en peux plus! Roi des Gobelins! Roi des Gobelins! Où que tu sois! Emmène cette %cible% loin de moi!', 'Ich ertrage es nicht länger! Goblinkönig! Goblinkönig! Wo immer du bist! Nimm diesen weit weg von mir!', '我再也受不了了!地精之王!地精之王!不管你在哪!把这个带离我远点!', '', '¡No puedo soportarlo más! ¡Rey de los duendes! ¡Rey de los duendes! ¡WHERE quiera que estés! ¡Lleva a este lejos de mí!', '', 'Я больше не могу это терпеть! Король гоблинов! Король гоблинов! Где бы ты ни был! Унеси этого далеко от меня!'), + (1540, 'taunt', 'You have thirteen hours in which to solve the labyrinth, before your baby brother becomes one of us... forever.', 0, 0, '', 'Tu as treize heures pour résoudre le labyrinthe, avant que ton petit frère ne devienne l’un d’entre nous... pour toujours.', 'Ihr habt dreizehn Stunden, um das Labyrinth zu lösen, bevor euer kleiner Bruder einer von uns wird... für immer.', '你有十三小时来解开迷宫,否则你的小弟弟将永远成为我们的一员……', '', 'Tienes trece horas para resolver el laberinto, antes de que tu hermanito se convierta en uno de nosotros... para siempre', '', 'У вас есть тринадцать часов, чтобы решить лабиринт, прежде чем ваш младший брат станет одним из нас... навсегда.'), + (1541, 'taunt', 'So, the is a piece of cake, is it? Well, let’s see how you deal with this little slice... ', 0, 0, '', 'Alors, la est du gâteau, vous dites? Hmm, voyons comment vous vous en sortez avec cette petite portion...', 'Also, die ist Kinderkram, ja? Dann sehen wir mal, wie ihr dieses kleine Stückchen verdaut...', '所以,对你来说是小菜一碟,对吧?好吧,看看你怎么应付这小块“蛋糕”……', '', 'Entonces, la es pan comido, ¿verdad? Bueno, veamos cómo lidias con esta pequeña porción... ', '', ''), + (1542, 'taunt', 'Back off, I’ll take you on, headstrong to take on anyone, I know that you are wrong, and this is not where you belong', 0, 0, '', 'Alors, recule, je vais m’occuper de toi, déterminé à affronter n’importe qui, je sais que tu as tort, et ce n’est pas là que tu appartiens', 'Weiche! Ich stelle mich jedem Kampf. Du irrst dich, dies ist nicht dein Revier.', '退后,我来对付你,胆大包天敢挑战任何人,我知道你错了,这里不是你该来的地方', '', 'Atrás, te enfrentaré, testarudo para enfrentar a cualquiera, sé que estás equivocado, y este no es el lugar al que perteneces', '', ''), + (1543, 'taunt', 'Show me whatcha got!', 0, 0, '', 'Montre-moi ce que t’as dans le ventre!', 'Zeig, was du drauf hast!', '让我瞧瞧你有啥能耐!', '', '¡Muéstrame lo que tienes!', '', 'Покажи, что у тебя есть!'), + (1544, 'taunt', 'To the death!', 0, 0, '', '% Sorry, I can’t translate that.', 'Bis zum Tod!', '决一死战!', '', '¡Hasta la muerte!', '', 'До смерти!'), + (1545, 'taunt', 'Twin blade action, for a clean close shave every time.', 0, 0, '', 'Lame jumelle, pour un rasage de près impeccable à chaque fois.', 'Zweiklingen-Action für eine glatte und saubere Rasur jedes mal.', '双刃出击,每次都能干净利落!', '', 'Acción de cuchilla doble, para un afeitado limpio y apurado en todo momento.', '', 'Двойное лезвие, для чистого бритья каждый раз.'), + (1546, 'taunt', 'Bring it on!', 0, 0, '', 'Allez, viens ici !', 'Los, komm schon!', '来吧!', '', '¡Adelante!', '', 'Давай!'), + (1547, 'taunt', 'You’re goin’ down!', 0, 0, '', 'Tu vas tomber !', 'Du gehst drauf!', '你死定了!', '', '¡Vas a caer!', '', ''), + (1548, 'taunt', 'Stabby stab stab!', 0, 0, '', 'Stabby stab stab!', 'Stich, stich, stich!', '刺刺刺!', '', '¡Puñalada puñalada puñalada!', '', 'Ударь, ударь, ударь!'), + (1549, 'taunt', 'Let’s get this over quick, time is mana.', 0, 0, '', 'Finissons-en vite , le temps c’est du mana.', 'Machen wir das schnell, Zeit ist Mana.', '咱们速战速决,时间就是魔法!', '', 'Terminemos con esto rápido, el tiempo es maná.', '', ''), + (1550, 'taunt', 'I do not think you realise the gravity of your situation.', 0, 0, '', 'Tu ne réalise pas encore la gravité de ta situation.', 'Ich glaube nicht, dass ihr die Schwere eurer Lage begreift.', '我想你还没意识到你处境的严重性。', '', 'No creo que te des cuenta de la gravedad de tu situación.', '', 'Я не думаю, что вы осознаете серьезность вашей ситуации.'), + (1551, 'taunt', 'I will bring honor to my family and my kingdom!', 0, 0, '', 'Je vais apporter honneur à ma famille et à mon royaume!', 'Ich bringe meiner Familie und meinem Königreich Ehre!', '我会为我的家族和王国带来荣耀!', '', '¡Llevaré honor a mi familia y mi reino!', '', 'Я принесу честь своей семье и своему королевству!'), + (1552, 'taunt', 'Light, give me strength!', 0, 0, '', 'Lumière, donne-moi de la force!', 'Licht, gib mir Stärke!', '圣光,赐予我力量!', '', '¡Luz, dame fuerza!', '', 'Свет, дай мне силу!'), + (1553, 'taunt', 'My church is the field of battle - time to worship...', 0, 0, '', 'Mon terrain de bataille, c’est mon église - il est temps de rendre hommage...', 'Meine Kirche ist das Schlachtfeld, Zeit zum Gottesdienst...', '战场就是我的教堂——是时候膜拜了……', '', 'Mi iglesia es el campo de batalla - hora de adorar...', '', 'Моя церковь - это поле битвы - время поклоняться...'), + (1554, 'taunt', 'I hold you in contempt...', 0, 0, '', 'Je te méprise...', 'Ich verachte euch...', '我鄙视你……', '', 'Te tengo en desacato...', '', 'Я держу тебя в презрении...'), + (1555, 'taunt', 'Face the hammer of justice!', 0, 0, '', 'Affronte le marteau de la justice!', 'Stellt euch dem Hammer der Gerechtigkeit!', '面对正义之锤吧!', '', '¡Enfréntate al martillo de la justicia!', '', 'Столкнись с молотом справедливости!'), + (1556, 'taunt', 'Prove your worth in the test of arms under the Light!', 0, 0, '', 'Prouve ta valeur dans le test des armes sous la Lumière!', 'Beweist euren Wert in der Prüfung der Waffen im Lichte!', '在圣光的见证下证明你的武艺价值!', '', '¡Demuestra tu valía en la prueba de las armas bajo la Luz!', '', 'Докажи свою ценность в испытании оружием под Светом!'), + (1557, 'taunt', 'All must fall before the might and right of my cause, you shall be next!', 0, 0, '', 'Tous doivent tomber devant la puissance et la justesse de ma cause, tu seras le prochain!', 'Alles wird vor der Macht und der Gerechtigkeit meiner Sache fallen. Ihr werdet der Nächste sein!', '所有人都将在我的正义与力量前倒下,你就是下一个!', '', 'Todo debe caer ante el poder y el derecho de mi causa, ¡tú serás el próximo!', '', 'Все должны пасть перед могуществом и правом моего дела, ты будешь следующим!'), + (1558, 'taunt', 'Prepare to die!', 0, 0, '', 'Prépare-toi à mourir!', 'Bereitet euch aufs Sterben vor!', '准备受死吧!', '', '¡Prepárate para morir!', '', 'Приготовься умереть!'), + (1559, 'taunt', 'The beast with me is nothing compared to the beast within...', 0, 0, '', 'La bête qui est avec moi n’est rien comparée à la bête qui est en moi...', 'Die Bestie an meiner Seite ist nichts gegen die Bestie in mir...', '我身边的野兽与我内心的猛兽相比简直不值一提……', '', 'La bestia conmigo no es nada comparada con la bestia interior...', '', 'Зверь со мной ничто по сравнению с зверем внутри...'), + (1560, 'taunt', 'Witness the firepower of this fully armed huntsman!', 0, 0, '', 'Découvrez la puissance de feu de ce chasseur entièrement armé!', 'Erlebt die Feuerkraft dieses voll bewaffneten Jägers!', '见识一下全副武装猎人的火力吧!', '', '¡Sea testigo de la potencia de fuego de este cazador completamente armado!', '', 'Стань свидетелем огневой мощи этого полностью вооруженного охотника!'), + (1561, 'critical health', 'Heal me! Quick!', 0, 0, '', 'Je vais te soigner tout de suite !', 'Heilt mich! Schnell!', '快治疗我!快点!', '', '¡Cúrame! ¡Rápido!', '', 'Исцели меня! Быстро!'), + (1562, 'critical health', 'Almost dead! Heal me!', 0, 0, '', 'Presque mort ! Soignez-moi !', 'Fast tot! Heilt mich!', '快不行了!治疗我!', '', '¡Estoy casi muerto! ¡Cúrame!', '', 'Почти мертв! Исцели меня!'), + (1563, 'critical health', 'Help! Heal me!', 0, 0, '', 'S’il te plaît, aide-moi! Soigne-moi!', 'Hilfe! Heilt mich!', '救命!治疗我!', '', '¡Ayuda! ¡Cúrame!', '', 'Помогите! Исцели меня!'), + (1564, 'critical health', 'Somebody! Heal me!', 0, 0, '', 'Quelqu’un ! Soigne-moi !', 'Irgendwer! Heilt mich!', '谁来!治疗我!', '', '¡Alguien! ¡Cúrame!', '', 'Кто-нибудь! Исцели меня!'), + (1565, 'critical health', 'Heal! Heal! Heal!', 0, 0, '', 'Guérir! Guérir! Guérir!', 'Heal! Heal! Heal!', '治疗!治疗!治疗!', '', '¡Curame! ¡Saname! ¡Socorro!', '', 'Исцели! Исцели! Исцели!'), + (1566, 'critical health', 'I am dying! Heal! Aaaaarhg!', 0, 0, '', 'Je meurs! Au secours! Aaaaarhg!', 'Ich sterbe! Heal! Aaaaarhg!', '我要死了!治疗!啊啊啊啊!', '', '¡Me estoy muriendo! ¡Saname! ¡Aaaaarhg!', '', 'Я умираю! Исцели! Ааааргх!'), + (1567, 'critical health', 'Heal me!', 0, 0, '', 'Soigne moi !', 'Heal mich!', '快治疗我!', '', '¡Cúrame!', '', 'Исцели меня!'), + (1568, 'critical health', 'I will die. I will die. I will die. Heal!', 0, 0, '', 'Je vais mourir. Je vais mourir. Je vais mourir. Soigne-moi !', 'Ich sterbe. Ich sterbe. Ich sterbe. Heal!', '我要死了!我要死了!快治疗!', '', 'Voy a morir. Voy a morir. Voy a morir. ¡Saname!', '', 'Я умру. Я умру. Я умру. Исцели!'), + (1569, 'critical health', 'Healers, where are you? I am dying!', 0, 0, '', 'Healers, où êtes-vous? Je suis en train de mourir!', 'Heiler, wo seid ihr? Ich sterbe!', '治疗呢?你们在哪儿?我快挂了!', '', 'Sanadores, ¿dónde estais? ¡Me estoy muriendo!', '', 'Целители, где вы? Я умираю!'), + (1570, 'critical health', 'Oh the pain. Heal me quick!', 0, 0, '', 'Oh la douleur. Guérissez-moi vite!', 'Oh, der Schmerz. Heilt mich schnell!', '哎哟,好痛!快给我治疗!', '', 'Ouch, qué dolor. ¡Cúrame rápido!', '', 'О, боль. Исцели меня быстро!'), + (1571, 'low health', 'Need heal', 0, 0, '', 'Besoin de soins', 'Brauche Heal', '需要治疗!', '', 'Necesito una cura', '', 'Нужна исцеление'), + (1572, 'low health', 'Low health', 0, 0, '', 'Faible santé', 'HP low', '血量低了!', '', 'Salud baja', '', 'Низкое здоровье'), + (1573, 'low health', 'Drop a heal. Please.', 0, 0, '', 'HEAL. S’il te plaît.', 'Gebt mal nen Heal. Bitte.', '来个治疗,求你了!', '', 'Una sanación por favor.', '', 'Сделай исцеление. Пожалуйста.'), + (1574, 'low health', 'Could somebody drop a heal on me?', 0, 0, '', 'Est-ce que quelqu’un pourrait me soigner?', 'Kann mir jemand nen Heal geben?', '谁能给我加点血啊?', '', '¿Alguien podría curarme?', '', 'Может кто-нибудь исцелить меня?'), + (1575, 'low health', 'Hey! Better heal me now than rez later', 0, 0, '', 'Salut! Mieux vaut me soigner maintenant que me ressusciter plus tard.', 'Hey! Lieber jetzt heilen als später zu rezzen', '嘿!现在治好我总比待会儿复活强吧!', '', '¡Oye! Mejor curarme ahora que resucitarme después', '', 'Эй! Лучше исцели меня сейчас, чем воскрешать позже.'), + (1576, 'low health', 'I am sorry. Need another heal', 0, 0, '', 'Je suis désolé. J’ai besoin d’une autre guérison.', 'Sorry. Brauche noch einen Heal', '抱歉,又需要治疗了!', '', 'Lo siento. Necesito otra curación', '', 'Извини. Нужна еще одна исцеление.'), + (1577, 'low health', 'Damn mobs. Heal me please', 0, 0, '', 'Maudits mobs. Guéris moi s’il te plait', 'Verdammte Mobs. Heal mich bitte', '该死的怪物,拜托治疗一下!', '', '¡Malditos bichos! ¡Una curita por favor!', '', 'Черт возьми, мобы. Исцели меня, пожалуйста.'), + (1578, 'low health', 'One more hit and I am done for. Heal please', 0, 0, '', 'Un coup de plus et c’est fini. Guéris moi s’il te plait', 'Noch ein Treffer und ich bin hinüber. Heal bitte', '再挨一下我就完了,求治疗!', '', 'Un golpe más y estoy acabado. Cura por favor', '', 'Еще один удар, и я пропал. Исцели, пожалуйста.'), + (1579, 'low health', 'Are there any healers?', 0, 0, '', 'Y a-t-il des guérisseurs?', 'Heiler da?', '有治疗吗?', '', '¿Hay sanadores?', '', 'Есть ли целители?'), + (1580, 'low health', 'Why do they always punch me in the face? Need heal', 0, 0, '', 'Pourquoi est-ce qu’ils me frappent toujours au visage? J’ai besoin de guérison', 'Warum hauen die mir immer ins Gesicht? Brauche Heal', '为什么总是打我脸?需要治疗!', '', '¿Por qué siempre me golpean en la cara? Necesito sanacion', '', 'Почему они всегда бьют меня в лицо? Нужна исцеление.'), + (1581, 'low health', 'Can anybody heal me a bit?', 0, 0, '', 'Soignez moi, même juste un peu ?', 'Kann mich wer ein bisschen heilen?', '谁能给我加一点血?', '', '¿Alguien puede curarme un poco?', '', 'Может кто-нибудь немного исцелить меня?'), + (1582, 'low mana', 'OOM', 0, 0, '', 'Plus de mana !', 'OOM', '没蓝了!', '', 'No tengo mana!', '', 'OOM'), + (1583, 'low mana', 'I am out of mana', 0, 0, '', 'Je n’ai plus de mana', 'Kein Mana mehr', '我没蓝了!', '', 'No tengo maná', '', 'У меня закончилась мана'), + (1584, 'low mana', 'Damn I wasted all my mana on this', 0, 0, '', 'Damn j’ai gaspillé toute ma mana sur ça.', 'Verdammt, dafür hab ich mein ganzes Mana verballert', '该死,我的蓝全浪费在这上面了!', '', 'Me cago en todo! Malgasté todo mi maná enseguida', '', 'Черт, я потратил всю свою ману на это'), + (1585, 'low mana', 'You should wait until I drink or regenerate my mana', 0, 0, '', 'Tu devrais attendre que je boive ou régénère ma mana.', 'Wartet bis ich trinke oder mein Mana wieder da ist', '等我喝点水或者回点蓝再继续吧!', '', 'Deberías esperar hasta que beba o regenere mi maná', '', 'Тебе стоит подождать, пока я выпью или восстановлю свою ману'), + (1586, 'low mana', 'Low mana', 0, 0, '', 'Bas niveau de mana.', 'Mana low', '蓝量低了!', '', 'Maná bajo', '', 'Низкая мана'), + (1587, 'low mana', 'No mana. Again?', 0, 0, '', 'Pas de mana. Encore ?', 'Kein Mana. Schon wieder?', '没蓝了,又是这样?', '', 'Sin maná. ¿Otra vez?', '', 'Нет маны. Снова?'), + (1588, 'low mana', 'Low mana. Wanna drink', 0, 0, '', 'Plus de mana, je prendrai bien un peu d eau', 'Mana low. Muss trinken', '蓝量低了,想喝点水!', '', 'Maná bajo. Quiero beber', '', 'Низкая мана. Хочу выпить.'), + (1589, 'low mana', 'Do we have a vending machine? Out of mana again', 0, 0, '', 'Nous avons une machine distributrice ? Encore à court de mana.', 'Haben wir nen Getränkeautomaten? Wieder OOM', '有自动售卖机吗?又没蓝了!', '', '¿Tenemos una máquina expendedora? Sin maná otra vez', '', 'У нас есть торговый автомат? Снова нет маны.'), + (1590, 'low mana', 'My mana is history', 0, 0, '', 'Mon mana est à sec.', 'Mein Mana ist Geschichte', '我的蓝已经见底了!', '', 'Mi maná es historia', '', 'Моя мана в истории.'), + (1591, 'low mana', 'I’d get some drinks next time. Out of mana', 0, 0, '', 'Je prendrai des verres la prochaine fois. Plus de mana.', 'Ich besorge mir nächstes mal Drinks. OOM', '下次一定要多带点喝的,没蓝了!', '', '¡No tengo maná! Tengo que comprar bebidas la próxima vez...', '', ''), + (1592, 'low mana', 'Where is my mana?', 0, 0, '', 'Oh là là, désolé, mais je ne sais pas du tout où se trouve ta mana. As-tu vérifié tous tes sacs et tes poches? Peut-être que tu l’as laissé quelque part dans la maison ou dans ton sac à dos. Il est toujours embêtant de perdre quelque chose d’aussi précieux que sa mana, j’espère que tu vas la retrouver bientôt!', 'Wo ist mein Mana?', '我的蓝去哪儿了?', '', '¿Dónde está mi maná?', '', 'Где моя мана?'), + (1593, 'low ammo', 'I have few left!', 0, 0, '', 'Il ne me reste que quelques !', 'Ich habe nur noch wenig !', '我只剩一点了!', '', '¡Me quedan pocas !', '', 'У меня осталось немного !'), + (1594, 'low ammo', 'I need more !', 0, 0, '', 'J’ai besoin de plus de !', 'Ich brauche mehr !', '我需要更多的!', '', '¡Necesito más !', '', 'Мне нужно больше !'), + (1595, 'low ammo', '100 left!', 0, 0, '', 'Il me reste 100 !', 'Noch 100 !', '还剩100个!', '', '¡Quedan 100 !', '', '100 осталось!'), + (1596, 'no ammo', 'That’s it! No !', 0, 0, '', 'C’est ça! Pas de !', 'Das war es! Keine mehr!', '完了!没了!', '', '¡Eso es todo! ¡Sin !', '', ''), + (1597, 'no ammo', 'And you have my bow... Oops, no !', 0, 0, '', 'Et vous avez mon arc... Oops, pas de !', 'Und ihr habt meinen Bogen... ups, keine !', '你拿了我的弓……呃,没了!', '', 'Y tú tienes mi arco... ¡Uy, sin !', '', 'И у тебя есть мой лук... Ой, нет !'), + (1598, 'no ammo', 'Need ammo!', 0, 0, '', 'Besoin de munitions !', 'Brauche !', '需要弹药!', '', '¡Necesito munición!', '', 'Нужна патроны!'), + (1599, 'aoe', 'Oh god!', 0, 0, '', 'Oh mon Dieu!', 'Oh Gott!', '天哪!', '', '¡Oh, Dios!', '', 'О боже!'), + (1600, 'aoe', 'I am scared', 0, 0, '', 'Je suis effrayé.', 'Ich habe Angst', '我好害怕!', '', 'Tengo miedo', '', 'Мне страшно'), + (1601, 'aoe', 'We are done for', 0, 0, '', 'Nous sommes foutus.', 'Wir sind am Arsch.', '我们完了!', '', 'Hemos terminado', '', 'Мы пропали'), + (1602, 'aoe', 'This is over', 0, 0, '', 'C’est terminé', 'Das wars.', '这下完了!', '', 'Esto se acabó', '', 'Это закончено'), + (1603, 'aoe', 'This ends now', 0, 0, '', 'Cela se termine maintenant.', 'Hier ist jetzt Schluss.', '现在就结束了!', '', 'Esto termina ahora', '', 'Это заканчивается сейчас'), + (1604, 'aoe', 'Could somebody cast blizzard or something?', 0, 0, '', 'Est-ce que quelqu’un pourrait lancer une tempête de neige ou quelque chose du genre, s’il vous plaît?', 'Kann mal wer Blizzard anrufen?', '谁能扔个暴风雪啥的吗?', '', '¿Alguien podría lanzar ventisca o algo así?', '', 'Может кто-нибудь вызвать метель или что-то подобное?'), + (1605, 'aoe', 'Damn. The tank aggroed all the mobs around', 0, 0, '', 'Maudit tank a attiré tous les ennemis autour.', 'Verdammt. Der Tank hat alle Mobs gepullt.', '该死,坦克拉了周围所有的怪!', '', '¡Me cago en todo! El tanque atrajo a todos los mobs...', '', 'Черт. Танку агрировал всех мобов вокруг.'), + (1606, 'aoe', 'We gonna die. We gonna die. We gonna die.', 0, 0, '', 'On va mourir. On va mourir. On va mourir.', 'Wir sterben. Wir sterben. Wir sterben.', '我们要死了!我们要死了!我们要死了!', '', 'Vamos a morir. Vamos a morir. Vamos a morir.', '', 'Мы умрем. Мы умрем. Мы умрем.'), + (1607, 'aoe', 'Whoa! So many toys to play with', 0, 0, '', 'Oh là là ! Tellement de jouets avec lesquels jouer !', 'Whoa! So viel Spielzeug zum Austoben.', '哇!这么多小怪可以玩!', '', '¡Vaya! Tantos juguetes con los que jugar', '', 'Ух ты! Так много игрушек, с которыми можно играть.'), + (1608, 'aoe', 'I gonna kill them all!', 0, 0, '', 'Je vais les tuer tous !', 'Ich kill sie alle!', '我要干掉他们所有人!', '', '¡Voy a matarlos a todos!', '', 'Я убью их всех!'), + (1609, 'aoe', 'If the tank dies we are history', 0, 0, '', 'Si le tank meurt, c’est la fin pour nous.', 'Wenn der Tank stirbt, sind wir Geschichte.', '如果坦克挂了,我们就完蛋了!', '', 'Si el tanque muere somos historia', '', 'Если танк умрет, мы в истории.'), + (1610, 'aoe', 'Aaaaaargh!', 0, 0, '', 'Aaaaaaah!', 'Aaaaaargh!', '啊啊啊啊!', '', '¡Aaaaaargh!', '', 'Аааааргх!'), + (1611, 'aoe', 'LEEEEERROOOYYYYYYYYYYYY JENNKINNNSSSSSS!!!!!!!', 0, 0, '', 'LEEROOOOOOOYYYYYYYYYYY JENNKINNNSSSSSSS!!!!!!!', 'LEEEEERROOOYYYYYYYYYYYY JENNKINNNSSSSSS!!!!!!!', '李洛伊·詹金斯!!!', '', 'LEEEEERROOOYYYYYYYYYYYY JENNKINNNSSSSSS!!!!!!!', '', 'ЛЕЕЕЕЕЕЕЕЕЕЕЕЕЕЕЕЕЕЕЕЙ ДЖЕНКИНС!!!!!!!'), + (1612, 'aoe', 'Right. What do we have in AOE?', 0, 0, '', 'Alors, qu’est-ce qu’on a dans AOE?', 'Okay. Was haben wir an AoE?', '好吧,我们有什么群攻技能?', '', 'Claro. ¿Qué tenemos en AOE?', '', 'Правильно. Что у нас есть в AOE?'), + (1613, 'aoe', 'This gets interesting', 0, 0, '', 'Cela devient intéressant', 'Jetzt wirds interessant.', '这下有意思了!', '', 'Esto se pone interesante', '', 'Это становится интересным.'), + (1614, 'aoe', 'Cool. Get them in one place for a good flamestrike', 0, 0, '', 'Cool. Rassemble-les au même endroit pour un bon Coup de Flamme.', 'Cool. Zieht sie zusammen für nen schönen Flammenstoß.', '酷!把他们聚一起,给我来个烈焰冲击!', '', 'Genial! Ponlos todos juntos para un buen ataque en area', '', 'Круто. Соберите их в одном месте для хорошего огненного удара.'), + (1615, 'aoe', 'Kill! Kill! Kill!', 0, 0, '', 'TUER! TUER! TUER!.', 'Kill! Kill! Kill!', '杀!杀!杀!', '', '¡Matar! ¡Matar! ¡Matar!', '', 'Убей! Убей! Убей!'), + (1616, 'aoe', 'I think my pants are wet', 0, 0, '', 'Je crois que mon pantalon est mouillé.', 'Ich glaub, meine Hose ist nass.', '我感觉我的裤子湿了……', '', 'Creo que mis pantalones están mojados', '', 'Я думаю, что мои штаны мокрые.'), + (1617, 'aoe', 'We are history', 0, 0, '', 'Nous sommes l’histoire.', 'Wir sind Geschichte.', '我们完蛋了!', '', 'Somos historia', '', 'Мы в истории.'), + (1618, 'aoe', 'I hope healers are ready. Leeeeroy!', 0, 0, '', 'J’espère que les heal sont prêt. Leeeeroy!', 'Hoffentlich sind die Heiler ready. Leeeeroy!', '希望治疗准备好了,李洛伊!', '', 'Espero que los sanadores estén listos. ¡Leeeroy!', '', 'Надеюсь, целители готовы. Лееерой!'), + (1619, 'aoe', 'I hope they won’t come for me', 0, 0, '', 'J’espère qu’ils ne viendront pas me chercher.', 'Hoffentlich kommen die nicht zu mir.', '希望他们别冲着我来!', '', 'Espero que no vengan por mí', '', ''), + (1620, 'aoe', 'Oh no. I can’t see at this slaugther', 0, 0, '', 'Oh non. Je ne peux pas regarder ce massacre.', 'Oh nein. Ich kann bei dem Gemetzel nicht hinsehen.', '哦不,我看不下这场屠杀!', '', 'Oh, no. No puedo ver esta matanza', '', ''), + (1621, 'loot', 'I hope there will be some money', 0, 0, '', 'J’espère qu’il y aura de l’argent.', 'Hoffentlich gibts Gold.', '希望能有点金币!', '', 'Espero que haya algo de dinero...', '', 'Надеюсь, там будет немного денег.'), + (1622, 'loot', 'Loot! Loot!', 0, 0, '', 'Butin! Butin!', 'Loot! Loot!', '战利品!战利品!', '', '¡Botín! ¡Botín!', '', 'Лут! Лут!'), + (1623, 'loot', 'My precious', 0, 0, '', 'Mon précieux', 'Mein Schatz.', '我的宝贝!', '', 'Mi tesoro...', '', 'Мое драгоценное.'), + (1624, 'loot', 'I hope there is a shiny epic item waiting for me there', 0, 0, '', 'j’espère qu’il y a un objet épique brillant qui m’attend ici', 'Hoffentlich wartet da ein schickes episches Item auf mich.', '希望那儿有件闪闪发光的史诗装备等着我!', '', 'Espero que haya un objeto épico brillante esperándome allí', '', 'Надеюсь, там ждет меня блестящий эпический предмет.'), + (1625, 'loot', 'I have deep pockets and bags', 0, 0, '', 'J’ai de grandes poches et des sacs profonds.', 'Ich hab tiefe Taschen und Beutel.', '我的口袋和背包都很大!', '', 'Tengo bolsillos profundos y infinitos', '', 'У меня глубокие карманы и сумки.'), + (1626, 'loot', 'All is mine!', 0, 0, '', 'Tout m’appartient!', 'Alles meins!', '全是我的!', '', '¡Todo es mío!', '', 'Все мое!'), + (1627, 'loot', 'Hope no gray shit today', 0, 0, '', 'J’espère qu’il n’y aura pas de merde grise aujourd’hui.', 'Hoffentlich heute kein grauer Müll.', '希望今天别出垃圾灰装!', '', 'Espero que hoy no haya mierda gris', '', 'Надеюсь, сегодня не будет серой ерунды.'), + (1628, 'loot', 'This loot is MINE!', 0, 0, '', 'Ce butin est MIEN !', 'Dieser Loot ist MEINER!', '这战利品是我的!', '', '¡Este botín es MÍO!', '', 'Этот лут - МОЙ!'), + (1629, 'loot', 'Looting is disgusting but I need money', 0, 0, '', 'Le pillage est répugnant mais j’ai besoin d’argent', 'Looten ist eklig, aber ich brauch Gold.', '摸尸体真恶心,但我需要金币!', '', 'El saqueo es repugnante pero necesito dinero...', '', 'Лутать отвратительно, но мне нужны деньги.'), + (1630, 'loot', 'Gold!', 0, 0, '', 'ARGENT !', 'Gold!', '金币!', '', '¡Oro!', '', 'Золото!'), + (1631, 'loot', 'OK. Let’s see what they’ve got', 0, 0, '', 'D’accord. Voyons ce qu’ils ont.', 'Okay. Mal sehen, was sie droppen.', '好吧,看看他们身上有啥!', '', 'OK. Veamos qué tienen', '', ''), + (1632, 'loot', 'Do not worry. I will loot eveything', 0, 0, '', 'Ne t’inquiète pas. je vais tout piller', 'Keine Sorge. Ich loote alles.', '别担心,我会把所有东西都搜刮干净!', '', 'No te preocupes. Saquearé todo', '', 'Не волнуйтесь. Я все залутаю.'), + (1633, 'loot', 'I am loot ninja', 0, 0, '', 'Je suis un ninja du butin', 'Ich bin ein Ninja-Looter.', '我是战利品忍者!', '', 'Soy un ladron de tesoros', '', 'Я ниндзя лута.'), + (1634, 'loot', 'Do I neeed to roll?', 0, 0, '', 'Est-ce que je dois lancer les dés?', 'Muss ich würfeln?', '我需要roll点吗?', '', '¿Necesito lanzar dados?', '', 'Мне нужно бросить кубик?'), + (1635, 'loot', 'Somebody explain me, where they did put all this stuff?', 0, 0, '', 'Quelqu’un m’explique, où ils ont mis tout ça?', 'Kann mir mal wer erklären, wo die das ganze Zeug versteckt haben?', '谁能告诉我,他们把这些东西都藏哪儿了?', '', 'Alguien me explica, ¿dónde pusieron todas estas cosas?', '', 'Кто-нибудь объясните мне, куда они положили все эти вещи?'), + (1636, 'loot', 'No, I won’t loot gray shit', 0, 0, '', 'No way, je ne vais pas voler de la merde grise.', 'Nein, ich loote keinen grauen Müll.', '不行,我才不捡这些破烂灰色装备!', '', 'No, no saquearé mierda gris', '', ''), + (1637, 'loot', 'I’m first. I’m first. I’m first.', 0, 0, '', 'Je suis le premier. Je suis le premier. Je suis le premier.', 'Ich bin zuerst. Ich bin zuerst. Ich bin zuerst.', '我先来!我先来!我先来!', '', '¡Es mio! ¡Es mio! ¡Es mio!', '', ''), + (1638, 'loot', 'Give me your money!', 0, 0, '', 'Désolé, je ne peux pas faire ça.', 'Gebt mir euer Gold!', '把你的金币都交出来!', '', '¡Dame todo tu dinero!', '', 'Отдай мне свои деньги!'), + (1639, 'loot', 'My pockets are empty, I need to fill them', 0, 0, '', 'Mes poches sont vides, il faut que je les remplisse.', 'Meine Taschen sind leer, müssen aber voll werden.', '我的口袋空空如也,得赶紧装点东西!', '', 'Mis bolsillos están vacíos y necesitan llenarse!', '', 'Мои карманы пусты, мне нужно их заполнить.'), + (1640, 'loot', 'I’ve got a new bag for this', 0, 0, '', 'J’ai un nouveau sac pour cela.', 'Hab dafür ne neue Tasche.', '我刚弄了个新背包,正好装这些!', '', 'Tengo una nueva bolsa para esto', '', ''), + (1641, 'loot', 'I hope I won’t aggro anybody while looting', 0, 0, '', 'J’espère ne pas aggro, je vais loot !', 'Hoffentlich zieh ich beim Looten keine Aggro.', '希望我捡东西的时候别引到什么怪物!', '', 'Espero no ofender a nadie mientras saqueo', '', ''), + (1642, 'loot', 'Please don’t watch. I am looting', 0, 0, '', 'S’il vous plaît, ne regardez pas. je loot', 'Ich kann nicht looten wenn jemand zusieht.', '别盯着看,我在捡装备呢!', '', 'Por favor, no mires. Estoy saqueando', '', ''), + (1643, 'loot', 'Ha! You won’t get any piece of it!', 0, 0, '', 'Ha! Tu ne verras rien de cela !', 'Ha! Ihr kriegt davon keinen Krümel!', '哈!你别想分到半点东西!', '', '¡Ja! ¡No obtendrás nada de eso!', '', ''), + (1644, 'loot', 'Looting is cool', 0, 0, '', 'Le pillage c’est cool', 'Looten ist cool.', '捡战利品真是爽!', '', 'Saquear es genial', '', 'Лутать круто.'), + (1645, 'loot', 'I like new gear', 0, 0, '', 'J’aime les nouveaux équipements.', 'Ich mag neues Gear.', '我超喜欢新装备!', '', 'Me gusta el equipo nuevo', '', 'Мне нравится новая экипировка.'), + (1646, 'loot', 'I’l quit if there is nothing valuable again', 0, 0, '', 'j’arrêterai s’il n’y a plus rien de précieux', 'Ich höre auf, wenn schon wieder nichts Wertvolles droppt.', '要是再没什么值钱的东西,我就走人了!', '', 'Me doy por vencido si no hay nada util', '', ''), + (1647, 'loot', 'I hope it is be a pretty ring', 0, 0, '', 'j’espère que c’est une jolie bague', 'Hoffentlich ist es ein hübscher Ring.', '希望这次能捡到个漂亮的戒指!', '', 'Espero que sea un anillo bonito', '', 'Надеюсь, это будет красивое кольцо.'), + (1648, 'loot', 'I’l rip the loot from you', 0, 0, '', 'Je t’arracherai le butin', 'Ich reiß dir den Loot aus den Händen.', '我要把战利品从你手里抢过来!', '', 'Te arrancaré el botín', '', ''), + (1649, 'loot', 'Everybody stay off. I’m going to loot', 0, 0, '', 'Tout le monde, restez loin. Je vais piller.', 'Alle weg da. Ich loote jetzt.', '都别靠近,我要开始捡东西了!', '', '¡Alejaos todos! Voy a lootear', '', ''), + (1650, 'loot', 'Sweet loot', 0, 0, '', 'Je me suis procuré un butin incroyable !', 'Geiler Loot.', '真是甜美的战利品!', '', 'Dulce botín', '', 'Сладкий лут.'), + (1651, 'loot', 'The Roll God! Give me an epic today', 0, 0, '', 'Le Dieu des Lancés! Fais-moi vivre une épopée aujourd’hui', 'Oh ihr RNG-Götter! Gewährt mir heute einen Epic beim würfeln.', '掷骰之神!今天赐我一件史诗装备吧!', '', '¡El Dios de los dados! Dame un epico hoy', '', 'Бог Ролла! Дай мне эпик сегодня.'), + (1652, 'loot', 'Please give me new toys', 0, 0, '', 'S’il te plaît, donne-moi de nouveaux jouets', 'Bitte gib mir neues Spielzeug.', '求你了,给我点新玩意儿吧!', '', 'Por favor, dame nuevos juguetes', '', 'Пожалуйста, дай мне новые игрушки.'), + (1653, 'loot', 'I hope they carry tasties', 0, 0, '', 'J’espère qu’ils auront des délices.', 'Hoffe, die haben was Leckeres dabei.', '希望他们身上带了点好吃的!', '', 'Espero que caiga algo sabrosos', '', 'Надеюсь, они принесут вкусняшки.'), + (1654, 'loot', 'The gold is mine. I’ll leave everyting, I promise', 0, 0, '', 'L’or m’appartient. Je laisserai tout, je te le promets.', 'Das Gold gehört mir. Ich lasse euch den Rest, versprochen.', '金币是我的,其他都留给你们,我保证!', '', 'El oro es mío. Dejaré todo lo demas, lo prometo!', '', ''), + (1655, 'loot', 'No, I can’t resist', 0, 0, '', 'Non, je ne peux pas résister.', 'Nein, ich kann nicht widerstehen.', '不,我忍不住了!', '', 'No, no puedo resistir', '', ''), + (1656, 'loot', 'I want more!', 0, 0, '', 'Oh, vous en voulez plus!', 'Ich will mehr!', '我还想要更多!', '', '¡Quiero más!', '', 'Я хочу больше!'), + (1657, 'wait_travel_close', 'I am close, wait for me!', 0, 0, '', 'Je suis presque là, attends-moi !', 'Bin gleich da, wartet auf mich!', '我就在附近,等我一下!', '', '¡Estoy cerca, espérame!', '', 'Я близко, подожди меня!'), + (1658, 'wait_travel_close', 'I’m not far, please wait!', 0, 0, '', 'Je ne suis pas loin, s’il vous plaît patientez !', 'Bin nicht mehr weit, bitte wartet!', '我不远了,请稍等我!', '', 'No estoy lejos, ¡esperame por favor!', '', 'Я недалеко, погодите!'), + (1659, 'wait_travel_medium', 'I’m heading to your location', 0, 0, '', 'Je me dirige vers votre emplacement.', 'Bin auf dem Weg zu euch.', '我正朝你的位置赶去!', '', 'Me dirijo a tu ubicación', '', 'Двигаюсь к тебе'), + (1660, 'wait_travel_medium', 'I’m coming to you', 0, 0, '', 'Je viens vers vous.', 'Ich komme zu euch.', '我正向你靠近!', '', 'Voy hacia ti', '', 'Иду к тебе'), + (1661, 'wait_travel_far', 'I’m traveling to your location', 0, 0, '', 'Je voyage vers votre destination.', 'Ich reise zu euch.', '我正在前往你的位置!', '', 'Voy a viajar a tu ubicación', '', 'Направляюсь к тебе'), + (1662, 'wait_travel_far', 'I’m trying to get to you', 0, 0, '', 'Je suis en train de l’atteindre', 'Ich versuche zu euch zu kommen.', '我正努力赶到你那儿!', '', 'Estoy tratando de llegar a ti', '', 'Пытаюсь до тебя добраться'), + (1663, 'equip_command', 'Equipping %item', 0, 0, '', 'Équipement %item.', 'Rüste %item aus', '装备上 %item', '', 'Me he equipado %item', '', 'Экипирую %item.'), + (1664, 'unequip_command', '%item unequipped', 0, 0, '', '%item Déséquipé', '%item abgelegt.', '卸下了 %item', '', 'Me he quitado %item', '', '%item снят.'), + (1665, 'auto_learn_spell', 'I have learned the spells: %spells', 0, 0, '', 'Je viens d’apprendre: %spells', 'Ich habe die Zauber gelernt: %spells', '我学会了这些法术:%spells', '', 'He aprendido los hechizos: %spells', '', 'Я выучил заклинания: %spells.'), + (1666, 'use_command_item_cooldown', '%item is in cooldown', 0, 0, '', 'L’objet %item est en temps de recharge.', '%item hat Abklingzeit.', '%item 还在冷却中', '', '%item esta recargandose', '', '%item в перезарядке.'), + (1667, 'use_command_item_not_owned', 'I don’t have %item in my inventory', 0, 0, '', 'Je n’ai pas %item dans mon inventaire.', 'Ich habe %item nicht im Inventar.', '我的背包里没有 %item', '', 'No tengo %item en mi inventario', '', ''), + (1668, 'use_command_invalid_item', 'The item with the id %item does not exist', 0, 0, '', 'L’article avec l’identifiant %item n’existe pas.', 'Das Item mit der ID %item existiert nicht.', 'ID 为 %item 的物品不存在', '', 'El objeto con el id %item no existe', '', 'Предмет с ID %item не существует.'), + (1669, 'use_command_socket', 'Socketing %gem into %item', 0, 0, '', 'J’encastre le %gem dans %item.', 'Setze %gem in %item ein.', '将 %gem 镶嵌到 %item 上', '', 'Insertando %gem en %item', '', 'Вставляю %gem в %item.'), + (1670, 'use_command_item_error', 'I can’t use %item', 0, 0, '', 'Je ne peux pas utiliser %item.', 'Ich kann %item nicht benutzen.', '我无法使用 %item', '', 'No puedo usar %item', '', ''), + (1671, 'following', 'Following', 0, 0, '', 'Je suis', 'Folge.', '跟随中', '', 'Te sigo', '', 'Следую.'), + (1672, 'staying', 'Staying', 0, 0, '', 'Je reste ici', 'Bleibe hier.', '待命中', '', 'Me quedo aqui', '', 'Остаюсь.'), + (1673, 'fleeing', 'Fleeing', 0, 0, '', 'Je m’enfui', 'Fliehe.', '逃跑中', '', 'Huyendo', '', 'Убегаю.'), + (1674, 'fleeing_far', 'I won’t flee with you, you are too far away', 0, 0, '', 'Je ne vais pas partir avec toi, tu es trop loin.', 'Ich fliehe nicht mit dir, du bist zu weit weg.', '我不会跟你一起逃,你离我太远了!', '', 'No huiré contigo, estás demasiado lejos', '', 'Я с тобой не побегу, ты слишком далеко'), + (1675, 'grinding', 'Grinding', 0, 0, '', 'Grinding', 'Grinde.', '刷怪中', '', 'Farmeando', '', 'Фармлю.'), + (1676, 'attacking', 'Attacking', 0, 0, '', 'Attaque en cours !', 'Greife an.', '攻击中', '', 'Atacando', '', 'Атакую.'), + (1677, 'error_far', 'It is too far away', 0, 0, '', 'C’est trop loin.', 'Das ist zu weit weg.', '距离太远了', '', 'Estas demasiado lejos', '', 'Это слишком далеко.'), + (1678, 'error_water', 'It is under water', 0, 0, '', 'Cela se trouve sous l’eau', 'Das ist unter Wasser.', '目标在水下', '', 'Estas debajo del agua', '', 'Это под водой.'), + (1679, 'error_cant_go', 'I can’t go there', 0, 0, '', 'Je ne peux pas y aller.', 'Da kann ich nicht hin.', '我去不了那里', '', 'No puedo ir allí', '', 'Я не могу туда пройти'), + (1680, 'error_guild', 'I’m not in your guild!', 0, 0, '', 'Je ne fais pas partie de votre guilde!', 'Ich bin nicht in deiner Gilde!', '我不在你的公会里!', '', '¡No estoy en tu hermandad!', '', 'Я не в твоей гильдии'), + (1681, 'error_gbank_found', 'Can not find a guild bank nearby', 0, 0, '', 'Je ne trouve pas de banque de guilde à proximité.', 'Keine Gildenbank in der Nähe gefunden.', '附近找不到公会银行', '', 'No se puede encontrar un banco de hermandad cercano', '', 'Не могу найти гильдейский банк поблизости.'), + (1682, 'error_cant_put', 'I can’t put ', 0, 0, '', 'Je ne peux pas déposer ', 'Ich kann nicht ', '我无法存放', '', 'No puedo depositar ', '', 'Я не могу положить'), + (1683, 'error_gbank_rights', 'I have no rights to put items in the first guild bank tab', 0, 0, '', 'Je n’ai pas le droit de mettre des objets dans le premier onglet de la banque de guilde.', 'Ich habe keine Rechte, Items in den ersten Gildenbank-Reiter zu legen.', '我没有权限将物品放入公会银行的第一个标签页', '', 'No tengo derechos para depositar objetos en la primera pestaña del banco de hermandad', '', 'У меня нет прав на размещение предметов в первой вкладке гильдейского банка.'), + (1684, 'gbank_put', ' put to guild bank', 0, 0, '', ' déposé dans la banque de guilde.', ' in die Gildenbank gelegt', '已存入公会银行', '', ' depositado en el banco de hermandad', '', ' положено в гильдейский банк.'), + (1685, 'free_moving', 'Free moving', 0, 0, '', 'Déplacement libre', 'Freies Bewegen.', '自由移动中', '', 'Moviendome libremente', '', 'Свободное движение.'), + (1686, 'guarding', 'Guarding', 0, 0, '', 'Je garle la position.', 'Bewache.', '守卫中', '', 'Protegiendo la posición', '', 'Охраняю.'), + (1687, 'use_command', 'Using ', 0, 0, '', 'Utilisation de ', 'Benutze ', '使用 ', '', 'Usando ', '', ''), + (1688, 'command_target_unit', 'on %unit', 0, 0, '', 'sur %unit', 'auf %unit', '对 %unit', '', 'en %unit', '', 'на %unit.'), + (1689, 'use_command_remaining', '(%amount available)', 0, 0, '', '(%amount restant)', '(%amount verfügbar)', '(剩余 %amount)', '', '(%amount restante)', '', '(%amount доступно)'), + (1690, 'use_command_last', '(the last one)', 0, 0, '', '(La dernière fois)', '(das letzte)', '(最后一个)', '', '(el último)', '', '(последний)'), + (1691, 'use_command_socket_error', 'The socket does not fit', 0, 0, '', 'Le socket ne s’adapte pas.', 'Der Sockel passt nicht.', '插槽不匹配', '', 'La ranura no sirve', '', 'Сокет не подходит.'), + (1692, 'command_target_trade', 'on trade item', 0, 0, '', 'Sur le commerce de l’article', 'auf Handelsgegenstand', '对交易物品', '', 'en objeto comerciado', '', 'на торговом предмете.'), + (1693, 'command_target_self', 'on myself', 0, 0, '', 'sur moi-même', 'auf mich selbst', '对自己', '', 'en mi', '', 'на себе.'), + (1694, 'command_target_item', 'on %item', 0, 0, '', 'sur %item', 'auf %item', '对 %item', '', 'en %item', '', 'на %item.'), + (1695, 'command_target_go', 'on %gameobject', 0, 0, '', 'sur %gameobject', 'auf %gameobject', '对 %gameobject', '', 'en %gameobject', '', 'на %gameobject.'), + (1696, 'loot_command', 'Looting %item', 0, 0, '', 'Ramassage de %item', 'Loote %item', '拾取 %item', '', 'Despojando %item', '', 'Лутаю %item.'), + (1697, 'cast_spell_command_summon', 'Summoning ', 0, 0, '', 'Invocation de ', 'Beschwöre ', '召唤 ', '', 'Invocando a ', '', ''), + (1698, 'cast_spell_command_summon_error_members', 'I don’t have enough party members around to cast a summon', 0, 0, '', 'Je n’ai pas assez de membres de groupe autour de moi pour lancer une invocation.', 'Ich habe nicht genug Gruppenmitglieder in der Nähe, um beschwören zu können.', '我周围没有足够的队友,无法施放召唤术。', '', 'No tengo suficientes miembros de grupo alrededor para invocar', '', ''), + (1699, 'cast_spell_command_summon_error_target', 'Failed to find the summon target', 0, 0, '', 'Je n’ai pas réussi à trouver la cible à invoquer.', 'Beschwörungsziel nicht gefunden.', '找不到召唤目标!', '', 'No he podido encontrar al objetivo de la invocación', '', 'Не удалось найти цель для призыва.'), + (1700, 'cast_spell_command_summon_error_combat', 'I can’t summon while I’m in combat', 0, 0, '', 'Je ne peux pas invoquer pendant que je suis en combat', 'Ich kann im Kampf nicht beschwören.', '战斗中无法施放召唤术!', '', 'No puedo invocar durante un combate', '', ''), + (1701, 'cast_spell_command_error_unknown_spell', 'I don’t know the spell %spell', 0, 0, '', 'Je ne connais pas le sort %spell.', 'Ich kenne den Zauber %spell nicht.', '我不会这个技能 %spell。', '', 'No conozco el hechizo %spell', '', ''), + (1702, 'cast_spell_command_spell', 'Casting %spell', 0, 0, '', 'Lancement du sort %spell', 'Wirke %spell', '正在施放 %spell', '', 'Lanzando %spell', '', 'Кастую %spell.'), + (1703, 'cast_spell_command_craft', 'Crafting %spell', 0, 0, '', 'Je suis en train de fabriquer le sort %spell.', 'Stelle %spell her', '正在制作 %spell', '', 'Creando %spell', '', 'Создаю %spell.'), + (1704, 'cast_spell_command_error', 'Cannot cast %spell', 0, 0, '', 'Je ne peux pas lancer le sort %spell.', 'Kann %spell nicht wirken.', '无法施放 %spell!', '', 'No puedo lanzar %spell', '', 'Не могу кастовать %spell.'), + (1705, 'cast_spell_command_error_failed', 'Failed to cast %spell', 0, 0, '', 'J’ai échoué à lancer %spell.', 'Konnte %spell nicht wirken.', '施放 %spell 失败了!', '', 'Fallo al lanzar %spell', '', 'Не удалось кастовать %spell.'), + (1706, 'cast_spell_command_amount', ' |cffffff00(x%amount left)|r', 0, 0, '', ' |cffffff00(x%amount restant)|r.', ' |cffffff00(x%amount übrig)|r', ' |cffffff00(剩余 x%amount)|r', '', ' |cffffff00(x%amount restante)|r', '', ' |cffffff00(x%amount осталось)|r'), + (1707, 'dummy_end', 'dummy', 0, 0, '', 'dummy', 'Dummy', '', '', '', '', 'dummy'), + (1708, 'rp_missing_reagent_greater_blessing', 'By the Light... I forgot my Symbols of Kings. We’ll make do with %base_spell!', 0, 0, '', 'Par la Lumière... J\'ai oublié mes Symboles du roi. On se contentera de %base_spell !', 'Beim Licht… Ich hab meine Königssymbole vergessen. Na gut, dann nehmen wir eben %base_spell!', '', '', '', '', ''), + (1709, 'rp_missing_reagent_gift_of_the_wild', 'Nature is generous, my bags are not... out of herbs for %group_spell. Take %base_spell for now!', 0, 0, '', 'La nature est généreuse, pas mes sacs... plus d\'herbes pour %group_spell. Prenez %base_spell pour l\'instant !', 'Die Natur ist großzügig, meine Taschen nicht... keine Kräuter für %group_spell mehr. Nehmt fürs Erste %base_spell!', '', '', '', '', ''), + (1710, 'rp_missing_reagent_arcane_brilliance', 'Out of Arcane Powder... %group_spell will have to wait. Casting %base_spell!', 0, 0, '', 'Plus de poudre des arcanes... %group_spell attendra. Je lance %base_spell !', 'Arkanes Pulver alle... %group_spell muss warten. Wirke %base_spell!', '', '', '', '', ''), + (1711, 'rp_missing_reagent_generic', 'Oops, I’m out of components for %group_spell. We’ll go with %base_spell!', 0, 0, '', 'Oups, je n\'ai plus de composants pour %group_spell. On fera avec %base_spell !', 'Ups, mir fehlen die Komponenten für %group_spell. Wir nehmen %base_spell!', '', '', '', '', ''), + (1712, 'netherspite_beam_blocking_red', '%player is moving to block the red beam!', 0, 0, '', '', '%player bewegt sich, um den roten Strahl zu blocken!', '', '', '', '', ''), + (1713, 'netherspite_beam_blocking_blue', '%player is moving to block the blue beam!', 0, 0, '', '', '%player bewegt sich, um den blauen Strahl zu blocken!', '', '', '', '', ''), + (1714, 'netherspite_beam_blocking_green', '%player is moving to block the green beam!', 0, 0, '', '', '%player bewegt sich, um den grünen Strahl zu blocken!', '', '', '', '', ''), + (1715, 'netherspite_beam_leaving_blue', '%player is leaving the blue beam--next blocker up!', 0, 0, '', '', '%player verlässt den blauen Strahl--nächster Blocker los!', '', '', '', '', ''), + (1716, 'netherspite_beam_leaving_green', '%player is leaving the green beam--next blocker up!', 0, 0, '', '', '%player verlässt den grünen Strahl--nächster Blocker los!', '', '', '', '', ''), + (1717, 'pet_usage_error', 'Usage: pet ', 0, 0, '사용법: pet ', 'Utilisation: pet ', 'Verwendung: pet ', '用法: pet ', '用法: pet ', 'Uso: pet ', 'Uso: pet ', 'Использование: pet '), + (1718, 'pet_no_pet_error', 'You have no pet or guardian pet.', 0, 0, '펫이나 수호자 펫이 없습니다.', 'Vous n\'avez pas de familier ou gardien.', 'Du hast kein Tier oder Wächter.', '你没有宠物或守护者宠物。', '你沒有寵物或守護者寵物。', 'No tienes mascota o mascota guardián.', 'No tienes mascota o mascota guardián.', 'У вас нет питомца или защитника.'), + (1719, 'pet_stance_report', 'Current stance of %type "%name": %stance.', 0, 0, '%type "%name"의 현재 태세: %stance.', 'Position actuelle du %type "%name": %stance.', 'Aktuelle Haltung des %type "%name": %stance.', '%type "%name" 的当前姿态: %stance。', '%type "%name" 的當前姿態: %stance。', 'Postura actual del %type "%name": %stance.', 'Postura actual del %type "%name": %stance.', 'Текущая позиция %type "%name": %stance.'), + (1720, 'pet_no_target_error', 'No valid target selected by master.', 0, 0, '주인이 유효한 대상을 선택하지 않았습니다.', 'Aucune cible valide sélectionnée par le maître.', 'Kein gültiges Ziel vom Meister ausgewählt.', '主人未选择有效目标。', '主人未選擇有效目標。', 'No hay objetivo válido seleccionado por el maestro.', 'No hay objetivo válido seleccionado por el maestro.', 'Хозяин не выбрал действительную цель.'), + (1721, 'pet_target_dead_error', 'Target is not alive.', 0, 0, '대상이 살아있지 않습니다.', 'La cible n\'est pas vivante.', 'Das Ziel ist nicht am Leben.', '目标未存活。', '目標未存活。', 'El objetivo no está vivo.', 'El objetivo no está vivo.', 'Цель не жива.'), + (1722, 'pet_invalid_target_error', 'Target is not a valid attack target for the bot.', 0, 0, '대상이 봇에게 유효한 공격 대상이 아닙니다.', 'La cible n\'est pas une cible d\'attaque valide pour le bot.', 'Das Ziel ist kein gültiges Angriffsziel für den Bot.', '目标不是机器人的有效攻击目标。', '目標不是機器人的有效攻擊目標。', 'El objetivo no es un objetivo de ataque válido para el bot.', 'El objetivo no es un objetivo de ataque válido para el bot.', 'Цель не является допустимой целью атаки для бота.'), + (1723, 'pet_pvp_prohibited_error', 'I cannot command my pet to attack players in PvP prohibited areas.', 0, 0, 'PvP 금지 지역에서는 펫에게 플레이어 공격 명령을 내릴 수 없습니다.', 'Je ne peux pas commander à mon familier d\'attaquer des joueurs dans les zones où le PvP est interdit.', 'Ich kann meinem Tier nicht befehlen, Spieler in PvP-verbotenen Gebieten anzugreifen.', '我不能命令我的宠物在禁止PvP的区域攻击玩家。', '我不能命令我的寵物在禁止PvP的區域攻擊玩家。', 'No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.', 'No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.', 'Я не могу приказать своему питомцу атаковать игроков в зонах, где PvP запрещено.'), + (1724, 'pet_attack_success', 'Pet commanded to attack your target.', 0, 0, '펫이 당신의 대상을 공격하도록 명령했습니다.', 'Le familier a reçu l\'ordre d\'attaquer votre cible.', 'Tier wurde befohlen, dein Ziel anzugreifen.', '宠物已命令攻击你的目标。', '寵物已命令攻擊你的目標。', 'Mascota ordenada a atacar tu objetivo.', 'Mascota ordenada a atacar tu objetivo.', 'Питомцу приказано атаковать вашу цель.'), + (1725, 'pet_attack_failed', 'Pet did not attack. (Already attacking or unable to attack target)', 0, 0, '펫이 공격하지 않았습니다. (이미 공격 중이거나 대상 공격 불가)', 'Le familier n\'a pas attaqué. (Attaque déjà en cours ou impossible d\'attaquer la cible)', 'Tier hat nicht angegriffen. (Greift bereits an oder kann Ziel nicht angreifen)', '宠物未攻击。(已在攻击或无法攻击目标)', '寵物未攻擊。(已在攻擊或無法攻擊目標)', 'La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)', 'La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)', 'Питомец не атаковал. (Уже атакует или не может атаковать цель)'), + (1726, 'pet_follow_success', 'Pet commanded to follow.', 0, 0, '펫이 따라오도록 명령했습니다.', 'Le familier a reçu l\'ordre de suivre.', 'Tier wurde befohlen zu folgen.', '宠物已命令跟随。', '寵物已命令跟隨。', 'Mascota ordenada a seguir.', 'Mascota ordenada a seguir.', 'Питомцу приказано следовать.'), + (1727, 'pet_stay_success', 'Pet commanded to stay.', 0, 0, '펫이 머물도록 명령했습니다.', 'Le familier a reçu l\'ordre de rester.', 'Tier wurde befohlen zu bleiben.', '宠物已命令停留。', '寵物已命令停留。', 'Mascota ordenada a quedarse.', 'Mascota ordenada a quedarse.', 'Питомцу приказано остаться.'), + (1728, 'pet_unknown_command_error', 'Unknown pet command: %param. Use: pet ', 0, 0, '알 수 없는 펫 명령: %param. 사용법: pet ', 'Commande de familier inconnue: %param. Utilisation: pet ', 'Unbekannter Tierbefehl: %param. Verwendung: pet ', '未知宠物命令: %param。用法: pet ', '未知寵物命令: %param。用法: pet ', 'Comando de mascota desconocido: %param. Uso: pet ', 'Comando de mascota desconocido: %param. Uso: pet ', 'Неизвестная команда питомца: %param. Использование: pet '), + (1729, 'pet_stance_set_success', 'Pet stance set to %stance.', 0, 0, '펫 태세가 %stance(으)로 설정되었습니다.', 'Position du familier définie sur %stance.', 'Tierhaltung auf %stance gesetzt.', '宠物姿态设置为 %stance。', '寵物姿態設置為 %stance。', 'Postura de mascota establecida en %stance.', 'Postura de mascota establecida en %stance.', 'Позиция питомца установлена на %stance.'), + (1730, 'pet_type_pet', 'pet', 0, 0, '펫', 'familier', 'Tier', '宠物', '寵物', 'mascota', 'mascota', 'питомец'), + (1731, 'pet_type_guardian', 'guardian', 0, 0, '수호자', 'gardien', 'Wächter', '守护者', '守護者', 'guardián', 'guardián', 'защитник'), + (1732, 'pet_stance_aggressive', 'aggressive', 0, 0, '공격적', 'agressif', 'aggressiv', '进攻', '進攻', 'agresivo', 'agresivo', 'агрессивная'), + (1733, 'pet_stance_defensive', 'defensive', 0, 0, '방어적', 'défensif', 'defensiv', '防御', '防禦', 'defensivo', 'defensivo', 'защитная'), + (1734, 'pet_stance_passive', 'passive', 0, 0, '수동적', 'passif', 'passiv', '被动', '被動', 'pasivo', 'pasivo', 'пассивная'), + (1735, 'pet_stance_unknown', 'unknown', 0, 0, '알 수 없음', 'inconnu', 'unbekannt', '未知', '未知', 'desconocido', 'desconocido', 'неизвестная'), + (1736, 'no_fishing_pole_error', 'I don\'t have a Fishing Pole', 0, 0, '낚싯대가 없습니다', 'Je n’ai pas de canne à pêche', 'Ich habe keine Angelrute', '我沒有釣魚竿', '我没有钓鱼竿', 'No tengo una caña de pescar', 'No tengo una caña de pescar', 'У меня нет удочки'), + (1737, 'pvp_currency', '[PVP] Arena points: %arena_points | Honor Points: %honor_points', 0, 0, '[PVP] 투기장 점수: %arena_points | 명예 점수: %honor_points', '[PVP] Points d\'arène : %arena_points | Points d\'honneur : %honor_points', '[PVP] Arenapunkte: %arena_points | Ehrenpunkte: %honor_points', '[PVP] 竞技场点数:%arena_points | 荣誉点数:%honor_points', '[PVP] 競技場點數:%arena_points | 榮譽點數:%honor_points', '[PVP] Puntos de arena: %arena_points | Puntos de honor: %honor_points', '[PVP] Puntos de arena: %arena_points | Puntos de honor: %honor_points', '[PVP] Очки арены: %arena_points | Очки чести: %honor_points'), + (1738, 'pvp_arena_team', '[PVP] %bracket: <%team_name> (rating %team_rating)', 0, 0, '[PVP] %bracket: <%team_name> (평점 %team_rating)', '[PVP] %bracket : <%team_name> (cote %team_rating)', '[PVP] %bracket: <%team_name> (Wertung %team_rating)', '[PVP] %bracket: <%team_name> (评分 %team_rating)', '[PVP] %bracket: <%team_name> (評分 %team_rating)', '[PVP] %bracket: <%team_name> (índice %team_rating)', '[PVP] %bracket: <%team_name> (índice %team_rating)', '[PVP] %bracket: <%team_name> (рейтинг %team_rating)'), + (1739, 'pvp_no_arena_team', '[PVP] I have no Arena Team.', 0, 0, '[PVP] 투기장 팀이 없습니다.', '[PVP] Je n\'ai aucune équipe d\'arène.', '[PVP] Ich habe kein Arenateam.', '[PVP] 我没有竞技场战队。', '[PVP] 我沒有競技場隊伍。', '[PVP] No tengo equipo de arena.', '[PVP] No tengo equipo de arena.', '[PVP] У меня нет команды арены.'); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql index 9889147f6..e75112cce 100644 --- a/modules/mod-playerbots/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql @@ -4,15 +4,19 @@ -- zhTW, esES, esMX, ruRU) -- ######################################################### +DELETE FROM ai_playerbot_texts WHERE name IN ('pvp_currency', 'pvp_arena_team', 'pvp_no_arena_team'); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ('pvp_currency', 'pvp_arena_team', 'pvp_no_arena_team'); + -- --------------------------------------------------------- -- pvp_currency -- [PVP] Arena points: %arena_points | Honor Points: %honor_points -- --------------------------------------------------------- INSERT INTO `ai_playerbot_texts` - (`name`, `text`, `say_type`, `reply_type`, + (`id`, `name`, `text`, `say_type`, `reply_type`, `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) -SELECT +VALUES ( + 1737, 'pvp_currency', '[PVP] Arena points: %arena_points | Honor Points: %honor_points', 0, 0, @@ -31,20 +35,20 @@ SELECT -- esMX '[PVP] Puntos de arena: %arena_points | Puntos de honor: %honor_points', -- ruRU - '[PVP] Очки арены: %arena_points | Очки чести: %honor_points' -WHERE NOT EXISTS ( - SELECT 1 FROM `ai_playerbot_texts` WHERE `name` = 'pvp_currency' -); + '[PVP] Очки арены: %arena_points | Очки чести: %honor_points'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pvp_currency', 100); -- --------------------------------------------------------- -- pvp_arena_team -- [PVP] %bracket: <%team_name> (rating %team_rating) -- --------------------------------------------------------- INSERT INTO `ai_playerbot_texts` - (`name`, `text`, `say_type`, `reply_type`, + (`id`, `name`, `text`, `say_type`, `reply_type`, `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) -SELECT +VALUES ( + 1738, 'pvp_arena_team', '[PVP] %bracket: <%team_name> (rating %team_rating)', 0, 0, @@ -63,20 +67,20 @@ SELECT -- esMX '[PVP] %bracket: <%team_name> (índice %team_rating)', -- ruRU - '[PVP] %bracket: <%team_name> (рейтинг %team_rating)' -WHERE NOT EXISTS ( - SELECT 1 FROM `ai_playerbot_texts` WHERE `name` = 'pvp_arena_team' -); + '[PVP] %bracket: <%team_name> (рейтинг %team_rating)'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pvp_arena_team', 100); -- --------------------------------------------------------- -- pvp_no_arena_team -- [PVP] I have no Arena Team. -- --------------------------------------------------------- INSERT INTO `ai_playerbot_texts` - (`name`, `text`, `say_type`, `reply_type`, + (`id`, `name`, `text`, `say_type`, `reply_type`, `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) -SELECT +VALUES ( + 1739, 'pvp_no_arena_team', '[PVP] I have no Arena Team.', 0, 0, @@ -95,7 +99,6 @@ SELECT -- esMX '[PVP] No tengo equipo de arena.', -- ruRU - '[PVP] У меня нет команды арены.' -WHERE NOT EXISTS ( - SELECT 1 FROM `ai_playerbot_texts` WHERE `name` = 'pvp_no_arena_team' -); + '[PVP] У меня нет команды арены.'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('pvp_no_arena_team', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql new file mode 100644 index 000000000..9d254eb4c --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_02_13_00_ai_playerbot_texts.sql @@ -0,0 +1 @@ +UPDATE `ai_playerbot_texts` SET `text_loc3`='%s, du hörst den triefenden Sarkasmus in meinem text nicht' WHERE `id`=1353; diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql new file mode 100644 index 000000000..3c01179c4 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_14_00_ai_playerbot_wait_for_attack_texts.sql @@ -0,0 +1,104 @@ +-- ######################################################### +-- Playerbots - Add texts for SetWaitForAttackTimeAction +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ('wait_for_attack_provide_time', 'wait_for_attack_invalid_time', 'wait_for_attack_time_set'); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ('wait_for_attack_provide_time', 'wait_for_attack_invalid_time', 'wait_for_attack_time_set'); + +-- --------------------------------------------------------- +-- wait_for_attack_provide_time +-- Please provide a time to set (in seconds) +-- --------------------------------------------------------- +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 ( + 1740, + 'wait_for_attack_provide_time', + 'Please provide a time to set (in seconds)', + 0, 0, + -- koKR + '설정할 시간을 입력해 주세요 (초 단위)', + -- frFR + 'Veuillez indiquer un temps à définir (en secondes)', + -- deDE + 'Bitte gib eine Zeit an (in Sekunden)', + -- zhCN + '请提供要设置的时间(以秒为单位)', + -- zhTW + '請提供要設定的時間(以秒為單位)', + -- esES + 'Por favor, indica un tiempo a establecer (en segundos)', + -- esMX + 'Por favor, indica un tiempo a establecer (en segundos)', + -- ruRU + 'Пожалуйста, укажите время (в секундах)'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('wait_for_attack_provide_time', 100); + +-- --------------------------------------------------------- +-- wait_for_attack_invalid_time +-- Please provide valid time to set (in seconds) between 0 and 99 +-- --------------------------------------------------------- +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 ( + 1741, + 'wait_for_attack_invalid_time', + 'Please provide valid time to set (in seconds) between 0 and 99', + 0, 0, + -- koKR + '0에서 99 사이의 유효한 시간을 입력해 주세요 (초 단위)', + -- frFR + 'Veuillez indiquer un temps valide (en secondes) entre 0 et 99', + -- deDE + 'Bitte gib eine gültige Zeit an (in Sekunden) zwischen 0 und 99', + -- zhCN + '请提供有效的时间(以秒为单位),范围为 0 到 99', + -- zhTW + '請提供有效的時間(以秒為單位),範圍為 0 到 99', + -- esES + 'Por favor, indica un tiempo válido (en segundos) entre 0 y 99', + -- esMX + 'Por favor, indica un tiempo válido (en segundos) entre 0 y 99', + -- ruRU + 'Пожалуйста, укажите допустимое время (в секундах) от 0 до 99'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('wait_for_attack_invalid_time', 100); + +-- --------------------------------------------------------- +-- wait_for_attack_time_set +-- Wait for attack time set to %new_time seconds +-- --------------------------------------------------------- +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 ( + 1742, + 'wait_for_attack_time_set', + 'Wait for attack time set to %new_time seconds', + 0, 0, + -- koKR + '공격 대기 시간이 %new_time초로 설정되었습니다', + -- frFR + 'Temps d''attente avant l''attaque défini à %new_time secondes', + -- deDE + 'Wartezeit vor dem Angriff auf %new_time Sekunden gesetzt', + -- zhCN + '等待攻击时间已设置为 %new_time 秒', + -- zhTW + '等待攻擊時間已設定為 %new_time 秒', + -- esES + 'Tiempo de espera para atacar establecido en %new_time segundos', + -- esMX + 'Tiempo de espera para atacar establecido en %new_time segundos', + -- ruRU + 'Время ожидания атаки установлено на %new_time секунд'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('wait_for_attack_time_set', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql new file mode 100644 index 000000000..05f6d5d03 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_26_ai_playerbot_logout_texts.sql @@ -0,0 +1,59 @@ +-- Translations for additional logout related messages +DELETE FROM ai_playerbot_texts WHERE name IN ('bot_not_your_master', 'bot_rndbot_no_logout'); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ('bot_not_your_master', 'bot_rndbot_no_logout'); + +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 ( + 1743, + 'bot_not_your_master', + "You are not my master!", + 0, 0, + -- koKR + "당신은 내 주인이 아닙니다!", + -- frFR + "Tu n'es pas mon maître !", + -- deDE + "Du bist nicht mein Meister!", + -- zhCN + "你不是我的主人!", + -- zhTW + "你不是我的主人!", + -- esES + "¡No eres mi amo!", + -- esMX + "¡No eres mi amo!", + -- ruRU + "Ты не мой хозяин!"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('bot_not_your_master', 100); + +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 ( + 1744, + 'bot_rndbot_no_logout', + "You can't command me to logout!", + 0, 0, + -- koKR + "당신은 나에게 로그아웃을 명령할 수 없습니다!", + -- frFR + "Tu ne peux pas m'ordonner de me déconnecter !", + -- deDE + "Du kannst mir nicht befehlen, mich auszuloggen!", + -- zhCN + "你不能命令我下线!", + -- zhTW + "你不能命令我登出!", + -- esES + "¡No puedes ordenarme que cierre sesión!", + -- esMX + "¡No puedes ordenarme que cierre sesión!", + -- ruRU + "Ты не можешь приказать мне выйти из игры!"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('bot_rndbot_no_logout', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql new file mode 100644 index 000000000..3d356a2a2 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_03_28_00_ai_playerbot_focus_heal_texts.sql @@ -0,0 +1,240 @@ +-- ######################################################### +-- Playerbots - Add focus heal command texts +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'focus_heal_not_healer', + 'focus_heal_provide_names', + 'focus_heal_no_targets', + 'focus_heal_current_targets', + 'focus_heal_cleared', + 'focus_heal_add_remove_syntax', + 'focus_heal_not_in_group', + 'focus_heal_not_in_group_with', + 'focus_heal_added', + 'focus_heal_removed' +); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'focus_heal_not_healer', + 'focus_heal_provide_names', + 'focus_heal_no_targets', + 'focus_heal_current_targets', + 'focus_heal_cleared', + 'focus_heal_add_remove_syntax', + 'focus_heal_not_in_group', + 'focus_heal_not_in_group_with', + 'focus_heal_added', + 'focus_heal_removed' +); + +-- focus_heal_not_healer +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 ( + 1745, + 'focus_heal_not_healer', + 'I''m not a healer or offhealer (please change my strats to heal or offheal)', + 0, 0, + '저는 힐러나 오프힐러가 아닙니다 (전략을 heal 또는 offheal로 변경해주세요)', + 'Je ne suis pas un soigneur ou un soigneur secondaire (veuillez changer mes strats en heal ou offheal)', + 'Ich bin kein Heiler oder Nebenheiler (bitte ändere meine Strategien auf heal oder offheal)', + '我不是治疗者或副治疗者(请将我的策略更改为 heal 或 offheal)', + '我不是治療者或副治療者(請將我的策略更改為 heal 或 offheal)', + 'No soy un sanador ni un sanador secundario (por favor cambia mis estrategias a heal o offheal)', + 'No soy un sanador ni un sanador secundario (por favor cambia mis estrategias a heal o offheal)', + 'Я не лекарь и не побочный лекарь (пожалуйста, измените мои стратегии на heal или offheal)'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_healer', 100); + +-- focus_heal_provide_names +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 ( + 1746, + 'focus_heal_provide_names', + 'Please provide one or more player names', + 0, 0, + '하나 이상의 플레이어 이름을 제공해주세요', + 'Veuillez fournir un ou plusieurs noms de joueurs', + 'Bitte geben Sie einen oder mehrere Spielernamen an', + '请提供一个或多个玩家名称', + '請提供一個或多個玩家名稱', + 'Por favor proporciona uno o más nombres de jugadores', + 'Por favor proporciona uno o más nombres de jugadores', + 'Пожалуйста, укажите одно или несколько имён игроков'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_provide_names', 100); + +-- focus_heal_no_targets +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 ( + 1747, + 'focus_heal_no_targets', + 'I don''t have any focus heal targets', + 0, 0, + '지정된 집중 치유 대상이 없습니다', + 'Je n''ai aucune cible de soin prioritaire', + 'Ich habe keine fokussierten Heilziele', + '我没有任何集中治疗目标', + '我沒有任何集中治療目標', + 'No tengo ningún objetivo de sanación prioritario', + 'No tengo ningún objetivo de sanación prioritario', + 'У меня нет целей приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_no_targets', 100); + +-- focus_heal_current_targets: %targets is replaced with comma-separated player names +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 ( + 1748, + 'focus_heal_current_targets', + 'My focus heal targets are %targets', + 0, 0, + '나의 집중 치유 대상: %targets', + 'Mes cibles de soin prioritaire sont %targets', + 'Meine fokussierten Heilziele sind %targets', + '我的集中治疗目标是 %targets', + '我的集中治療目標是 %targets', + 'Mis objetivos de sanación prioritarios son %targets', + 'Mis objetivos de sanación prioritarios son %targets', + 'Мои цели приоритетного лечения: %targets'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_current_targets', 100); + +-- focus_heal_cleared +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 ( + 1749, + 'focus_heal_cleared', + 'Removed focus heal targets', + 0, 0, + '집중 치유 대상을 제거했습니다', + 'Cibles de soin prioritaire supprimées', + 'Fokussierte Heilziele entfernt', + '已移除集中治疗目标', + '已移除集中治療目標', + 'Objetivos de sanación prioritarios eliminados', + 'Objetivos de sanación prioritarios eliminados', + 'Цели приоритетного лечения удалены'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_cleared', 100); + +-- focus_heal_add_remove_syntax +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 ( + 1750, + 'focus_heal_add_remove_syntax', + 'Please specify a + for add or - to remove a target', + 0, 0, + '대상을 추가하려면 +, 제거하려면 -를 지정해주세요', + 'Veuillez spécifier + pour ajouter ou - pour retirer une cible', + 'Bitte geben Sie + zum Hinzufügen oder - zum Entfernen eines Ziels an', + '请指定 + 添加或 - 移除目标', + '請指定 + 添加或 - 移除目標', + 'Por favor especifica + para agregar o - para eliminar un objetivo', + 'Por favor especifica + para agregar o - para eliminar un objetivo', + 'Пожалуйста, укажите + для добавления или - для удаления цели'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_add_remove_syntax', 100); + +-- focus_heal_not_in_group +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 ( + 1751, + 'focus_heal_not_in_group', + 'I''m not in a group', + 0, 0, + '저는 파티에 속해있지 않습니다', + 'Je ne suis pas dans un groupe', + 'Ich bin in keiner Gruppe', + '我不在队伍中', + '我不在隊伍中', + 'No estoy en un grupo', + 'No estoy en un grupo', + 'Я не в группе'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_in_group', 100); + +-- focus_heal_not_in_group_with: %player_name is replaced with the target player's name +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 ( + 1752, + 'focus_heal_not_in_group_with', + 'I''m not in a group with %player_name', + 0, 0, + '%player_name 와(과) 같은 파티에 없습니다', + 'Je ne suis pas dans un groupe avec %player_name', + 'Ich bin nicht in einer Gruppe mit %player_name', + '我与 %player_name 不在同一队伍中', + '我與 %player_name 不在同一隊伍中', + 'No estoy en un grupo con %player_name', + 'No estoy en un grupo con %player_name', + 'Я не в группе с %player_name'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_not_in_group_with', 100); + +-- focus_heal_added: %player_name is replaced with the added player's name +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 ( + 1753, + 'focus_heal_added', + 'Added %player_name to focus heal targets', + 0, 0, + '%player_name 을(를) 집중 치유 대상에 추가했습니다', + '%player_name ajouté aux cibles de soin prioritaire', + '%player_name zu den fokussierten Heilzielen hinzugefügt', + '已将 %player_name 添加到集中治疗目标', + '已將 %player_name 添加到集中治療目標', + '%player_name agregado a los objetivos de sanación prioritarios', + '%player_name agregado a los objetivos de sanación prioritarios', + '%player_name добавлен в цели приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_added', 100); + +-- focus_heal_removed: %player_name is replaced with the removed player's name +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 ( + 1754, + 'focus_heal_removed', + 'Removed %player_name from focus heal targets', + 0, 0, + '%player_name 을(를) 집중 치유 대상에서 제거했습니다', + '%player_name retiré des cibles de soin prioritaire', + '%player_name aus den fokussierten Heilzielen entfernt', + '已将 %player_name 从集中治疗目标中移除', + '已將 %player_name 從集中治療目標中移除', + '%player_name eliminado de los objetivos de sanación prioritarios', + '%player_name eliminado de los objetivos de sanación prioritarios', + '%player_name удалён из целей приоритетного лечения'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('focus_heal_removed', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_12_00_ai_playerbot_pull_texts.sql new file mode 100644 index 000000000..4e9c17583 --- /dev/null +++ b/modules/mod-playerbots/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/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_00_playerbots_bis_gear.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_00_playerbots_bis_gear.sql new file mode 100644 index 000000000..55737d025 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_00_playerbots_bis_gear.sql @@ -0,0 +1,7972 @@ +-- BiS gear table for the /p bis party-chat command . Unified across tiers. +-- Lookup keys: (class, tab, slot, faction, auto_gear_score_limit). +-- faction: 0=Both, 1=Alliance, 2=Horde. Faction-specific overrides Both. +-- auto_gear_score_limit: matched exactly against AiPlayerbot.AutoGearScoreLimit. +-- phase: free-text label for readability (e.g. 'Phase 5', 'Pre-Raid'). +-- Slot uses AzerothCore EquipmentSlots enum: +-- head=0, neck=1, shoulders=2, chest=4, waist=5, legs=6, feet=7, wrists=8, hands=9, +-- finger1=10, finger2=11, trinket1=12, trinket2=13, back=14, mainhand=15, offhand=16, ranged=17. +-- Druid Bear lives under sentinel tab=10 (resolved at runtime when bot has Tank strategy). + +DROP TABLE IF EXISTS `playerbots_bis`; +DROP TABLE IF EXISTS `playerbots_bis_gear`; +CREATE TABLE `playerbots_bis_gear` ( + `class` TINYINT UNSIGNED NOT NULL, + `tab` TINYINT UNSIGNED NOT NULL, + `slot` TINYINT UNSIGNED NOT NULL, + `faction` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `auto_gear_score_limit` SMALLINT UNSIGNED NOT NULL, + `item_id` INT UNSIGNED NOT NULL, + `phase` VARCHAR(32) NOT NULL DEFAULT '', + `class_name` VARCHAR(16) NOT NULL, + `spec_name` VARCHAR(32) NOT NULL, + `slot_name` VARCHAR(16) NOT NULL, + `faction_name` VARCHAR(8) NOT NULL DEFAULT 'Both', + `item_name` VARCHAR(96) NOT NULL, + PRIMARY KEY (`class`, `tab`, `slot`, `faction`, `auto_gear_score_limit`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- ============================================================ +-- Warrior (1) +-- ============================================================ +-- Arms (tab 0) +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 120, 32087, 'Pre-Raid', 'Warrior', 'Arms', 'Head', 'Both', 'Mask of the Deceiver'), +(1, 0, 1, 0, 120, 29349, 'Pre-Raid', 'Warrior', 'Arms', 'Neck', 'Both', 'Adamantine Chain of the Unbroken'), +(1, 0, 2, 0, 120, 33173, 'Pre-Raid', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Ragesteel Shoulders'), +(1, 0, 4, 0, 120, 23522, 'Pre-Raid', 'Warrior', 'Arms', 'Chest', 'Both', 'Ragesteel Breastplate'), +(1, 0, 5, 0, 120, 27985, 'Pre-Raid', 'Warrior', 'Arms', 'Waist', 'Both', 'Deathforge Girdle'), +(1, 0, 6, 0, 120, 30538, 'Pre-Raid', 'Warrior', 'Arms', 'Legs', 'Both', 'Midnight Legguards'), +(1, 0, 7, 0, 120, 25686, 'Pre-Raid', 'Warrior', 'Arms', 'Feet', 'Both', 'Fel Leather Boots'), +(1, 0, 8, 0, 120, 23537, 'Pre-Raid', 'Warrior', 'Arms', 'Wrists', 'Both', 'Black Felsteel Bracers'), +(1, 0, 9, 0, 120, 25685, 'Pre-Raid', 'Warrior', 'Arms', 'Hands', 'Both', 'Fel Leather Gloves'), +(1, 0, 10, 0, 120, 29379, 'Pre-Raid', 'Warrior', 'Arms', 'Finger1', 'Both', 'Ring of Arathi Warlords'), +(1, 0, 11, 0, 120, 30834, 'Pre-Raid', 'Warrior', 'Arms', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(1, 0, 12, 0, 120, 29383, 'Pre-Raid', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(1, 0, 13, 0, 120, 28034, 'Pre-Raid', 'Warrior', 'Arms', 'Trinket2', 'Both', 'Hourglass of the Unraveller'), +(1, 0, 14, 0, 120, 24259, 'Pre-Raid', 'Warrior', 'Arms', 'Back', 'Both', 'Vengeance Wrap'), +(1, 0, 15, 0, 120, 28438, 'Pre-Raid', 'Warrior', 'Arms', 'MainHand', 'Both', 'Dragonmaw'), +(1, 0, 16, 0, 120, 23542, 'Pre-Raid', 'Warrior', 'Arms', 'OffHand', 'Both', 'Fel Edged Battleaxe'), +(1, 0, 17, 0, 120, 30279, 'Pre-Raid', 'Warrior', 'Arms', 'Ranged', 'Both', 'Mama''s Insurance'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 125, 29021, 'Phase 1', 'Warrior', 'Arms', 'Head', 'Both', 'Warbringer Battle-Helm'), +(1, 0, 1, 0, 125, 29349, 'Phase 1', 'Warrior', 'Arms', 'Neck', 'Both', 'Adamantine Chain of the Unbroken'), +(1, 0, 2, 0, 125, 30740, 'Phase 1', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Ripfiend Shoulderplates'), +(1, 0, 4, 0, 125, 29019, 'Phase 1', 'Warrior', 'Arms', 'Chest', 'Both', 'Warbringer Breastplate'), +(1, 0, 5, 0, 125, 28779, 'Phase 1', 'Warrior', 'Arms', 'Waist', 'Both', 'Girdle of the Endless Pit'), +(1, 0, 6, 0, 125, 28741, 'Phase 1', 'Warrior', 'Arms', 'Legs', 'Both', 'Skulker''s Greaves'), +(1, 0, 7, 0, 125, 28608, 'Phase 1', 'Warrior', 'Arms', 'Feet', 'Both', 'Ironstriders of Urgency'), +(1, 0, 8, 0, 125, 28795, 'Phase 1', 'Warrior', 'Arms', 'Wrists', 'Both', 'Bladespire Warbands'), +(1, 0, 9, 0, 125, 28824, 'Phase 1', 'Warrior', 'Arms', 'Hands', 'Both', 'Gauntlets of Martial Perfection'), +(1, 0, 10, 0, 125, 28757, 'Phase 1', 'Warrior', 'Arms', 'Finger1', 'Both', 'Ring of a Thousand Marks'), +(1, 0, 11, 0, 125, 30834, 'Phase 1', 'Warrior', 'Arms', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(1, 0, 12, 0, 125, 29383, 'Phase 1', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(1, 0, 13, 0, 125, 28830, 'Phase 1', 'Warrior', 'Arms', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(1, 0, 14, 0, 125, 24259, 'Phase 1', 'Warrior', 'Arms', 'Back', 'Both', 'Vengeance Wrap'), +(1, 0, 15, 0, 125, 28438, 'Phase 1', 'Warrior', 'Arms', 'MainHand', 'Both', 'Dragonmaw'), +(1, 0, 16, 0, 125, 31332, 'Phase 1', 'Warrior', 'Arms', 'OffHand', 'Both', 'Blinkstrike'), +(1, 0, 17, 0, 125, 28772, 'Phase 1', 'Warrior', 'Arms', 'Ranged', 'Both', 'Sunfury Bow of the Phoenix'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 141, 30120, 'Phase 2', 'Warrior', 'Arms', 'Head', 'Both', 'Destroyer Battle-Helm'), +(1, 0, 1, 0, 141, 30022, 'Phase 2', 'Warrior', 'Arms', 'Neck', 'Both', 'Pendant of the Perilous'), +(1, 0, 2, 0, 141, 30122, 'Phase 2', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Destroyer Shoulderblades'), +(1, 0, 4, 0, 141, 30118, 'Phase 2', 'Warrior', 'Arms', 'Chest', 'Both', 'Destroyer Breastplate'), +(1, 0, 5, 0, 141, 30106, 'Phase 2', 'Warrior', 'Arms', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(1, 0, 6, 0, 141, 29995, 'Phase 2', 'Warrior', 'Arms', 'Legs', 'Both', 'Leggings of Murderous Intent'), +(1, 0, 7, 0, 141, 30081, 'Phase 2', 'Warrior', 'Arms', 'Feet', 'Both', 'Warboots of Obliteration'), +(1, 0, 8, 0, 141, 30057, 'Phase 2', 'Warrior', 'Arms', 'Wrists', 'Both', 'Bracers of Eradication'), +(1, 0, 9, 0, 141, 30119, 'Phase 2', 'Warrior', 'Arms', 'Hands', 'Both', 'Destroyer Gauntlets'), +(1, 0, 10, 0, 141, 29997, 'Phase 2', 'Warrior', 'Arms', 'Finger1', 'Both', 'Band of the Ranger-General'), +(1, 0, 11, 0, 141, 30834, 'Phase 2', 'Warrior', 'Arms', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(1, 0, 12, 0, 141, 29383, 'Phase 2', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(1, 0, 13, 0, 141, 28830, 'Phase 2', 'Warrior', 'Arms', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(1, 0, 14, 0, 141, 24259, 'Phase 2', 'Warrior', 'Arms', 'Back', 'Both', 'Vengeance Wrap'), +(1, 0, 15, 0, 141, 28439, 'Phase 2', 'Warrior', 'Arms', 'MainHand', 'Both', 'Dragonstrike'), +(1, 0, 16, 0, 141, 30082, 'Phase 2', 'Warrior', 'Arms', 'OffHand', 'Both', 'Talon of Azshara'), +(1, 0, 17, 0, 141, 30105, 'Phase 2', 'Warrior', 'Arms', 'Ranged', 'Both', 'Serpent Spine Longbow'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 156, 32235, 'Phase 3', 'Warrior', 'Arms', 'Head', 'Both', 'Cursed Vision of Sargeras'), +(1, 0, 1, 0, 156, 32591, 'Phase 3', 'Warrior', 'Arms', 'Neck', 'Both', 'Choker of Serrated Blades'), +(1, 0, 2, 0, 156, 30979, 'Phase 3', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Onslaught Shoulderblades'), +(1, 0, 4, 0, 156, 30975, 'Phase 3', 'Warrior', 'Arms', 'Chest', 'Both', 'Onslaught Breastplate'), +(1, 0, 5, 0, 156, 30106, 'Phase 3', 'Warrior', 'Arms', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(1, 0, 6, 0, 156, 32341, 'Phase 3', 'Warrior', 'Arms', 'Legs', 'Both', 'Leggings of Divine Retribution'), +(1, 0, 7, 0, 156, 32345, 'Phase 3', 'Warrior', 'Arms', 'Feet', 'Both', 'Dreadboots of the Legion'), +(1, 0, 8, 0, 156, 30863, 'Phase 3', 'Warrior', 'Arms', 'Wrists', 'Both', 'Deadly Cuffs'), +(1, 0, 9, 0, 156, 32278, 'Phase 3', 'Warrior', 'Arms', 'Hands', 'Both', 'Grips of Silent Justice'), +(1, 0, 10, 0, 156, 32497, 'Phase 3', 'Warrior', 'Arms', 'Finger1', 'Both', 'Stormrage Signet Ring'), +(1, 0, 11, 0, 156, 32335, 'Phase 3', 'Warrior', 'Arms', 'Finger2', 'Both', 'Unstoppable Aggressor''s Ring'), +(1, 0, 12, 0, 156, 28830, 'Phase 3', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Dragonspine Trophy'), +(1, 0, 13, 0, 156, 32505, 'Phase 3', 'Warrior', 'Arms', 'Trinket2', 'Both', 'Madness of the Betrayer'), +(1, 0, 14, 0, 156, 32323, 'Phase 3', 'Warrior', 'Arms', 'Back', 'Both', 'Shadowmoon Destroyer''s Drape'), +(1, 0, 15, 0, 156, 32837, 'Phase 3', 'Warrior', 'Arms', 'MainHand', 'Both', 'Warglaive of Azzinoth'), +(1, 0, 16, 0, 156, 32838, 'Phase 3', 'Warrior', 'Arms', 'OffHand', 'Both', 'Warglaive of Azzinoth'), +(1, 0, 17, 0, 156, 32326, 'Phase 3', 'Warrior', 'Arms', 'Ranged', 'Both', 'Twisted Blades of Zarak'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 164, 32235, 'Phase 4', 'Warrior', 'Arms', 'Head', 'Both', 'Cursed Vision of Sargeras'), +(1, 0, 1, 0, 164, 32591, 'Phase 4', 'Warrior', 'Arms', 'Neck', 'Both', 'Choker of Serrated Blades'), +(1, 0, 2, 0, 164, 30979, 'Phase 4', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Onslaught Shoulderblades'), +(1, 0, 4, 0, 164, 30975, 'Phase 4', 'Warrior', 'Arms', 'Chest', 'Both', 'Onslaught Breastplate'), +(1, 0, 5, 0, 164, 30106, 'Phase 4', 'Warrior', 'Arms', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(1, 0, 6, 0, 164, 32341, 'Phase 4', 'Warrior', 'Arms', 'Legs', 'Both', 'Leggings of Divine Retribution'), +(1, 0, 7, 0, 164, 32345, 'Phase 4', 'Warrior', 'Arms', 'Feet', 'Both', 'Dreadboots of the Legion'), +(1, 0, 8, 0, 164, 30863, 'Phase 4', 'Warrior', 'Arms', 'Wrists', 'Both', 'Deadly Cuffs'), +(1, 0, 9, 0, 164, 32278, 'Phase 4', 'Warrior', 'Arms', 'Hands', 'Both', 'Grips of Silent Justice'), +(1, 0, 10, 0, 164, 33496, 'Phase 4', 'Warrior', 'Arms', 'Finger1', 'Both', 'Signet of Primal Wrath'), +(1, 0, 11, 0, 164, 32497, 'Phase 4', 'Warrior', 'Arms', 'Finger2', 'Both', 'Stormrage Signet Ring'), +(1, 0, 12, 0, 164, 28830, 'Phase 4', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Dragonspine Trophy'), +(1, 0, 13, 0, 164, 32505, 'Phase 4', 'Warrior', 'Arms', 'Trinket2', 'Both', 'Madness of the Betrayer'), +(1, 0, 14, 0, 164, 32323, 'Phase 4', 'Warrior', 'Arms', 'Back', 'Both', 'Shadowmoon Destroyer''s Drape'), +(1, 0, 15, 0, 164, 32837, 'Phase 4', 'Warrior', 'Arms', 'MainHand', 'Both', 'Warglaive of Azzinoth'), +(1, 0, 16, 0, 164, 32838, 'Phase 4', 'Warrior', 'Arms', 'OffHand', 'Both', 'Warglaive of Azzinoth'), +(1, 0, 17, 0, 164, 33474, 'Phase 4', 'Warrior', 'Arms', 'Ranged', 'Both', 'Ancient Amani Longbow'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 200, 41386, 'Pre-Raid', 'Warrior', 'Arms', 'Head', 'Both', 'Spiked Titansteel Helm'), +(1, 0, 1, 0, 200, 42645, 'Pre-Raid', 'Warrior', 'Arms', 'Neck', 'Both', 'Titanium Impact Choker'), +(1, 0, 2, 0, 200, 37627, 'Pre-Raid', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Snake Den Spaulders'), +(1, 0, 4, 0, 200, 37612, 'Pre-Raid', 'Warrior', 'Arms', 'Chest', 'Both', 'Bonegrinder Breastplate'), +(1, 0, 5, 0, 200, 37171, 'Pre-Raid', 'Warrior', 'Arms', 'Waist', 'Both', 'Flame-Bathed Steel Girdle'), +(1, 0, 6, 0, 200, 37193, 'Pre-Raid', 'Warrior', 'Arms', 'Legs', 'Both', 'Staggering Legplates'), +(1, 0, 7, 0, 200, 41391, 'Pre-Raid', 'Warrior', 'Arms', 'Feet', 'Both', 'Spiked Titansteel Treads'), +(1, 0, 8, 0, 200, 37668, 'Pre-Raid', 'Warrior', 'Arms', 'Wrists', 'Both', 'Bands of the Stoneforge'), +(1, 0, 9, 0, 200, 37363, 'Pre-Raid', 'Warrior', 'Arms', 'Hands', 'Both', 'Gauntlets of Dragon Wrath'), +(1, 0, 10, 0, 200, 37642, 'Pre-Raid', 'Warrior', 'Arms', 'Finger1', 'Both', 'Hemorrhaging Circle'), +(1, 0, 12, 0, 200, 40684, 'Pre-Raid', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Mirror of Truth'), +(1, 0, 14, 0, 200, 43566, 'Pre-Raid', 'Warrior', 'Arms', 'Back', 'Both', 'Ice Striker''s Cloak'), +(1, 0, 15, 0, 200, 41257, 'Pre-Raid', 'Warrior', 'Arms', 'MainHand', 'Both', 'Titansteel Destroyer'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 224, 40528, 'Phase 1', 'Warrior', 'Arms', 'Head', 'Both', 'Valorous Dreadnaught Helmet'), +(1, 0, 1, 0, 224, 44664, 'Phase 1', 'Warrior', 'Arms', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(1, 0, 2, 0, 224, 40530, 'Phase 1', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Valorous Dreadnaught Shoulderplates'), +(1, 0, 4, 0, 224, 40525, 'Phase 1', 'Warrior', 'Arms', 'Chest', 'Both', 'Valorous Dreadnaught Battleplate'), +(1, 0, 5, 0, 224, 40317, 'Phase 1', 'Warrior', 'Arms', 'Waist', 'Both', 'Girdle of Razuvious'), +(1, 0, 6, 0, 224, 40318, 'Phase 1', 'Warrior', 'Arms', 'Legs', 'Both', 'Legplates of Double Strikes'), +(1, 0, 7, 0, 224, 40206, 'Phase 1', 'Warrior', 'Arms', 'Feet', 'Both', 'Iron-Spring Jumpers'), +(1, 0, 8, 0, 224, 40733, 'Phase 1', 'Warrior', 'Arms', 'Wrists', 'Both', 'Wristbands of the Sentinel Huntress'), +(1, 0, 9, 0, 224, 40527, 'Phase 1', 'Warrior', 'Arms', 'Hands', 'Both', 'Valorous Dreadnaught Gauntlets'), +(1, 0, 10, 0, 224, 40075, 'Phase 1', 'Warrior', 'Arms', 'Finger1', 'Both', 'Ruthlessness'), +(1, 0, 12, 0, 224, 40256, 'Phase 1', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Grim Toll'), +(1, 0, 14, 0, 224, 40250, 'Phase 1', 'Warrior', 'Arms', 'Back', 'Both', 'Aged Winter Cloak'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 245, 46151, 'Phase 2', 'Warrior', 'Arms', 'Head', 'Both', 'Conqueror''s Siegebreaker Helmet'), +(1, 0, 1, 0, 245, 45459, 'Phase 2', 'Warrior', 'Arms', 'Neck', 'Both', 'Frigid Strength of Hodir'), +(1, 0, 2, 0, 245, 46149, 'Phase 2', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Conqueror''s Siegebreaker Shoulderplates'), +(1, 0, 4, 0, 245, 46146, 'Phase 2', 'Warrior', 'Arms', 'Chest', 'Both', 'Conqueror''s Siegebreaker Battleplate'), +(1, 0, 5, 0, 245, 45241, 'Phase 2', 'Warrior', 'Arms', 'Waist', 'Both', 'Belt of Colossal Rage'), +(1, 0, 6, 0, 245, 45536, 'Phase 2', 'Warrior', 'Arms', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(1, 0, 7, 0, 245, 45599, 'Phase 2', 'Warrior', 'Arms', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(1, 0, 8, 0, 245, 45663, 'Phase 2', 'Warrior', 'Arms', 'Wrists', 'Both', 'Armbands of Bedlam'), +(1, 0, 9, 0, 245, 46148, 'Phase 2', 'Warrior', 'Arms', 'Hands', 'Both', 'Conqueror''s Siegebreaker Gauntlets'), +(1, 0, 10, 0, 245, 45608, 'Phase 2', 'Warrior', 'Arms', 'Finger1', 'Both', 'Brann''s Signet Ring'), +(1, 0, 12, 0, 245, 46038, 'Phase 2', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Dark Matter'), +(1, 0, 14, 0, 245, 46032, 'Phase 2', 'Warrior', 'Arms', 'Back', 'Both', 'Drape of the Faceless General'), +(1, 0, 17, 0, 245, 45296, 'Phase 2', 'Warrior', 'Arms', 'Ranged', 'Both', 'Twirling Blades'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 1, 258, 48383, 'Phase 3', 'Warrior', 'Arms', 'Head', 'Alliance', 'Wrynn''s Helmet of Triumph'), +(1, 0, 0, 2, 258, 48398, 'Phase 3', 'Warrior', 'Arms', 'Head', 'Horde', 'Hellscream''s Helmet of Triumph'), +(1, 0, 1, 1, 258, 47915, 'Phase 3', 'Warrior', 'Arms', 'Neck', 'Alliance', 'Collar of Ceaseless Torment'), +(1, 0, 1, 2, 258, 47988, 'Phase 3', 'Warrior', 'Arms', 'Neck', 'Horde', 'Collar of Unending Torment'), +(1, 0, 2, 1, 258, 48381, 'Phase 3', 'Warrior', 'Arms', 'Shoulders', 'Alliance', 'Wrynn''s Shoulderplates of Triumph'), +(1, 0, 2, 2, 258, 48400, 'Phase 3', 'Warrior', 'Arms', 'Shoulders', 'Horde', 'Hellscream''s Shoulderplates of Triumph'), +(1, 0, 4, 1, 258, 48385, 'Phase 3', 'Warrior', 'Arms', 'Chest', 'Alliance', 'Wrynn''s Battleplate of Triumph'), +(1, 0, 4, 2, 258, 48396, 'Phase 3', 'Warrior', 'Arms', 'Chest', 'Horde', 'Hellscream''s Battleplate of Triumph'), +(1, 0, 5, 1, 258, 47153, 'Phase 3', 'Warrior', 'Arms', 'Waist', 'Alliance', 'Belt of Deathly Dominion'), +(1, 0, 5, 2, 258, 47472, 'Phase 3', 'Warrior', 'Arms', 'Waist', 'Horde', 'Waistguard of Deathly Dominion'), +(1, 0, 6, 1, 258, 48382, 'Phase 3', 'Warrior', 'Arms', 'Legs', 'Alliance', 'Wrynn''s Legplates of Triumph'), +(1, 0, 6, 2, 258, 48399, 'Phase 3', 'Warrior', 'Arms', 'Legs', 'Horde', 'Hellscream''s Legplates of Triumph'), +(1, 0, 7, 1, 258, 47077, 'Phase 3', 'Warrior', 'Arms', 'Feet', 'Alliance', 'Treads of the Icewalker'), +(1, 0, 7, 2, 258, 47445, 'Phase 3', 'Warrior', 'Arms', 'Feet', 'Horde', 'Icewalker Treads'), +(1, 0, 8, 1, 258, 47074, 'Phase 3', 'Warrior', 'Arms', 'Wrists', 'Alliance', 'Bracers of the Untold Massacre'), +(1, 0, 8, 2, 258, 47442, 'Phase 3', 'Warrior', 'Arms', 'Wrists', 'Horde', 'Bracers of the Silent Massacre'), +(1, 0, 9, 1, 258, 47240, 'Phase 3', 'Warrior', 'Arms', 'Hands', 'Alliance', 'Gloves of Bitter Reprisal'), +(1, 0, 9, 2, 258, 47492, 'Phase 3', 'Warrior', 'Arms', 'Hands', 'Horde', 'Gauntlets of Bitter Reprisal'), +(1, 0, 10, 0, 258, 45608, 'Phase 3', 'Warrior', 'Arms', 'Finger1', 'Both', 'Brann''s Signet Ring'), +(1, 0, 12, 0, 258, 46038, 'Phase 3', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Dark Matter'), +(1, 0, 14, 1, 258, 47545, 'Phase 3', 'Warrior', 'Arms', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(1, 0, 14, 2, 258, 47546, 'Phase 3', 'Warrior', 'Arms', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(1, 0, 17, 1, 258, 46995, 'Phase 3', 'Warrior', 'Arms', 'Ranged', 'Alliance', 'Talonstrike'), +(1, 0, 17, 2, 258, 47428, 'Phase 3', 'Warrior', 'Arms', 'Ranged', 'Horde', 'Death''s Head Crossbow'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 264, 51227, 'Phase 4', 'Warrior', 'Arms', 'Head', 'Both', 'Sanctified Ymirjar Lord''s Helmet'), +(1, 0, 1, 0, 264, 50728, 'Phase 4', 'Warrior', 'Arms', 'Neck', 'Both', 'Lana''thel''s Chain of Flagellation'), +(1, 0, 2, 0, 264, 51229, 'Phase 4', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Sanctified Ymirjar Lord''s Shoulderplates'), +(1, 0, 4, 0, 264, 51225, 'Phase 4', 'Warrior', 'Arms', 'Chest', 'Both', 'Sanctified Ymirjar Lord''s Battleplate'), +(1, 0, 5, 0, 264, 50707, 'Phase 4', 'Warrior', 'Arms', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(1, 0, 6, 0, 264, 50645, 'Phase 4', 'Warrior', 'Arms', 'Legs', 'Both', 'Leggings of Northern Lights'), +(1, 0, 7, 0, 264, 50639, 'Phase 4', 'Warrior', 'Arms', 'Feet', 'Both', 'Blood-Soaked Saronite Stompers'), +(1, 0, 8, 0, 264, 50670, 'Phase 4', 'Warrior', 'Arms', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(1, 0, 9, 0, 264, 51226, 'Phase 4', 'Warrior', 'Arms', 'Hands', 'Both', 'Sanctified Ymirjar Lord''s Gauntlets'), +(1, 0, 10, 0, 264, 50402, 'Phase 4', 'Warrior', 'Arms', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(1, 0, 12, 0, 264, 50363, 'Phase 4', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(1, 0, 14, 1, 264, 47545, 'Phase 4', 'Warrior', 'Arms', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(1, 0, 14, 2, 264, 47546, 'Phase 4', 'Warrior', 'Arms', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(1, 0, 17, 0, 264, 50733, 'Phase 4', 'Warrior', 'Arms', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 0, 0, 0, 290, 51227, 'Phase 5', 'Warrior', 'Arms', 'Head', 'Both', 'Sanctified Ymirjar Lord''s Helmet'), +(1, 0, 1, 0, 290, 54581, 'Phase 5', 'Warrior', 'Arms', 'Neck', 'Both', 'Penumbra Pendant'), +(1, 0, 2, 0, 290, 51229, 'Phase 5', 'Warrior', 'Arms', 'Shoulders', 'Both', 'Sanctified Ymirjar Lord''s Shoulderplates'), +(1, 0, 4, 0, 290, 51225, 'Phase 5', 'Warrior', 'Arms', 'Chest', 'Both', 'Sanctified Ymirjar Lord''s Battleplate'), +(1, 0, 5, 0, 290, 50620, 'Phase 5', 'Warrior', 'Arms', 'Waist', 'Both', 'Coldwraith Links'), +(1, 0, 6, 0, 290, 51228, 'Phase 5', 'Warrior', 'Arms', 'Legs', 'Both', 'Sanctified Ymirjar Lord''s Legplates'), +(1, 0, 7, 0, 290, 54578, 'Phase 5', 'Warrior', 'Arms', 'Feet', 'Both', 'Apocalypse''s Advance'), +(1, 0, 8, 0, 290, 50670, 'Phase 5', 'Warrior', 'Arms', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(1, 0, 9, 0, 290, 50675, 'Phase 5', 'Warrior', 'Arms', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(1, 0, 10, 0, 290, 50657, 'Phase 5', 'Warrior', 'Arms', 'Finger1', 'Both', 'Skeleton Lord''s Circle'), +(1, 0, 11, 0, 290, 52572, 'Phase 5', 'Warrior', 'Arms', 'Finger2', 'Both', 'Ashen Band of Endless Might'), +(1, 0, 12, 0, 290, 50363, 'Phase 5', 'Warrior', 'Arms', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(1, 0, 13, 0, 290, 54590, 'Phase 5', 'Warrior', 'Arms', 'Trinket2', 'Both', 'Sharpened Twilight Scale'), +(1, 0, 14, 0, 290, 47545, 'Phase 5', 'Warrior', 'Arms', 'Back', 'Both', 'Vereesa''s Dexterity'), +(1, 0, 15, 0, 290, 49623, 'Phase 5', 'Warrior', 'Arms', 'MainHand', 'Both', 'Shadowmourne'), +(1, 0, 17, 0, 290, 50733, 'Phase 5', 'Warrior', 'Arms', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- Fury (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 66, 13404, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Head', 'Both', 'Mask of the Unforgiven'), +(1, 1, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Neck', 'Both', 'Mark of Fordring'), +(1, 1, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(1, 1, 4, 0, 66, 11726, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Chest', 'Both', 'Savage Gladiator Chain'), +(1, 1, 5, 0, 66, 13142, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Waist', 'Both', 'Brigam Girdle'), +(1, 1, 6, 0, 66, 14554, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Legs', 'Both', 'Cloudkeeper Legplates'), +(1, 1, 7, 0, 66, 14616, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Feet', 'Both', 'Bloodmail Boots'), +(1, 1, 8, 0, 66, 12936, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Wrists', 'Both', 'Battleborn Armbraces'), +(1, 1, 9, 0, 66, 14551, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Hands', 'Both', 'Edgemaster''s Handguards'), +(1, 1, 10, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Finger1', 'Both', 'Blackstone Ring'), +(1, 1, 11, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Finger2', 'Both', 'Painweaver Band'), +(1, 1, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(1, 1, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Hand of Justice'), +(1, 1, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Back', 'Both', 'Cape of the Black Baron'), +(1, 1, 15, 0, 66, 12940, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'MainHand', 'Both', 'Dal''Rend''s Sacred Charge'), +(1, 1, 16, 0, 66, 12939, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'OffHand', 'Both', 'Dal''Rend''s Tribal Guardian'), +(1, 1, 17, 0, 66, 12651, 'Phase 1 (Pre-Raid)', 'Warrior', 'Fury', 'Ranged', 'Both', 'Blackcrow'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 76, 13404, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Head', 'Both', 'Mask of the Unforgiven'), +(1, 1, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Neck', 'Both', 'Mark of Fordring'), +(1, 1, 2, 0, 76, 12927, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(1, 1, 4, 0, 76, 11726, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Chest', 'Both', 'Savage Gladiator Chain'), +(1, 1, 5, 0, 76, 13142, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Waist', 'Both', 'Brigam Girdle'), +(1, 1, 6, 0, 76, 14554, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Legs', 'Both', 'Cloudkeeper Legplates'), +(1, 1, 7, 0, 76, 14616, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Feet', 'Both', 'Bloodmail Boots'), +(1, 1, 8, 0, 76, 12936, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Wrists', 'Both', 'Battleborn Armbraces'), +(1, 1, 9, 0, 76, 14551, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Hands', 'Both', 'Edgemaster''s Handguards'), +(1, 1, 10, 0, 76, 17713, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Finger1', 'Both', 'Blackstone Ring'), +(1, 1, 11, 0, 76, 13098, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Finger2', 'Both', 'Painweaver Band'), +(1, 1, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(1, 1, 13, 0, 76, 11815, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Hand of Justice'), +(1, 1, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Back', 'Both', 'Cape of the Black Baron'), +(1, 1, 15, 0, 76, 12940, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'MainHand', 'Both', 'Dal''Rend''s Sacred Charge'), +(1, 1, 16, 0, 76, 12939, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'OffHand', 'Both', 'Dal''Rend''s Tribal Guardian'), +(1, 1, 17, 0, 76, 18323, 'Phase 2 (Pre-Raid)', 'Warrior', 'Fury', 'Ranged', 'Both', 'Satyr''s Bow'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 78, 12640, 'Phase 2', 'Warrior', 'Fury', 'Head', 'Both', 'Lionheart Helm'), +(1, 1, 1, 0, 78, 18404, 'Phase 2', 'Warrior', 'Fury', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(1, 1, 2, 0, 78, 12927, 'Phase 2', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(1, 1, 4, 0, 78, 11726, 'Phase 2', 'Warrior', 'Fury', 'Chest', 'Both', 'Savage Gladiator Chain'), +(1, 1, 5, 0, 78, 19137, 'Phase 2', 'Warrior', 'Fury', 'Waist', 'Both', 'Onslaught Girdle'), +(1, 1, 6, 0, 78, 14554, 'Phase 2', 'Warrior', 'Fury', 'Legs', 'Both', 'Cloudkeeper Legplates'), +(1, 1, 7, 0, 78, 14616, 'Phase 2', 'Warrior', 'Fury', 'Feet', 'Both', 'Bloodmail Boots'), +(1, 1, 8, 0, 78, 19146, 'Phase 2', 'Warrior', 'Fury', 'Wrists', 'Both', 'Wristguards of Stability'), +(1, 1, 9, 0, 78, 14551, 'Phase 2', 'Warrior', 'Fury', 'Hands', 'Both', 'Edgemaster''s Handguards'), +(1, 1, 10, 0, 78, 17063, 'Phase 2', 'Warrior', 'Fury', 'Finger1', 'Both', 'Band of Accuria'), +(1, 1, 11, 0, 78, 18821, 'Phase 2', 'Warrior', 'Fury', 'Finger2', 'Both', 'Quick Strike Ring'), +(1, 1, 12, 0, 78, 13965, 'Phase 2', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(1, 1, 13, 0, 78, 11815, 'Phase 2', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Hand of Justice'), +(1, 1, 14, 0, 78, 18541, 'Phase 2', 'Warrior', 'Fury', 'Back', 'Both', 'Puissant Cape'), +(1, 1, 15, 0, 78, 17075, 'Phase 2', 'Warrior', 'Fury', 'MainHand', 'Both', 'Vis''kag the Bloodletter'), +(1, 1, 16, 0, 78, 18832, 'Phase 2', 'Warrior', 'Fury', 'OffHand', 'Both', 'Brutality Blade'), +(1, 1, 17, 0, 78, 17069, 'Phase 2', 'Warrior', 'Fury', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 83, 12640, 'Phase 3', 'Warrior', 'Fury', 'Head', 'Both', 'Lionheart Helm'), +(1, 1, 1, 0, 83, 18404, 'Phase 3', 'Warrior', 'Fury', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(1, 1, 2, 0, 83, 19394, 'Phase 3', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Drake Talon Pauldrons'), +(1, 1, 4, 0, 83, 11726, 'Phase 3', 'Warrior', 'Fury', 'Chest', 'Both', 'Savage Gladiator Chain'), +(1, 1, 5, 0, 83, 19137, 'Phase 3', 'Warrior', 'Fury', 'Waist', 'Both', 'Onslaught Girdle'), +(1, 1, 6, 0, 83, 19402, 'Phase 3', 'Warrior', 'Fury', 'Legs', 'Both', 'Legguards of the Fallen Crusader'), +(1, 1, 7, 0, 83, 19387, 'Phase 3', 'Warrior', 'Fury', 'Feet', 'Both', 'Chromatic Boots'), +(1, 1, 8, 0, 83, 19146, 'Phase 3', 'Warrior', 'Fury', 'Wrists', 'Both', 'Wristguards of Stability'), +(1, 1, 9, 0, 83, 14551, 'Phase 3', 'Warrior', 'Fury', 'Hands', 'Both', 'Edgemaster''s Handguards'), +(1, 1, 10, 0, 83, 17063, 'Phase 3', 'Warrior', 'Fury', 'Finger1', 'Both', 'Band of Accuria'), +(1, 1, 11, 0, 83, 19384, 'Phase 3', 'Warrior', 'Fury', 'Finger2', 'Both', 'Master Dragonslayer''s Ring'), +(1, 1, 12, 0, 83, 19406, 'Phase 3', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(1, 1, 13, 0, 83, 11815, 'Phase 3', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Hand of Justice'), +(1, 1, 14, 0, 83, 18541, 'Phase 3', 'Warrior', 'Fury', 'Back', 'Both', 'Puissant Cape'), +(1, 1, 15, 0, 83, 19352, 'Phase 3', 'Warrior', 'Fury', 'MainHand', 'Both', 'Chromatically Tempered Sword'), +(1, 1, 16, 0, 83, 19351, 'Phase 3', 'Warrior', 'Fury', 'OffHand', 'Both', 'Maladath, Runed Blade of the Black Flight'), +(1, 1, 17, 0, 83, 17069, 'Phase 3', 'Warrior', 'Fury', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 88, 12640, 'Phase 5', 'Warrior', 'Fury', 'Head', 'Both', 'Lionheart Helm'), +(1, 1, 1, 0, 88, 18404, 'Phase 5', 'Warrior', 'Fury', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(1, 1, 2, 0, 88, 21330, 'Phase 5', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Conqueror''s Spaulders'), +(1, 1, 4, 0, 88, 21814, 'Phase 5', 'Warrior', 'Fury', 'Chest', 'Both', 'Breastplate of Annihilation'), +(1, 1, 5, 0, 88, 19137, 'Phase 5', 'Warrior', 'Fury', 'Waist', 'Both', 'Onslaught Girdle'), +(1, 1, 6, 0, 88, 22385, 'Phase 5', 'Warrior', 'Fury', 'Legs', 'Both', 'Titanic Leggings'), +(1, 1, 7, 0, 88, 19387, 'Phase 5', 'Warrior', 'Fury', 'Feet', 'Both', 'Chromatic Boots'), +(1, 1, 8, 0, 88, 21618, 'Phase 5', 'Warrior', 'Fury', 'Wrists', 'Both', 'Hive Defiler Wristguards'), +(1, 1, 9, 0, 88, 14551, 'Phase 5', 'Warrior', 'Fury', 'Hands', 'Both', 'Edgemaster''s Handguards'), +(1, 1, 10, 0, 88, 17063, 'Phase 5', 'Warrior', 'Fury', 'Finger1', 'Both', 'Band of Accuria'), +(1, 1, 11, 0, 88, 19384, 'Phase 5', 'Warrior', 'Fury', 'Finger2', 'Both', 'Master Dragonslayer''s Ring'), +(1, 1, 12, 0, 88, 19406, 'Phase 5', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(1, 1, 13, 0, 88, 21670, 'Phase 5', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Badge of the Swarmguard'), +(1, 1, 14, 0, 88, 18541, 'Phase 5', 'Warrior', 'Fury', 'Back', 'Both', 'Puissant Cape'), +(1, 1, 15, 0, 88, 21650, 'Phase 5', 'Warrior', 'Fury', 'MainHand', 'Both', 'Ancient Qiraji Ripper'), +(1, 1, 16, 0, 88, 21650, 'Phase 5', 'Warrior', 'Fury', 'OffHand', 'Both', 'Ancient Qiraji Ripper'), +(1, 1, 17, 0, 88, 17069, 'Phase 5', 'Warrior', 'Fury', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 92, 12640, 'Phase 6', 'Warrior', 'Fury', 'Head', 'Both', 'Lionheart Helm'), +(1, 1, 1, 0, 92, 18404, 'Phase 6', 'Warrior', 'Fury', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(1, 1, 2, 0, 92, 21330, 'Phase 6', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Conqueror''s Spaulders'), +(1, 1, 4, 0, 92, 23000, 'Phase 6', 'Warrior', 'Fury', 'Chest', 'Both', 'Plated Abomination Ribcage'), +(1, 1, 5, 0, 92, 19137, 'Phase 6', 'Warrior', 'Fury', 'Waist', 'Both', 'Onslaught Girdle'), +(1, 1, 6, 0, 92, 22385, 'Phase 6', 'Warrior', 'Fury', 'Legs', 'Both', 'Titanic Leggings'), +(1, 1, 7, 0, 92, 19387, 'Phase 6', 'Warrior', 'Fury', 'Feet', 'Both', 'Chromatic Boots'), +(1, 1, 8, 0, 92, 22936, 'Phase 6', 'Warrior', 'Fury', 'Wrists', 'Both', 'Wristguards of Vengeance'), +(1, 1, 9, 0, 92, 21581, 'Phase 6', 'Warrior', 'Fury', 'Hands', 'Both', 'Gauntlets of Annihilation'), +(1, 1, 11, 0, 92, 19384, 'Phase 6', 'Warrior', 'Fury', 'Finger2', 'Both', 'Master Dragonslayer''s Ring'), +(1, 1, 12, 0, 92, 19406, 'Phase 6', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(1, 1, 13, 0, 92, 21670, 'Phase 6', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Badge of the Swarmguard'), +(1, 1, 14, 0, 92, 23045, 'Phase 6', 'Warrior', 'Fury', 'Back', 'Both', 'Shroud of Dominion'), +(1, 1, 15, 0, 92, 23054, 'Phase 6', 'Warrior', 'Fury', 'MainHand', 'Both', 'Gressil, Dawn of Ruin'), +(1, 1, 16, 0, 92, 23577, 'Phase 6', 'Warrior', 'Fury', 'OffHand', 'Both', 'The Hungering Cold'), +(1, 1, 17, 0, 92, 17069, 'Phase 6', 'Warrior', 'Fury', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 120, 32087, 'Pre-Raid', 'Warrior', 'Fury', 'Head', 'Both', 'Mask of the Deceiver'), +(1, 1, 1, 0, 120, 29349, 'Pre-Raid', 'Warrior', 'Fury', 'Neck', 'Both', 'Adamantine Chain of the Unbroken'), +(1, 1, 2, 0, 120, 33173, 'Pre-Raid', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Ragesteel Shoulders'), +(1, 1, 4, 0, 120, 23522, 'Pre-Raid', 'Warrior', 'Fury', 'Chest', 'Both', 'Ragesteel Breastplate'), +(1, 1, 5, 0, 120, 27985, 'Pre-Raid', 'Warrior', 'Fury', 'Waist', 'Both', 'Deathforge Girdle'), +(1, 1, 6, 0, 120, 30538, 'Pre-Raid', 'Warrior', 'Fury', 'Legs', 'Both', 'Midnight Legguards'), +(1, 1, 7, 0, 120, 25686, 'Pre-Raid', 'Warrior', 'Fury', 'Feet', 'Both', 'Fel Leather Boots'), +(1, 1, 8, 0, 120, 23537, 'Pre-Raid', 'Warrior', 'Fury', 'Wrists', 'Both', 'Black Felsteel Bracers'), +(1, 1, 9, 0, 120, 25685, 'Pre-Raid', 'Warrior', 'Fury', 'Hands', 'Both', 'Fel Leather Gloves'), +(1, 1, 10, 0, 120, 29379, 'Pre-Raid', 'Warrior', 'Fury', 'Finger1', 'Both', 'Ring of Arathi Warlords'), +(1, 1, 11, 0, 120, 30834, 'Pre-Raid', 'Warrior', 'Fury', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(1, 1, 12, 0, 120, 29383, 'Pre-Raid', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(1, 1, 13, 0, 120, 28034, 'Pre-Raid', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Hourglass of the Unraveller'), +(1, 1, 14, 0, 120, 24259, 'Pre-Raid', 'Warrior', 'Fury', 'Back', 'Both', 'Vengeance Wrap'), +(1, 1, 15, 0, 120, 28438, 'Pre-Raid', 'Warrior', 'Fury', 'MainHand', 'Both', 'Dragonmaw'), +(1, 1, 16, 0, 120, 23542, 'Pre-Raid', 'Warrior', 'Fury', 'OffHand', 'Both', 'Fel Edged Battleaxe'), +(1, 1, 17, 0, 120, 30279, 'Pre-Raid', 'Warrior', 'Fury', 'Ranged', 'Both', 'Mama''s Insurance'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 125, 29021, 'Phase 1', 'Warrior', 'Fury', 'Head', 'Both', 'Warbringer Battle-Helm'), +(1, 1, 1, 0, 125, 29349, 'Phase 1', 'Warrior', 'Fury', 'Neck', 'Both', 'Adamantine Chain of the Unbroken'), +(1, 1, 2, 0, 125, 29023, 'Phase 1', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Warbringer Shoulderplates'), +(1, 1, 4, 0, 125, 29019, 'Phase 1', 'Warrior', 'Fury', 'Chest', 'Both', 'Warbringer Breastplate'), +(1, 1, 5, 0, 125, 28779, 'Phase 1', 'Warrior', 'Fury', 'Waist', 'Both', 'Girdle of the Endless Pit'), +(1, 1, 6, 0, 125, 28741, 'Phase 1', 'Warrior', 'Fury', 'Legs', 'Both', 'Skulker''s Greaves'), +(1, 1, 7, 0, 125, 28608, 'Phase 1', 'Warrior', 'Fury', 'Feet', 'Both', 'Ironstriders of Urgency'), +(1, 1, 8, 0, 125, 28795, 'Phase 1', 'Warrior', 'Fury', 'Wrists', 'Both', 'Bladespire Warbands'), +(1, 1, 9, 0, 125, 28824, 'Phase 1', 'Warrior', 'Fury', 'Hands', 'Both', 'Gauntlets of Martial Perfection'), +(1, 1, 10, 0, 125, 28757, 'Phase 1', 'Warrior', 'Fury', 'Finger1', 'Both', 'Ring of a Thousand Marks'), +(1, 1, 11, 0, 125, 30834, 'Phase 1', 'Warrior', 'Fury', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(1, 1, 12, 0, 125, 29383, 'Phase 1', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(1, 1, 13, 0, 125, 28830, 'Phase 1', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(1, 1, 14, 0, 125, 24259, 'Phase 1', 'Warrior', 'Fury', 'Back', 'Both', 'Vengeance Wrap'), +(1, 1, 15, 0, 125, 28438, 'Phase 1', 'Warrior', 'Fury', 'MainHand', 'Both', 'Dragonmaw'), +(1, 1, 16, 0, 125, 31332, 'Phase 1', 'Warrior', 'Fury', 'OffHand', 'Both', 'Blinkstrike'), +(1, 1, 17, 0, 125, 30724, 'Phase 1', 'Warrior', 'Fury', 'Ranged', 'Both', 'Barrel-Blade Longrifle'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 141, 30120, 'Phase 2', 'Warrior', 'Fury', 'Head', 'Both', 'Destroyer Battle-Helm'), +(1, 1, 1, 0, 141, 30022, 'Phase 2', 'Warrior', 'Fury', 'Neck', 'Both', 'Pendant of the Perilous'), +(1, 1, 2, 0, 141, 30122, 'Phase 2', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Destroyer Shoulderblades'), +(1, 1, 4, 0, 141, 30118, 'Phase 2', 'Warrior', 'Fury', 'Chest', 'Both', 'Destroyer Breastplate'), +(1, 1, 5, 0, 141, 30106, 'Phase 2', 'Warrior', 'Fury', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(1, 1, 6, 0, 141, 29995, 'Phase 2', 'Warrior', 'Fury', 'Legs', 'Both', 'Leggings of Murderous Intent'), +(1, 1, 7, 0, 141, 30081, 'Phase 2', 'Warrior', 'Fury', 'Feet', 'Both', 'Warboots of Obliteration'), +(1, 1, 8, 0, 141, 30057, 'Phase 2', 'Warrior', 'Fury', 'Wrists', 'Both', 'Bracers of Eradication'), +(1, 1, 9, 0, 141, 30119, 'Phase 2', 'Warrior', 'Fury', 'Hands', 'Both', 'Destroyer Gauntlets'), +(1, 1, 10, 0, 141, 29997, 'Phase 2', 'Warrior', 'Fury', 'Finger1', 'Both', 'Band of the Ranger-General'), +(1, 1, 11, 0, 141, 28757, 'Phase 2', 'Warrior', 'Fury', 'Finger2', 'Both', 'Ring of a Thousand Marks'), +(1, 1, 12, 0, 141, 29383, 'Phase 2', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(1, 1, 13, 0, 141, 28830, 'Phase 2', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(1, 1, 14, 0, 141, 24259, 'Phase 2', 'Warrior', 'Fury', 'Back', 'Both', 'Vengeance Wrap'), +(1, 1, 15, 0, 141, 28439, 'Phase 2', 'Warrior', 'Fury', 'MainHand', 'Both', 'Dragonstrike'), +(1, 1, 16, 0, 141, 30082, 'Phase 2', 'Warrior', 'Fury', 'OffHand', 'Both', 'Talon of Azshara'), +(1, 1, 17, 0, 141, 30105, 'Phase 2', 'Warrior', 'Fury', 'Ranged', 'Both', 'Serpent Spine Longbow'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 200, 41386, 'Pre-Raid', 'Warrior', 'Fury', 'Head', 'Both', 'Spiked Titansteel Helm'), +(1, 1, 1, 0, 200, 42645, 'Pre-Raid', 'Warrior', 'Fury', 'Neck', 'Both', 'Titanium Impact Choker'), +(1, 1, 2, 0, 200, 37627, 'Pre-Raid', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Snake Den Spaulders'), +(1, 1, 4, 0, 200, 39606, 'Pre-Raid', 'Warrior', 'Fury', 'Chest', 'Both', 'Heroes'' Dreadnaught Battleplate'), +(1, 1, 5, 0, 200, 37826, 'Pre-Raid', 'Warrior', 'Fury', 'Waist', 'Both', 'The General''s Steel Girdle'), +(1, 1, 6, 0, 200, 39607, 'Pre-Raid', 'Warrior', 'Fury', 'Legs', 'Both', 'Heroes'' Dreadnaught Legplates'), +(1, 1, 7, 0, 200, 43402, 'Pre-Raid', 'Warrior', 'Fury', 'Feet', 'Both', 'The Obliterator Greaves'), +(1, 1, 8, 0, 200, 37891, 'Pre-Raid', 'Warrior', 'Fury', 'Wrists', 'Both', 'Cast Iron Shackles'), +(1, 1, 9, 0, 200, 39609, 'Pre-Raid', 'Warrior', 'Fury', 'Hands', 'Both', 'Heroes'' Dreadnaught Gauntlets'), +(1, 1, 10, 0, 200, 37151, 'Pre-Raid', 'Warrior', 'Fury', 'Finger1', 'Both', 'Band of Frosted Thorns'), +(1, 1, 12, 0, 200, 40684, 'Pre-Raid', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Mirror of Truth'), +(1, 1, 14, 0, 200, 43566, 'Pre-Raid', 'Warrior', 'Fury', 'Back', 'Both', 'Ice Striker''s Cloak'), +(1, 1, 15, 0, 200, 41257, 'Pre-Raid', 'Warrior', 'Fury', 'MainHand', 'Both', 'Titansteel Destroyer'), +(1, 1, 17, 0, 200, 43284, 'Pre-Raid', 'Warrior', 'Fury', 'Ranged', 'Both', 'Amanitar Skullbow'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 224, 44006, 'Phase 1', 'Warrior', 'Fury', 'Head', 'Both', 'Obsidian Greathelm'), +(1, 1, 1, 0, 224, 44664, 'Phase 1', 'Warrior', 'Fury', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(1, 1, 2, 0, 224, 40530, 'Phase 1', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Valorous Dreadnaught Shoulderplates'), +(1, 1, 4, 0, 224, 40539, 'Phase 1', 'Warrior', 'Fury', 'Chest', 'Both', 'Chestguard of the Recluse'), +(1, 1, 5, 0, 224, 40205, 'Phase 1', 'Warrior', 'Fury', 'Waist', 'Both', 'Stalk-Skin Belt'), +(1, 1, 6, 0, 224, 40529, 'Phase 1', 'Warrior', 'Fury', 'Legs', 'Both', 'Valorous Dreadnaught Legplates'), +(1, 1, 7, 0, 224, 40591, 'Phase 1', 'Warrior', 'Fury', 'Feet', 'Both', 'Melancholy Sabatons'), +(1, 1, 8, 0, 224, 39765, 'Phase 1', 'Warrior', 'Fury', 'Wrists', 'Both', 'Sinner''s Bindings'), +(1, 1, 9, 0, 224, 40541, 'Phase 1', 'Warrior', 'Fury', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(1, 1, 10, 0, 224, 40717, 'Phase 1', 'Warrior', 'Fury', 'Finger1', 'Both', 'Ring of Invincibility'), +(1, 1, 12, 0, 224, 40256, 'Phase 1', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Grim Toll'), +(1, 1, 14, 0, 224, 40403, 'Phase 1', 'Warrior', 'Fury', 'Back', 'Both', 'Drape of the Deadly Foe'), +(1, 1, 17, 0, 224, 40385, 'Phase 1', 'Warrior', 'Fury', 'Ranged', 'Both', 'Envoy of Mortality'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 245, 46151, 'Phase 2', 'Warrior', 'Fury', 'Head', 'Both', 'Conqueror''s Siegebreaker Helmet'), +(1, 1, 1, 0, 245, 45517, 'Phase 2', 'Warrior', 'Fury', 'Neck', 'Both', 'Pendulum of Infinity'), +(1, 1, 2, 0, 245, 46037, 'Phase 2', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Shoulderplates of the Celestial Watch'), +(1, 1, 4, 0, 245, 46146, 'Phase 2', 'Warrior', 'Fury', 'Chest', 'Both', 'Conqueror''s Siegebreaker Battleplate'), +(1, 1, 5, 0, 245, 46041, 'Phase 2', 'Warrior', 'Fury', 'Waist', 'Both', 'Starfall Girdle'), +(1, 1, 6, 0, 245, 45536, 'Phase 2', 'Warrior', 'Fury', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(1, 1, 7, 0, 245, 45599, 'Phase 2', 'Warrior', 'Fury', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(1, 1, 9, 0, 245, 46148, 'Phase 2', 'Warrior', 'Fury', 'Hands', 'Both', 'Conqueror''s Siegebreaker Gauntlets'), +(1, 1, 10, 0, 245, 45608, 'Phase 2', 'Warrior', 'Fury', 'Finger1', 'Both', 'Brann''s Signet Ring'), +(1, 1, 12, 0, 245, 46038, 'Phase 2', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Dark Matter'), +(1, 1, 14, 0, 245, 46032, 'Phase 2', 'Warrior', 'Fury', 'Back', 'Both', 'Drape of the Faceless General'), +(1, 1, 17, 0, 245, 45296, 'Phase 2', 'Warrior', 'Fury', 'Ranged', 'Both', 'Twirling Blades'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 1, 258, 48383, 'Phase 3', 'Warrior', 'Fury', 'Head', 'Alliance', 'Wrynn''s Helmet of Triumph'), +(1, 1, 0, 2, 258, 48398, 'Phase 3', 'Warrior', 'Fury', 'Head', 'Horde', 'Hellscream''s Helmet of Triumph'), +(1, 1, 1, 1, 258, 47060, 'Phase 3', 'Warrior', 'Fury', 'Neck', 'Alliance', 'Charge of the Demon Lord'), +(1, 1, 1, 2, 258, 47433, 'Phase 3', 'Warrior', 'Fury', 'Neck', 'Horde', 'Charge of the Eredar'), +(1, 1, 2, 1, 258, 48381, 'Phase 3', 'Warrior', 'Fury', 'Shoulders', 'Alliance', 'Wrynn''s Shoulderplates of Triumph'), +(1, 1, 2, 2, 258, 48400, 'Phase 3', 'Warrior', 'Fury', 'Shoulders', 'Horde', 'Hellscream''s Shoulderplates of Triumph'), +(1, 1, 4, 1, 258, 48385, 'Phase 3', 'Warrior', 'Fury', 'Chest', 'Alliance', 'Wrynn''s Battleplate of Triumph'), +(1, 1, 4, 2, 258, 48396, 'Phase 3', 'Warrior', 'Fury', 'Chest', 'Horde', 'Hellscream''s Battleplate of Triumph'), +(1, 1, 5, 1, 258, 47002, 'Phase 3', 'Warrior', 'Fury', 'Waist', 'Alliance', 'Bloodbath Belt'), +(1, 1, 5, 2, 258, 47429, 'Phase 3', 'Warrior', 'Fury', 'Waist', 'Horde', 'Bloodbath Girdle'), +(1, 1, 6, 1, 258, 48382, 'Phase 3', 'Warrior', 'Fury', 'Legs', 'Alliance', 'Wrynn''s Legplates of Triumph'), +(1, 1, 6, 2, 258, 48399, 'Phase 3', 'Warrior', 'Fury', 'Legs', 'Horde', 'Hellscream''s Legplates of Triumph'), +(1, 1, 7, 1, 258, 47154, 'Phase 3', 'Warrior', 'Fury', 'Feet', 'Alliance', 'Greaves of the 7th Legion'), +(1, 1, 7, 2, 258, 47473, 'Phase 3', 'Warrior', 'Fury', 'Feet', 'Horde', 'Greaves of the Saronite Citadel'), +(1, 1, 8, 1, 258, 47074, 'Phase 3', 'Warrior', 'Fury', 'Wrists', 'Alliance', 'Bracers of the Untold Massacre'), +(1, 1, 8, 2, 258, 47442, 'Phase 3', 'Warrior', 'Fury', 'Wrists', 'Horde', 'Bracers of the Silent Massacre'), +(1, 1, 9, 1, 258, 47240, 'Phase 3', 'Warrior', 'Fury', 'Hands', 'Alliance', 'Gloves of Bitter Reprisal'), +(1, 1, 9, 2, 258, 47492, 'Phase 3', 'Warrior', 'Fury', 'Hands', 'Horde', 'Gauntlets of Bitter Reprisal'), +(1, 1, 10, 1, 258, 47075, 'Phase 3', 'Warrior', 'Fury', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(1, 1, 10, 2, 258, 47443, 'Phase 3', 'Warrior', 'Fury', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(1, 1, 12, 1, 258, 47131, 'Phase 3', 'Warrior', 'Fury', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(1, 1, 12, 2, 258, 47464, 'Phase 3', 'Warrior', 'Fury', 'Trinket1', 'Horde', 'Death''s Choice'), +(1, 1, 14, 1, 258, 47545, 'Phase 3', 'Warrior', 'Fury', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(1, 1, 14, 2, 258, 47546, 'Phase 3', 'Warrior', 'Fury', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(1, 1, 17, 1, 258, 46995, 'Phase 3', 'Warrior', 'Fury', 'Ranged', 'Alliance', 'Talonstrike'), +(1, 1, 17, 2, 258, 47428, 'Phase 3', 'Warrior', 'Fury', 'Ranged', 'Horde', 'Death''s Head Crossbow'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 264, 51227, 'Phase 4', 'Warrior', 'Fury', 'Head', 'Both', 'Sanctified Ymirjar Lord''s Helmet'), +(1, 1, 1, 0, 264, 50633, 'Phase 4', 'Warrior', 'Fury', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(1, 1, 2, 0, 264, 51229, 'Phase 4', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Sanctified Ymirjar Lord''s Shoulderplates'), +(1, 1, 4, 0, 264, 51225, 'Phase 4', 'Warrior', 'Fury', 'Chest', 'Both', 'Sanctified Ymirjar Lord''s Battleplate'), +(1, 1, 5, 0, 264, 50620, 'Phase 4', 'Warrior', 'Fury', 'Waist', 'Both', 'Coldwraith Links'), +(1, 1, 6, 0, 264, 51228, 'Phase 4', 'Warrior', 'Fury', 'Legs', 'Both', 'Sanctified Ymirjar Lord''s Legplates'), +(1, 1, 7, 0, 264, 50639, 'Phase 4', 'Warrior', 'Fury', 'Feet', 'Both', 'Blood-Soaked Saronite Stompers'), +(1, 1, 8, 0, 264, 50670, 'Phase 4', 'Warrior', 'Fury', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(1, 1, 9, 0, 264, 50675, 'Phase 4', 'Warrior', 'Fury', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(1, 1, 10, 1, 264, 47075, 'Phase 4', 'Warrior', 'Fury', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(1, 1, 10, 2, 264, 47443, 'Phase 4', 'Warrior', 'Fury', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(1, 1, 12, 0, 264, 50363, 'Phase 4', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(1, 1, 14, 0, 264, 50653, 'Phase 4', 'Warrior', 'Fury', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(1, 1, 17, 0, 264, 50733, 'Phase 4', 'Warrior', 'Fury', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 1, 0, 0, 290, 51227, 'Phase 5', 'Warrior', 'Fury', 'Head', 'Both', 'Sanctified Ymirjar Lord''s Helmet'), +(1, 1, 1, 0, 290, 54581, 'Phase 5', 'Warrior', 'Fury', 'Neck', 'Both', 'Penumbra Pendant'), +(1, 1, 2, 0, 290, 51229, 'Phase 5', 'Warrior', 'Fury', 'Shoulders', 'Both', 'Sanctified Ymirjar Lord''s Shoulderplates'), +(1, 1, 4, 0, 290, 51225, 'Phase 5', 'Warrior', 'Fury', 'Chest', 'Both', 'Sanctified Ymirjar Lord''s Battleplate'), +(1, 1, 5, 0, 290, 50620, 'Phase 5', 'Warrior', 'Fury', 'Waist', 'Both', 'Coldwraith Links'), +(1, 1, 6, 0, 290, 51228, 'Phase 5', 'Warrior', 'Fury', 'Legs', 'Both', 'Sanctified Ymirjar Lord''s Legplates'), +(1, 1, 7, 0, 290, 54578, 'Phase 5', 'Warrior', 'Fury', 'Feet', 'Both', 'Apocalypse''s Advance'), +(1, 1, 8, 0, 290, 50670, 'Phase 5', 'Warrior', 'Fury', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(1, 1, 9, 0, 290, 50675, 'Phase 5', 'Warrior', 'Fury', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(1, 1, 10, 0, 290, 50657, 'Phase 5', 'Warrior', 'Fury', 'Finger1', 'Both', 'Skeleton Lord''s Circle'), +(1, 1, 11, 0, 290, 52572, 'Phase 5', 'Warrior', 'Fury', 'Finger2', 'Both', 'Ashen Band of Endless Might'), +(1, 1, 12, 0, 290, 50363, 'Phase 5', 'Warrior', 'Fury', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(1, 1, 13, 0, 290, 54590, 'Phase 5', 'Warrior', 'Fury', 'Trinket2', 'Both', 'Sharpened Twilight Scale'), +(1, 1, 14, 0, 290, 47545, 'Phase 5', 'Warrior', 'Fury', 'Back', 'Both', 'Vereesa''s Dexterity'), +(1, 1, 15, 0, 290, 50603, 'Phase 5', 'Warrior', 'Fury', 'MainHand', 'Both', 'Cryptmaker'), +(1, 1, 16, 0, 290, 50603, 'Phase 5', 'Warrior', 'Fury', 'OffHand', 'Both', 'Cryptmaker'), +(1, 1, 17, 0, 290, 50733, 'Phase 5', 'Warrior', 'Fury', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- Protection (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 66, 12952, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Head', 'Both', 'Gyth''s Skull'), +(1, 2, 1, 0, 66, 13091, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Neck', 'Both', 'Medallion of Grand Marshal Morris'), +(1, 2, 2, 0, 66, 14552, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Stockade Pauldrons'), +(1, 2, 4, 0, 66, 14624, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Chest', 'Both', 'Deathbone Chestplate'), +(1, 2, 5, 0, 66, 14620, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Waist', 'Both', 'Deathbone Girdle'), +(1, 2, 6, 0, 66, 11927, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Legs', 'Both', 'Legplates of the Eternal Guardian'), +(1, 2, 7, 0, 66, 14621, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Feet', 'Both', 'Deathbone Sabatons'), +(1, 2, 8, 0, 66, 12550, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Wrists', 'Both', 'Runed Golem Shackles'), +(1, 2, 9, 0, 66, 13072, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Hands', 'Both', 'Stonegrip Gauntlets'), +(1, 2, 10, 0, 66, 11669, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Finger1', 'Both', 'Naglering'), +(1, 2, 11, 0, 66, 10795, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Finger2', 'Both', 'Drakeclaw Band'), +(1, 2, 13, 0, 66, 10779, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Demon''s Blood'), +(1, 2, 14, 0, 66, 13397, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Back', 'Both', 'Stoneskin Gargoyle Cape'), +(1, 2, 15, 0, 66, 15806, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'MainHand', 'Both', 'Mirah''s Song'), +(1, 2, 16, 0, 66, 12602, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'OffHand', 'Both', 'Draconian Deflector'), +(1, 2, 17, 0, 66, 12651, 'Phase 1 (Pre-Raid)', 'Warrior', 'Protection', 'Ranged', 'Both', 'Blackcrow'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 76, 12952, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Head', 'Both', 'Gyth''s Skull'), +(1, 2, 1, 0, 76, 13091, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Neck', 'Both', 'Medallion of Grand Marshal Morris'), +(1, 2, 2, 0, 76, 14552, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Stockade Pauldrons'), +(1, 2, 4, 0, 76, 14624, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Chest', 'Both', 'Deathbone Chestplate'), +(1, 2, 5, 0, 76, 14620, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Waist', 'Both', 'Deathbone Girdle'), +(1, 2, 6, 0, 76, 11927, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Legs', 'Both', 'Legplates of the Eternal Guardian'), +(1, 2, 7, 0, 76, 14621, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Feet', 'Both', 'Deathbone Sabatons'), +(1, 2, 8, 0, 76, 18754, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Wrists', 'Both', 'Fel Hardened Bracers'), +(1, 2, 9, 0, 76, 13072, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Hands', 'Both', 'Stonegrip Gauntlets'), +(1, 2, 10, 0, 76, 11669, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Finger1', 'Both', 'Naglering'), +(1, 2, 11, 0, 76, 10795, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Finger2', 'Both', 'Drakeclaw Band'), +(1, 2, 13, 0, 76, 10779, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Demon''s Blood'), +(1, 2, 14, 0, 76, 18495, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Back', 'Both', 'Redoubt Cloak'), +(1, 2, 15, 0, 76, 15806, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'MainHand', 'Both', 'Mirah''s Song'), +(1, 2, 16, 0, 76, 12602, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'OffHand', 'Both', 'Draconian Deflector'), +(1, 2, 17, 0, 76, 18323, 'Phase 2 (Pre-Raid)', 'Warrior', 'Protection', 'Ranged', 'Both', 'Satyr''s Bow'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 78, 16963, 'Phase 2', 'Warrior', 'Protection', 'Head', 'Both', 'Helm of Wrath'), +(1, 2, 1, 0, 78, 17065, 'Phase 2', 'Warrior', 'Protection', 'Neck', 'Both', 'Medallion of Steadfast Might'), +(1, 2, 2, 0, 78, 16868, 'Phase 2', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Pauldrons of Might'), +(1, 2, 4, 0, 78, 14624, 'Phase 2', 'Warrior', 'Protection', 'Chest', 'Both', 'Deathbone Chestplate'), +(1, 2, 5, 0, 78, 16864, 'Phase 2', 'Warrior', 'Protection', 'Waist', 'Both', 'Belt of Might'), +(1, 2, 6, 0, 78, 16962, 'Phase 2', 'Warrior', 'Protection', 'Legs', 'Both', 'Legplates of Wrath'), +(1, 2, 7, 0, 78, 16862, 'Phase 2', 'Warrior', 'Protection', 'Feet', 'Both', 'Sabatons of Might'), +(1, 2, 8, 0, 78, 16861, 'Phase 2', 'Warrior', 'Protection', 'Wrists', 'Both', 'Bracers of Might'), +(1, 2, 9, 0, 78, 13072, 'Phase 2', 'Warrior', 'Protection', 'Hands', 'Both', 'Stonegrip Gauntlets'), +(1, 2, 10, 0, 78, 11669, 'Phase 2', 'Warrior', 'Protection', 'Finger1', 'Both', 'Naglering'), +(1, 2, 11, 0, 78, 18879, 'Phase 2', 'Warrior', 'Protection', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(1, 2, 13, 0, 78, 18406, 'Phase 2', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(1, 2, 14, 0, 78, 18495, 'Phase 2', 'Warrior', 'Protection', 'Back', 'Both', 'Redoubt Cloak'), +(1, 2, 15, 0, 78, 18203, 'Phase 2', 'Warrior', 'Protection', 'MainHand', 'Both', 'Eskhandar''s Right Claw'), +(1, 2, 16, 0, 78, 17066, 'Phase 2', 'Warrior', 'Protection', 'OffHand', 'Both', 'Drillborer Disk'), +(1, 2, 17, 0, 78, 18323, 'Phase 2', 'Warrior', 'Protection', 'Ranged', 'Both', 'Satyr''s Bow'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 83, 16963, 'Phase 3', 'Warrior', 'Protection', 'Head', 'Both', 'Helm of Wrath'), +(1, 2, 1, 0, 83, 19383, 'Phase 3', 'Warrior', 'Protection', 'Neck', 'Both', 'Master Dragonslayer''s Medallion'), +(1, 2, 2, 0, 83, 16961, 'Phase 3', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Pauldrons of Wrath'), +(1, 2, 4, 0, 83, 16966, 'Phase 3', 'Warrior', 'Protection', 'Chest', 'Both', 'Breastplate of Wrath'), +(1, 2, 5, 0, 83, 16960, 'Phase 3', 'Warrior', 'Protection', 'Waist', 'Both', 'Waistband of Wrath'), +(1, 2, 6, 0, 83, 16962, 'Phase 3', 'Warrior', 'Protection', 'Legs', 'Both', 'Legplates of Wrath'), +(1, 2, 7, 0, 83, 16965, 'Phase 3', 'Warrior', 'Protection', 'Feet', 'Both', 'Sabatons of Wrath'), +(1, 2, 8, 0, 83, 16959, 'Phase 3', 'Warrior', 'Protection', 'Wrists', 'Both', 'Bracelets of Wrath'), +(1, 2, 9, 0, 83, 16964, 'Phase 3', 'Warrior', 'Protection', 'Hands', 'Both', 'Gauntlets of Wrath'), +(1, 2, 10, 0, 83, 11669, 'Phase 3', 'Warrior', 'Protection', 'Finger1', 'Both', 'Naglering'), +(1, 2, 11, 0, 83, 18879, 'Phase 3', 'Warrior', 'Protection', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(1, 2, 12, 0, 83, 19431, 'Phase 3', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Styleen''s Impeding Scarab'), +(1, 2, 13, 0, 83, 18406, 'Phase 3', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(1, 2, 14, 0, 83, 18495, 'Phase 3', 'Warrior', 'Protection', 'Back', 'Both', 'Redoubt Cloak'), +(1, 2, 15, 0, 83, 19335, 'Phase 3', 'Warrior', 'Protection', 'MainHand', 'Both', 'Spineshatter'), +(1, 2, 17, 0, 83, 19368, 'Phase 3', 'Warrior', 'Protection', 'Ranged', 'Both', 'Dragonbreath Hand Cannon'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 88, 16963, 'Phase 5', 'Warrior', 'Protection', 'Head', 'Both', 'Helm of Wrath'), +(1, 2, 1, 0, 88, 22732, 'Phase 5', 'Warrior', 'Protection', 'Neck', 'Both', 'Mark of C''Thun'), +(1, 2, 2, 0, 88, 21639, 'Phase 5', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Pauldrons of the Unrelenting'), +(1, 2, 4, 0, 88, 16966, 'Phase 5', 'Warrior', 'Protection', 'Chest', 'Both', 'Breastplate of Wrath'), +(1, 2, 5, 0, 88, 21598, 'Phase 5', 'Warrior', 'Protection', 'Waist', 'Both', 'Royal Qiraji Belt'), +(1, 2, 6, 0, 88, 16962, 'Phase 5', 'Warrior', 'Protection', 'Legs', 'Both', 'Legplates of Wrath'), +(1, 2, 7, 0, 88, 16965, 'Phase 5', 'Warrior', 'Protection', 'Feet', 'Both', 'Sabatons of Wrath'), +(1, 2, 8, 0, 88, 16959, 'Phase 5', 'Warrior', 'Protection', 'Wrists', 'Both', 'Bracelets of Wrath'), +(1, 2, 9, 0, 88, 21674, 'Phase 5', 'Warrior', 'Protection', 'Hands', 'Both', 'Gauntlets of Steadfast Determination'), +(1, 2, 10, 0, 88, 21601, 'Phase 5', 'Warrior', 'Protection', 'Finger1', 'Both', 'Ring of Emperor Vek''lor'), +(1, 2, 11, 0, 88, 18879, 'Phase 5', 'Warrior', 'Protection', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(1, 2, 12, 0, 88, 19431, 'Phase 5', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Styleen''s Impeding Scarab'), +(1, 2, 13, 0, 88, 18406, 'Phase 5', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(1, 2, 14, 0, 88, 19888, 'Phase 5', 'Warrior', 'Protection', 'Back', 'Both', 'Overlord''s Embrace'), +(1, 2, 15, 0, 88, 19363, 'Phase 5', 'Warrior', 'Protection', 'MainHand', 'Both', 'Crul''shorukh, Edge of Chaos'), +(1, 2, 16, 0, 88, 21269, 'Phase 5', 'Warrior', 'Protection', 'OffHand', 'Both', 'Blessed Qiraji Bulwark'), +(1, 2, 17, 0, 88, 19368, 'Phase 5', 'Warrior', 'Protection', 'Ranged', 'Both', 'Dragonbreath Hand Cannon'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 92, 22418, 'Phase 6', 'Warrior', 'Protection', 'Head', 'Both', 'Dreadnaught Helmet'), +(1, 2, 1, 0, 92, 22732, 'Phase 6', 'Warrior', 'Protection', 'Neck', 'Both', 'Mark of C''Thun'), +(1, 2, 2, 0, 92, 22419, 'Phase 6', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Dreadnaught Pauldrons'), +(1, 2, 4, 0, 92, 22416, 'Phase 6', 'Warrior', 'Protection', 'Chest', 'Both', 'Dreadnaught Breastplate'), +(1, 2, 5, 0, 92, 22422, 'Phase 6', 'Warrior', 'Protection', 'Waist', 'Both', 'Dreadnaught Waistguard'), +(1, 2, 6, 0, 92, 22417, 'Phase 6', 'Warrior', 'Protection', 'Legs', 'Both', 'Dreadnaught Legplates'), +(1, 2, 7, 0, 92, 22420, 'Phase 6', 'Warrior', 'Protection', 'Feet', 'Both', 'Dreadnaught Sabatons'), +(1, 2, 8, 0, 92, 22423, 'Phase 6', 'Warrior', 'Protection', 'Wrists', 'Both', 'Dreadnaught Bracers'), +(1, 2, 9, 0, 92, 22421, 'Phase 6', 'Warrior', 'Protection', 'Hands', 'Both', 'Dreadnaught Gauntlets'), +(1, 2, 10, 0, 92, 21601, 'Phase 6', 'Warrior', 'Protection', 'Finger1', 'Both', 'Ring of Emperor Vek''lor'), +(1, 2, 11, 0, 92, 23059, 'Phase 6', 'Warrior', 'Protection', 'Finger2', 'Both', 'Ring of the Dreadnaught'), +(1, 2, 12, 0, 92, 19431, 'Phase 6', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Styleen''s Impeding Scarab'), +(1, 2, 13, 0, 92, 19406, 'Phase 6', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Drake Fang Talisman'), +(1, 2, 14, 0, 92, 22938, 'Phase 6', 'Warrior', 'Protection', 'Back', 'Both', 'Cryptfiend Silk Cloak'), +(1, 2, 15, 0, 92, 23577, 'Phase 6', 'Warrior', 'Protection', 'MainHand', 'Both', 'The Hungering Cold'), +(1, 2, 16, 0, 92, 23043, 'Phase 6', 'Warrior', 'Protection', 'OffHand', 'Both', 'The Face of Death'), +(1, 2, 17, 0, 92, 19368, 'Phase 6', 'Warrior', 'Protection', 'Ranged', 'Both', 'Dragonbreath Hand Cannon'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 120, 32083, 'Pre-Raid', 'Warrior', 'Protection', 'Head', 'Both', 'Faceguard of Determination'), +(1, 2, 1, 0, 120, 29386, 'Pre-Raid', 'Warrior', 'Protection', 'Neck', 'Both', 'Necklace of the Juggernaut'), +(1, 2, 2, 0, 120, 27803, 'Pre-Raid', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Shoulderguards of the Bold'), +(1, 2, 4, 0, 120, 28205, 'Pre-Raid', 'Warrior', 'Protection', 'Chest', 'Both', 'Breastplate of the Bold'), +(1, 2, 5, 0, 120, 31460, 'Pre-Raid', 'Warrior', 'Protection', 'Waist', 'Both', 'Sha''tari Vindicator''s Waistguard'), +(1, 2, 6, 0, 120, 29184, 'Pre-Raid', 'Warrior', 'Protection', 'Legs', 'Both', 'Timewarden''s Leggings'), +(1, 2, 7, 0, 120, 29239, 'Pre-Raid', 'Warrior', 'Protection', 'Feet', 'Both', 'Eaglecrest Warboots'), +(1, 2, 8, 0, 120, 29463, 'Pre-Raid', 'Warrior', 'Protection', 'Wrists', 'Both', 'Amber Bands of the Aggressor'), +(1, 2, 9, 0, 120, 27475, 'Pre-Raid', 'Warrior', 'Protection', 'Hands', 'Both', 'Gauntlets of the Bold'), +(1, 2, 10, 0, 120, 30834, 'Pre-Raid', 'Warrior', 'Protection', 'Finger1', 'Both', 'Shapeshifter''s Signet'), +(1, 2, 12, 0, 120, 29387, 'Pre-Raid', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Gnomeregan Auto-Blocker 600'), +(1, 2, 13, 0, 120, 23836, 'Pre-Raid', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Goblin Rocket Launcher'), +(1, 2, 14, 0, 120, 27804, 'Pre-Raid', 'Warrior', 'Protection', 'Back', 'Both', 'Devilshark Cape'), +(1, 2, 15, 0, 120, 28438, 'Pre-Raid', 'Warrior', 'Protection', 'MainHand', 'Both', 'Dragonmaw'), +(1, 2, 16, 0, 120, 29266, 'Pre-Raid', 'Warrior', 'Protection', 'OffHand', 'Both', 'Azure-Shield of Coldarra'), +(1, 2, 17, 0, 120, 32756, 'Pre-Raid', 'Warrior', 'Protection', 'Ranged', 'Both', 'Gyro-Balanced Khorium Destroyer'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 125, 29011, 'Phase 1', 'Warrior', 'Protection', 'Head', 'Both', 'Warbringer Greathelm'), +(1, 2, 1, 0, 125, 28516, 'Phase 1', 'Warrior', 'Protection', 'Neck', 'Both', 'Barbed Choker of Discipline'), +(1, 2, 2, 0, 125, 29016, 'Phase 1', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Warbringer Shoulderguards'), +(1, 2, 4, 0, 125, 29012, 'Phase 1', 'Warrior', 'Protection', 'Chest', 'Both', 'Warbringer Chestguard'), +(1, 2, 5, 0, 125, 28566, 'Phase 1', 'Warrior', 'Protection', 'Waist', 'Both', 'Crimson Girdle of the Indomitable'), +(1, 2, 6, 0, 125, 28621, 'Phase 1', 'Warrior', 'Protection', 'Legs', 'Both', 'Wrynn Dynasty Greaves'), +(1, 2, 7, 0, 125, 28747, 'Phase 1', 'Warrior', 'Protection', 'Feet', 'Both', 'Battlescar Boots'), +(1, 2, 8, 0, 125, 28502, 'Phase 1', 'Warrior', 'Protection', 'Wrists', 'Both', 'Vambraces of Courage'), +(1, 2, 9, 0, 125, 28518, 'Phase 1', 'Warrior', 'Protection', 'Hands', 'Both', 'Iron Gauntlets of the Maiden'), +(1, 2, 10, 0, 125, 29279, 'Phase 1', 'Warrior', 'Protection', 'Finger1', 'Both', 'Violet Signet of the Great Protector'), +(1, 2, 11, 0, 125, 30834, 'Phase 1', 'Warrior', 'Protection', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(1, 2, 12, 0, 125, 29387, 'Phase 1', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Gnomeregan Auto-Blocker 600'), +(1, 2, 13, 0, 125, 23836, 'Phase 1', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Goblin Rocket Launcher'), +(1, 2, 14, 0, 125, 28660, 'Phase 1', 'Warrior', 'Protection', 'Back', 'Both', 'Gilded Thorium Cloak'), +(1, 2, 15, 0, 125, 28438, 'Phase 1', 'Warrior', 'Protection', 'MainHand', 'Both', 'Dragonmaw'), +(1, 2, 16, 0, 125, 28825, 'Phase 1', 'Warrior', 'Protection', 'OffHand', 'Both', 'Aldori Legacy Defender'), +(1, 2, 17, 0, 125, 30724, 'Phase 1', 'Warrior', 'Protection', 'Ranged', 'Both', 'Barrel-Blade Longrifle'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 141, 30115, 'Phase 2', 'Warrior', 'Protection', 'Head', 'Both', 'Destroyer Greathelm'), +(1, 2, 1, 0, 141, 33066, 'Phase 2', 'Warrior', 'Protection', 'Neck', 'Both', 'Veteran''s Pendant of Triumph'), +(1, 2, 2, 0, 141, 30117, 'Phase 2', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Destroyer Shoulderguards'), +(1, 2, 4, 0, 141, 30113, 'Phase 2', 'Warrior', 'Protection', 'Chest', 'Both', 'Destroyer Chestguard'), +(1, 2, 5, 0, 141, 30106, 'Phase 2', 'Warrior', 'Protection', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(1, 2, 6, 0, 141, 30116, 'Phase 2', 'Warrior', 'Protection', 'Legs', 'Both', 'Destroyer Legguards'), +(1, 2, 7, 0, 141, 32793, 'Phase 2', 'Warrior', 'Protection', 'Feet', 'Both', 'Veteran''s Plate Greaves'), +(1, 2, 8, 0, 141, 32818, 'Phase 2', 'Warrior', 'Protection', 'Wrists', 'Both', 'Veteran''s Plate Bracers'), +(1, 2, 9, 2, 141, 29998, 'Phase 2', 'Warrior', 'Protection', 'Hands', 'Horde', 'Royal Gauntlets of Silvermoon'), +(1, 2, 10, 0, 141, 30834, 'Phase 2', 'Warrior', 'Protection', 'Finger1', 'Both', 'Shapeshifter''s Signet'), +(1, 2, 11, 0, 141, 29283, 'Phase 2', 'Warrior', 'Protection', 'Finger2', 'Both', 'Violet Signet of the Master Assassin'), +(1, 2, 12, 0, 141, 23836, 'Phase 2', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Goblin Rocket Launcher'), +(1, 2, 13, 0, 141, 28121, 'Phase 2', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Icon of Unyielding Courage'), +(1, 2, 14, 0, 141, 29994, 'Phase 2', 'Warrior', 'Protection', 'Back', 'Both', 'Thalassian Wildercloak'), +(1, 2, 15, 0, 141, 28439, 'Phase 2', 'Warrior', 'Protection', 'MainHand', 'Both', 'Dragonstrike'), +(1, 2, 16, 0, 141, 28825, 'Phase 2', 'Warrior', 'Protection', 'OffHand', 'Both', 'Aldori Legacy Defender'), +(1, 2, 17, 0, 141, 32756, 'Phase 2', 'Warrior', 'Protection', 'Ranged', 'Both', 'Gyro-Balanced Khorium Destroyer'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 156, 32521, 'Phase 3', 'Warrior', 'Protection', 'Head', 'Both', 'Faceplate of the Impenetrable'), +(1, 2, 1, 0, 156, 33066, 'Phase 3', 'Warrior', 'Protection', 'Neck', 'Both', 'Veteran''s Pendant of Triumph'), +(1, 2, 2, 0, 156, 33732, 'Phase 3', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Vengeful Gladiator''s Plate Shoulders'), +(1, 2, 4, 0, 156, 33728, 'Phase 3', 'Warrior', 'Protection', 'Chest', 'Both', 'Vengeful Gladiator''s Plate Chestpiece'), +(1, 2, 5, 0, 156, 30106, 'Phase 3', 'Warrior', 'Protection', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(1, 2, 6, 0, 156, 30978, 'Phase 3', 'Warrior', 'Protection', 'Legs', 'Both', 'Onslaught Legguards'), +(1, 2, 7, 0, 156, 33812, 'Phase 3', 'Warrior', 'Protection', 'Feet', 'Both', 'Vindicator''s Plate Greaves'), +(1, 2, 8, 0, 156, 33813, 'Phase 3', 'Warrior', 'Protection', 'Wrists', 'Both', 'Vindicator''s Plate Bracers'), +(1, 2, 10, 0, 156, 30834, 'Phase 3', 'Warrior', 'Protection', 'Finger1', 'Both', 'Shapeshifter''s Signet'), +(1, 2, 11, 0, 156, 33919, 'Phase 3', 'Warrior', 'Protection', 'Finger2', 'Both', 'Vindicator''s Band of Triumph'), +(1, 2, 12, 0, 156, 31858, 'Phase 3', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Darkmoon Card: Vengeance'), +(1, 2, 13, 0, 156, 32501, 'Phase 3', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Shadowmoon Insignia'), +(1, 2, 14, 0, 156, 34010, 'Phase 3', 'Warrior', 'Protection', 'Back', 'Both', 'Pepe''s Shroud of Pacification'), +(1, 2, 15, 0, 156, 32254, 'Phase 3', 'Warrior', 'Protection', 'MainHand', 'Both', 'The Brutalizer'), +(1, 2, 16, 0, 156, 32375, 'Phase 3', 'Warrior', 'Protection', 'OffHand', 'Both', 'Bulwark of Azzinoth'), +(1, 2, 17, 0, 156, 32253, 'Phase 3', 'Warrior', 'Protection', 'Ranged', 'Both', 'Legionkiller'); + +-- ilvl 164 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 164, 35068, 'Phase 5', 'Warrior', 'Protection', 'Head', 'Both', 'Brutal Gladiator''s Plate Helm'), +(1, 2, 1, 0, 164, 34178, 'Phase 5', 'Warrior', 'Protection', 'Neck', 'Both', 'Collar of the Pit Lord'), +(1, 2, 2, 0, 164, 34388, 'Phase 5', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Pauldrons of Berserking'), +(1, 2, 4, 0, 164, 35066, 'Phase 5', 'Warrior', 'Protection', 'Chest', 'Both', 'Brutal Gladiator''s Plate Chestpiece'), +(1, 2, 5, 0, 164, 34547, 'Phase 5', 'Warrior', 'Protection', 'Waist', 'Both', 'Onslaught Waistguard'), +(1, 2, 6, 0, 164, 34381, 'Phase 5', 'Warrior', 'Protection', 'Legs', 'Both', 'Felstrength Legplates'), +(1, 2, 7, 0, 164, 34568, 'Phase 5', 'Warrior', 'Protection', 'Feet', 'Both', 'Onslaught Boots'), +(1, 2, 8, 0, 164, 34442, 'Phase 5', 'Warrior', 'Protection', 'Wrists', 'Both', 'Onslaught Wristguards'), +(1, 2, 9, 0, 164, 34378, 'Phase 5', 'Warrior', 'Protection', 'Hands', 'Both', 'Hard Khorium Battlefists'), +(1, 2, 10, 0, 164, 35131, 'Phase 5', 'Warrior', 'Protection', 'Finger1', 'Both', 'Guardian''s Band of Triumph'), +(1, 2, 11, 0, 164, 34213, 'Phase 5', 'Warrior', 'Protection', 'Finger2', 'Both', 'Ring of Hardened Resolve'), +(1, 2, 12, 0, 164, 34473, 'Phase 5', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Commendation of Kael''thas'), +(1, 2, 13, 0, 164, 31858, 'Phase 5', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Darkmoon Card: Vengeance'), +(1, 2, 14, 0, 164, 34190, 'Phase 5', 'Warrior', 'Protection', 'Back', 'Both', 'Crimson Paragon''s Cover'), +(1, 2, 15, 0, 164, 34164, 'Phase 5', 'Warrior', 'Protection', 'MainHand', 'Both', 'Dragonscale-Encrusted Longblade'), +(1, 2, 16, 0, 164, 34185, 'Phase 5', 'Warrior', 'Protection', 'OffHand', 'Both', 'Sword Breaker''s Bulwark'), +(1, 2, 17, 0, 164, 32253, 'Phase 5', 'Warrior', 'Protection', 'Ranged', 'Both', 'Legionkiller'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 200, 41387, 'Pre-Raid', 'Warrior', 'Protection', 'Head', 'Both', 'Tempered Titansteel Helm'), +(1, 2, 1, 0, 200, 40679, 'Pre-Raid', 'Warrior', 'Protection', 'Neck', 'Both', 'Chained Military Gorget'), +(1, 2, 2, 0, 200, 34389, 'Pre-Raid', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Spaulders of the Thalassian Defender'), +(1, 2, 4, 0, 200, 39611, 'Pre-Raid', 'Warrior', 'Protection', 'Chest', 'Both', 'Heroes'' Dreadnaught Breastplate'), +(1, 2, 5, 0, 200, 37379, 'Pre-Raid', 'Warrior', 'Protection', 'Waist', 'Both', 'Skadi''s Iron Belt'), +(1, 2, 6, 0, 200, 43500, 'Pre-Raid', 'Warrior', 'Protection', 'Legs', 'Both', 'Bolstered Legplates'), +(1, 2, 7, 0, 200, 44201, 'Pre-Raid', 'Warrior', 'Protection', 'Feet', 'Both', 'Sabatons of Draconic Vigor'), +(1, 2, 8, 0, 200, 37620, 'Pre-Raid', 'Warrior', 'Protection', 'Wrists', 'Both', 'Bracers of the Herald'), +(1, 2, 9, 0, 200, 39622, 'Pre-Raid', 'Warrior', 'Protection', 'Hands', 'Both', 'Heroes'' Dreadnaught Handguards'), +(1, 2, 10, 0, 200, 34213, 'Pre-Raid', 'Warrior', 'Protection', 'Finger1', 'Both', 'Ring of Hardened Resolve'), +(1, 2, 12, 0, 200, 37220, 'Pre-Raid', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Essence of Gossamer'), +(1, 2, 14, 0, 200, 43565, 'Pre-Raid', 'Warrior', 'Protection', 'Back', 'Both', 'Durable Nerubhide Cape'), +(1, 2, 15, 0, 200, 37401, 'Pre-Raid', 'Warrior', 'Protection', 'MainHand', 'Both', 'Red Sword of Courage'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 224, 40546, 'Phase 1', 'Warrior', 'Protection', 'Head', 'Both', 'Valorous Dreadnaught Greathelm'), +(1, 2, 1, 0, 224, 44665, 'Phase 1', 'Warrior', 'Protection', 'Neck', 'Both', 'Nexus War Champion Beads'), +(1, 2, 2, 0, 224, 40548, 'Phase 1', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Valorous Dreadnaught Pauldrons'), +(1, 2, 4, 0, 224, 40544, 'Phase 1', 'Warrior', 'Protection', 'Chest', 'Both', 'Valorous Dreadnaught Breastplate'), +(1, 2, 5, 0, 224, 39759, 'Phase 1', 'Warrior', 'Protection', 'Waist', 'Both', 'Ablative Chitin Girdle'), +(1, 2, 6, 0, 224, 40589, 'Phase 1', 'Warrior', 'Protection', 'Legs', 'Both', 'Legplates of Sovereignty'), +(1, 2, 7, 0, 224, 39717, 'Phase 1', 'Warrior', 'Protection', 'Feet', 'Both', 'Inexorable Sabatons'), +(1, 2, 8, 0, 224, 39764, 'Phase 1', 'Warrior', 'Protection', 'Wrists', 'Both', 'Bindings of the Hapless Prey'), +(1, 2, 9, 0, 224, 40545, 'Phase 1', 'Warrior', 'Protection', 'Hands', 'Both', 'Valorous Dreadnaught Handguards'), +(1, 2, 10, 0, 224, 40718, 'Phase 1', 'Warrior', 'Protection', 'Finger1', 'Both', 'Signet of the Impregnable Fortress'), +(1, 2, 12, 0, 224, 40257, 'Phase 1', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Defender''s Code'), +(1, 2, 14, 0, 224, 40722, 'Phase 1', 'Warrior', 'Protection', 'Back', 'Both', 'Platinum Mesh Cloak'), +(1, 2, 15, 0, 224, 40402, 'Phase 1', 'Warrior', 'Protection', 'MainHand', 'Both', 'Last Laugh'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 245, 46166, 'Phase 2', 'Warrior', 'Protection', 'Head', 'Both', 'Conqueror''s Siegebreaker Greathelm'), +(1, 2, 1, 0, 245, 45485, 'Phase 2', 'Warrior', 'Protection', 'Neck', 'Both', 'Bronze Pendant of the Vanir'), +(1, 2, 2, 0, 245, 46167, 'Phase 2', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Conqueror''s Siegebreaker Pauldrons'), +(1, 2, 4, 0, 245, 46162, 'Phase 2', 'Warrior', 'Protection', 'Chest', 'Both', 'Conqueror''s Siegebreaker Breastplate'), +(1, 2, 5, 0, 245, 45139, 'Phase 2', 'Warrior', 'Protection', 'Waist', 'Both', 'Dragonslayer''s Brace'), +(1, 2, 6, 0, 245, 46169, 'Phase 2', 'Warrior', 'Protection', 'Legs', 'Both', 'Conqueror''s Siegebreaker Legguards'), +(1, 2, 7, 0, 245, 45542, 'Phase 2', 'Warrior', 'Protection', 'Feet', 'Both', 'Greaves of the Stonewarder'), +(1, 2, 8, 0, 245, 45111, 'Phase 2', 'Warrior', 'Protection', 'Wrists', 'Both', 'Mimiron''s Inferno Couplings'), +(1, 2, 9, 0, 245, 45487, 'Phase 2', 'Warrior', 'Protection', 'Hands', 'Both', 'Handguards of Revitalization'), +(1, 2, 10, 0, 245, 45471, 'Phase 2', 'Warrior', 'Protection', 'Finger1', 'Both', 'Fate''s Clutch'), +(1, 2, 12, 0, 245, 45158, 'Phase 2', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Heart of Iron'), +(1, 2, 14, 0, 245, 45496, 'Phase 2', 'Warrior', 'Protection', 'Back', 'Both', 'Titanskin Cloak'), +(1, 2, 17, 0, 245, 45137, 'Phase 2', 'Warrior', 'Protection', 'Ranged', 'Both', 'Veranus'' Bane'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 258, 48433, 'Phase 3', 'Warrior', 'Protection', 'Head', 'Both', 'Greathelm of Triumph'), +(1, 2, 2, 0, 258, 48455, 'Phase 3', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Pauldrons of Triumph'), +(1, 2, 4, 1, 258, 46968, 'Phase 3', 'Warrior', 'Protection', 'Chest', 'Alliance', 'Chestplate of the Towering Monstrosity'), +(1, 2, 4, 2, 258, 47415, 'Phase 3', 'Warrior', 'Protection', 'Chest', 'Horde', 'Hauberk of the Towering Monstrosity'), +(1, 2, 5, 1, 258, 47076, 'Phase 3', 'Warrior', 'Protection', 'Waist', 'Alliance', 'Girdle of Bloodied Scars'), +(1, 2, 5, 2, 258, 47444, 'Phase 3', 'Warrior', 'Protection', 'Waist', 'Horde', 'Belt of Bloodied Scars'), +(1, 2, 6, 0, 258, 48447, 'Phase 3', 'Warrior', 'Protection', 'Legs', 'Both', 'Legguards of Triumph'), +(1, 2, 7, 1, 258, 47952, 'Phase 3', 'Warrior', 'Protection', 'Feet', 'Alliance', 'Sabatons of the Lingering Vortex & Dawnbreaker Greaves'), +(1, 2, 8, 1, 258, 47918, 'Phase 3', 'Warrior', 'Protection', 'Wrists', 'Alliance', 'Dreadscale Armguards & Bracers of the Shieldmaiden'), +(1, 2, 9, 0, 258, 48453, 'Phase 3', 'Warrior', 'Protection', 'Hands', 'Both', 'Handguards of Triumph'), +(1, 2, 10, 1, 258, 45471, 'Phase 3', 'Warrior', 'Protection', 'Finger1', 'Alliance', 'Fate''s Clutch'), +(1, 2, 12, 1, 258, 47216, 'Phase 3', 'Warrior', 'Protection', 'Trinket1', 'Alliance', 'The Black Heart'), +(1, 2, 14, 1, 258, 47549, 'Phase 3', 'Warrior', 'Protection', 'Back', 'Alliance', 'Magni''s Resolution'), +(1, 2, 14, 2, 258, 47550, 'Phase 3', 'Warrior', 'Protection', 'Back', 'Horde', 'Cairne''s Endurance'), +(1, 2, 17, 0, 258, 47660, 'Phase 3', 'Warrior', 'Protection', 'Ranged', 'Both', 'Blades of the Sable Cross'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 264, 50640, 'Phase 4', 'Warrior', 'Protection', 'Head', 'Both', 'Broken Ram Skull Helm'), +(1, 2, 2, 0, 264, 51224, 'Phase 4', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Sanctified Ymirjar Lord''s Pauldrons'), +(1, 2, 4, 0, 264, 51220, 'Phase 4', 'Warrior', 'Protection', 'Chest', 'Both', 'Sanctified Ymirjar Lord''s Breastplate'), +(1, 2, 5, 0, 264, 50691, 'Phase 4', 'Warrior', 'Protection', 'Waist', 'Both', 'Belt of Broken Bones'), +(1, 2, 6, 0, 264, 51223, 'Phase 4', 'Warrior', 'Protection', 'Legs', 'Both', 'Sanctified Ymirjar Lord''s Legguards'), +(1, 2, 7, 0, 264, 50625, 'Phase 4', 'Warrior', 'Protection', 'Feet', 'Both', 'Grinning Skull Greatboots'), +(1, 2, 8, 0, 264, 50611, 'Phase 4', 'Warrior', 'Protection', 'Wrists', 'Both', 'Bracers of Dark Reckoning'), +(1, 2, 9, 0, 264, 51222, 'Phase 4', 'Warrior', 'Protection', 'Hands', 'Both', 'Sanctified Ymirjar Lord''s Handguards'), +(1, 2, 10, 0, 264, 50622, 'Phase 4', 'Warrior', 'Protection', 'Finger1', 'Both', 'Devium''s Eternally Cold Ring'), +(1, 2, 12, 0, 264, 50364, 'Phase 4', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Sindragosa''s Flawless Fang'), +(1, 2, 14, 0, 264, 50718, 'Phase 4', 'Warrior', 'Protection', 'Back', 'Both', 'Royal Crimson Cloak'), +(1, 2, 17, 0, 264, 51834, 'Phase 4', 'Warrior', 'Protection', 'Ranged', 'Both', 'Dreamhunter''s Carbine'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(1, 2, 0, 0, 290, 50640, 'Phase 5', 'Warrior', 'Protection', 'Head', 'Both', 'Broken Ram Skull Helm'), +(1, 2, 1, 0, 290, 50682, 'Phase 5', 'Warrior', 'Protection', 'Neck', 'Both', 'Bile-Encrusted Medallion'), +(1, 2, 2, 0, 290, 51847, 'Phase 5', 'Warrior', 'Protection', 'Shoulders', 'Both', 'Spaulders of the Blood Princes'), +(1, 2, 4, 0, 290, 51220, 'Phase 5', 'Warrior', 'Protection', 'Chest', 'Both', 'Sanctified Ymirjar Lord''s Breastplate'), +(1, 2, 5, 0, 290, 50691, 'Phase 5', 'Warrior', 'Protection', 'Waist', 'Both', 'Belt of Broken Bones'), +(1, 2, 6, 0, 290, 50612, 'Phase 5', 'Warrior', 'Protection', 'Legs', 'Both', 'Legguards of Lost Hope'), +(1, 2, 7, 0, 290, 54579, 'Phase 5', 'Warrior', 'Protection', 'Feet', 'Both', 'Treads of Impending Resurrection'), +(1, 2, 8, 0, 290, 51901, 'Phase 5', 'Warrior', 'Protection', 'Wrists', 'Both', 'Gargoyle Spit Bracers'), +(1, 2, 9, 0, 290, 51222, 'Phase 5', 'Warrior', 'Protection', 'Hands', 'Both', 'Sanctified Ymirjar Lord''s Handguards'), +(1, 2, 10, 0, 290, 50622, 'Phase 5', 'Warrior', 'Protection', 'Finger1', 'Both', 'Devium''s Eternally Cold Ring'), +(1, 2, 11, 0, 290, 50404, 'Phase 5', 'Warrior', 'Protection', 'Finger2', 'Both', 'Ashen Band of Endless Courage'), +(1, 2, 12, 0, 290, 54591, 'Phase 5', 'Warrior', 'Protection', 'Trinket1', 'Both', 'Petrified Twilight Scale'), +(1, 2, 13, 0, 290, 50364, 'Phase 5', 'Warrior', 'Protection', 'Trinket2', 'Both', 'Sindragosa''s Flawless Fang'), +(1, 2, 14, 0, 290, 50466, 'Phase 5', 'Warrior', 'Protection', 'Back', 'Both', 'Sentinel''s Winter Cloak'), +(1, 2, 15, 0, 290, 50738, 'Phase 5', 'Warrior', 'Protection', 'MainHand', 'Both', 'Mithrios, Bronzebeard''s Legacy'), +(1, 2, 16, 0, 290, 50729, 'Phase 5', 'Warrior', 'Protection', 'OffHand', 'Both', 'Icecrown Glacial Wall'), +(1, 2, 17, 0, 290, 51834, 'Phase 5', 'Warrior', 'Protection', 'Ranged', 'Both', 'Dreamhunter''s Carbine'); + + +-- ============================================================ +-- Paladin (2) +-- ============================================================ +-- Holy (tab 0) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 66, 12633, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Head', 'Both', 'Whitesoul Helm'), +(2, 0, 1, 0, 66, 18723, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(2, 0, 2, 0, 66, 18720, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Shroud of the Nathrezim'), +(2, 0, 4, 0, 66, 13346, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Chest', 'Both', 'Robes of the Exalted'), +(2, 0, 5, 0, 66, 18702, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Waist', 'Both', 'Belt of the Ordained'), +(2, 0, 6, 0, 66, 11841, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Legs', 'Both', 'Senior Designer''s Pantaloons'), +(2, 0, 7, 0, 66, 13954, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Feet', 'Both', 'Verdant Footpads'), +(2, 0, 8, 0, 66, 13969, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Wrists', 'Both', 'Loomguard Armbraces'), +(2, 0, 9, 0, 66, 10787, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Hands', 'Both', 'Atal''ai Gloves'), +(2, 0, 10, 0, 66, 16058, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Finger1', 'Both', 'Fordring''s Seal'), +(2, 0, 11, 0, 66, 18103, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Finger2', 'Both', 'Band of Rumination'), +(2, 0, 12, 0, 66, 11819, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Second Wind'), +(2, 0, 13, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Briarwood Reed'), +(2, 0, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'Back', 'Both', 'Archivist Cape'), +(2, 0, 15, 0, 66, 11923, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'MainHand', 'Both', 'The Hammer of Grace'), +(2, 0, 16, 0, 66, 11928, 'Phase 1 (Pre-Raid)', 'Paladin', 'Holy', 'OffHand', 'Both', 'Thaurissan''s Royal Scepter'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 76, 18490, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Head', 'Both', 'Insightful Hood'), +(2, 0, 1, 0, 76, 18723, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(2, 0, 2, 0, 76, 18720, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Shroud of the Nathrezim'), +(2, 0, 4, 0, 76, 13346, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Chest', 'Both', 'Robes of the Exalted'), +(2, 0, 5, 0, 76, 18702, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Waist', 'Both', 'Belt of the Ordained'), +(2, 0, 6, 0, 76, 18386, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Legs', 'Both', 'Padre''s Trousers'), +(2, 0, 7, 0, 76, 18507, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Feet', 'Both', 'Boots of the Full Moon'), +(2, 0, 8, 0, 76, 13969, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Wrists', 'Both', 'Loomguard Armbraces'), +(2, 0, 9, 0, 76, 18527, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Hands', 'Both', 'Harmonious Gauntlets'), +(2, 0, 10, 0, 76, 16058, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Finger1', 'Both', 'Fordring''s Seal'), +(2, 0, 11, 0, 76, 18103, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Finger2', 'Both', 'Band of Rumination'), +(2, 0, 12, 0, 76, 11819, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Second Wind'), +(2, 0, 13, 0, 76, 12930, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Briarwood Reed'), +(2, 0, 14, 0, 76, 18510, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'Back', 'Both', 'Hide of the Wild'), +(2, 0, 15, 0, 76, 11923, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'MainHand', 'Both', 'The Hammer of Grace'), +(2, 0, 16, 0, 76, 18523, 'Phase 2 (Pre-Raid)', 'Paladin', 'Holy', 'OffHand', 'Both', 'Brightly Glowing Stone'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 78, 18490, 'Phase 2', 'Paladin', 'Holy', 'Head', 'Both', 'Insightful Hood'), +(2, 0, 1, 0, 78, 18723, 'Phase 2', 'Paladin', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(2, 0, 2, 0, 78, 18810, 'Phase 2', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(2, 0, 4, 0, 78, 19145, 'Phase 2', 'Paladin', 'Holy', 'Chest', 'Both', 'Robe of Volatile Power'), +(2, 0, 5, 0, 78, 19162, 'Phase 2', 'Paladin', 'Holy', 'Waist', 'Both', 'Corehound Belt'), +(2, 0, 6, 0, 78, 18875, 'Phase 2', 'Paladin', 'Holy', 'Legs', 'Both', 'Salamander Scale Pants'), +(2, 0, 7, 0, 78, 18507, 'Phase 2', 'Paladin', 'Holy', 'Feet', 'Both', 'Boots of the Full Moon'), +(2, 0, 8, 0, 78, 13969, 'Phase 2', 'Paladin', 'Holy', 'Wrists', 'Both', 'Loomguard Armbraces'), +(2, 0, 9, 0, 78, 18527, 'Phase 2', 'Paladin', 'Holy', 'Hands', 'Both', 'Harmonious Gauntlets'), +(2, 0, 10, 0, 78, 19140, 'Phase 2', 'Paladin', 'Holy', 'Finger1', 'Both', 'Cauterizing Band'), +(2, 0, 11, 0, 78, 19140, 'Phase 2', 'Paladin', 'Holy', 'Finger2', 'Both', 'Cauterizing Band'), +(2, 0, 12, 0, 78, 17064, 'Phase 2', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Shard of the Scale'), +(2, 0, 13, 0, 78, 12930, 'Phase 2', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Briarwood Reed'), +(2, 0, 14, 0, 78, 18510, 'Phase 2', 'Paladin', 'Holy', 'Back', 'Both', 'Hide of the Wild'), +(2, 0, 15, 0, 78, 17103, 'Phase 2', 'Paladin', 'Holy', 'MainHand', 'Both', 'Azuresong Mageblade'), +(2, 0, 16, 0, 78, 18523, 'Phase 2', 'Paladin', 'Holy', 'OffHand', 'Both', 'Brightly Glowing Stone'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 83, 19375, 'Phase 3', 'Paladin', 'Holy', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(2, 0, 1, 0, 83, 18723, 'Phase 3', 'Paladin', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(2, 0, 2, 0, 83, 18810, 'Phase 3', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(2, 0, 4, 0, 83, 19145, 'Phase 3', 'Paladin', 'Holy', 'Chest', 'Both', 'Robe of Volatile Power'), +(2, 0, 5, 0, 83, 19162, 'Phase 3', 'Paladin', 'Holy', 'Waist', 'Both', 'Corehound Belt'), +(2, 0, 6, 0, 83, 19385, 'Phase 3', 'Paladin', 'Holy', 'Legs', 'Both', 'Empowered Leggings'), +(2, 0, 7, 0, 83, 19437, 'Phase 3', 'Paladin', 'Holy', 'Feet', 'Both', 'Boots of Pure Thought'), +(2, 0, 8, 0, 83, 13969, 'Phase 3', 'Paladin', 'Holy', 'Wrists', 'Both', 'Loomguard Armbraces'), +(2, 0, 9, 0, 83, 19390, 'Phase 3', 'Paladin', 'Holy', 'Hands', 'Both', 'Taut Dragonhide Gloves'), +(2, 0, 10, 0, 83, 19382, 'Phase 3', 'Paladin', 'Holy', 'Finger1', 'Both', 'Pure Elementium Band'), +(2, 0, 11, 0, 83, 19140, 'Phase 3', 'Paladin', 'Holy', 'Finger2', 'Both', 'Cauterizing Band'), +(2, 0, 12, 0, 83, 17064, 'Phase 3', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Shard of the Scale'), +(2, 0, 13, 0, 83, 19395, 'Phase 3', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Rejuvenating Gem'), +(2, 0, 14, 0, 83, 19430, 'Phase 3', 'Paladin', 'Holy', 'Back', 'Both', 'Shroud of Pure Thought'), +(2, 0, 15, 0, 83, 19360, 'Phase 3', 'Paladin', 'Holy', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(2, 0, 16, 0, 83, 19312, 'Phase 3', 'Paladin', 'Holy', 'OffHand', 'Both', 'Lei of the Lifegiver'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 88, 20628, 'Phase 5', 'Paladin', 'Holy', 'Head', 'Both', 'Deviate Growth Cap'), +(2, 0, 1, 0, 88, 21712, 'Phase 5', 'Paladin', 'Holy', 'Neck', 'Both', 'Amulet of the Fallen God'), +(2, 0, 2, 0, 88, 18810, 'Phase 5', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(2, 0, 4, 0, 88, 21663, 'Phase 5', 'Paladin', 'Holy', 'Chest', 'Both', 'Robes of the Guardian Saint'), +(2, 0, 5, 0, 88, 21582, 'Phase 5', 'Paladin', 'Holy', 'Waist', 'Both', 'Grasp of the Old God'), +(2, 0, 6, 0, 88, 21667, 'Phase 5', 'Paladin', 'Holy', 'Legs', 'Both', 'Legplates of Blazing Light'), +(2, 0, 7, 0, 88, 19437, 'Phase 5', 'Paladin', 'Holy', 'Feet', 'Both', 'Boots of Pure Thought'), +(2, 0, 8, 0, 88, 21604, 'Phase 5', 'Paladin', 'Holy', 'Wrists', 'Both', 'Bracelets of Royal Redemption'), +(2, 0, 9, 0, 88, 20264, 'Phase 5', 'Paladin', 'Holy', 'Hands', 'Both', 'Peacekeeper Gauntlets'), +(2, 0, 10, 0, 88, 19382, 'Phase 5', 'Paladin', 'Holy', 'Finger1', 'Both', 'Pure Elementium Band'), +(2, 0, 11, 0, 88, 21620, 'Phase 5', 'Paladin', 'Holy', 'Finger2', 'Both', 'Ring of the Martyr'), +(2, 0, 12, 0, 88, 17064, 'Phase 5', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Shard of the Scale'), +(2, 0, 13, 0, 88, 19395, 'Phase 5', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Rejuvenating Gem'), +(2, 0, 14, 0, 88, 21583, 'Phase 5', 'Paladin', 'Holy', 'Back', 'Both', 'Cloak of Clarity'), +(2, 0, 15, 0, 88, 21839, 'Phase 5', 'Paladin', 'Holy', 'MainHand', 'Both', 'Scepter of the False Prophet'), +(2, 0, 16, 0, 88, 21666, 'Phase 5', 'Paladin', 'Holy', 'OffHand', 'Both', 'Sartura''s Might'), +(2, 0, 17, 0, 88, 22402, 'Phase 5', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Grace'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 92, 20628, 'Phase 6', 'Paladin', 'Holy', 'Head', 'Both', 'Deviate Growth Cap'), +(2, 0, 1, 0, 92, 23057, 'Phase 6', 'Paladin', 'Holy', 'Neck', 'Both', 'Gem of Trapped Innocents'), +(2, 0, 2, 0, 92, 22429, 'Phase 6', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Redemption Spaulders'), +(2, 0, 4, 0, 92, 22425, 'Phase 6', 'Paladin', 'Holy', 'Chest', 'Both', 'Redemption Tunic'), +(2, 0, 5, 0, 92, 21582, 'Phase 6', 'Paladin', 'Holy', 'Waist', 'Both', 'Grasp of the Old God'), +(2, 0, 6, 0, 92, 22427, 'Phase 6', 'Paladin', 'Holy', 'Legs', 'Both', 'Redemption Legguards'), +(2, 0, 7, 0, 92, 22430, 'Phase 6', 'Paladin', 'Holy', 'Feet', 'Both', 'Redemption Boots'), +(2, 0, 8, 0, 92, 21604, 'Phase 6', 'Paladin', 'Holy', 'Wrists', 'Both', 'Bracelets of Royal Redemption'), +(2, 0, 9, 0, 92, 20264, 'Phase 6', 'Paladin', 'Holy', 'Hands', 'Both', 'Peacekeeper Gauntlets'), +(2, 0, 10, 0, 92, 19382, 'Phase 6', 'Paladin', 'Holy', 'Finger1', 'Both', 'Pure Elementium Band'), +(2, 0, 11, 0, 92, 23066, 'Phase 6', 'Paladin', 'Holy', 'Finger2', 'Both', 'Ring of Redemption'), +(2, 0, 12, 0, 92, 23047, 'Phase 6', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Eye of the Dead'), +(2, 0, 13, 0, 92, 19395, 'Phase 6', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Rejuvenating Gem'), +(2, 0, 14, 0, 92, 23050, 'Phase 6', 'Paladin', 'Holy', 'Back', 'Both', 'Cloak of the Necropolis'), +(2, 0, 15, 0, 92, 23056, 'Phase 6', 'Paladin', 'Holy', 'MainHand', 'Both', 'Hammer of the Twisting Nether'), +(2, 0, 16, 0, 92, 23075, 'Phase 6', 'Paladin', 'Holy', 'OffHand', 'Both', 'Death''s Bargain'), +(2, 0, 17, 0, 92, 23006, 'Phase 6', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Light'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 120, 32084, 'Pre-Raid', 'Paladin', 'Holy', 'Head', 'Both', 'Helmet of the Steadfast Champion'), +(2, 0, 1, 0, 120, 29374, 'Pre-Raid', 'Paladin', 'Holy', 'Neck', 'Both', 'Necklace of Eternal Hope'), +(2, 0, 2, 0, 120, 27775, 'Pre-Raid', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Hallowed Pauldrons'), +(2, 0, 4, 0, 120, 28230, 'Pre-Raid', 'Paladin', 'Holy', 'Chest', 'Both', 'Hallowed Garments'), +(2, 0, 5, 0, 120, 24256, 'Pre-Raid', 'Paladin', 'Holy', 'Waist', 'Both', 'Girdle of Ruination'), +(2, 0, 6, 0, 120, 30541, 'Pre-Raid', 'Paladin', 'Holy', 'Legs', 'Both', 'Stormsong Kilt'), +(2, 0, 7, 0, 120, 27411, 'Pre-Raid', 'Paladin', 'Holy', 'Feet', 'Both', 'Slippers of Serenity'), +(2, 0, 8, 0, 120, 23539, 'Pre-Raid', 'Paladin', 'Holy', 'Wrists', 'Both', 'Blessed Bracers'), +(2, 0, 9, 0, 120, 27457, 'Pre-Raid', 'Paladin', 'Holy', 'Hands', 'Both', 'Life Bearer''s Gauntlets'), +(2, 0, 10, 0, 120, 29373, 'Pre-Raid', 'Paladin', 'Holy', 'Finger1', 'Both', 'Band of Halos'), +(2, 0, 10, 2, 120, 29168, 'Pre-Raid', 'Paladin', 'Holy', 'Finger1', 'Horde', 'Ancestral Band'), +(2, 0, 12, 0, 120, 29376, 'Pre-Raid', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(2, 0, 13, 0, 120, 30841, 'Pre-Raid', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Lower City Prayer Book'), +(2, 0, 14, 0, 120, 29354, 'Pre-Raid', 'Paladin', 'Holy', 'Back', 'Both', 'Light-Touched Stole of Altruism'), +(2, 0, 15, 0, 120, 23556, 'Pre-Raid', 'Paladin', 'Holy', 'MainHand', 'Both', 'Hand of Eternity'), +(2, 0, 16, 0, 120, 29267, 'Pre-Raid', 'Paladin', 'Holy', 'OffHand', 'Both', 'Light-Bearer''s Faith Shield'), +(2, 0, 17, 0, 120, 25644, 'Pre-Raid', 'Paladin', 'Holy', 'Ranged', 'Both', 'Blessed Book of Nagrand'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 125, 29061, 'Phase 1', 'Paladin', 'Holy', 'Head', 'Both', 'Justicar Diadem'), +(2, 0, 1, 0, 125, 30726, 'Phase 1', 'Paladin', 'Holy', 'Neck', 'Both', 'Archaic Charm of Presence'), +(2, 0, 2, 0, 125, 29064, 'Phase 1', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Justicar Pauldrons'), +(2, 0, 4, 0, 125, 29062, 'Phase 1', 'Paladin', 'Holy', 'Chest', 'Both', 'Justicar Chestpiece'), +(2, 0, 5, 0, 125, 28799, 'Phase 1', 'Paladin', 'Holy', 'Waist', 'Both', 'Belt of Divine Inspiration'), +(2, 0, 6, 0, 125, 30727, 'Phase 1', 'Paladin', 'Holy', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(2, 0, 7, 0, 125, 30737, 'Phase 1', 'Paladin', 'Holy', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(2, 0, 8, 0, 125, 28512, 'Phase 1', 'Paladin', 'Holy', 'Wrists', 'Both', 'Bracers of Justice'), +(2, 0, 9, 0, 125, 28505, 'Phase 1', 'Paladin', 'Holy', 'Hands', 'Both', 'Gauntlets of Renewed Hope'), +(2, 0, 10, 0, 125, 28790, 'Phase 1', 'Paladin', 'Holy', 'Finger1', 'Both', 'Naaru Lightwarden''s Band'), +(2, 0, 11, 0, 125, 30736, 'Phase 1', 'Paladin', 'Holy', 'Finger2', 'Both', 'Ring of Flowing Light'), +(2, 0, 12, 0, 125, 28590, 'Phase 1', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Ribbon of Sacrifice'), +(2, 0, 13, 0, 125, 29376, 'Phase 1', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Essence of the Martyr'), +(2, 0, 14, 0, 125, 28765, 'Phase 1', 'Paladin', 'Holy', 'Back', 'Both', 'Stainless Cloak of the Pure Hearted'), +(2, 0, 15, 0, 125, 28771, 'Phase 1', 'Paladin', 'Holy', 'MainHand', 'Both', 'Light''s Justice'), +(2, 0, 16, 0, 125, 29458, 'Phase 1', 'Paladin', 'Holy', 'OffHand', 'Both', 'Aegis of the Vindicator'), +(2, 0, 17, 0, 125, 28592, 'Phase 1', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Souls Redeemed'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 141, 30136, 'Phase 2', 'Paladin', 'Holy', 'Head', 'Both', 'Crystalforge Greathelm'), +(2, 0, 1, 0, 141, 30018, 'Phase 2', 'Paladin', 'Holy', 'Neck', 'Both', 'Lord Sanguinar''s Claim'), +(2, 0, 2, 0, 141, 30138, 'Phase 2', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Crystalforge Pauldrons'), +(2, 0, 4, 0, 141, 30134, 'Phase 2', 'Paladin', 'Holy', 'Chest', 'Both', 'Crystalforge Chestpiece'), +(2, 0, 5, 0, 141, 29965, 'Phase 2', 'Paladin', 'Holy', 'Waist', 'Both', 'Girdle of the Righteous Path'), +(2, 0, 6, 0, 141, 29991, 'Phase 2', 'Paladin', 'Holy', 'Legs', 'Both', 'Sunhawk Leggings'), +(2, 0, 7, 0, 141, 30027, 'Phase 2', 'Paladin', 'Holy', 'Feet', 'Both', 'Boots of Courage Unending'), +(2, 0, 8, 0, 141, 30047, 'Phase 2', 'Paladin', 'Holy', 'Wrists', 'Both', 'Blackfathom Warbands'), +(2, 0, 9, 0, 141, 30112, 'Phase 2', 'Paladin', 'Holy', 'Hands', 'Both', 'Glorious Gauntlets of Crestfall'), +(2, 0, 10, 0, 141, 28790, 'Phase 2', 'Paladin', 'Holy', 'Finger1', 'Both', 'Naaru Lightwarden''s Band'), +(2, 0, 11, 0, 141, 29920, 'Phase 2', 'Paladin', 'Holy', 'Finger2', 'Both', 'Phoenix-Ring of Rebirth'), +(2, 0, 12, 0, 141, 29376, 'Phase 2', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(2, 0, 13, 0, 141, 28590, 'Phase 2', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Ribbon of Sacrifice'), +(2, 0, 14, 0, 141, 29989, 'Phase 2', 'Paladin', 'Holy', 'Back', 'Both', 'Sunshower Light Cloak'), +(2, 0, 15, 0, 141, 30108, 'Phase 2', 'Paladin', 'Holy', 'MainHand', 'Both', 'Lightfathom Scepter'), +(2, 0, 16, 0, 141, 29458, 'Phase 2', 'Paladin', 'Holy', 'OffHand', 'Both', 'Aegis of the Vindicator'), +(2, 0, 17, 0, 141, 28592, 'Phase 2', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Souls Redeemed'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 156, 30988, 'Phase 3', 'Paladin', 'Holy', 'Head', 'Both', 'Lightbringer Greathelm'), +(2, 0, 1, 0, 156, 32370, 'Phase 3', 'Paladin', 'Holy', 'Neck', 'Both', 'Nadina''s Pendant of Purity'), +(2, 0, 2, 0, 156, 30996, 'Phase 3', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Lightbringer Pauldrons'), +(2, 0, 4, 0, 156, 30992, 'Phase 3', 'Paladin', 'Holy', 'Chest', 'Both', 'Lightbringer Chestpiece'), +(2, 0, 5, 0, 156, 30897, 'Phase 3', 'Paladin', 'Holy', 'Waist', 'Both', 'Girdle of Hope'), +(2, 0, 6, 0, 156, 30994, 'Phase 3', 'Paladin', 'Holy', 'Legs', 'Both', 'Lightbringer Leggings'), +(2, 0, 7, 0, 156, 32243, 'Phase 3', 'Paladin', 'Holy', 'Feet', 'Both', 'Pearl Inlaid Boots'), +(2, 0, 8, 0, 156, 30862, 'Phase 3', 'Paladin', 'Holy', 'Wrists', 'Both', 'Blessed Adamantite Bracers'), +(2, 0, 9, 0, 156, 30112, 'Phase 3', 'Paladin', 'Holy', 'Hands', 'Both', 'Glorious Gauntlets of Crestfall'), +(2, 0, 10, 0, 156, 32528, 'Phase 3', 'Paladin', 'Holy', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(2, 0, 11, 0, 156, 32238, 'Phase 3', 'Paladin', 'Holy', 'Finger2', 'Both', 'Ring of Calming Waves'), +(2, 0, 12, 0, 156, 29376, 'Phase 3', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(2, 0, 13, 0, 156, 32496, 'Phase 3', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Memento of Tyrande'), +(2, 0, 14, 0, 156, 32524, 'Phase 3', 'Paladin', 'Holy', 'Back', 'Both', 'Shroud of the Highborne'), +(2, 0, 15, 0, 156, 32500, 'Phase 3', 'Paladin', 'Holy', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(2, 0, 16, 0, 156, 32255, 'Phase 3', 'Paladin', 'Holy', 'OffHand', 'Both', 'Felstone Bulwark'), +(2, 0, 17, 0, 156, 28592, 'Phase 3', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Souls Redeemed'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 164, 30988, 'Phase 4', 'Paladin', 'Holy', 'Head', 'Both', 'Lightbringer Greathelm'), +(2, 0, 1, 0, 164, 33281, 'Phase 4', 'Paladin', 'Holy', 'Neck', 'Both', 'Brooch of Nature''s Mercy'), +(2, 0, 2, 0, 164, 30996, 'Phase 4', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Lightbringer Pauldrons'), +(2, 0, 4, 0, 164, 30992, 'Phase 4', 'Paladin', 'Holy', 'Chest', 'Both', 'Lightbringer Chestpiece'), +(2, 0, 5, 0, 164, 30897, 'Phase 4', 'Paladin', 'Holy', 'Waist', 'Both', 'Girdle of Hope'), +(2, 0, 6, 0, 164, 30994, 'Phase 4', 'Paladin', 'Holy', 'Legs', 'Both', 'Lightbringer Leggings'), +(2, 0, 7, 0, 164, 33324, 'Phase 4', 'Paladin', 'Holy', 'Feet', 'Both', 'Treads of the Life Path'), +(2, 0, 8, 0, 164, 30862, 'Phase 4', 'Paladin', 'Holy', 'Wrists', 'Both', 'Blessed Adamantite Bracers'), +(2, 0, 9, 0, 164, 30112, 'Phase 4', 'Paladin', 'Holy', 'Hands', 'Both', 'Glorious Gauntlets of Crestfall'), +(2, 0, 10, 0, 164, 32528, 'Phase 4', 'Paladin', 'Holy', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(2, 0, 11, 0, 164, 32238, 'Phase 4', 'Paladin', 'Holy', 'Finger2', 'Both', 'Ring of Calming Waves'), +(2, 0, 12, 0, 164, 29376, 'Phase 4', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(2, 0, 13, 0, 164, 32496, 'Phase 4', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Memento of Tyrande'), +(2, 0, 14, 0, 164, 32524, 'Phase 4', 'Paladin', 'Holy', 'Back', 'Both', 'Shroud of the Highborne'), +(2, 0, 15, 0, 164, 32500, 'Phase 4', 'Paladin', 'Holy', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(2, 0, 16, 0, 164, 32255, 'Phase 4', 'Paladin', 'Holy', 'OffHand', 'Both', 'Felstone Bulwark'), +(2, 0, 17, 0, 164, 28592, 'Phase 4', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Souls Redeemed'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 200, 44949, 'Pre-Raid', 'Paladin', 'Holy', 'Head', 'Both', 'Unbreakable Healing Amplifiers'), +(2, 0, 1, 0, 200, 42647, 'Pre-Raid', 'Paladin', 'Holy', 'Neck', 'Both', 'Titanium Spellshock Necklace'), +(2, 0, 2, 0, 200, 37673, 'Pre-Raid', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Dark Runic Mantle'), +(2, 0, 4, 0, 200, 39629, 'Pre-Raid', 'Paladin', 'Holy', 'Chest', 'Both', 'Heroes'' Redemption Tunic'), +(2, 0, 5, 0, 200, 40691, 'Pre-Raid', 'Paladin', 'Holy', 'Waist', 'Both', 'Magroth''s Meditative Cincture'), +(2, 0, 6, 0, 200, 37362, 'Pre-Raid', 'Paladin', 'Holy', 'Legs', 'Both', 'Leggings of Protective Auras'), +(2, 0, 7, 0, 200, 44202, 'Pre-Raid', 'Paladin', 'Holy', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(2, 0, 8, 0, 200, 40741, 'Pre-Raid', 'Paladin', 'Holy', 'Wrists', 'Both', 'Cuffs of the Shadow Ascendant'), +(2, 0, 9, 0, 200, 39632, 'Pre-Raid', 'Paladin', 'Holy', 'Hands', 'Both', 'Heroes'' Redemption Gloves'), +(2, 0, 12, 0, 200, 44255, 'Pre-Raid', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(2, 0, 14, 0, 200, 34242, 'Pre-Raid', 'Paladin', 'Holy', 'Back', 'Both', 'Tattered Cape of Antonidas'), +(2, 0, 15, 0, 200, 37169, 'Pre-Raid', 'Paladin', 'Holy', 'MainHand', 'Both', 'War Mace of Unrequited Love'), +(2, 0, 17, 0, 200, 40705, 'Pre-Raid', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Renewal'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 224, 44007, 'Phase 1', 'Paladin', 'Holy', 'Head', 'Both', 'Headpiece of Reconciliation'), +(2, 0, 1, 0, 224, 44661, 'Phase 1', 'Paladin', 'Holy', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(2, 0, 2, 0, 224, 40573, 'Phase 1', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Valorous Redemption Spaulders'), +(2, 0, 4, 0, 224, 40569, 'Phase 1', 'Paladin', 'Holy', 'Chest', 'Both', 'Valorous Redemption Tunic'), +(2, 0, 5, 0, 224, 40561, 'Phase 1', 'Paladin', 'Holy', 'Waist', 'Both', 'Leash of Heedless Magic'), +(2, 0, 6, 0, 224, 40572, 'Phase 1', 'Paladin', 'Holy', 'Legs', 'Both', 'Valorous Redemption Greaves'), +(2, 0, 7, 0, 224, 40592, 'Phase 1', 'Paladin', 'Holy', 'Feet', 'Both', 'Boots of Healing Energies'), +(2, 0, 8, 0, 224, 40332, 'Phase 1', 'Paladin', 'Holy', 'Wrists', 'Both', 'Abetment Bracers'), +(2, 0, 9, 0, 224, 40570, 'Phase 1', 'Paladin', 'Holy', 'Hands', 'Both', 'Valorous Redemption Gloves'), +(2, 0, 12, 0, 224, 44255, 'Phase 1', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(2, 0, 14, 0, 224, 44005, 'Phase 1', 'Paladin', 'Holy', 'Back', 'Both', 'Pennant Cloak'), +(2, 0, 17, 0, 224, 40705, 'Phase 1', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Renewal'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 245, 46180, 'Phase 2', 'Paladin', 'Holy', 'Head', 'Both', 'Conqueror''s Aegis Headpiece'), +(2, 0, 1, 0, 245, 45443, 'Phase 2', 'Paladin', 'Holy', 'Neck', 'Both', 'Charm of Meticulous Timing'), +(2, 0, 2, 0, 245, 46182, 'Phase 2', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Conqueror''s Aegis Spaulders'), +(2, 0, 4, 0, 245, 45445, 'Phase 2', 'Paladin', 'Holy', 'Chest', 'Both', 'Breastplate of the Devoted'), +(2, 0, 5, 0, 245, 45616, 'Phase 2', 'Paladin', 'Holy', 'Waist', 'Both', 'Star-beaded Clutch'), +(2, 0, 6, 0, 245, 46181, 'Phase 2', 'Paladin', 'Holy', 'Legs', 'Both', 'Conqueror''s Aegis Greaves'), +(2, 0, 7, 0, 245, 45537, 'Phase 2', 'Paladin', 'Holy', 'Feet', 'Both', 'Treads of the False Oracle'), +(2, 0, 8, 0, 245, 45460, 'Phase 2', 'Paladin', 'Holy', 'Wrists', 'Both', 'Bindings of Winter Gale'), +(2, 0, 9, 0, 245, 46155, 'Phase 2', 'Paladin', 'Holy', 'Hands', 'Both', 'Conqueror''s Aegis Gauntlets'), +(2, 0, 12, 0, 245, 46051, 'Phase 2', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Meteorite Crystal'), +(2, 0, 14, 0, 245, 45486, 'Phase 2', 'Paladin', 'Holy', 'Back', 'Both', 'Drape of the Sullen Goddess'), +(2, 0, 15, 0, 245, 46017, 'Phase 2', 'Paladin', 'Holy', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(2, 0, 17, 0, 245, 40705, 'Phase 2', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Renewal'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 258, 48582, 'Phase 3', 'Paladin', 'Holy', 'Head', 'Both', 'Headpiece of Triumph'), +(2, 0, 2, 0, 258, 48580, 'Phase 3', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Spaulders of Triumph'), +(2, 0, 4, 1, 258, 47147, 'Phase 3', 'Paladin', 'Holy', 'Chest', 'Alliance', 'Breastplate of the Frozen Lake'), +(2, 0, 4, 2, 258, 47471, 'Phase 3', 'Paladin', 'Holy', 'Chest', 'Horde', 'Chestplate of the Frozen Lake'), +(2, 0, 5, 1, 258, 47924, 'Phase 3', 'Paladin', 'Holy', 'Waist', 'Alliance', 'Belt of the Frozen Reach'), +(2, 0, 5, 2, 258, 47997, 'Phase 3', 'Paladin', 'Holy', 'Waist', 'Horde', 'Girdle of the Frozen Reach'), +(2, 0, 6, 1, 258, 47190, 'Phase 3', 'Paladin', 'Holy', 'Legs', 'Alliance', 'Legwraps of the Awakening'), +(2, 0, 6, 2, 258, 47479, 'Phase 3', 'Paladin', 'Holy', 'Legs', 'Horde', 'Leggings of the Awakening'), +(2, 0, 7, 1, 258, 46986, 'Phase 3', 'Paladin', 'Holy', 'Feet', 'Alliance', 'Boots of the Courageous'), +(2, 0, 7, 2, 258, 47424, 'Phase 3', 'Paladin', 'Holy', 'Feet', 'Horde', 'Sabatons of the Courageous'), +(2, 0, 8, 0, 258, 45460, 'Phase 3', 'Paladin', 'Holy', 'Wrists', 'Both', 'Bindings of Winter Gale'), +(2, 0, 9, 0, 258, 45665, 'Phase 3', 'Paladin', 'Holy', 'Hands', 'Both', 'Pharos Gloves'), +(2, 0, 10, 1, 258, 47224, 'Phase 3', 'Paladin', 'Holy', 'Finger1', 'Alliance', 'Ring of the Darkmender'), +(2, 0, 10, 2, 258, 47439, 'Phase 3', 'Paladin', 'Holy', 'Finger1', 'Horde', 'Circle of the Darkmender'), +(2, 0, 12, 0, 258, 46051, 'Phase 3', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Meteorite Crystal'), +(2, 0, 14, 1, 258, 47552, 'Phase 3', 'Paladin', 'Holy', 'Back', 'Alliance', 'Jaina''s Radiance'), +(2, 0, 14, 2, 258, 47551, 'Phase 3', 'Paladin', 'Holy', 'Back', 'Horde', 'Aethas'' Intensity'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 264, 51272, 'Phase 4', 'Paladin', 'Holy', 'Head', 'Both', 'Sanctified Lightsworn Headpiece'), +(2, 0, 2, 0, 264, 51273, 'Phase 4', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Sanctified Lightsworn Spaulders'), +(2, 0, 4, 0, 264, 50680, 'Phase 4', 'Paladin', 'Holy', 'Chest', 'Both', 'Rot-Resistant Breastplate'), +(2, 0, 5, 0, 264, 50613, 'Phase 4', 'Paladin', 'Holy', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(2, 0, 6, 0, 264, 51928, 'Phase 4', 'Paladin', 'Holy', 'Legs', 'Both', 'Corrupted Silverplate Leggings'), +(2, 0, 7, 0, 264, 50699, 'Phase 4', 'Paladin', 'Holy', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(2, 0, 8, 0, 264, 50721, 'Phase 4', 'Paladin', 'Holy', 'Wrists', 'Both', 'Crypt Keeper''s Bracers'), +(2, 0, 9, 0, 264, 50650, 'Phase 4', 'Paladin', 'Holy', 'Hands', 'Both', 'Fallen Lord''s Handguards'), +(2, 0, 10, 0, 264, 50400, 'Phase 4', 'Paladin', 'Holy', 'Finger1', 'Both', 'Ashen Band of Endless Wisdom'), +(2, 0, 12, 0, 264, 46051, 'Phase 4', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Meteorite Crystal'), +(2, 0, 14, 0, 264, 50628, 'Phase 4', 'Paladin', 'Holy', 'Back', 'Both', 'Frostbinder''s Shredded Cape'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 0, 0, 0, 290, 51272, 'Phase 5', 'Paladin', 'Holy', 'Head', 'Both', 'Sanctified Lightsworn Headpiece'), +(2, 0, 1, 0, 290, 50182, 'Phase 5', 'Paladin', 'Holy', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(2, 0, 2, 0, 290, 51273, 'Phase 5', 'Paladin', 'Holy', 'Shoulders', 'Both', 'Sanctified Lightsworn Spaulders'), +(2, 0, 4, 0, 290, 50680, 'Phase 5', 'Paladin', 'Holy', 'Chest', 'Both', 'Rot-Resistant Breastplate'), +(2, 0, 5, 0, 290, 54587, 'Phase 5', 'Paladin', 'Holy', 'Waist', 'Both', 'Split Shape Belt'), +(2, 0, 6, 0, 290, 50694, 'Phase 5', 'Paladin', 'Holy', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(2, 0, 7, 0, 290, 54586, 'Phase 5', 'Paladin', 'Holy', 'Feet', 'Both', 'Foreshadow Steps'), +(2, 0, 8, 0, 290, 54582, 'Phase 5', 'Paladin', 'Holy', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(2, 0, 9, 0, 290, 50650, 'Phase 5', 'Paladin', 'Holy', 'Hands', 'Both', 'Fallen Lord''s Handguards'), +(2, 0, 10, 0, 290, 50664, 'Phase 5', 'Paladin', 'Holy', 'Finger1', 'Both', 'Ring of Rapid Ascent'), +(2, 0, 11, 0, 290, 50400, 'Phase 5', 'Paladin', 'Holy', 'Finger2', 'Both', 'Ashen Band of Endless Wisdom'), +(2, 0, 12, 0, 290, 46051, 'Phase 5', 'Paladin', 'Holy', 'Trinket1', 'Both', 'Meteorite Crystal'), +(2, 0, 13, 0, 290, 48724, 'Phase 5', 'Paladin', 'Holy', 'Trinket2', 'Both', 'Talisman of Resurgence'), +(2, 0, 14, 0, 290, 54583, 'Phase 5', 'Paladin', 'Holy', 'Back', 'Both', 'Cloak of Burning Dusk'), +(2, 0, 15, 0, 290, 46017, 'Phase 5', 'Paladin', 'Holy', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(2, 0, 16, 0, 290, 50616, 'Phase 5', 'Paladin', 'Holy', 'OffHand', 'Both', 'Bulwark of Smouldering Steel'), +(2, 0, 17, 0, 290, 40705, 'Phase 5', 'Paladin', 'Holy', 'Ranged', 'Both', 'Libram of Renewal'); + +-- Protection (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 66, 12952, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Head', 'Both', 'Gyth''s Skull'), +(2, 1, 1, 0, 66, 13091, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Neck', 'Both', 'Medallion of Grand Marshal Morris'), +(2, 1, 2, 0, 66, 14552, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Stockade Pauldrons'), +(2, 1, 4, 0, 66, 14624, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Chest', 'Both', 'Deathbone Chestplate'), +(2, 1, 5, 0, 66, 14620, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Waist', 'Both', 'Deathbone Girdle'), +(2, 1, 6, 0, 66, 14623, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Legs', 'Both', 'Deathbone Legguards'), +(2, 1, 7, 0, 66, 14621, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Feet', 'Both', 'Deathbone Sabatons'), +(2, 1, 8, 0, 66, 12550, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Wrists', 'Both', 'Runed Golem Shackles'), +(2, 1, 9, 0, 66, 14622, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Hands', 'Both', 'Deathbone Gauntlets'), +(2, 1, 10, 0, 66, 11669, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Finger1', 'Both', 'Naglering'), +(2, 1, 11, 0, 66, 10795, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Finger2', 'Both', 'Drakeclaw Band'), +(2, 1, 13, 0, 66, 10779, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Demon''s Blood'), +(2, 1, 14, 0, 66, 13397, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'Back', 'Both', 'Stoneskin Gargoyle Cape'), +(2, 1, 15, 0, 66, 11784, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'MainHand', 'Both', 'Arbiter''s Blade'), +(2, 1, 16, 0, 66, 12602, 'Phase 1 (Pre-Raid)', 'Paladin', 'Protection', 'OffHand', 'Both', 'Draconian Deflector'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 76, 12952, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Head', 'Both', 'Gyth''s Skull'), +(2, 1, 1, 0, 76, 13091, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Neck', 'Both', 'Medallion of Grand Marshal Morris'), +(2, 1, 2, 0, 76, 14552, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Stockade Pauldrons'), +(2, 1, 4, 0, 76, 14624, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Chest', 'Both', 'Deathbone Chestplate'), +(2, 1, 5, 0, 76, 14620, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Waist', 'Both', 'Deathbone Girdle'), +(2, 1, 6, 0, 76, 14623, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Legs', 'Both', 'Deathbone Legguards'), +(2, 1, 7, 0, 76, 14621, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Feet', 'Both', 'Deathbone Sabatons'), +(2, 1, 8, 0, 76, 18754, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Wrists', 'Both', 'Fel Hardened Bracers'), +(2, 1, 9, 0, 76, 14622, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Hands', 'Both', 'Deathbone Gauntlets'), +(2, 1, 10, 0, 76, 11669, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Finger1', 'Both', 'Naglering'), +(2, 1, 11, 0, 76, 10795, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Finger2', 'Both', 'Drakeclaw Band'), +(2, 1, 13, 0, 76, 10779, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Demon''s Blood'), +(2, 1, 14, 0, 76, 18495, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'Back', 'Both', 'Redoubt Cloak'), +(2, 1, 15, 0, 76, 18396, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'MainHand', 'Both', 'Mind Carver'), +(2, 1, 16, 0, 76, 12602, 'Phase 2 (Pre-Raid)', 'Paladin', 'Protection', 'OffHand', 'Both', 'Draconian Deflector'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 78, 12952, 'Phase 2', 'Paladin', 'Protection', 'Head', 'Both', 'Gyth''s Skull'), +(2, 1, 1, 0, 78, 17065, 'Phase 2', 'Paladin', 'Protection', 'Neck', 'Both', 'Medallion of Steadfast Might'), +(2, 1, 2, 0, 78, 14552, 'Phase 2', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Stockade Pauldrons'), +(2, 1, 4, 0, 78, 14624, 'Phase 2', 'Paladin', 'Protection', 'Chest', 'Both', 'Deathbone Chestplate'), +(2, 1, 5, 0, 78, 14620, 'Phase 2', 'Paladin', 'Protection', 'Waist', 'Both', 'Deathbone Girdle'), +(2, 1, 6, 0, 78, 14623, 'Phase 2', 'Paladin', 'Protection', 'Legs', 'Both', 'Deathbone Legguards'), +(2, 1, 7, 0, 78, 18806, 'Phase 2', 'Paladin', 'Protection', 'Feet', 'Both', 'Core Forged Greaves'), +(2, 1, 8, 0, 78, 18754, 'Phase 2', 'Paladin', 'Protection', 'Wrists', 'Both', 'Fel Hardened Bracers'), +(2, 1, 9, 0, 78, 13072, 'Phase 2', 'Paladin', 'Protection', 'Hands', 'Both', 'Stonegrip Gauntlets'), +(2, 1, 10, 0, 78, 11669, 'Phase 2', 'Paladin', 'Protection', 'Finger1', 'Both', 'Naglering'), +(2, 1, 11, 0, 78, 18879, 'Phase 2', 'Paladin', 'Protection', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(2, 1, 13, 0, 78, 18406, 'Phase 2', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(2, 1, 14, 0, 78, 18495, 'Phase 2', 'Paladin', 'Protection', 'Back', 'Both', 'Redoubt Cloak'), +(2, 1, 15, 0, 78, 17103, 'Phase 2', 'Paladin', 'Protection', 'MainHand', 'Both', 'Azuresong Mageblade'), +(2, 1, 16, 0, 78, 17066, 'Phase 2', 'Paladin', 'Protection', 'OffHand', 'Both', 'Drillborer Disk'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 83, 12620, 'Phase 3', 'Paladin', 'Protection', 'Head', 'Both', 'Enchanted Thorium Helm'), +(2, 1, 1, 0, 83, 19383, 'Phase 3', 'Paladin', 'Protection', 'Neck', 'Both', 'Master Dragonslayer''s Medallion'), +(2, 1, 2, 0, 83, 14552, 'Phase 3', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Stockade Pauldrons'), +(2, 1, 4, 0, 83, 12618, 'Phase 3', 'Paladin', 'Protection', 'Chest', 'Both', 'Enchanted Thorium Breastplate'), +(2, 1, 5, 0, 83, 14620, 'Phase 3', 'Paladin', 'Protection', 'Waist', 'Both', 'Deathbone Girdle'), +(2, 1, 6, 0, 83, 14623, 'Phase 3', 'Paladin', 'Protection', 'Legs', 'Both', 'Deathbone Legguards'), +(2, 1, 7, 0, 83, 18806, 'Phase 3', 'Paladin', 'Protection', 'Feet', 'Both', 'Core Forged Greaves'), +(2, 1, 8, 0, 83, 18754, 'Phase 3', 'Paladin', 'Protection', 'Wrists', 'Both', 'Fel Hardened Bracers'), +(2, 1, 9, 0, 83, 13072, 'Phase 3', 'Paladin', 'Protection', 'Hands', 'Both', 'Stonegrip Gauntlets'), +(2, 1, 10, 0, 83, 11669, 'Phase 3', 'Paladin', 'Protection', 'Finger1', 'Both', 'Naglering'), +(2, 1, 11, 0, 83, 18879, 'Phase 3', 'Paladin', 'Protection', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(2, 1, 12, 0, 83, 19431, 'Phase 3', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Styleen''s Impeding Scarab'), +(2, 1, 13, 0, 83, 18406, 'Phase 3', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(2, 1, 14, 0, 83, 18495, 'Phase 3', 'Paladin', 'Protection', 'Back', 'Both', 'Redoubt Cloak'), +(2, 1, 15, 0, 83, 19360, 'Phase 3', 'Paladin', 'Protection', 'MainHand', 'Both', 'Lok''amir il Romathis'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 88, 21387, 'Phase 5', 'Paladin', 'Protection', 'Head', 'Both', 'Avenger''s Crown'), +(2, 1, 1, 0, 88, 22732, 'Phase 5', 'Paladin', 'Protection', 'Neck', 'Both', 'Mark of C''Thun'), +(2, 1, 2, 0, 88, 21639, 'Phase 5', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Pauldrons of the Unrelenting'), +(2, 1, 4, 0, 88, 21389, 'Phase 5', 'Paladin', 'Protection', 'Chest', 'Both', 'Avenger''s Breastplate'), +(2, 1, 5, 0, 88, 21598, 'Phase 5', 'Paladin', 'Protection', 'Waist', 'Both', 'Royal Qiraji Belt'), +(2, 1, 6, 0, 88, 19855, 'Phase 5', 'Paladin', 'Protection', 'Legs', 'Both', 'Bloodsoaked Legplates'), +(2, 1, 7, 0, 88, 21706, 'Phase 5', 'Paladin', 'Protection', 'Feet', 'Both', 'Boots of the Unwavering Will'), +(2, 1, 8, 0, 88, 18754, 'Phase 5', 'Paladin', 'Protection', 'Wrists', 'Both', 'Fel Hardened Bracers'), +(2, 1, 9, 0, 88, 21674, 'Phase 5', 'Paladin', 'Protection', 'Hands', 'Both', 'Gauntlets of Steadfast Determination'), +(2, 1, 10, 0, 88, 21200, 'Phase 5', 'Paladin', 'Protection', 'Finger1', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(2, 1, 11, 0, 88, 21601, 'Phase 5', 'Paladin', 'Protection', 'Finger2', 'Both', 'Ring of Emperor Vek''lor'), +(2, 1, 12, 0, 88, 19431, 'Phase 5', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Styleen''s Impeding Scarab'), +(2, 1, 13, 0, 88, 18406, 'Phase 5', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(2, 1, 14, 0, 88, 19888, 'Phase 5', 'Paladin', 'Protection', 'Back', 'Both', 'Overlord''s Embrace'), +(2, 1, 15, 0, 88, 21622, 'Phase 5', 'Paladin', 'Protection', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(2, 1, 16, 0, 88, 21269, 'Phase 5', 'Paladin', 'Protection', 'OffHand', 'Both', 'Blessed Qiraji Bulwark'), +(2, 1, 17, 0, 88, 22401, 'Phase 5', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Hope'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 92, 21387, 'Phase 6', 'Paladin', 'Protection', 'Head', 'Both', 'Avenger''s Crown'), +(2, 1, 1, 0, 92, 22732, 'Phase 6', 'Paladin', 'Protection', 'Neck', 'Both', 'Mark of C''Thun'), +(2, 1, 2, 0, 92, 21639, 'Phase 6', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Pauldrons of the Unrelenting'), +(2, 1, 4, 0, 92, 21389, 'Phase 6', 'Paladin', 'Protection', 'Chest', 'Both', 'Avenger''s Breastplate'), +(2, 1, 5, 0, 92, 21598, 'Phase 6', 'Paladin', 'Protection', 'Waist', 'Both', 'Royal Qiraji Belt'), +(2, 1, 6, 0, 92, 19855, 'Phase 6', 'Paladin', 'Protection', 'Legs', 'Both', 'Bloodsoaked Legplates'), +(2, 1, 7, 0, 92, 21706, 'Phase 6', 'Paladin', 'Protection', 'Feet', 'Both', 'Boots of the Unwavering Will'), +(2, 1, 8, 0, 92, 18754, 'Phase 6', 'Paladin', 'Protection', 'Wrists', 'Both', 'Fel Hardened Bracers'), +(2, 1, 9, 0, 92, 21674, 'Phase 6', 'Paladin', 'Protection', 'Hands', 'Both', 'Gauntlets of Steadfast Determination'), +(2, 1, 10, 0, 92, 21200, 'Phase 6', 'Paladin', 'Protection', 'Finger1', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(2, 1, 11, 0, 92, 21601, 'Phase 6', 'Paladin', 'Protection', 'Finger2', 'Both', 'Ring of Emperor Vek''lor'), +(2, 1, 12, 0, 92, 19431, 'Phase 6', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Styleen''s Impeding Scarab'), +(2, 1, 13, 0, 92, 18406, 'Phase 6', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Onyxia Blood Talisman'), +(2, 1, 14, 0, 92, 22938, 'Phase 6', 'Paladin', 'Protection', 'Back', 'Both', 'Cryptfiend Silk Cloak'), +(2, 1, 15, 0, 92, 22988, 'Phase 6', 'Paladin', 'Protection', 'MainHand', 'Both', 'The End of Dreams'), +(2, 1, 16, 0, 92, 22818, 'Phase 6', 'Paladin', 'Protection', 'OffHand', 'Both', 'The Plague Bearer'), +(2, 1, 17, 0, 92, 22401, 'Phase 6', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Hope'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 120, 32083, 'Pre-Raid', 'Paladin', 'Protection', 'Head', 'Both', 'Faceguard of Determination'), +(2, 1, 1, 0, 120, 29386, 'Pre-Raid', 'Paladin', 'Protection', 'Neck', 'Both', 'Necklace of the Juggernaut'), +(2, 1, 2, 0, 120, 27739, 'Pre-Raid', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Spaulders of the Righteous'), +(2, 1, 4, 0, 120, 28262, 'Pre-Raid', 'Paladin', 'Protection', 'Chest', 'Both', 'Jade-Skull Breastplate'), +(2, 1, 5, 0, 120, 29253, 'Pre-Raid', 'Paladin', 'Protection', 'Waist', 'Both', 'Girdle of Valorous Deeds'), +(2, 1, 6, 0, 120, 29184, 'Pre-Raid', 'Paladin', 'Protection', 'Legs', 'Both', 'Timewarden''s Leggings'), +(2, 1, 7, 0, 120, 29254, 'Pre-Raid', 'Paladin', 'Protection', 'Feet', 'Both', 'Boots of the Righteous Path'), +(2, 1, 8, 0, 120, 29252, 'Pre-Raid', 'Paladin', 'Protection', 'Wrists', 'Both', 'Bracers of Dignity'), +(2, 1, 9, 0, 120, 27535, 'Pre-Raid', 'Paladin', 'Protection', 'Hands', 'Both', 'Gauntlets of the Righteous'), +(2, 1, 10, 0, 120, 29323, 'Pre-Raid', 'Paladin', 'Protection', 'Finger1', 'Both', 'Andormu''s Tear'), +(2, 1, 11, 0, 120, 28407, 'Pre-Raid', 'Paladin', 'Protection', 'Finger2', 'Both', 'Elementium Band of the Sentry'), +(2, 1, 12, 0, 120, 27891, 'Pre-Raid', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Adamantine Figure'), +(2, 1, 13, 0, 120, 27529, 'Pre-Raid', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Figurine of the Colossus'), +(2, 1, 14, 0, 120, 27804, 'Pre-Raid', 'Paladin', 'Protection', 'Back', 'Both', 'Devilshark Cape'), +(2, 1, 15, 0, 120, 30832, 'Pre-Raid', 'Paladin', 'Protection', 'MainHand', 'Both', 'Gavel of Unearthed Secrets'), +(2, 1, 16, 0, 120, 29266, 'Pre-Raid', 'Paladin', 'Protection', 'OffHand', 'Both', 'Azure-Shield of Coldarra'), +(2, 1, 17, 0, 120, 29388, 'Pre-Raid', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Repentance'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 125, 29068, 'Phase 1', 'Paladin', 'Protection', 'Head', 'Both', 'Justicar Faceguard'), +(2, 1, 1, 0, 125, 28516, 'Phase 1', 'Paladin', 'Protection', 'Neck', 'Both', 'Barbed Choker of Discipline'), +(2, 1, 2, 0, 125, 29070, 'Phase 1', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Justicar Shoulderguards'), +(2, 1, 4, 0, 125, 29066, 'Phase 1', 'Paladin', 'Protection', 'Chest', 'Both', 'Justicar Chestguard'), +(2, 1, 5, 0, 125, 28566, 'Phase 1', 'Paladin', 'Protection', 'Waist', 'Both', 'Crimson Girdle of the Indomitable'), +(2, 1, 6, 0, 125, 29069, 'Phase 1', 'Paladin', 'Protection', 'Legs', 'Both', 'Justicar Legguards'), +(2, 1, 7, 0, 125, 30641, 'Phase 1', 'Paladin', 'Protection', 'Feet', 'Both', 'Boots of Elusion'), +(2, 1, 8, 0, 125, 23538, 'Phase 1', 'Paladin', 'Protection', 'Wrists', 'Both', 'Bracers of the Green Fortress'), +(2, 1, 9, 0, 125, 28518, 'Phase 1', 'Paladin', 'Protection', 'Hands', 'Both', 'Iron Gauntlets of the Maiden'), +(2, 1, 10, 0, 125, 28792, 'Phase 1', 'Paladin', 'Protection', 'Finger1', 'Both', 'A''dal''s Signet of Defense'), +(2, 1, 11, 0, 125, 29279, 'Phase 1', 'Paladin', 'Protection', 'Finger2', 'Both', 'Violet Signet of the Great Protector'), +(2, 1, 12, 0, 125, 28789, 'Phase 1', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Eye of Magtheridon'), +(2, 1, 13, 0, 125, 29370, 'Phase 1', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Icon of the Silver Crescent'), +(2, 1, 14, 0, 125, 28660, 'Phase 1', 'Paladin', 'Protection', 'Back', 'Both', 'Gilded Thorium Cloak'), +(2, 1, 15, 0, 125, 28802, 'Phase 1', 'Paladin', 'Protection', 'MainHand', 'Both', 'Bloodmaw Magus-Blade'), +(2, 1, 16, 0, 125, 28825, 'Phase 1', 'Paladin', 'Protection', 'OffHand', 'Both', 'Aldori Legacy Defender'), +(2, 1, 17, 0, 125, 29388, 'Phase 1', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Repentance'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 141, 30125, 'Phase 2', 'Paladin', 'Protection', 'Head', 'Both', 'Crystalforge Faceguard'), +(2, 1, 1, 0, 141, 30007, 'Phase 2', 'Paladin', 'Protection', 'Neck', 'Both', 'The Darkener''s Grasp'), +(2, 1, 2, 0, 141, 29070, 'Phase 2', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Justicar Shoulderguards'), +(2, 1, 4, 0, 141, 29066, 'Phase 2', 'Paladin', 'Protection', 'Chest', 'Both', 'Justicar Chestguard'), +(2, 1, 5, 0, 141, 30034, 'Phase 2', 'Paladin', 'Protection', 'Waist', 'Both', 'Belt of the Guardian'), +(2, 1, 6, 0, 141, 30126, 'Phase 2', 'Paladin', 'Protection', 'Legs', 'Both', 'Crystalforge Legguards'), +(2, 1, 7, 0, 141, 30033, 'Phase 2', 'Paladin', 'Protection', 'Feet', 'Both', 'Boots of the Protector'), +(2, 1, 8, 0, 141, 32515, 'Phase 2', 'Paladin', 'Protection', 'Wrists', 'Both', 'Wristguards of Determination'), +(2, 1, 9, 0, 141, 30124, 'Phase 2', 'Paladin', 'Protection', 'Hands', 'Both', 'Crystalforge Handguards'), +(2, 1, 10, 0, 141, 30083, 'Phase 2', 'Paladin', 'Protection', 'Finger1', 'Both', 'Ring of Sundered Souls'), +(2, 1, 11, 0, 141, 33054, 'Phase 2', 'Paladin', 'Protection', 'Finger2', 'Both', 'The Seal of Danzalar'), +(2, 1, 12, 0, 141, 28789, 'Phase 2', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Eye of Magtheridon'), +(2, 1, 13, 0, 141, 29370, 'Phase 2', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Icon of the Silver Crescent'), +(2, 1, 14, 0, 141, 29925, 'Phase 2', 'Paladin', 'Protection', 'Back', 'Both', 'Phoenix-Wing Cloak'), +(2, 1, 15, 0, 141, 32963, 'Phase 2', 'Paladin', 'Protection', 'MainHand', 'Both', 'Merciless Gladiator''s Gavel'), +(2, 1, 16, 0, 141, 33313, 'Phase 2', 'Paladin', 'Protection', 'OffHand', 'Both', 'Merciless Gladiator''s Barrier'), +(2, 1, 17, 0, 141, 29388, 'Phase 2', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Repentance'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 156, 32521, 'Phase 3', 'Paladin', 'Protection', 'Head', 'Both', 'Faceplate of the Impenetrable'), +(2, 1, 1, 0, 156, 32362, 'Phase 3', 'Paladin', 'Protection', 'Neck', 'Both', 'Pendant of Titans'), +(2, 1, 2, 0, 156, 30998, 'Phase 3', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Lightbringer Shoulderguards'), +(2, 1, 4, 0, 156, 30991, 'Phase 3', 'Paladin', 'Protection', 'Chest', 'Both', 'Lightbringer Chestguard'), +(2, 1, 5, 0, 156, 32342, 'Phase 3', 'Paladin', 'Protection', 'Waist', 'Both', 'Girdle of Mighty Resolve'), +(2, 1, 6, 0, 156, 30995, 'Phase 3', 'Paladin', 'Protection', 'Legs', 'Both', 'Lightbringer Legguards'), +(2, 1, 7, 0, 156, 32245, 'Phase 3', 'Paladin', 'Protection', 'Feet', 'Both', 'Tide-Stomper''s Greaves'), +(2, 1, 8, 0, 156, 32279, 'Phase 3', 'Paladin', 'Protection', 'Wrists', 'Both', 'The Seeker''s Wristguards'), +(2, 1, 9, 0, 156, 30985, 'Phase 3', 'Paladin', 'Protection', 'Hands', 'Both', 'Lightbringer Handguards'), +(2, 1, 10, 0, 156, 32261, 'Phase 3', 'Paladin', 'Protection', 'Finger1', 'Both', 'Band of the Abyssal Lord'), +(2, 1, 11, 0, 156, 33054, 'Phase 3', 'Paladin', 'Protection', 'Finger2', 'Both', 'The Seal of Danzalar'), +(2, 1, 12, 0, 156, 31858, 'Phase 3', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Darkmoon Card: Vengeance'), +(2, 1, 13, 0, 156, 32501, 'Phase 3', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Shadowmoon Insignia'), +(2, 1, 14, 0, 156, 34010, 'Phase 3', 'Paladin', 'Protection', 'Back', 'Both', 'Pepe''s Shroud of Pacification'), +(2, 1, 15, 0, 156, 30910, 'Phase 3', 'Paladin', 'Protection', 'MainHand', 'Both', 'Tempest of Chaos'), +(2, 1, 16, 0, 156, 32375, 'Phase 3', 'Paladin', 'Protection', 'OffHand', 'Both', 'Bulwark of Azzinoth'), +(2, 1, 17, 0, 156, 29388, 'Phase 3', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Repentance'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 164, 32521, 'Phase 4', 'Paladin', 'Protection', 'Head', 'Both', 'Faceplate of the Impenetrable'), +(2, 1, 1, 0, 164, 32362, 'Phase 4', 'Paladin', 'Protection', 'Neck', 'Both', 'Pendant of Titans'), +(2, 1, 2, 0, 164, 30998, 'Phase 4', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Lightbringer Shoulderguards'), +(2, 1, 4, 0, 164, 30991, 'Phase 4', 'Paladin', 'Protection', 'Chest', 'Both', 'Lightbringer Chestguard'), +(2, 1, 5, 0, 164, 32342, 'Phase 4', 'Paladin', 'Protection', 'Waist', 'Both', 'Girdle of Mighty Resolve'), +(2, 1, 6, 0, 164, 30995, 'Phase 4', 'Paladin', 'Protection', 'Legs', 'Both', 'Lightbringer Legguards'), +(2, 1, 7, 0, 164, 32245, 'Phase 4', 'Paladin', 'Protection', 'Feet', 'Both', 'Tide-Stomper''s Greaves'), +(2, 1, 8, 0, 164, 32279, 'Phase 4', 'Paladin', 'Protection', 'Wrists', 'Both', 'The Seeker''s Wristguards'), +(2, 1, 9, 0, 164, 30985, 'Phase 4', 'Paladin', 'Protection', 'Hands', 'Both', 'Lightbringer Handguards'), +(2, 1, 10, 0, 164, 32261, 'Phase 4', 'Paladin', 'Protection', 'Finger1', 'Both', 'Band of the Abyssal Lord'), +(2, 1, 11, 0, 164, 33054, 'Phase 4', 'Paladin', 'Protection', 'Finger2', 'Both', 'The Seal of Danzalar'), +(2, 1, 12, 0, 164, 31858, 'Phase 4', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Darkmoon Card: Vengeance'), +(2, 1, 13, 0, 164, 32501, 'Phase 4', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Shadowmoon Insignia'), +(2, 1, 14, 0, 164, 33593, 'Phase 4', 'Paladin', 'Protection', 'Back', 'Both', 'Slikk''s Cloak of Placation'), +(2, 1, 15, 0, 164, 30910, 'Phase 4', 'Paladin', 'Protection', 'MainHand', 'Both', 'Tempest of Chaos'), +(2, 1, 16, 0, 164, 32375, 'Phase 4', 'Paladin', 'Protection', 'OffHand', 'Both', 'Bulwark of Azzinoth'), +(2, 1, 17, 0, 164, 29388, 'Phase 4', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Repentance'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 200, 41387, 'Pre-Raid', 'Paladin', 'Protection', 'Head', 'Both', 'Tempered Titansteel Helm'), +(2, 1, 1, 0, 200, 40679, 'Pre-Raid', 'Paladin', 'Protection', 'Neck', 'Both', 'Chained Military Gorget'), +(2, 1, 2, 0, 200, 37814, 'Pre-Raid', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Iron Dwarf Smith Pauldrons'), +(2, 1, 4, 0, 200, 44198, 'Pre-Raid', 'Paladin', 'Protection', 'Chest', 'Both', 'Breastplate of the Solemn Council'), +(2, 1, 5, 0, 200, 37241, 'Pre-Raid', 'Paladin', 'Protection', 'Waist', 'Both', 'Ancient Aligned Girdle'), +(2, 1, 6, 0, 200, 37193, 'Pre-Raid', 'Paladin', 'Protection', 'Legs', 'Both', 'Staggering Legplates'), +(2, 1, 7, 0, 200, 44201, 'Pre-Raid', 'Paladin', 'Protection', 'Feet', 'Both', 'Sabatons of Draconic Vigor'), +(2, 1, 8, 0, 200, 37620, 'Pre-Raid', 'Paladin', 'Protection', 'Wrists', 'Both', 'Bracers of the Herald'), +(2, 1, 9, 0, 200, 37645, 'Pre-Raid', 'Paladin', 'Protection', 'Hands', 'Both', 'Horn-Tipped Gauntlets'), +(2, 1, 10, 0, 200, 36961, 'Pre-Raid', 'Paladin', 'Protection', 'Finger1', 'Both', 'Dragonflight Great-Ring'), +(2, 1, 12, 0, 200, 37220, 'Pre-Raid', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Essence of Gossamer'), +(2, 1, 14, 0, 200, 43565, 'Pre-Raid', 'Paladin', 'Protection', 'Back', 'Both', 'Durable Nerubhide Cape'), +(2, 1, 15, 0, 200, 37401, 'Pre-Raid', 'Paladin', 'Protection', 'MainHand', 'Both', 'Red Sword of Courage'), +(2, 1, 17, 0, 200, 40707, 'Pre-Raid', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Obstruction'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 224, 40581, 'Phase 1', 'Paladin', 'Protection', 'Head', 'Both', 'Valorous Redemption Faceguard'), +(2, 1, 1, 0, 224, 44665, 'Phase 1', 'Paladin', 'Protection', 'Neck', 'Both', 'Nexus War Champion Beads'), +(2, 1, 2, 0, 224, 40584, 'Phase 1', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Valorous Redemption Shoulderguards'), +(2, 1, 4, 0, 224, 40579, 'Phase 1', 'Paladin', 'Protection', 'Chest', 'Both', 'Valorous Redemption Breastplate'), +(2, 1, 5, 0, 224, 39759, 'Phase 1', 'Paladin', 'Protection', 'Waist', 'Both', 'Ablative Chitin Girdle'), +(2, 1, 6, 0, 224, 40589, 'Phase 1', 'Paladin', 'Protection', 'Legs', 'Both', 'Legplates of Sovereignty'), +(2, 1, 7, 0, 224, 40297, 'Phase 1', 'Paladin', 'Protection', 'Feet', 'Both', 'Sabatons of Endurance'), +(2, 1, 8, 0, 224, 39764, 'Phase 1', 'Paladin', 'Protection', 'Wrists', 'Both', 'Bindings of the Hapless Prey'), +(2, 1, 9, 0, 224, 40580, 'Phase 1', 'Paladin', 'Protection', 'Hands', 'Both', 'Valorous Redemption Handguards'), +(2, 1, 10, 0, 224, 40107, 'Phase 1', 'Paladin', 'Protection', 'Finger1', 'Both', 'Sand-Worn Band'), +(2, 1, 12, 0, 224, 40372, 'Phase 1', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Rune of Repulsion'), +(2, 1, 14, 0, 224, 40410, 'Phase 1', 'Paladin', 'Protection', 'Back', 'Both', 'Shadow of the Ghoul'), +(2, 1, 15, 0, 224, 40402, 'Phase 1', 'Paladin', 'Protection', 'MainHand', 'Both', 'Last Laugh'), +(2, 1, 17, 0, 224, 40337, 'Phase 1', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Resurgence'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 245, 46175, 'Phase 2', 'Paladin', 'Protection', 'Head', 'Both', 'Conqueror''s Aegis Faceguard'), +(2, 1, 1, 0, 245, 45485, 'Phase 2', 'Paladin', 'Protection', 'Neck', 'Both', 'Bronze Pendant of the Vanir'), +(2, 1, 2, 0, 245, 46177, 'Phase 2', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Conqueror''s Aegis Shoulderguards'), +(2, 1, 4, 0, 245, 46039, 'Phase 2', 'Paladin', 'Protection', 'Chest', 'Both', 'Breastplate of the Timeless'), +(2, 1, 5, 0, 245, 45825, 'Phase 2', 'Paladin', 'Protection', 'Waist', 'Both', 'Shieldwarder Girdle'), +(2, 1, 6, 0, 245, 45594, 'Phase 2', 'Paladin', 'Protection', 'Legs', 'Both', 'Legplates of the Endless Void'), +(2, 1, 7, 0, 245, 45988, 'Phase 2', 'Paladin', 'Protection', 'Feet', 'Both', 'Greaves of the Iron Army'), +(2, 1, 8, 0, 245, 45111, 'Phase 2', 'Paladin', 'Protection', 'Wrists', 'Both', 'Mimiron''s Inferno Couplings'), +(2, 1, 9, 0, 245, 45487, 'Phase 2', 'Paladin', 'Protection', 'Hands', 'Both', 'Handguards of Revitalization'), +(2, 1, 10, 0, 245, 45471, 'Phase 2', 'Paladin', 'Protection', 'Finger1', 'Both', 'Fate''s Clutch'), +(2, 1, 12, 0, 245, 45158, 'Phase 2', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Heart of Iron'), +(2, 1, 14, 0, 245, 45496, 'Phase 2', 'Paladin', 'Protection', 'Back', 'Both', 'Titanskin Cloak'), +(2, 1, 17, 0, 245, 45145, 'Phase 2', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of the Sacred Shield'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 258, 48644, 'Phase 3', 'Paladin', 'Protection', 'Head', 'Both', 'Faceguard of Triumph'), +(2, 1, 2, 0, 258, 48646, 'Phase 3', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Shoulderguards of Triumph'), +(2, 1, 4, 1, 258, 46968, 'Phase 3', 'Paladin', 'Protection', 'Chest', 'Alliance', 'Chestplate of the Towering Monstrosity'), +(2, 1, 4, 2, 258, 47415, 'Phase 3', 'Paladin', 'Protection', 'Chest', 'Horde', 'Hauberk of the Towering Monstrosity'), +(2, 1, 5, 1, 258, 47076, 'Phase 3', 'Paladin', 'Protection', 'Waist', 'Alliance', 'Girdle of Bloodied Scars'), +(2, 1, 5, 2, 258, 47444, 'Phase 3', 'Paladin', 'Protection', 'Waist', 'Horde', 'Belt of Bloodied Scars'), +(2, 1, 6, 0, 258, 48645, 'Phase 3', 'Paladin', 'Protection', 'Legs', 'Both', 'Legguards of Triumph'), +(2, 1, 7, 1, 258, 47003, 'Phase 3', 'Paladin', 'Protection', 'Feet', 'Alliance', 'Dawnbreaker Greaves'), +(2, 1, 7, 2, 258, 47430, 'Phase 3', 'Paladin', 'Protection', 'Feet', 'Horde', 'Dawnbreaker Sabatons'), +(2, 1, 8, 1, 258, 47918, 'Phase 3', 'Paladin', 'Protection', 'Wrists', 'Alliance', 'Dreadscale Armguards'), +(2, 1, 8, 2, 258, 47991, 'Phase 3', 'Paladin', 'Protection', 'Wrists', 'Horde', 'Dreadscale Bracers'), +(2, 1, 9, 0, 258, 48643, 'Phase 3', 'Paladin', 'Protection', 'Hands', 'Both', 'Handguards of Triumph'), +(2, 1, 10, 1, 258, 45471, 'Phase 3', 'Paladin', 'Protection', 'Finger1', 'Alliance', 'Fate''s Clutch'), +(2, 1, 12, 1, 258, 47216, 'Phase 3', 'Paladin', 'Protection', 'Trinket1', 'Alliance', 'The Black Heart'), +(2, 1, 14, 1, 258, 47549, 'Phase 3', 'Paladin', 'Protection', 'Back', 'Alliance', 'Magni''s Resolution'), +(2, 1, 14, 2, 258, 47550, 'Phase 3', 'Paladin', 'Protection', 'Back', 'Horde', 'Cairne''s Endurance'), +(2, 1, 17, 0, 258, 47664, 'Phase 3', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of Defiance'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 264, 50640, 'Phase 4', 'Paladin', 'Protection', 'Head', 'Both', 'Broken Ram Skull Helm'), +(2, 1, 1, 0, 264, 50627, 'Phase 4', 'Paladin', 'Protection', 'Neck', 'Both', 'Noose of Malachite'), +(2, 1, 2, 0, 264, 51269, 'Phase 4', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Sanctified Lightsworn Shoulderguards'), +(2, 1, 4, 0, 264, 51265, 'Phase 4', 'Paladin', 'Protection', 'Chest', 'Both', 'Sanctified Lightsworn Chestguard'), +(2, 1, 5, 0, 264, 50691, 'Phase 4', 'Paladin', 'Protection', 'Waist', 'Both', 'Belt of Broken Bones'), +(2, 1, 6, 0, 264, 51268, 'Phase 4', 'Paladin', 'Protection', 'Legs', 'Both', 'Sanctified Lightsworn Legguards'), +(2, 1, 7, 0, 264, 50625, 'Phase 4', 'Paladin', 'Protection', 'Feet', 'Both', 'Grinning Skull Greatboots'), +(2, 1, 8, 0, 264, 50611, 'Phase 4', 'Paladin', 'Protection', 'Wrists', 'Both', 'Bracers of Dark Reckoning'), +(2, 1, 9, 0, 264, 51267, 'Phase 4', 'Paladin', 'Protection', 'Hands', 'Both', 'Sanctified Lightsworn Handguards'), +(2, 1, 10, 0, 264, 50404, 'Phase 4', 'Paladin', 'Protection', 'Finger1', 'Both', 'Ashen Band of Endless Courage'), +(2, 1, 12, 0, 264, 50349, 'Phase 4', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Corpse Tongue Coin'), +(2, 1, 14, 0, 264, 50718, 'Phase 4', 'Paladin', 'Protection', 'Back', 'Both', 'Royal Crimson Cloak'), +(2, 1, 15, 0, 264, 50738, 'Phase 4', 'Paladin', 'Protection', 'MainHand', 'Both', 'Mithrios, Bronzebeard''s Legacy'), +(2, 1, 17, 0, 264, 50461, 'Phase 4', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of the Eternal Tower'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 1, 0, 0, 290, 50640, 'Phase 5', 'Paladin', 'Protection', 'Head', 'Both', 'Broken Ram Skull Helm'), +(2, 1, 1, 0, 290, 50682, 'Phase 5', 'Paladin', 'Protection', 'Neck', 'Both', 'Bile-Encrusted Medallion'), +(2, 1, 2, 0, 290, 50660, 'Phase 5', 'Paladin', 'Protection', 'Shoulders', 'Both', 'Boneguard Commander''s Pauldrons'), +(2, 1, 4, 0, 290, 51265, 'Phase 5', 'Paladin', 'Protection', 'Chest', 'Both', 'Sanctified Lightsworn Chestguard'), +(2, 1, 5, 0, 290, 50991, 'Phase 5', 'Paladin', 'Protection', 'Waist', 'Both', 'Verdigris Chain Belt'), +(2, 1, 6, 0, 290, 49904, 'Phase 5', 'Paladin', 'Protection', 'Legs', 'Both', 'Pillars of Might'), +(2, 1, 7, 0, 290, 54579, 'Phase 5', 'Paladin', 'Protection', 'Feet', 'Both', 'Treads of Impending Resurrection'), +(2, 1, 8, 0, 290, 51901, 'Phase 5', 'Paladin', 'Protection', 'Wrists', 'Both', 'Gargoyle Spit Bracers'), +(2, 1, 9, 0, 290, 51267, 'Phase 5', 'Paladin', 'Protection', 'Hands', 'Both', 'Sanctified Lightsworn Handguards'), +(2, 1, 10, 0, 290, 50622, 'Phase 5', 'Paladin', 'Protection', 'Finger1', 'Both', 'Devium''s Eternally Cold Ring'), +(2, 1, 11, 0, 290, 50404, 'Phase 5', 'Paladin', 'Protection', 'Finger2', 'Both', 'Ashen Band of Endless Courage'), +(2, 1, 12, 0, 290, 54591, 'Phase 5', 'Paladin', 'Protection', 'Trinket1', 'Both', 'Petrified Twilight Scale'), +(2, 1, 13, 0, 290, 50364, 'Phase 5', 'Paladin', 'Protection', 'Trinket2', 'Both', 'Sindragosa''s Flawless Fang'), +(2, 1, 14, 0, 290, 50466, 'Phase 5', 'Paladin', 'Protection', 'Back', 'Both', 'Sentinel''s Winter Cloak'), +(2, 1, 15, 0, 290, 50738, 'Phase 5', 'Paladin', 'Protection', 'MainHand', 'Both', 'Mithrios, Bronzebeard''s Legacy'), +(2, 1, 16, 0, 290, 50729, 'Phase 5', 'Paladin', 'Protection', 'OffHand', 'Both', 'Icecrown Glacial Wall'), +(2, 1, 17, 0, 290, 50461, 'Phase 5', 'Paladin', 'Protection', 'Ranged', 'Both', 'Libram of the Eternal Tower'); + +-- Retribution (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 66, 13404, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Head', 'Both', 'Mask of the Unforgiven'), +(2, 2, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Neck', 'Both', 'Mark of Fordring'), +(2, 2, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(2, 2, 4, 0, 66, 11726, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Chest', 'Both', 'Savage Gladiator Chain'), +(2, 2, 5, 0, 66, 13959, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Waist', 'Both', 'Omokk''s Girth Restrainer'), +(2, 2, 6, 0, 66, 14554, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Legs', 'Both', 'Cloudkeeper Legplates'), +(2, 2, 7, 0, 66, 14616, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Feet', 'Both', 'Bloodmail Boots'), +(2, 2, 8, 0, 66, 12936, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Battleborn Armbraces'), +(2, 2, 9, 0, 66, 13957, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gargoyle Slashers'), +(2, 2, 10, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Painweaver Band'), +(2, 2, 11, 0, 66, 12548, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Magni''s Will'), +(2, 2, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(2, 2, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Hand of Justice'), +(2, 2, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'Back', 'Both', 'Cape of the Black Baron'), +(2, 2, 15, 0, 66, 12784, 'Phase 1 (Pre-Raid)', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Arcanite Reaper'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 76, 13404, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Head', 'Both', 'Mask of the Unforgiven'), +(2, 2, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Neck', 'Both', 'Mark of Fordring'), +(2, 2, 2, 0, 76, 12927, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(2, 2, 4, 0, 76, 11726, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Chest', 'Both', 'Savage Gladiator Chain'), +(2, 2, 5, 0, 76, 13959, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Waist', 'Both', 'Omokk''s Girth Restrainer'), +(2, 2, 6, 0, 76, 14554, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Legs', 'Both', 'Cloudkeeper Legplates'), +(2, 2, 7, 0, 76, 14616, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Feet', 'Both', 'Bloodmail Boots'), +(2, 2, 8, 0, 76, 12936, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Battleborn Armbraces'), +(2, 2, 9, 0, 76, 13957, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gargoyle Slashers'), +(2, 2, 10, 0, 76, 13098, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Painweaver Band'), +(2, 2, 11, 0, 76, 12548, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Magni''s Will'), +(2, 2, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(2, 2, 13, 0, 76, 11815, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Hand of Justice'), +(2, 2, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'Back', 'Both', 'Cape of the Black Baron'), +(2, 2, 15, 0, 76, 12784, 'Phase 2 (Pre-Raid)', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Arcanite Reaper'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 78, 12640, 'Phase 2', 'Paladin', 'Retribution', 'Head', 'Both', 'Lionheart Helm'), +(2, 2, 1, 0, 78, 18404, 'Phase 2', 'Paladin', 'Retribution', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(2, 2, 2, 0, 78, 12927, 'Phase 2', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(2, 2, 4, 0, 78, 11726, 'Phase 2', 'Paladin', 'Retribution', 'Chest', 'Both', 'Savage Gladiator Chain'), +(2, 2, 5, 0, 78, 19137, 'Phase 2', 'Paladin', 'Retribution', 'Waist', 'Both', 'Onslaught Girdle'), +(2, 2, 6, 0, 78, 14554, 'Phase 2', 'Paladin', 'Retribution', 'Legs', 'Both', 'Cloudkeeper Legplates'), +(2, 2, 7, 0, 78, 14616, 'Phase 2', 'Paladin', 'Retribution', 'Feet', 'Both', 'Bloodmail Boots'), +(2, 2, 8, 0, 78, 19146, 'Phase 2', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Wristguards of Stability'), +(2, 2, 9, 0, 78, 19143, 'Phase 2', 'Paladin', 'Retribution', 'Hands', 'Both', 'Flameguard Gauntlets'), +(2, 2, 10, 0, 78, 13098, 'Phase 2', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Painweaver Band'), +(2, 2, 11, 0, 78, 18821, 'Phase 2', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Quick Strike Ring'), +(2, 2, 12, 0, 78, 13965, 'Phase 2', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(2, 2, 13, 0, 78, 11815, 'Phase 2', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Hand of Justice'), +(2, 2, 14, 0, 78, 13340, 'Phase 2', 'Paladin', 'Retribution', 'Back', 'Both', 'Cape of the Black Baron'), +(2, 2, 15, 0, 78, 17076, 'Phase 2', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Bonereaver''s Edge'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 83, 12640, 'Phase 3', 'Paladin', 'Retribution', 'Head', 'Both', 'Lionheart Helm'), +(2, 2, 1, 0, 83, 18404, 'Phase 3', 'Paladin', 'Retribution', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(2, 2, 2, 0, 83, 19394, 'Phase 3', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Drake Talon Pauldrons'), +(2, 2, 4, 0, 83, 11726, 'Phase 3', 'Paladin', 'Retribution', 'Chest', 'Both', 'Savage Gladiator Chain'), +(2, 2, 5, 0, 83, 19137, 'Phase 3', 'Paladin', 'Retribution', 'Waist', 'Both', 'Onslaught Girdle'), +(2, 2, 6, 0, 83, 19402, 'Phase 3', 'Paladin', 'Retribution', 'Legs', 'Both', 'Legguards of the Fallen Crusader'), +(2, 2, 7, 0, 83, 19387, 'Phase 3', 'Paladin', 'Retribution', 'Feet', 'Both', 'Chromatic Boots'), +(2, 2, 8, 0, 83, 19146, 'Phase 3', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Wristguards of Stability'), +(2, 2, 9, 0, 83, 19143, 'Phase 3', 'Paladin', 'Retribution', 'Hands', 'Both', 'Flameguard Gauntlets'), +(2, 2, 11, 0, 83, 19384, 'Phase 3', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Master Dragonslayer''s Ring'), +(2, 2, 12, 0, 83, 13965, 'Phase 3', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(2, 2, 13, 0, 83, 11815, 'Phase 3', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Hand of Justice'), +(2, 2, 14, 0, 83, 19436, 'Phase 3', 'Paladin', 'Retribution', 'Back', 'Both', 'Cloak of Draconic Might'), +(2, 2, 15, 0, 83, 19334, 'Phase 3', 'Paladin', 'Retribution', 'MainHand', 'Both', 'The Untamed Blade'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 88, 21387, 'Phase 5', 'Paladin', 'Retribution', 'Head', 'Both', 'Avenger''s Crown'), +(2, 2, 1, 0, 88, 18404, 'Phase 5', 'Paladin', 'Retribution', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(2, 2, 2, 0, 88, 21391, 'Phase 5', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Avenger''s Pauldrons'), +(2, 2, 4, 0, 88, 21389, 'Phase 5', 'Paladin', 'Retribution', 'Chest', 'Both', 'Avenger''s Breastplate'), +(2, 2, 5, 0, 88, 21463, 'Phase 5', 'Paladin', 'Retribution', 'Waist', 'Both', 'Ossirian''s Binding'), +(2, 2, 6, 0, 88, 21390, 'Phase 5', 'Paladin', 'Retribution', 'Legs', 'Both', 'Avenger''s Legguards'), +(2, 2, 7, 0, 88, 21388, 'Phase 5', 'Paladin', 'Retribution', 'Feet', 'Both', 'Avenger''s Greaves'), +(2, 2, 8, 0, 88, 21618, 'Phase 5', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Hive Defiler Wristguards'), +(2, 2, 9, 0, 88, 21623, 'Phase 5', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gauntlets of the Righteous Champion'), +(2, 2, 10, 0, 88, 17063, 'Phase 5', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Band of Accuria'), +(2, 2, 11, 0, 88, 21205, 'Phase 5', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(2, 2, 12, 0, 88, 22321, 'Phase 5', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Heart of Wyrmthalak'), +(2, 2, 13, 0, 88, 19289, 'Phase 5', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Darkmoon Card: Maelstrom'), +(2, 2, 14, 0, 88, 21701, 'Phase 5', 'Paladin', 'Retribution', 'Back', 'Both', 'Cloak of Concentrated Hatred'), +(2, 2, 15, 0, 88, 21134, 'Phase 5', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Dark Edge of Insanity'), +(2, 2, 17, 0, 88, 23203, 'Phase 5', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Fervor'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 92, 21387, 'Phase 6', 'Paladin', 'Retribution', 'Head', 'Both', 'Avenger''s Crown'), +(2, 2, 1, 0, 92, 18404, 'Phase 6', 'Paladin', 'Retribution', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(2, 2, 2, 0, 92, 21391, 'Phase 6', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Avenger''s Pauldrons'), +(2, 2, 4, 0, 92, 21389, 'Phase 6', 'Paladin', 'Retribution', 'Chest', 'Both', 'Avenger''s Breastplate'), +(2, 2, 5, 0, 92, 23219, 'Phase 6', 'Paladin', 'Retribution', 'Waist', 'Both', 'Girdle of the Mentor'), +(2, 2, 6, 0, 92, 21390, 'Phase 6', 'Paladin', 'Retribution', 'Legs', 'Both', 'Avenger''s Legguards'), +(2, 2, 7, 0, 92, 21388, 'Phase 6', 'Paladin', 'Retribution', 'Feet', 'Both', 'Avenger''s Greaves'), +(2, 2, 8, 0, 92, 22936, 'Phase 6', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Wristguards of Vengeance'), +(2, 2, 9, 0, 92, 21623, 'Phase 6', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gauntlets of the Righteous Champion'), +(2, 2, 11, 0, 92, 21205, 'Phase 6', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(2, 2, 12, 0, 92, 22321, 'Phase 6', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Heart of Wyrmthalak'), +(2, 2, 13, 0, 92, 19289, 'Phase 6', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Darkmoon Card: Maelstrom'), +(2, 2, 14, 0, 92, 23045, 'Phase 6', 'Paladin', 'Retribution', 'Back', 'Both', 'Shroud of Dominion'), +(2, 2, 15, 0, 92, 22691, 'Phase 6', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Corrupted Ashbringer'), +(2, 2, 17, 0, 92, 23203, 'Phase 6', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Fervor'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 200, 41386, 'Pre-Raid', 'Paladin', 'Retribution', 'Head', 'Both', 'Spiked Titansteel Helm'), +(2, 2, 1, 0, 200, 42645, 'Pre-Raid', 'Paladin', 'Retribution', 'Neck', 'Both', 'Titanium Impact Choker'), +(2, 2, 2, 0, 200, 37627, 'Pre-Raid', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Snake Den Spaulders'), +(2, 2, 4, 0, 200, 37612, 'Pre-Raid', 'Paladin', 'Retribution', 'Chest', 'Both', 'Bonegrinder Breastplate'), +(2, 2, 5, 0, 200, 37178, 'Pre-Raid', 'Paladin', 'Retribution', 'Waist', 'Both', 'Strategist''s Belt'), +(2, 2, 6, 0, 200, 37193, 'Pre-Raid', 'Paladin', 'Retribution', 'Legs', 'Both', 'Staggering Legplates'), +(2, 2, 7, 0, 200, 43402, 'Pre-Raid', 'Paladin', 'Retribution', 'Feet', 'Both', 'The Obliterator Greaves'), +(2, 2, 8, 0, 200, 37668, 'Pre-Raid', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Bands of the Stoneforge'), +(2, 2, 9, 0, 200, 37363, 'Pre-Raid', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gauntlets of Dragon Wrath'), +(2, 2, 10, 0, 200, 37151, 'Pre-Raid', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Band of Frosted Thorns'), +(2, 2, 12, 0, 200, 40684, 'Pre-Raid', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Mirror of Truth'), +(2, 2, 14, 0, 200, 43566, 'Pre-Raid', 'Paladin', 'Retribution', 'Back', 'Both', 'Ice Striker''s Cloak'), +(2, 2, 15, 0, 200, 41257, 'Pre-Raid', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Titansteel Destroyer'), +(2, 2, 17, 0, 200, 40706, 'Pre-Raid', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Reciprocation'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 224, 44006, 'Phase 1', 'Paladin', 'Retribution', 'Head', 'Both', 'Obsidian Greathelm'), +(2, 2, 1, 0, 224, 44664, 'Phase 1', 'Paladin', 'Retribution', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(2, 2, 2, 0, 224, 40578, 'Phase 1', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Valorous Redemption Shoulderplates'), +(2, 2, 4, 0, 224, 40574, 'Phase 1', 'Paladin', 'Retribution', 'Chest', 'Both', 'Valorous Redemption Chestpiece'), +(2, 2, 5, 0, 224, 40278, 'Phase 1', 'Paladin', 'Retribution', 'Waist', 'Both', 'Girdle of Chivalry'), +(2, 2, 6, 0, 224, 44011, 'Phase 1', 'Paladin', 'Retribution', 'Legs', 'Both', 'Leggings of the Honored'), +(2, 2, 7, 0, 224, 40591, 'Phase 1', 'Paladin', 'Retribution', 'Feet', 'Both', 'Melancholy Sabatons'), +(2, 2, 8, 0, 224, 40186, 'Phase 1', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Thrusting Bands'), +(2, 2, 9, 0, 224, 40541, 'Phase 1', 'Paladin', 'Retribution', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(2, 2, 10, 0, 224, 40075, 'Phase 1', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Ruthlessness'), +(2, 2, 12, 0, 224, 42987, 'Phase 1', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(2, 2, 14, 0, 224, 40403, 'Phase 1', 'Paladin', 'Retribution', 'Back', 'Both', 'Drape of the Deadly Foe'), +(2, 2, 17, 0, 224, 40191, 'Phase 1', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Radiance'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 245, 45472, 'Phase 2', 'Paladin', 'Retribution', 'Head', 'Both', 'Warhelm of the Champion'), +(2, 2, 2, 0, 245, 45245, 'Phase 2', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Shoulderpads of the Intruder'), +(2, 2, 4, 0, 245, 45473, 'Phase 2', 'Paladin', 'Retribution', 'Chest', 'Both', 'Embrace of the Gladiator'), +(2, 2, 5, 0, 245, 46095, 'Phase 2', 'Paladin', 'Retribution', 'Waist', 'Both', 'Soul-Devouring Cinch'), +(2, 2, 6, 0, 245, 45134, 'Phase 2', 'Paladin', 'Retribution', 'Legs', 'Both', 'Plated Leggings of Ruination'), +(2, 2, 7, 0, 245, 45599, 'Phase 2', 'Paladin', 'Retribution', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(2, 2, 8, 0, 245, 45663, 'Phase 2', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Armbands of Bedlam'), +(2, 2, 9, 0, 245, 45444, 'Phase 2', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gloves of the Steady Hand'), +(2, 2, 10, 0, 245, 45608, 'Phase 2', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Brann''s Signet Ring'), +(2, 2, 12, 0, 245, 45609, 'Phase 2', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Comet''s Trail'), +(2, 2, 14, 0, 245, 46032, 'Phase 2', 'Paladin', 'Retribution', 'Back', 'Both', 'Drape of the Faceless General'), +(2, 2, 17, 0, 245, 42853, 'Phase 2', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Furious Gladiator''s Libram of Fortitude'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 258, 48614, 'Phase 3', 'Paladin', 'Retribution', 'Head', 'Both', 'Helm of Triumph'), +(2, 2, 2, 0, 258, 48612, 'Phase 3', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Shoulderplates of Triumph'), +(2, 2, 4, 1, 258, 46965, 'Phase 3', 'Paladin', 'Retribution', 'Chest', 'Alliance', 'Breastplate of Cruel Intent'), +(2, 2, 4, 2, 258, 47412, 'Phase 3', 'Paladin', 'Retribution', 'Chest', 'Horde', 'Cuirass of Cruel Intent'), +(2, 2, 5, 1, 258, 47002, 'Phase 3', 'Paladin', 'Retribution', 'Waist', 'Alliance', 'Bloodbath Belt'), +(2, 2, 5, 2, 258, 47429, 'Phase 3', 'Paladin', 'Retribution', 'Waist', 'Horde', 'Bloodbath Girdle'), +(2, 2, 6, 1, 258, 47132, 'Phase 3', 'Paladin', 'Retribution', 'Legs', 'Alliance', 'Legguards of Ascension'), +(2, 2, 6, 2, 258, 47465, 'Phase 3', 'Paladin', 'Retribution', 'Legs', 'Horde', 'Legplates of Ascension'), +(2, 2, 7, 1, 258, 47154, 'Phase 3', 'Paladin', 'Retribution', 'Feet', 'Alliance', 'Greaves of the 7th Legion'), +(2, 2, 7, 2, 258, 47473, 'Phase 3', 'Paladin', 'Retribution', 'Feet', 'Horde', 'Greaves of the Saronite Citadel'), +(2, 2, 8, 1, 258, 47155, 'Phase 3', 'Paladin', 'Retribution', 'Wrists', 'Alliance', 'Bracers of Dark Determination'), +(2, 2, 8, 2, 258, 47474, 'Phase 3', 'Paladin', 'Retribution', 'Wrists', 'Horde', 'Armbands of Dark Determination'), +(2, 2, 9, 0, 258, 48615, 'Phase 3', 'Paladin', 'Retribution', 'Hands', 'Both', 'Gauntlets of Triumph'), +(2, 2, 10, 1, 258, 46966, 'Phase 3', 'Paladin', 'Retribution', 'Finger1', 'Alliance', 'Band of the Violent Temperment'), +(2, 2, 10, 2, 258, 47413, 'Phase 3', 'Paladin', 'Retribution', 'Finger1', 'Horde', 'Ring of the Violent Temperament'), +(2, 2, 12, 1, 258, 47131, 'Phase 3', 'Paladin', 'Retribution', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(2, 2, 12, 2, 258, 47464, 'Phase 3', 'Paladin', 'Retribution', 'Trinket1', 'Horde', 'Death''s Choice'), +(2, 2, 14, 1, 258, 47547, 'Phase 3', 'Paladin', 'Retribution', 'Back', 'Alliance', 'Varian''s Furor'), +(2, 2, 14, 2, 258, 47548, 'Phase 3', 'Paladin', 'Retribution', 'Back', 'Horde', 'Garrosh''s Rage'), +(2, 2, 17, 0, 258, 47661, 'Phase 3', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Valiance'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 264, 51277, 'Phase 4', 'Paladin', 'Retribution', 'Head', 'Both', 'Sanctified Lightsworn Helmet'), +(2, 2, 1, 0, 264, 50633, 'Phase 4', 'Paladin', 'Retribution', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(2, 2, 2, 0, 264, 51279, 'Phase 4', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Sanctified Lightsworn Shoulderplates'), +(2, 2, 4, 0, 264, 51275, 'Phase 4', 'Paladin', 'Retribution', 'Chest', 'Both', 'Sanctified Lightsworn Battleplate'), +(2, 2, 5, 0, 264, 50707, 'Phase 4', 'Paladin', 'Retribution', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(2, 2, 6, 0, 264, 51278, 'Phase 4', 'Paladin', 'Retribution', 'Legs', 'Both', 'Sanctified Lightsworn Legplates'), +(2, 2, 7, 0, 264, 50607, 'Phase 4', 'Paladin', 'Retribution', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(2, 2, 8, 0, 264, 50659, 'Phase 4', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Polar Bear Claw Bracers'), +(2, 2, 9, 0, 264, 50690, 'Phase 4', 'Paladin', 'Retribution', 'Hands', 'Both', 'Fleshrending Gauntlets'), +(2, 2, 10, 0, 264, 50657, 'Phase 4', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Skeleton Lord''s Circle'), +(2, 2, 12, 0, 264, 50706, 'Phase 4', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Tiny Abomination in a Jar'), +(2, 2, 14, 0, 264, 50653, 'Phase 4', 'Paladin', 'Retribution', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(2, 2, 15, 0, 264, 49623, 'Phase 4', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Shadowmourne'), +(2, 2, 17, 0, 264, 50455, 'Phase 4', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Three Truths'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(2, 2, 0, 0, 290, 51277, 'Phase 5', 'Paladin', 'Retribution', 'Head', 'Both', 'Sanctified Lightsworn Helmet'), +(2, 2, 1, 0, 290, 54581, 'Phase 5', 'Paladin', 'Retribution', 'Neck', 'Both', 'Penumbra Pendant'), +(2, 2, 2, 0, 290, 51279, 'Phase 5', 'Paladin', 'Retribution', 'Shoulders', 'Both', 'Sanctified Lightsworn Shoulderplates'), +(2, 2, 4, 0, 290, 51275, 'Phase 5', 'Paladin', 'Retribution', 'Chest', 'Both', 'Sanctified Lightsworn Battleplate'), +(2, 2, 5, 0, 290, 50707, 'Phase 5', 'Paladin', 'Retribution', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(2, 2, 6, 0, 290, 51278, 'Phase 5', 'Paladin', 'Retribution', 'Legs', 'Both', 'Sanctified Lightsworn Legplates'), +(2, 2, 7, 0, 290, 54578, 'Phase 5', 'Paladin', 'Retribution', 'Feet', 'Both', 'Apocalypse''s Advance'), +(2, 2, 8, 0, 290, 54580, 'Phase 5', 'Paladin', 'Retribution', 'Wrists', 'Both', 'Umbrage Armbands'), +(2, 2, 9, 0, 290, 50690, 'Phase 5', 'Paladin', 'Retribution', 'Hands', 'Both', 'Fleshrending Gauntlets'), +(2, 2, 10, 0, 290, 50402, 'Phase 5', 'Paladin', 'Retribution', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(2, 2, 11, 0, 290, 54576, 'Phase 5', 'Paladin', 'Retribution', 'Finger2', 'Both', 'Signet of Twilight'), +(2, 2, 12, 0, 290, 54590, 'Phase 5', 'Paladin', 'Retribution', 'Trinket1', 'Both', 'Sharpened Twilight Scale'), +(2, 2, 13, 0, 290, 50706, 'Phase 5', 'Paladin', 'Retribution', 'Trinket2', 'Both', 'Tiny Abomination in a Jar'), +(2, 2, 14, 0, 290, 50653, 'Phase 5', 'Paladin', 'Retribution', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(2, 2, 15, 0, 290, 49623, 'Phase 5', 'Paladin', 'Retribution', 'MainHand', 'Both', 'Shadowmourne'), +(2, 2, 17, 0, 290, 50455, 'Phase 5', 'Paladin', 'Retribution', 'Ranged', 'Both', 'Libram of Three Truths'); + + +-- ============================================================ +-- Hunter (3) +-- ============================================================ +-- Beast Mastery (tab 0) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 66, 13404, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Mask of the Unforgiven'), +(3, 0, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Mark of Fordring'), +(3, 0, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(3, 0, 4, 0, 66, 11726, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Savage Gladiator Chain'), +(3, 0, 5, 0, 66, 14502, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Frostbite Girdle'), +(3, 0, 6, 0, 66, 15062, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Devilsaur Leggings'), +(3, 0, 7, 0, 66, 13967, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Windreaver Greaves'), +(3, 0, 8, 0, 66, 13211, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Slashclaw Bracers'), +(3, 0, 9, 0, 66, 15063, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(3, 0, 10, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Painweaver Band'), +(3, 0, 11, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Finger2', 'Both', 'Blackstone Ring'), +(3, 0, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 0, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Trinket2', 'Both', 'Hand of Justice'), +(3, 0, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Cape of the Black Baron'), +(3, 0, 15, 0, 66, 12940, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Dal''Rend''s Sacred Charge'), +(3, 0, 16, 0, 66, 12939, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'OffHand', 'Both', 'Dal''Rend''s Tribal Guardian'), +(3, 0, 17, 0, 66, 18738, 'Phase 1 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Carapace Spine Crossbow'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 76, 18421, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Backwood Helm'), +(3, 0, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Mark of Fordring'), +(3, 0, 2, 0, 76, 13358, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Wyrmtongue Shoulders'), +(3, 0, 4, 0, 76, 11726, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Savage Gladiator Chain'), +(3, 0, 5, 0, 76, 18393, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Warpwood Binding'), +(3, 0, 6, 0, 76, 15062, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Devilsaur Leggings'), +(3, 0, 7, 0, 76, 13967, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Windreaver Greaves'), +(3, 0, 8, 0, 76, 13211, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Slashclaw Bracers'), +(3, 0, 9, 0, 76, 15063, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(3, 0, 10, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Tarnished Elven Ring'), +(3, 0, 11, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(3, 0, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 0, 13, 0, 76, 18473, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Trinket2', 'Both', 'Royal Seal of Eldre''Thalas'), +(3, 0, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Cape of the Black Baron'), +(3, 0, 15, 0, 76, 18520, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Barbarous Blade'), +(3, 0, 17, 0, 76, 18738, 'Phase 2 (Pre-Raid)', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Carapace Spine Crossbow'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 78, 16846, 'Phase 2', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Giantstalker''s Helmet'), +(3, 0, 1, 0, 78, 18404, 'Phase 2', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(3, 0, 2, 0, 78, 16848, 'Phase 2', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Giantstalker''s Epaulets'), +(3, 0, 4, 0, 78, 16845, 'Phase 2', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Giantstalker''s Breastplate'), +(3, 0, 5, 0, 78, 16851, 'Phase 2', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Giantstalker''s Belt'), +(3, 0, 6, 0, 78, 16847, 'Phase 2', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Giantstalker''s Leggings'), +(3, 0, 7, 0, 78, 16849, 'Phase 2', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Giantstalker''s Boots'), +(3, 0, 8, 0, 78, 16850, 'Phase 2', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Giantstalker''s Bracers'), +(3, 0, 9, 0, 78, 16852, 'Phase 2', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Giantstalker''s Gloves'), +(3, 0, 10, 0, 78, 17063, 'Phase 2', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Band of Accuria'), +(3, 0, 11, 0, 78, 18821, 'Phase 2', 'Hunter', 'Beast Mastery', 'Finger2', 'Both', 'Quick Strike Ring'), +(3, 0, 12, 0, 78, 13965, 'Phase 2', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 0, 13, 0, 78, 18473, 'Phase 2', 'Hunter', 'Beast Mastery', 'Trinket2', 'Both', 'Royal Seal of Eldre''Thalas'), +(3, 0, 14, 0, 78, 17102, 'Phase 2', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Cloak of the Shrouded Mists'), +(3, 0, 15, 0, 78, 18832, 'Phase 2', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Brutality Blade'), +(3, 0, 16, 0, 78, 18805, 'Phase 2', 'Hunter', 'Beast Mastery', 'OffHand', 'Both', 'Core Hound Tooth'), +(3, 0, 17, 0, 78, 18713, 'Phase 2', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Rhok''delar, Longbow of the Ancient Keepers'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 83, 16939, 'Phase 3', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Dragonstalker''s Helm'), +(3, 0, 1, 0, 83, 19377, 'Phase 3', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(3, 0, 2, 0, 83, 16937, 'Phase 3', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Dragonstalker''s Spaulders'), +(3, 0, 4, 0, 83, 16942, 'Phase 3', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Dragonstalker''s Breastplate'), +(3, 0, 5, 0, 83, 16936, 'Phase 3', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Dragonstalker''s Belt'), +(3, 0, 6, 0, 83, 16938, 'Phase 3', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Dragonstalker''s Legguards'), +(3, 0, 7, 0, 83, 16941, 'Phase 3', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Dragonstalker''s Greaves'), +(3, 0, 8, 0, 83, 16935, 'Phase 3', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Dragonstalker''s Bracers'), +(3, 0, 9, 0, 83, 16940, 'Phase 3', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Dragonstalker''s Gauntlets'), +(3, 0, 10, 0, 83, 19325, 'Phase 3', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Don Julio''s Band'), +(3, 0, 11, 0, 83, 18821, 'Phase 3', 'Hunter', 'Beast Mastery', 'Finger2', 'Both', 'Quick Strike Ring'), +(3, 0, 12, 0, 83, 13965, 'Phase 3', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 0, 13, 0, 83, 19406, 'Phase 3', 'Hunter', 'Beast Mastery', 'Trinket2', 'Both', 'Drake Fang Talisman'), +(3, 0, 14, 0, 83, 17102, 'Phase 3', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Cloak of the Shrouded Mists'), +(3, 0, 15, 0, 83, 18832, 'Phase 3', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Brutality Blade'), +(3, 0, 16, 0, 83, 18805, 'Phase 3', 'Hunter', 'Beast Mastery', 'OffHand', 'Both', 'Core Hound Tooth'), +(3, 0, 17, 0, 83, 19361, 'Phase 3', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Ashjre''thul, Crossbow of Smiting'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 88, 16939, 'Phase 5', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Dragonstalker''s Helm'), +(3, 0, 1, 0, 88, 19377, 'Phase 5', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(3, 0, 2, 0, 88, 16937, 'Phase 5', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Dragonstalker''s Spaulders'), +(3, 0, 4, 0, 88, 16942, 'Phase 5', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Dragonstalker''s Breastplate'), +(3, 0, 5, 0, 88, 16936, 'Phase 5', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Dragonstalker''s Belt'), +(3, 0, 6, 0, 88, 16938, 'Phase 5', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Dragonstalker''s Legguards'), +(3, 0, 7, 0, 88, 16941, 'Phase 5', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Dragonstalker''s Greaves'), +(3, 0, 8, 0, 88, 16935, 'Phase 5', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Dragonstalker''s Bracers'), +(3, 0, 9, 0, 88, 16940, 'Phase 5', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Dragonstalker''s Gauntlets'), +(3, 0, 10, 0, 88, 19325, 'Phase 5', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Don Julio''s Band'), +(3, 0, 11, 0, 88, 17063, 'Phase 5', 'Hunter', 'Beast Mastery', 'Finger2', 'Both', 'Band of Accuria'), +(3, 0, 12, 0, 88, 21670, 'Phase 5', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Badge of the Swarmguard'), +(3, 0, 13, 0, 88, 23570, 'Phase 5', 'Hunter', 'Beast Mastery', 'Trinket2', 'Both', 'Jom Gabbar'), +(3, 0, 14, 0, 88, 21710, 'Phase 5', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Cloak of the Fallen God'), +(3, 0, 15, 0, 88, 21673, 'Phase 5', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Silithid Claw'), +(3, 0, 16, 0, 88, 19859, 'Phase 5', 'Hunter', 'Beast Mastery', 'OffHand', 'Both', 'Fang of the Faceless'), +(3, 0, 17, 0, 88, 19361, 'Phase 5', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Ashjre''thul, Crossbow of Smiting'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 92, 22438, 'Phase 6', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Cryptstalker Headpiece'), +(3, 0, 1, 0, 92, 23053, 'Phase 6', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Stormrage''s Talisman of Seething'), +(3, 0, 2, 0, 92, 22439, 'Phase 6', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Cryptstalker Spaulders'), +(3, 0, 4, 0, 92, 22436, 'Phase 6', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Cryptstalker Tunic'), +(3, 0, 5, 0, 92, 22442, 'Phase 6', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Cryptstalker Girdle'), +(3, 0, 6, 0, 92, 22437, 'Phase 6', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Cryptstalker Legguards'), +(3, 0, 7, 0, 92, 22440, 'Phase 6', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Cryptstalker Boots'), +(3, 0, 8, 0, 92, 22443, 'Phase 6', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Cryptstalker Wristguards'), +(3, 0, 9, 0, 92, 16571, 'Phase 6', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'General''s Chain Gloves'), +(3, 0, 10, 0, 92, 23067, 'Phase 6', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Ring of the Cryptstalker'), +(3, 0, 11, 0, 92, 22961, 'Phase 6', 'Hunter', 'Beast Mastery', 'Finger2', 'Both', 'Band of Reanimation'), +(3, 0, 12, 0, 92, 21670, 'Phase 6', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Badge of the Swarmguard'), +(3, 0, 13, 0, 92, 23041, 'Phase 6', 'Hunter', 'Beast Mastery', 'Trinket2', 'Both', 'Slayer''s Crest'), +(3, 0, 14, 0, 92, 23045, 'Phase 6', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Shroud of Dominion'), +(3, 0, 15, 0, 92, 22816, 'Phase 6', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Hatchet of Sundered Bone'), +(3, 0, 16, 0, 92, 22802, 'Phase 6', 'Hunter', 'Beast Mastery', 'OffHand', 'Both', 'Kingsfall'), +(3, 0, 17, 0, 92, 22812, 'Phase 6', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Nerubian Slavemaker'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 200, 37188, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Plunderer''s Helmet'), +(3, 0, 1, 0, 200, 40678, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Pendant of the Outcast Hero'), +(3, 0, 2, 0, 200, 37679, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Spaulders of the Abomination'), +(3, 0, 4, 0, 200, 39579, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Heroes'' Cryptstalker Tunic'), +(3, 0, 6, 0, 200, 37669, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Leggings of the Stone Halls'), +(3, 0, 7, 0, 200, 44297, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Boots of the Neverending Path'), +(3, 0, 8, 0, 200, 41224, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Deadly Gladiator''s Wristguards of Triumph'), +(3, 0, 9, 0, 200, 43734, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Heroes'' Cryptstalker Handguards'), +(3, 0, 10, 0, 200, 40586, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Band of the Kirin Tor'), +(3, 0, 12, 0, 200, 44253, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(3, 0, 14, 0, 200, 42068, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Deadly Gladiator''s Cloak of Victory'), +(3, 0, 15, 0, 200, 44249, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'MainHand', 'Both', 'Runeblade of Demonstrable Power'), +(3, 0, 17, 0, 200, 44504, 'Pre-Raid', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Nesingwary 4000'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 224, 40505, 'Phase 1', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Valorous Cryptstalker Headpiece'), +(3, 0, 1, 0, 224, 44664, 'Phase 1', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(3, 0, 2, 0, 224, 40507, 'Phase 1', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Valorous Cryptstalker Spaulders'), +(3, 0, 4, 0, 224, 43998, 'Phase 1', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Chestguard of Flagrant Prowess'), +(3, 0, 5, 0, 224, 39762, 'Phase 1', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Torn Web Wrapping'), +(3, 0, 6, 0, 224, 40331, 'Phase 1', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Leggings of Failed Escape'), +(3, 0, 7, 0, 224, 40549, 'Phase 1', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Boots of the Renewed Flight'), +(3, 0, 8, 0, 224, 40282, 'Phase 1', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Slime Stream Bands'), +(3, 0, 9, 0, 224, 40541, 'Phase 1', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(3, 0, 10, 0, 224, 40474, 'Phase 1', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Surge Needle Ring'), +(3, 0, 12, 0, 224, 40431, 'Phase 1', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Fury of the Five Flights'), +(3, 0, 14, 0, 224, 40403, 'Phase 1', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Drape of the Deadly Foe'), +(3, 0, 17, 0, 224, 40385, 'Phase 1', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Envoy of Mortality'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 245, 45610, 'Phase 2', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Boundless Gaze'), +(3, 0, 1, 0, 245, 45517, 'Phase 2', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Pendulum of Infinity'), +(3, 0, 2, 0, 245, 45300, 'Phase 2', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Mantle of Fiery Vengeance'), +(3, 0, 4, 0, 245, 45473, 'Phase 2', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Embrace of the Gladiator'), +(3, 0, 5, 0, 245, 45553, 'Phase 2', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Belt of Dragons'), +(3, 0, 6, 0, 245, 45536, 'Phase 2', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(3, 0, 7, 0, 245, 45989, 'Phase 2', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Tempered Mercury Greaves'), +(3, 0, 8, 0, 245, 45869, 'Phase 2', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Fluxing Energy Coils'), +(3, 0, 9, 0, 245, 45444, 'Phase 2', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Gloves of the Steady Hand'), +(3, 0, 12, 0, 245, 46038, 'Phase 2', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Dark Matter'), +(3, 0, 14, 0, 245, 46032, 'Phase 2', 'Hunter', 'Beast Mastery', 'Back', 'Both', 'Drape of the Faceless General'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 258, 48262, 'Phase 3', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Windrunner''s Headpiece of Triumph'), +(3, 0, 1, 1, 258, 47060, 'Phase 3', 'Hunter', 'Beast Mastery', 'Neck', 'Alliance', 'Charge of the Demon Lord'), +(3, 0, 1, 2, 258, 47433, 'Phase 3', 'Hunter', 'Beast Mastery', 'Neck', 'Horde', 'Charge of the Eredar'), +(3, 0, 2, 0, 258, 48260, 'Phase 3', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Windrunner''s Spaulders of Triumph'), +(3, 0, 4, 0, 258, 48264, 'Phase 3', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Windrunner''s Tunic of Triumph'), +(3, 0, 5, 1, 258, 47153, 'Phase 3', 'Hunter', 'Beast Mastery', 'Waist', 'Alliance', 'Belt of Deathly Dominion'), +(3, 0, 5, 2, 258, 47472, 'Phase 3', 'Hunter', 'Beast Mastery', 'Waist', 'Horde', 'Waistguard of Deathly Dominion'), +(3, 0, 6, 1, 258, 47191, 'Phase 3', 'Hunter', 'Beast Mastery', 'Legs', 'Alliance', 'Legguards of the Lurking Threat'), +(3, 0, 6, 2, 258, 47480, 'Phase 3', 'Hunter', 'Beast Mastery', 'Legs', 'Horde', 'Leggings of the Lurking Threat'), +(3, 0, 7, 1, 258, 47109, 'Phase 3', 'Hunter', 'Beast Mastery', 'Feet', 'Alliance', 'Sabatons of Ruthless Judgment'), +(3, 0, 7, 2, 258, 47457, 'Phase 3', 'Hunter', 'Beast Mastery', 'Feet', 'Horde', 'Greaves of Ruthless Judgment'), +(3, 0, 8, 1, 258, 47074, 'Phase 3', 'Hunter', 'Beast Mastery', 'Wrists', 'Alliance', 'Bracers of the Untold Massacre'), +(3, 0, 8, 2, 258, 47442, 'Phase 3', 'Hunter', 'Beast Mastery', 'Wrists', 'Horde', 'Bracers of the Silent Massacre'), +(3, 0, 9, 0, 258, 48263, 'Phase 3', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Windrunner''s Handguards of Triumph'), +(3, 0, 10, 1, 258, 47075, 'Phase 3', 'Hunter', 'Beast Mastery', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(3, 0, 10, 2, 258, 47443, 'Phase 3', 'Hunter', 'Beast Mastery', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(3, 0, 12, 2, 258, 45931, 'Phase 3', 'Hunter', 'Beast Mastery', 'Trinket1', 'Horde', 'Mjolnir Runestone'), +(3, 0, 14, 1, 258, 47545, 'Phase 3', 'Hunter', 'Beast Mastery', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(3, 0, 14, 2, 258, 47546, 'Phase 3', 'Hunter', 'Beast Mastery', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(3, 0, 17, 1, 258, 47521, 'Phase 3', 'Hunter', 'Beast Mastery', 'Ranged', 'Alliance', 'BRK 1000'), +(3, 0, 17, 2, 258, 47523, 'Phase 3', 'Hunter', 'Beast Mastery', 'Ranged', 'Horde', 'Fezzik''s Autocannon'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 0, 0, 0, 264, 51286, 'Phase 4', 'Hunter', 'Beast Mastery', 'Head', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Headpiece'), +(3, 0, 1, 0, 264, 50633, 'Phase 4', 'Hunter', 'Beast Mastery', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(3, 0, 2, 0, 264, 51288, 'Phase 4', 'Hunter', 'Beast Mastery', 'Shoulders', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Spaulders'), +(3, 0, 4, 0, 264, 51289, 'Phase 4', 'Hunter', 'Beast Mastery', 'Chest', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Tunic'), +(3, 0, 5, 0, 264, 50688, 'Phase 4', 'Hunter', 'Beast Mastery', 'Waist', 'Both', 'Nerub''ar Stalker''s Cord'), +(3, 0, 6, 0, 264, 50645, 'Phase 4', 'Hunter', 'Beast Mastery', 'Legs', 'Both', 'Leggings of Northern Lights'), +(3, 0, 7, 0, 264, 50607, 'Phase 4', 'Hunter', 'Beast Mastery', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(3, 0, 8, 0, 264, 50655, 'Phase 4', 'Hunter', 'Beast Mastery', 'Wrists', 'Both', 'Scourge Hunter''s Vambraces'), +(3, 0, 9, 0, 264, 51285, 'Phase 4', 'Hunter', 'Beast Mastery', 'Hands', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Handguards'), +(3, 0, 10, 0, 264, 50402, 'Phase 4', 'Hunter', 'Beast Mastery', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(3, 0, 12, 0, 264, 50363, 'Phase 4', 'Hunter', 'Beast Mastery', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(3, 0, 14, 1, 264, 47545, 'Phase 4', 'Hunter', 'Beast Mastery', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(3, 0, 14, 2, 264, 47546, 'Phase 4', 'Hunter', 'Beast Mastery', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(3, 0, 17, 0, 264, 50733, 'Phase 4', 'Hunter', 'Beast Mastery', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- Marksmanship (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 66, 13404, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Mask of the Unforgiven'), +(3, 1, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Mark of Fordring'), +(3, 1, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(3, 1, 4, 0, 66, 11726, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Savage Gladiator Chain'), +(3, 1, 5, 0, 66, 14502, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Frostbite Girdle'), +(3, 1, 6, 0, 66, 15062, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Devilsaur Leggings'), +(3, 1, 7, 0, 66, 13967, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Windreaver Greaves'), +(3, 1, 8, 0, 66, 13211, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Slashclaw Bracers'), +(3, 1, 9, 0, 66, 15063, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(3, 1, 10, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Painweaver Band'), +(3, 1, 11, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Blackstone Ring'), +(3, 1, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 1, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Hand of Justice'), +(3, 1, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Cape of the Black Baron'), +(3, 1, 15, 0, 66, 12940, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Dal''Rend''s Sacred Charge'), +(3, 1, 16, 0, 66, 12939, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'OffHand', 'Both', 'Dal''Rend''s Tribal Guardian'), +(3, 1, 17, 0, 66, 18738, 'Phase 1 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Carapace Spine Crossbow'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 76, 18421, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Backwood Helm'), +(3, 1, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Mark of Fordring'), +(3, 1, 2, 0, 76, 13358, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Wyrmtongue Shoulders'), +(3, 1, 4, 0, 76, 11726, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Savage Gladiator Chain'), +(3, 1, 5, 0, 76, 18393, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Warpwood Binding'), +(3, 1, 6, 0, 76, 15062, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Devilsaur Leggings'), +(3, 1, 7, 0, 76, 13967, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Windreaver Greaves'), +(3, 1, 8, 0, 76, 13211, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Slashclaw Bracers'), +(3, 1, 9, 0, 76, 15063, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(3, 1, 10, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Tarnished Elven Ring'), +(3, 1, 11, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(3, 1, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 1, 13, 0, 76, 18473, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Royal Seal of Eldre''Thalas'), +(3, 1, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Cape of the Black Baron'), +(3, 1, 15, 0, 76, 18520, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Barbarous Blade'), +(3, 1, 17, 0, 76, 18738, 'Phase 2 (Pre-Raid)', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Carapace Spine Crossbow'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 78, 16846, 'Phase 2', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Giantstalker''s Helmet'), +(3, 1, 1, 0, 78, 18404, 'Phase 2', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(3, 1, 2, 0, 78, 16848, 'Phase 2', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Giantstalker''s Epaulets'), +(3, 1, 4, 0, 78, 16845, 'Phase 2', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Giantstalker''s Breastplate'), +(3, 1, 5, 0, 78, 16851, 'Phase 2', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Giantstalker''s Belt'), +(3, 1, 6, 0, 78, 16847, 'Phase 2', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Giantstalker''s Leggings'), +(3, 1, 7, 0, 78, 16849, 'Phase 2', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Giantstalker''s Boots'), +(3, 1, 8, 0, 78, 16850, 'Phase 2', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Giantstalker''s Bracers'), +(3, 1, 9, 0, 78, 16852, 'Phase 2', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Giantstalker''s Gloves'), +(3, 1, 10, 0, 78, 17063, 'Phase 2', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Band of Accuria'), +(3, 1, 11, 0, 78, 18821, 'Phase 2', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Quick Strike Ring'), +(3, 1, 12, 0, 78, 13965, 'Phase 2', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 1, 13, 0, 78, 18473, 'Phase 2', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Royal Seal of Eldre''Thalas'), +(3, 1, 14, 0, 78, 17102, 'Phase 2', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Cloak of the Shrouded Mists'), +(3, 1, 15, 0, 78, 18832, 'Phase 2', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Brutality Blade'), +(3, 1, 16, 0, 78, 18805, 'Phase 2', 'Hunter', 'Marksmanship', 'OffHand', 'Both', 'Core Hound Tooth'), +(3, 1, 17, 0, 78, 18713, 'Phase 2', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Rhok''delar, Longbow of the Ancient Keepers'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 83, 16939, 'Phase 3', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Dragonstalker''s Helm'), +(3, 1, 1, 0, 83, 19377, 'Phase 3', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(3, 1, 2, 0, 83, 16937, 'Phase 3', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Dragonstalker''s Spaulders'), +(3, 1, 4, 0, 83, 16942, 'Phase 3', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Dragonstalker''s Breastplate'), +(3, 1, 5, 0, 83, 16936, 'Phase 3', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Dragonstalker''s Belt'), +(3, 1, 6, 0, 83, 16938, 'Phase 3', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Dragonstalker''s Legguards'), +(3, 1, 7, 0, 83, 16941, 'Phase 3', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Dragonstalker''s Greaves'), +(3, 1, 8, 0, 83, 16935, 'Phase 3', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Dragonstalker''s Bracers'), +(3, 1, 9, 0, 83, 16940, 'Phase 3', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Dragonstalker''s Gauntlets'), +(3, 1, 10, 0, 83, 19325, 'Phase 3', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Don Julio''s Band'), +(3, 1, 11, 0, 83, 18821, 'Phase 3', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Quick Strike Ring'), +(3, 1, 12, 0, 83, 13965, 'Phase 3', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 1, 13, 0, 83, 19406, 'Phase 3', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Drake Fang Talisman'), +(3, 1, 14, 0, 83, 17102, 'Phase 3', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Cloak of the Shrouded Mists'), +(3, 1, 15, 0, 83, 18832, 'Phase 3', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Brutality Blade'), +(3, 1, 16, 0, 83, 18805, 'Phase 3', 'Hunter', 'Marksmanship', 'OffHand', 'Both', 'Core Hound Tooth'), +(3, 1, 17, 0, 83, 19361, 'Phase 3', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Ashjre''thul, Crossbow of Smiting'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 88, 16939, 'Phase 5', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Dragonstalker''s Helm'), +(3, 1, 1, 0, 88, 19377, 'Phase 5', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(3, 1, 2, 0, 88, 16937, 'Phase 5', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Dragonstalker''s Spaulders'), +(3, 1, 4, 0, 88, 16942, 'Phase 5', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Dragonstalker''s Breastplate'), +(3, 1, 5, 0, 88, 16936, 'Phase 5', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Dragonstalker''s Belt'), +(3, 1, 6, 0, 88, 16938, 'Phase 5', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Dragonstalker''s Legguards'), +(3, 1, 7, 0, 88, 16941, 'Phase 5', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Dragonstalker''s Greaves'), +(3, 1, 8, 0, 88, 16935, 'Phase 5', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Dragonstalker''s Bracers'), +(3, 1, 9, 0, 88, 16940, 'Phase 5', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Dragonstalker''s Gauntlets'), +(3, 1, 10, 0, 88, 19325, 'Phase 5', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Don Julio''s Band'), +(3, 1, 11, 0, 88, 17063, 'Phase 5', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Band of Accuria'), +(3, 1, 12, 0, 88, 21670, 'Phase 5', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Badge of the Swarmguard'), +(3, 1, 13, 0, 88, 23570, 'Phase 5', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Jom Gabbar'), +(3, 1, 14, 0, 88, 21710, 'Phase 5', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Cloak of the Fallen God'), +(3, 1, 15, 0, 88, 21673, 'Phase 5', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Silithid Claw'), +(3, 1, 16, 0, 88, 19859, 'Phase 5', 'Hunter', 'Marksmanship', 'OffHand', 'Both', 'Fang of the Faceless'), +(3, 1, 17, 0, 88, 19361, 'Phase 5', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Ashjre''thul, Crossbow of Smiting'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 92, 22438, 'Phase 6', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Cryptstalker Headpiece'), +(3, 1, 1, 0, 92, 23053, 'Phase 6', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Stormrage''s Talisman of Seething'), +(3, 1, 2, 0, 92, 22439, 'Phase 6', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Cryptstalker Spaulders'), +(3, 1, 4, 0, 92, 22436, 'Phase 6', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Cryptstalker Tunic'), +(3, 1, 5, 0, 92, 22442, 'Phase 6', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Cryptstalker Girdle'), +(3, 1, 6, 0, 92, 22437, 'Phase 6', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Cryptstalker Legguards'), +(3, 1, 7, 0, 92, 22440, 'Phase 6', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Cryptstalker Boots'), +(3, 1, 8, 0, 92, 22443, 'Phase 6', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Cryptstalker Wristguards'), +(3, 1, 9, 0, 92, 16571, 'Phase 6', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'General''s Chain Gloves'), +(3, 1, 10, 0, 92, 23067, 'Phase 6', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Ring of the Cryptstalker'), +(3, 1, 11, 0, 92, 22961, 'Phase 6', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Band of Reanimation'), +(3, 1, 12, 0, 92, 21670, 'Phase 6', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Badge of the Swarmguard'), +(3, 1, 13, 0, 92, 23041, 'Phase 6', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Slayer''s Crest'), +(3, 1, 14, 0, 92, 23045, 'Phase 6', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Shroud of Dominion'), +(3, 1, 15, 0, 92, 22816, 'Phase 6', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Hatchet of Sundered Bone'), +(3, 1, 16, 0, 92, 22802, 'Phase 6', 'Hunter', 'Marksmanship', 'OffHand', 'Both', 'Kingsfall'), +(3, 1, 17, 0, 92, 22812, 'Phase 6', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Nerubian Slavemaker'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 200, 37188, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Plunderer''s Helmet'), +(3, 1, 1, 0, 200, 42645, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Titanium Impact Choker'), +(3, 1, 2, 0, 200, 37679, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Spaulders of the Abomination'), +(3, 1, 4, 0, 200, 37144, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Hauberk of the Arcane Wraith'), +(3, 1, 5, 0, 200, 37407, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Sovereign''s Belt'), +(3, 1, 6, 0, 200, 37669, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Leggings of the Stone Halls'), +(3, 1, 7, 0, 200, 37167, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Dragon Slayer''s Sabatons'), +(3, 1, 8, 0, 200, 37170, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Interwoven Scale Bracers'), +(3, 1, 9, 0, 200, 37886, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Handgrips of the Savage Emissary'), +(3, 1, 10, 0, 200, 42642, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Titanium Impact Band'), +(3, 1, 12, 0, 200, 40684, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Mirror of Truth'), +(3, 1, 14, 0, 200, 43566, 'Pre-Raid', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Ice Striker''s Cloak'), +(3, 1, 15, 0, 200, 44249, 'Pre-Raid', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Runeblade of Demonstrable Power'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 224, 40543, 'Phase 1', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Blue Aspect Helm'), +(3, 1, 1, 0, 224, 44664, 'Phase 1', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(3, 1, 2, 0, 224, 40507, 'Phase 1', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Valorous Cryptstalker Spaulders'), +(3, 1, 4, 0, 224, 40193, 'Phase 1', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Tunic of Masked Suffering'), +(3, 1, 5, 0, 224, 40275, 'Phase 1', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Depraved Linked Belt'), +(3, 1, 6, 0, 224, 40506, 'Phase 1', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Valorous Cryptstalker Legguards'), +(3, 1, 7, 0, 224, 40549, 'Phase 1', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Boots of the Renewed Flight'), +(3, 1, 8, 0, 224, 40282, 'Phase 1', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Slime Stream Bands'), +(3, 1, 9, 0, 224, 40541, 'Phase 1', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(3, 1, 10, 0, 224, 40074, 'Phase 1', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Strong-Handed Ring'), +(3, 1, 12, 0, 224, 40684, 'Phase 1', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Mirror of Truth'), +(3, 1, 14, 0, 224, 40403, 'Phase 1', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Drape of the Deadly Foe'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 245, 46143, 'Phase 2', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Conqueror''s Scourgestalker Headpiece'), +(3, 1, 1, 0, 245, 45517, 'Phase 2', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Pendulum of Infinity'), +(3, 1, 2, 0, 245, 46145, 'Phase 2', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Conqueror''s Scourgestalker Spaulders'), +(3, 1, 4, 0, 245, 46141, 'Phase 2', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Conqueror''s Scourgestalker Tunic'), +(3, 1, 5, 0, 245, 45467, 'Phase 2', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Belt of the Betrayed'), +(3, 1, 6, 0, 245, 46144, 'Phase 2', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Conqueror''s Scourgestalker Legguards'), +(3, 1, 7, 0, 245, 45244, 'Phase 2', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Greaves of Swift Vengeance'), +(3, 1, 8, 0, 245, 45454, 'Phase 2', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Frost-bound Chain Bracers'), +(3, 1, 9, 0, 245, 45444, 'Phase 2', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Gloves of the Steady Hand'), +(3, 1, 10, 0, 245, 45456, 'Phase 2', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Loop of the Agile'), +(3, 1, 12, 0, 245, 45931, 'Phase 2', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Mjolnir Runestone'), +(3, 1, 14, 0, 245, 46032, 'Phase 2', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Drape of the Faceless General'), +(3, 1, 15, 0, 245, 45498, 'Phase 2', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Lotrafen, Spear of the Damned'), +(3, 1, 17, 0, 245, 45570, 'Phase 2', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Skyforge Crossbow'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 258, 48262, 'Phase 3', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Windrunner''s Headpiece of Triumph'), +(3, 1, 1, 1, 258, 47060, 'Phase 3', 'Hunter', 'Marksmanship', 'Neck', 'Alliance', 'Charge of the Demon Lord'), +(3, 1, 1, 2, 258, 47433, 'Phase 3', 'Hunter', 'Marksmanship', 'Neck', 'Horde', 'Charge of the Eredar'), +(3, 1, 2, 0, 258, 48260, 'Phase 3', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Windrunner''s Spaulders of Triumph'), +(3, 1, 4, 0, 258, 48264, 'Phase 3', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Windrunner''s Tunic of Triumph'), +(3, 1, 5, 1, 258, 47153, 'Phase 3', 'Hunter', 'Marksmanship', 'Waist', 'Alliance', 'Belt of Deathly Dominion'), +(3, 1, 5, 2, 258, 47472, 'Phase 3', 'Hunter', 'Marksmanship', 'Waist', 'Horde', 'Waistguard of Deathly Dominion'), +(3, 1, 6, 1, 258, 47191, 'Phase 3', 'Hunter', 'Marksmanship', 'Legs', 'Alliance', 'Legguards of the Lurking Threat'), +(3, 1, 6, 2, 258, 47480, 'Phase 3', 'Hunter', 'Marksmanship', 'Legs', 'Horde', 'Leggings of the Lurking Threat'), +(3, 1, 7, 1, 258, 47109, 'Phase 3', 'Hunter', 'Marksmanship', 'Feet', 'Alliance', 'Sabatons of Ruthless Judgment'), +(3, 1, 7, 2, 258, 47457, 'Phase 3', 'Hunter', 'Marksmanship', 'Feet', 'Horde', 'Greaves of Ruthless Judgment'), +(3, 1, 8, 1, 258, 47074, 'Phase 3', 'Hunter', 'Marksmanship', 'Wrists', 'Alliance', 'Bracers of the Untold Massacre'), +(3, 1, 8, 2, 258, 47442, 'Phase 3', 'Hunter', 'Marksmanship', 'Wrists', 'Horde', 'Bracers of the Silent Massacre'), +(3, 1, 9, 0, 258, 48263, 'Phase 3', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Windrunner''s Handguards of Triumph'), +(3, 1, 10, 1, 258, 47075, 'Phase 3', 'Hunter', 'Marksmanship', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(3, 1, 10, 2, 258, 47443, 'Phase 3', 'Hunter', 'Marksmanship', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(3, 1, 12, 2, 258, 45931, 'Phase 3', 'Hunter', 'Marksmanship', 'Trinket1', 'Horde', 'Mjolnir Runestone'), +(3, 1, 14, 1, 258, 47545, 'Phase 3', 'Hunter', 'Marksmanship', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(3, 1, 14, 2, 258, 47546, 'Phase 3', 'Hunter', 'Marksmanship', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(3, 1, 17, 1, 258, 47521, 'Phase 3', 'Hunter', 'Marksmanship', 'Ranged', 'Alliance', 'BRK 1000'), +(3, 1, 17, 2, 258, 47523, 'Phase 3', 'Hunter', 'Marksmanship', 'Ranged', 'Horde', 'Fezzik''s Autocannon'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 264, 51286, 'Phase 4', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Headpiece'), +(3, 1, 1, 0, 264, 50633, 'Phase 4', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(3, 1, 2, 0, 264, 51288, 'Phase 4', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Spaulders'), +(3, 1, 4, 0, 264, 51289, 'Phase 4', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Tunic'), +(3, 1, 5, 0, 264, 50688, 'Phase 4', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Nerub''ar Stalker''s Cord'), +(3, 1, 6, 0, 264, 50645, 'Phase 4', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Leggings of Northern Lights'), +(3, 1, 7, 0, 264, 50607, 'Phase 4', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(3, 1, 8, 0, 264, 50655, 'Phase 4', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Scourge Hunter''s Vambraces'), +(3, 1, 9, 0, 264, 51285, 'Phase 4', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Handguards'), +(3, 1, 10, 0, 264, 50402, 'Phase 4', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(3, 1, 12, 0, 264, 50363, 'Phase 4', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(3, 1, 14, 1, 264, 47545, 'Phase 4', 'Hunter', 'Marksmanship', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(3, 1, 14, 2, 264, 47546, 'Phase 4', 'Hunter', 'Marksmanship', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(3, 1, 15, 0, 264, 50735, 'Phase 4', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Oathbinder, Charge of the Ranger-General'), +(3, 1, 17, 0, 264, 50733, 'Phase 4', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 1, 0, 0, 290, 51286, 'Phase 5', 'Hunter', 'Marksmanship', 'Head', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Headpiece'), +(3, 1, 1, 0, 290, 50633, 'Phase 5', 'Hunter', 'Marksmanship', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(3, 1, 2, 0, 290, 51288, 'Phase 5', 'Hunter', 'Marksmanship', 'Shoulders', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Spaulders'), +(3, 1, 4, 0, 290, 51289, 'Phase 5', 'Hunter', 'Marksmanship', 'Chest', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Tunic'), +(3, 1, 5, 0, 290, 50688, 'Phase 5', 'Hunter', 'Marksmanship', 'Waist', 'Both', 'Nerub''ar Stalker''s Cord'), +(3, 1, 6, 0, 290, 50645, 'Phase 5', 'Hunter', 'Marksmanship', 'Legs', 'Both', 'Leggings of Northern Lights'), +(3, 1, 7, 0, 290, 54577, 'Phase 5', 'Hunter', 'Marksmanship', 'Feet', 'Both', 'Returning Footfalls'), +(3, 1, 8, 0, 290, 50655, 'Phase 5', 'Hunter', 'Marksmanship', 'Wrists', 'Both', 'Scourge Hunter''s Vambraces'), +(3, 1, 9, 0, 290, 51285, 'Phase 5', 'Hunter', 'Marksmanship', 'Hands', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Handguards'), +(3, 1, 10, 0, 290, 54576, 'Phase 5', 'Hunter', 'Marksmanship', 'Finger1', 'Both', 'Signet of Twilight'), +(3, 1, 11, 0, 290, 50402, 'Phase 5', 'Hunter', 'Marksmanship', 'Finger2', 'Both', 'Ashen Band of Endless Vengeance'), +(3, 1, 12, 0, 290, 50363, 'Phase 5', 'Hunter', 'Marksmanship', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(3, 1, 13, 0, 290, 54590, 'Phase 5', 'Hunter', 'Marksmanship', 'Trinket2', 'Both', 'Sharpened Twilight Scale'), +(3, 1, 14, 0, 290, 47545, 'Phase 5', 'Hunter', 'Marksmanship', 'Back', 'Both', 'Vereesa''s Dexterity'), +(3, 1, 15, 0, 290, 50735, 'Phase 5', 'Hunter', 'Marksmanship', 'MainHand', 'Both', 'Oathbinder, Charge of the Ranger-General'), +(3, 1, 17, 0, 290, 50733, 'Phase 5', 'Hunter', 'Marksmanship', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- Survival (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 66, 13404, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Head', 'Both', 'Mask of the Unforgiven'), +(3, 2, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Neck', 'Both', 'Mark of Fordring'), +(3, 2, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(3, 2, 4, 0, 66, 11726, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Chest', 'Both', 'Savage Gladiator Chain'), +(3, 2, 5, 0, 66, 14502, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Waist', 'Both', 'Frostbite Girdle'), +(3, 2, 6, 0, 66, 15062, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Legs', 'Both', 'Devilsaur Leggings'), +(3, 2, 7, 0, 66, 13967, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Feet', 'Both', 'Windreaver Greaves'), +(3, 2, 8, 0, 66, 13211, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Wrists', 'Both', 'Slashclaw Bracers'), +(3, 2, 9, 0, 66, 15063, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(3, 2, 10, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Finger1', 'Both', 'Painweaver Band'), +(3, 2, 11, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Finger2', 'Both', 'Blackstone Ring'), +(3, 2, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 2, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Trinket2', 'Both', 'Hand of Justice'), +(3, 2, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Back', 'Both', 'Cape of the Black Baron'), +(3, 2, 15, 0, 66, 12940, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'MainHand', 'Both', 'Dal''Rend''s Sacred Charge'), +(3, 2, 16, 0, 66, 12939, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'OffHand', 'Both', 'Dal''Rend''s Tribal Guardian'), +(3, 2, 17, 0, 66, 18738, 'Phase 1 (Pre-Raid)', 'Hunter', 'Survival', 'Ranged', 'Both', 'Carapace Spine Crossbow'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 76, 18421, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Head', 'Both', 'Backwood Helm'), +(3, 2, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Neck', 'Both', 'Mark of Fordring'), +(3, 2, 2, 0, 76, 13358, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Wyrmtongue Shoulders'), +(3, 2, 4, 0, 76, 11726, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Chest', 'Both', 'Savage Gladiator Chain'), +(3, 2, 5, 0, 76, 18393, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Waist', 'Both', 'Warpwood Binding'), +(3, 2, 6, 0, 76, 15062, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Legs', 'Both', 'Devilsaur Leggings'), +(3, 2, 7, 0, 76, 13967, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Feet', 'Both', 'Windreaver Greaves'), +(3, 2, 8, 0, 76, 13211, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Wrists', 'Both', 'Slashclaw Bracers'), +(3, 2, 9, 0, 76, 15063, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(3, 2, 10, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Finger1', 'Both', 'Tarnished Elven Ring'), +(3, 2, 11, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(3, 2, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 2, 13, 0, 76, 18473, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Trinket2', 'Both', 'Royal Seal of Eldre''Thalas'), +(3, 2, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Back', 'Both', 'Cape of the Black Baron'), +(3, 2, 15, 0, 76, 18520, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'MainHand', 'Both', 'Barbarous Blade'), +(3, 2, 17, 0, 76, 18738, 'Phase 2 (Pre-Raid)', 'Hunter', 'Survival', 'Ranged', 'Both', 'Carapace Spine Crossbow'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 78, 16846, 'Phase 2', 'Hunter', 'Survival', 'Head', 'Both', 'Giantstalker''s Helmet'), +(3, 2, 1, 0, 78, 18404, 'Phase 2', 'Hunter', 'Survival', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(3, 2, 2, 0, 78, 16848, 'Phase 2', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Giantstalker''s Epaulets'), +(3, 2, 4, 0, 78, 16845, 'Phase 2', 'Hunter', 'Survival', 'Chest', 'Both', 'Giantstalker''s Breastplate'), +(3, 2, 5, 0, 78, 16851, 'Phase 2', 'Hunter', 'Survival', 'Waist', 'Both', 'Giantstalker''s Belt'), +(3, 2, 6, 0, 78, 16847, 'Phase 2', 'Hunter', 'Survival', 'Legs', 'Both', 'Giantstalker''s Leggings'), +(3, 2, 7, 0, 78, 16849, 'Phase 2', 'Hunter', 'Survival', 'Feet', 'Both', 'Giantstalker''s Boots'), +(3, 2, 8, 0, 78, 16850, 'Phase 2', 'Hunter', 'Survival', 'Wrists', 'Both', 'Giantstalker''s Bracers'), +(3, 2, 9, 0, 78, 16852, 'Phase 2', 'Hunter', 'Survival', 'Hands', 'Both', 'Giantstalker''s Gloves'), +(3, 2, 10, 0, 78, 17063, 'Phase 2', 'Hunter', 'Survival', 'Finger1', 'Both', 'Band of Accuria'), +(3, 2, 11, 0, 78, 18821, 'Phase 2', 'Hunter', 'Survival', 'Finger2', 'Both', 'Quick Strike Ring'), +(3, 2, 12, 0, 78, 13965, 'Phase 2', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 2, 13, 0, 78, 18473, 'Phase 2', 'Hunter', 'Survival', 'Trinket2', 'Both', 'Royal Seal of Eldre''Thalas'), +(3, 2, 14, 0, 78, 17102, 'Phase 2', 'Hunter', 'Survival', 'Back', 'Both', 'Cloak of the Shrouded Mists'), +(3, 2, 15, 0, 78, 18832, 'Phase 2', 'Hunter', 'Survival', 'MainHand', 'Both', 'Brutality Blade'), +(3, 2, 16, 0, 78, 18805, 'Phase 2', 'Hunter', 'Survival', 'OffHand', 'Both', 'Core Hound Tooth'), +(3, 2, 17, 0, 78, 18713, 'Phase 2', 'Hunter', 'Survival', 'Ranged', 'Both', 'Rhok''delar, Longbow of the Ancient Keepers'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 83, 16939, 'Phase 3', 'Hunter', 'Survival', 'Head', 'Both', 'Dragonstalker''s Helm'), +(3, 2, 1, 0, 83, 19377, 'Phase 3', 'Hunter', 'Survival', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(3, 2, 2, 0, 83, 16937, 'Phase 3', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Dragonstalker''s Spaulders'), +(3, 2, 4, 0, 83, 16942, 'Phase 3', 'Hunter', 'Survival', 'Chest', 'Both', 'Dragonstalker''s Breastplate'), +(3, 2, 5, 0, 83, 16936, 'Phase 3', 'Hunter', 'Survival', 'Waist', 'Both', 'Dragonstalker''s Belt'), +(3, 2, 6, 0, 83, 16938, 'Phase 3', 'Hunter', 'Survival', 'Legs', 'Both', 'Dragonstalker''s Legguards'), +(3, 2, 7, 0, 83, 16941, 'Phase 3', 'Hunter', 'Survival', 'Feet', 'Both', 'Dragonstalker''s Greaves'), +(3, 2, 8, 0, 83, 16935, 'Phase 3', 'Hunter', 'Survival', 'Wrists', 'Both', 'Dragonstalker''s Bracers'), +(3, 2, 9, 0, 83, 16940, 'Phase 3', 'Hunter', 'Survival', 'Hands', 'Both', 'Dragonstalker''s Gauntlets'), +(3, 2, 10, 0, 83, 19325, 'Phase 3', 'Hunter', 'Survival', 'Finger1', 'Both', 'Don Julio''s Band'), +(3, 2, 11, 0, 83, 18821, 'Phase 3', 'Hunter', 'Survival', 'Finger2', 'Both', 'Quick Strike Ring'), +(3, 2, 12, 0, 83, 13965, 'Phase 3', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(3, 2, 13, 0, 83, 19406, 'Phase 3', 'Hunter', 'Survival', 'Trinket2', 'Both', 'Drake Fang Talisman'), +(3, 2, 14, 0, 83, 17102, 'Phase 3', 'Hunter', 'Survival', 'Back', 'Both', 'Cloak of the Shrouded Mists'), +(3, 2, 15, 0, 83, 18832, 'Phase 3', 'Hunter', 'Survival', 'MainHand', 'Both', 'Brutality Blade'), +(3, 2, 16, 0, 83, 18805, 'Phase 3', 'Hunter', 'Survival', 'OffHand', 'Both', 'Core Hound Tooth'), +(3, 2, 17, 0, 83, 19361, 'Phase 3', 'Hunter', 'Survival', 'Ranged', 'Both', 'Ashjre''thul, Crossbow of Smiting'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 88, 16939, 'Phase 5', 'Hunter', 'Survival', 'Head', 'Both', 'Dragonstalker''s Helm'), +(3, 2, 1, 0, 88, 19377, 'Phase 5', 'Hunter', 'Survival', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(3, 2, 2, 0, 88, 16937, 'Phase 5', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Dragonstalker''s Spaulders'), +(3, 2, 4, 0, 88, 16942, 'Phase 5', 'Hunter', 'Survival', 'Chest', 'Both', 'Dragonstalker''s Breastplate'), +(3, 2, 5, 0, 88, 16936, 'Phase 5', 'Hunter', 'Survival', 'Waist', 'Both', 'Dragonstalker''s Belt'), +(3, 2, 6, 0, 88, 16938, 'Phase 5', 'Hunter', 'Survival', 'Legs', 'Both', 'Dragonstalker''s Legguards'), +(3, 2, 7, 0, 88, 16941, 'Phase 5', 'Hunter', 'Survival', 'Feet', 'Both', 'Dragonstalker''s Greaves'), +(3, 2, 8, 0, 88, 16935, 'Phase 5', 'Hunter', 'Survival', 'Wrists', 'Both', 'Dragonstalker''s Bracers'), +(3, 2, 9, 0, 88, 16940, 'Phase 5', 'Hunter', 'Survival', 'Hands', 'Both', 'Dragonstalker''s Gauntlets'), +(3, 2, 10, 0, 88, 19325, 'Phase 5', 'Hunter', 'Survival', 'Finger1', 'Both', 'Don Julio''s Band'), +(3, 2, 11, 0, 88, 17063, 'Phase 5', 'Hunter', 'Survival', 'Finger2', 'Both', 'Band of Accuria'), +(3, 2, 12, 0, 88, 21670, 'Phase 5', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Badge of the Swarmguard'), +(3, 2, 13, 0, 88, 23570, 'Phase 5', 'Hunter', 'Survival', 'Trinket2', 'Both', 'Jom Gabbar'), +(3, 2, 14, 0, 88, 21710, 'Phase 5', 'Hunter', 'Survival', 'Back', 'Both', 'Cloak of the Fallen God'), +(3, 2, 15, 0, 88, 21673, 'Phase 5', 'Hunter', 'Survival', 'MainHand', 'Both', 'Silithid Claw'), +(3, 2, 16, 0, 88, 19859, 'Phase 5', 'Hunter', 'Survival', 'OffHand', 'Both', 'Fang of the Faceless'), +(3, 2, 17, 0, 88, 19361, 'Phase 5', 'Hunter', 'Survival', 'Ranged', 'Both', 'Ashjre''thul, Crossbow of Smiting'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 92, 22438, 'Phase 6', 'Hunter', 'Survival', 'Head', 'Both', 'Cryptstalker Headpiece'), +(3, 2, 1, 0, 92, 23053, 'Phase 6', 'Hunter', 'Survival', 'Neck', 'Both', 'Stormrage''s Talisman of Seething'), +(3, 2, 2, 0, 92, 22439, 'Phase 6', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Cryptstalker Spaulders'), +(3, 2, 4, 0, 92, 22436, 'Phase 6', 'Hunter', 'Survival', 'Chest', 'Both', 'Cryptstalker Tunic'), +(3, 2, 5, 0, 92, 22442, 'Phase 6', 'Hunter', 'Survival', 'Waist', 'Both', 'Cryptstalker Girdle'), +(3, 2, 6, 0, 92, 22437, 'Phase 6', 'Hunter', 'Survival', 'Legs', 'Both', 'Cryptstalker Legguards'), +(3, 2, 7, 0, 92, 22440, 'Phase 6', 'Hunter', 'Survival', 'Feet', 'Both', 'Cryptstalker Boots'), +(3, 2, 8, 0, 92, 22443, 'Phase 6', 'Hunter', 'Survival', 'Wrists', 'Both', 'Cryptstalker Wristguards'), +(3, 2, 9, 0, 92, 16571, 'Phase 6', 'Hunter', 'Survival', 'Hands', 'Both', 'General''s Chain Gloves'), +(3, 2, 10, 0, 92, 23067, 'Phase 6', 'Hunter', 'Survival', 'Finger1', 'Both', 'Ring of the Cryptstalker'), +(3, 2, 11, 0, 92, 22961, 'Phase 6', 'Hunter', 'Survival', 'Finger2', 'Both', 'Band of Reanimation'), +(3, 2, 12, 0, 92, 21670, 'Phase 6', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Badge of the Swarmguard'), +(3, 2, 13, 0, 92, 23041, 'Phase 6', 'Hunter', 'Survival', 'Trinket2', 'Both', 'Slayer''s Crest'), +(3, 2, 14, 0, 92, 23045, 'Phase 6', 'Hunter', 'Survival', 'Back', 'Both', 'Shroud of Dominion'), +(3, 2, 15, 0, 92, 22816, 'Phase 6', 'Hunter', 'Survival', 'MainHand', 'Both', 'Hatchet of Sundered Bone'), +(3, 2, 16, 0, 92, 22802, 'Phase 6', 'Hunter', 'Survival', 'OffHand', 'Both', 'Kingsfall'), +(3, 2, 17, 0, 92, 22812, 'Phase 6', 'Hunter', 'Survival', 'Ranged', 'Both', 'Nerubian Slavemaker'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 200, 42551, 'Pre-Raid', 'Hunter', 'Survival', 'Head', 'Both', 'Truesight Ice Blinders'), +(3, 2, 1, 0, 200, 40678, 'Pre-Raid', 'Hunter', 'Survival', 'Neck', 'Both', 'Pendant of the Outcast Hero'), +(3, 2, 2, 0, 200, 37679, 'Pre-Raid', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Spaulders of the Abomination'), +(3, 2, 4, 0, 200, 44295, 'Pre-Raid', 'Hunter', 'Survival', 'Chest', 'Both', 'Polished Regimental Hauberk'), +(3, 2, 5, 0, 200, 40692, 'Pre-Raid', 'Hunter', 'Survival', 'Waist', 'Both', 'Vereesa''s Silver Chain Belt'), +(3, 2, 6, 0, 200, 37669, 'Pre-Raid', 'Hunter', 'Survival', 'Legs', 'Both', 'Leggings of the Stone Halls'), +(3, 2, 7, 0, 200, 44297, 'Pre-Raid', 'Hunter', 'Survival', 'Feet', 'Both', 'Boots of the Neverending Path'), +(3, 2, 8, 0, 200, 37170, 'Pre-Raid', 'Hunter', 'Survival', 'Wrists', 'Both', 'Interwoven Scale Bracers'), +(3, 2, 9, 0, 200, 37886, 'Pre-Raid', 'Hunter', 'Survival', 'Hands', 'Both', 'Handgrips of the Savage Emissary'), +(3, 2, 10, 0, 200, 37685, 'Pre-Raid', 'Hunter', 'Survival', 'Finger1', 'Both', 'Mobius Band'), +(3, 2, 12, 0, 200, 40684, 'Pre-Raid', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Mirror of Truth'), +(3, 2, 14, 0, 200, 43406, 'Pre-Raid', 'Hunter', 'Survival', 'Back', 'Both', 'Cloak of the Gushing Wound'), +(3, 2, 15, 0, 200, 37883, 'Pre-Raid', 'Hunter', 'Survival', 'MainHand', 'Both', 'Staff of Trickery'), +(3, 2, 17, 0, 200, 37191, 'Pre-Raid', 'Hunter', 'Survival', 'Ranged', 'Both', 'Drake-Mounted Crossbow'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 224, 40505, 'Phase 1', 'Hunter', 'Survival', 'Head', 'Both', 'Valorous Cryptstalker Headpiece'), +(3, 2, 1, 0, 224, 44664, 'Phase 1', 'Hunter', 'Survival', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(3, 2, 2, 0, 224, 40507, 'Phase 1', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Valorous Cryptstalker Spaulders'), +(3, 2, 4, 0, 224, 43998, 'Phase 1', 'Hunter', 'Survival', 'Chest', 'Both', 'Chestguard of Flagrant Prowess'), +(3, 2, 5, 0, 224, 39762, 'Phase 1', 'Hunter', 'Survival', 'Waist', 'Both', 'Torn Web Wrapping'), +(3, 2, 6, 0, 224, 40331, 'Phase 1', 'Hunter', 'Survival', 'Legs', 'Both', 'Leggings of Failed Escape'), +(3, 2, 7, 0, 224, 40549, 'Phase 1', 'Hunter', 'Survival', 'Feet', 'Both', 'Boots of the Renewed Flight'), +(3, 2, 8, 0, 224, 40282, 'Phase 1', 'Hunter', 'Survival', 'Wrists', 'Both', 'Slime Stream Bands'), +(3, 2, 9, 0, 224, 40541, 'Phase 1', 'Hunter', 'Survival', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(3, 2, 10, 0, 224, 40074, 'Phase 1', 'Hunter', 'Survival', 'Finger1', 'Both', 'Strong-Handed Ring'), +(3, 2, 12, 0, 224, 40431, 'Phase 1', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Fury of the Five Flights'), +(3, 2, 14, 0, 224, 40403, 'Phase 1', 'Hunter', 'Survival', 'Back', 'Both', 'Drape of the Deadly Foe'), +(3, 2, 15, 0, 224, 40388, 'Phase 1', 'Hunter', 'Survival', 'MainHand', 'Both', 'Journey''s End'), +(3, 2, 17, 0, 224, 40385, 'Phase 1', 'Hunter', 'Survival', 'Ranged', 'Both', 'Envoy of Mortality'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 245, 45610, 'Phase 2', 'Hunter', 'Survival', 'Head', 'Both', 'Boundless Gaze'), +(3, 2, 1, 0, 245, 45517, 'Phase 2', 'Hunter', 'Survival', 'Neck', 'Both', 'Pendulum of Infinity'), +(3, 2, 2, 0, 245, 45300, 'Phase 2', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Mantle of Fiery Vengeance'), +(3, 2, 4, 0, 245, 45473, 'Phase 2', 'Hunter', 'Survival', 'Chest', 'Both', 'Embrace of the Gladiator'), +(3, 2, 5, 0, 245, 46095, 'Phase 2', 'Hunter', 'Survival', 'Waist', 'Both', 'Soul-Devouring Cinch'), +(3, 2, 6, 0, 245, 45536, 'Phase 2', 'Hunter', 'Survival', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(3, 2, 7, 0, 245, 45244, 'Phase 2', 'Hunter', 'Survival', 'Feet', 'Both', 'Greaves of Swift Vengeance'), +(3, 2, 8, 0, 245, 45869, 'Phase 2', 'Hunter', 'Survival', 'Wrists', 'Both', 'Fluxing Energy Coils'), +(3, 2, 9, 0, 245, 45444, 'Phase 2', 'Hunter', 'Survival', 'Hands', 'Both', 'Gloves of the Steady Hand'), +(3, 2, 12, 0, 245, 44253, 'Phase 2', 'Hunter', 'Survival', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(3, 2, 14, 0, 245, 46032, 'Phase 2', 'Hunter', 'Survival', 'Back', 'Both', 'Drape of the Faceless General'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 258, 48262, 'Phase 3', 'Hunter', 'Survival', 'Head', 'Both', 'Windrunner''s Headpiece of Triumph'), +(3, 2, 1, 1, 258, 47060, 'Phase 3', 'Hunter', 'Survival', 'Neck', 'Alliance', 'Charge of the Demon Lord'), +(3, 2, 1, 2, 258, 47433, 'Phase 3', 'Hunter', 'Survival', 'Neck', 'Horde', 'Charge of the Eredar'), +(3, 2, 2, 0, 258, 48260, 'Phase 3', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Windrunner''s Spaulders of Triumph'), +(3, 2, 4, 0, 258, 48264, 'Phase 3', 'Hunter', 'Survival', 'Chest', 'Both', 'Windrunner''s Tunic of Triumph'), +(3, 2, 5, 1, 258, 47153, 'Phase 3', 'Hunter', 'Survival', 'Waist', 'Alliance', 'Belt of Deathly Dominion'), +(3, 2, 5, 2, 258, 47472, 'Phase 3', 'Hunter', 'Survival', 'Waist', 'Horde', 'Waistguard of Deathly Dominion'), +(3, 2, 6, 1, 258, 47191, 'Phase 3', 'Hunter', 'Survival', 'Legs', 'Alliance', 'Legguards of the Lurking Threat'), +(3, 2, 6, 2, 258, 47480, 'Phase 3', 'Hunter', 'Survival', 'Legs', 'Horde', 'Leggings of the Lurking Threat'), +(3, 2, 7, 1, 258, 47109, 'Phase 3', 'Hunter', 'Survival', 'Feet', 'Alliance', 'Sabatons of Ruthless Judgment'), +(3, 2, 7, 2, 258, 47457, 'Phase 3', 'Hunter', 'Survival', 'Feet', 'Horde', 'Greaves of Ruthless Judgment'), +(3, 2, 8, 1, 258, 47074, 'Phase 3', 'Hunter', 'Survival', 'Wrists', 'Alliance', 'Bracers of the Untold Massacre'), +(3, 2, 8, 2, 258, 47442, 'Phase 3', 'Hunter', 'Survival', 'Wrists', 'Horde', 'Bracers of the Silent Massacre'), +(3, 2, 9, 0, 258, 48263, 'Phase 3', 'Hunter', 'Survival', 'Hands', 'Both', 'Windrunner''s Handguards of Triumph'), +(3, 2, 10, 1, 258, 47075, 'Phase 3', 'Hunter', 'Survival', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(3, 2, 10, 2, 258, 47443, 'Phase 3', 'Hunter', 'Survival', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(3, 2, 12, 2, 258, 44253, 'Phase 3', 'Hunter', 'Survival', 'Trinket1', 'Horde', 'Darkmoon Card: Greatness'), +(3, 2, 14, 1, 258, 47545, 'Phase 3', 'Hunter', 'Survival', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(3, 2, 14, 2, 258, 47546, 'Phase 3', 'Hunter', 'Survival', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(3, 2, 17, 1, 258, 47521, 'Phase 3', 'Hunter', 'Survival', 'Ranged', 'Alliance', 'BRK 1000'), +(3, 2, 17, 2, 258, 47523, 'Phase 3', 'Hunter', 'Survival', 'Ranged', 'Horde', 'Fezzik''s Autocannon'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(3, 2, 0, 0, 264, 51286, 'Phase 4', 'Hunter', 'Survival', 'Head', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Headpiece'), +(3, 2, 1, 0, 264, 50633, 'Phase 4', 'Hunter', 'Survival', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(3, 2, 2, 0, 264, 51288, 'Phase 4', 'Hunter', 'Survival', 'Shoulders', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Spaulders'), +(3, 2, 4, 0, 264, 51289, 'Phase 4', 'Hunter', 'Survival', 'Chest', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Tunic'), +(3, 2, 5, 0, 264, 50688, 'Phase 4', 'Hunter', 'Survival', 'Waist', 'Both', 'Nerub''ar Stalker''s Cord'), +(3, 2, 6, 0, 264, 50645, 'Phase 4', 'Hunter', 'Survival', 'Legs', 'Both', 'Leggings of Northern Lights'), +(3, 2, 7, 0, 264, 50607, 'Phase 4', 'Hunter', 'Survival', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(3, 2, 8, 0, 264, 50655, 'Phase 4', 'Hunter', 'Survival', 'Wrists', 'Both', 'Scourge Hunter''s Vambraces'), +(3, 2, 9, 0, 264, 51285, 'Phase 4', 'Hunter', 'Survival', 'Hands', 'Both', 'Sanctified Ahn''Kahar Blood Hunter''s Handguards'), +(3, 2, 10, 0, 264, 50402, 'Phase 4', 'Hunter', 'Survival', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(3, 2, 12, 1, 264, 47131, 'Phase 4', 'Hunter', 'Survival', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(3, 2, 12, 2, 264, 47464, 'Phase 4', 'Hunter', 'Survival', 'Trinket1', 'Horde', 'Death''s Choice'), +(3, 2, 14, 1, 264, 47545, 'Phase 4', 'Hunter', 'Survival', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(3, 2, 14, 2, 264, 47546, 'Phase 4', 'Hunter', 'Survival', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(3, 2, 17, 0, 264, 50733, 'Phase 4', 'Hunter', 'Survival', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + + +-- ============================================================ +-- Rogue (4) +-- ============================================================ +-- Assassination (tab 0) +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 120, 32087, 'Pre-Raid', 'Rogue', 'Assassination', 'Head', 'Both', 'Mask of the Deceiver'), +(4, 0, 1, 0, 120, 29381, 'Pre-Raid', 'Rogue', 'Assassination', 'Neck', 'Both', 'Choker of Vile Intent'), +(4, 0, 2, 0, 120, 27797, 'Pre-Raid', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(4, 0, 4, 0, 120, 30730, 'Pre-Raid', 'Rogue', 'Assassination', 'Chest', 'Both', 'Terrorweave Tunic'), +(4, 0, 5, 0, 120, 29247, 'Pre-Raid', 'Rogue', 'Assassination', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(4, 0, 6, 0, 120, 30538, 'Pre-Raid', 'Rogue', 'Assassination', 'Legs', 'Both', 'Midnight Legguards'), +(4, 0, 7, 0, 120, 25686, 'Pre-Raid', 'Rogue', 'Assassination', 'Feet', 'Both', 'Fel Leather Boots'), +(4, 0, 8, 0, 120, 29246, 'Pre-Raid', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Nightfall Wristguards'), +(4, 0, 9, 0, 120, 27531, 'Pre-Raid', 'Rogue', 'Assassination', 'Hands', 'Both', 'Wastewalker Gloves'), +(4, 0, 10, 0, 120, 30738, 'Pre-Raid', 'Rogue', 'Assassination', 'Finger1', 'Both', 'Ring of Reciprocity'), +(4, 0, 12, 0, 120, 28288, 'Pre-Raid', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Abacus of Violent Odds'), +(4, 0, 13, 0, 120, 29383, 'Pre-Raid', 'Rogue', 'Assassination', 'Trinket2', 'Both', 'Bloodlust Brooch'), +(4, 0, 14, 0, 120, 24259, 'Pre-Raid', 'Rogue', 'Assassination', 'Back', 'Both', 'Vengeance Wrap'), +(4, 0, 15, 0, 120, 31331, 'Pre-Raid', 'Rogue', 'Assassination', 'MainHand', 'Both', 'The Night Blade'), +(4, 0, 16, 0, 120, 29346, 'Pre-Raid', 'Rogue', 'Assassination', 'OffHand', 'Both', 'Feltooth Eviscerator'), +(4, 0, 17, 2, 120, 29152, 'Pre-Raid', 'Rogue', 'Assassination', 'Ranged', 'Horde', 'Marksman''s Bow'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 125, 29044, 'Phase 1', 'Rogue', 'Assassination', 'Head', 'Both', 'Netherblade Facemask'), +(4, 0, 1, 0, 125, 29381, 'Phase 1', 'Rogue', 'Assassination', 'Neck', 'Both', 'Choker of Vile Intent'), +(4, 0, 2, 0, 125, 27797, 'Phase 1', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(4, 0, 4, 0, 125, 29045, 'Phase 1', 'Rogue', 'Assassination', 'Chest', 'Both', 'Netherblade Chestpiece'), +(4, 0, 5, 0, 125, 29247, 'Phase 1', 'Rogue', 'Assassination', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(4, 0, 6, 0, 125, 28741, 'Phase 1', 'Rogue', 'Assassination', 'Legs', 'Both', 'Skulker''s Greaves'), +(4, 0, 7, 0, 125, 28545, 'Phase 1', 'Rogue', 'Assassination', 'Feet', 'Both', 'Edgewalker Longboots'), +(4, 0, 8, 0, 125, 29246, 'Phase 1', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Nightfall Wristguards'), +(4, 0, 9, 0, 125, 27531, 'Phase 1', 'Rogue', 'Assassination', 'Hands', 'Both', 'Wastewalker Gloves'), +(4, 0, 10, 0, 125, 28649, 'Phase 1', 'Rogue', 'Assassination', 'Finger1', 'Both', 'Garona''s Signet Ring'), +(4, 0, 11, 0, 125, 28757, 'Phase 1', 'Rogue', 'Assassination', 'Finger2', 'Both', 'Ring of a Thousand Marks'), +(4, 0, 12, 0, 125, 29383, 'Phase 1', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(4, 0, 13, 0, 125, 28830, 'Phase 1', 'Rogue', 'Assassination', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(4, 0, 14, 0, 125, 28672, 'Phase 1', 'Rogue', 'Assassination', 'Back', 'Both', 'Drape of the Dark Reavers'), +(4, 0, 15, 0, 125, 28312, 'Phase 1', 'Rogue', 'Assassination', 'MainHand', 'Both', 'Gladiator''s Shanker'), +(4, 0, 16, 0, 125, 29346, 'Phase 1', 'Rogue', 'Assassination', 'OffHand', 'Both', 'Feltooth Eviscerator'), +(4, 0, 17, 0, 125, 28772, 'Phase 1', 'Rogue', 'Assassination', 'Ranged', 'Both', 'Sunfury Bow of the Phoenix'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 200, 42550, 'Pre-Raid', 'Rogue', 'Assassination', 'Head', 'Both', 'Weakness Spectralizers'), +(4, 0, 1, 0, 200, 40678, 'Pre-Raid', 'Rogue', 'Assassination', 'Neck', 'Both', 'Pendant of the Outcast Hero'), +(4, 0, 2, 0, 200, 43481, 'Pre-Raid', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Trollwoven Spaulders'), +(4, 0, 4, 0, 200, 39558, 'Pre-Raid', 'Rogue', 'Assassination', 'Chest', 'Both', 'Heroes'' Bonescythe Breastplate'), +(4, 0, 5, 0, 200, 40694, 'Pre-Raid', 'Rogue', 'Assassination', 'Waist', 'Both', 'Jorach''s Crocolisk Skin Belt'), +(4, 0, 8, 0, 200, 34448, 'Pre-Raid', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Slayer''s Bracers'), +(4, 0, 9, 0, 200, 39560, 'Pre-Raid', 'Rogue', 'Assassination', 'Hands', 'Both', 'Heroes'' Bonescythe Gauntlets'), +(4, 0, 12, 0, 200, 40684, 'Pre-Raid', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Mirror of Truth'), +(4, 0, 14, 0, 200, 43566, 'Pre-Raid', 'Rogue', 'Assassination', 'Back', 'Both', 'Ice Striker''s Cloak'), +(4, 0, 15, 0, 200, 37856, 'Pre-Raid', 'Rogue', 'Assassination', 'MainHand', 'Both', 'Librarian''s Paper Cutter'), +(4, 0, 17, 0, 200, 44504, 'Pre-Raid', 'Rogue', 'Assassination', 'Ranged', 'Both', 'Nesingwary 4000'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 224, 43729, 'Phase 1', 'Rogue', 'Assassination', 'Head', 'Both', 'Valorous Bonescythe Helmet'), +(4, 0, 1, 0, 224, 44664, 'Phase 1', 'Rogue', 'Assassination', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(4, 0, 2, 0, 224, 40502, 'Phase 1', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Valorous Bonescythe Pauldrons'), +(4, 0, 4, 0, 224, 40539, 'Phase 1', 'Rogue', 'Assassination', 'Chest', 'Both', 'Chestguard of the Recluse'), +(4, 0, 5, 0, 224, 40205, 'Phase 1', 'Rogue', 'Assassination', 'Waist', 'Both', 'Stalk-Skin Belt'), +(4, 0, 6, 0, 224, 44011, 'Phase 1', 'Rogue', 'Assassination', 'Legs', 'Both', 'Leggings of the Honored'), +(4, 0, 7, 0, 224, 39701, 'Phase 1', 'Rogue', 'Assassination', 'Feet', 'Both', 'Dawnwalkers'), +(4, 0, 8, 0, 224, 39765, 'Phase 1', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Sinner''s Bindings'), +(4, 0, 9, 0, 224, 40541, 'Phase 1', 'Rogue', 'Assassination', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(4, 0, 10, 0, 224, 40474, 'Phase 1', 'Rogue', 'Assassination', 'Finger1', 'Both', 'Surge Needle Ring'), +(4, 0, 12, 0, 224, 44253, 'Phase 1', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(4, 0, 14, 0, 224, 40403, 'Phase 1', 'Rogue', 'Assassination', 'Back', 'Both', 'Drape of the Deadly Foe'), +(4, 0, 15, 0, 224, 39714, 'Phase 1', 'Rogue', 'Assassination', 'MainHand', 'Both', 'Webbed Death'), +(4, 0, 17, 0, 224, 40385, 'Phase 1', 'Rogue', 'Assassination', 'Ranged', 'Both', 'Envoy of Mortality'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 245, 46125, 'Phase 2', 'Rogue', 'Assassination', 'Head', 'Both', 'Conqueror''s Terrorblade Helmet'), +(4, 0, 1, 0, 245, 45517, 'Phase 2', 'Rogue', 'Assassination', 'Neck', 'Both', 'Pendulum of Infinity'), +(4, 0, 2, 0, 245, 45245, 'Phase 2', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Shoulderpads of the Intruder'), +(4, 0, 4, 0, 245, 45473, 'Phase 2', 'Rogue', 'Assassination', 'Chest', 'Both', 'Embrace of the Gladiator'), +(4, 0, 5, 0, 245, 46095, 'Phase 2', 'Rogue', 'Assassination', 'Waist', 'Both', 'Soul-Devouring Cinch'), +(4, 0, 6, 0, 245, 45536, 'Phase 2', 'Rogue', 'Assassination', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(4, 0, 7, 0, 245, 45564, 'Phase 2', 'Rogue', 'Assassination', 'Feet', 'Both', 'Footpads of Silence'), +(4, 0, 8, 0, 245, 45611, 'Phase 2', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Solar Bindings'), +(4, 0, 9, 0, 245, 46124, 'Phase 2', 'Rogue', 'Assassination', 'Hands', 'Both', 'Conqueror''s Terrorblade Gauntlets'), +(4, 0, 12, 0, 245, 45609, 'Phase 2', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Comet''s Trail'), +(4, 0, 14, 0, 245, 45461, 'Phase 2', 'Rogue', 'Assassination', 'Back', 'Both', 'Drape of Icy Intent'), +(4, 0, 17, 0, 245, 45570, 'Phase 2', 'Rogue', 'Assassination', 'Ranged', 'Both', 'Skyforge Crossbow'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 258, 48230, 'Phase 3', 'Rogue', 'Assassination', 'Head', 'Both', 'Helmet of Triumph'), +(4, 0, 2, 0, 258, 48228, 'Phase 3', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Pauldrons of Triumph'), +(4, 0, 4, 0, 258, 48232, 'Phase 3', 'Rogue', 'Assassination', 'Chest', 'Both', 'Breastplate of Triumph'), +(4, 0, 5, 1, 258, 47112, 'Phase 3', 'Rogue', 'Assassination', 'Waist', 'Alliance', 'Belt of the Merciless Killer'), +(4, 0, 5, 2, 258, 47460, 'Phase 3', 'Rogue', 'Assassination', 'Waist', 'Horde', 'Belt of the Pitiless Killer'), +(4, 0, 6, 1, 258, 46975, 'Phase 3', 'Rogue', 'Assassination', 'Legs', 'Alliance', 'Leggings of the Broken Beast'), +(4, 0, 6, 2, 258, 47420, 'Phase 3', 'Rogue', 'Assassination', 'Legs', 'Horde', 'Legwraps of the Broken Beast'), +(4, 0, 7, 1, 258, 47077, 'Phase 3', 'Rogue', 'Assassination', 'Feet', 'Alliance', 'Treads of the Icewalker'), +(4, 0, 7, 2, 258, 47445, 'Phase 3', 'Rogue', 'Assassination', 'Feet', 'Horde', 'Icewalker Treads'), +(4, 0, 8, 1, 258, 47155, 'Phase 3', 'Rogue', 'Assassination', 'Wrists', 'Alliance', 'Bracers of Dark Determination'), +(4, 0, 8, 2, 258, 47474, 'Phase 3', 'Rogue', 'Assassination', 'Wrists', 'Horde', 'Armbands of Dark Determination'), +(4, 0, 9, 0, 258, 48231, 'Phase 3', 'Rogue', 'Assassination', 'Hands', 'Both', 'Gauntlets of Triumph'), +(4, 0, 10, 1, 258, 47075, 'Phase 3', 'Rogue', 'Assassination', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(4, 0, 10, 2, 258, 47443, 'Phase 3', 'Rogue', 'Assassination', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(4, 0, 12, 1, 258, 47131, 'Phase 3', 'Rogue', 'Assassination', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(4, 0, 12, 2, 258, 47464, 'Phase 3', 'Rogue', 'Assassination', 'Trinket1', 'Horde', 'Death''s Choice'), +(4, 0, 14, 1, 258, 47545, 'Phase 3', 'Rogue', 'Assassination', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(4, 0, 14, 2, 258, 47546, 'Phase 3', 'Rogue', 'Assassination', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(4, 0, 17, 1, 258, 47521, 'Phase 3', 'Rogue', 'Assassination', 'Ranged', 'Alliance', 'BRK 1000'), +(4, 0, 17, 2, 258, 47523, 'Phase 3', 'Rogue', 'Assassination', 'Ranged', 'Horde', 'Fezzik''s Autocannon'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 264, 51252, 'Phase 4', 'Rogue', 'Assassination', 'Head', 'Both', 'Sanctified Shadowblade Helmet'), +(4, 0, 2, 0, 264, 51254, 'Phase 4', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Sanctified Shadowblade Pauldrons'), +(4, 0, 4, 0, 264, 50656, 'Phase 4', 'Rogue', 'Assassination', 'Chest', 'Both', 'Ikfirus''s Sack of Wonder'), +(4, 0, 5, 0, 264, 50707, 'Phase 4', 'Rogue', 'Assassination', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(4, 0, 6, 0, 264, 51253, 'Phase 4', 'Rogue', 'Assassination', 'Legs', 'Both', 'Sanctified Shadowblade Legplates'), +(4, 0, 7, 0, 264, 50607, 'Phase 4', 'Rogue', 'Assassination', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(4, 0, 8, 0, 264, 50670, 'Phase 4', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(4, 0, 9, 0, 264, 51251, 'Phase 4', 'Rogue', 'Assassination', 'Hands', 'Both', 'Sanctified Shadowblade Gauntlets'), +(4, 0, 10, 0, 264, 50604, 'Phase 4', 'Rogue', 'Assassination', 'Finger1', 'Both', 'Band of the Bone Colossus'), +(4, 0, 12, 0, 264, 50363, 'Phase 4', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(4, 0, 14, 0, 264, 50653, 'Phase 4', 'Rogue', 'Assassination', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(4, 0, 17, 0, 264, 50733, 'Phase 4', 'Rogue', 'Assassination', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 0, 0, 0, 290, 50713, 'Phase 5', 'Rogue', 'Assassination', 'Head', 'Both', 'Geistlord''s Punishment Sack'), +(4, 0, 1, 0, 290, 50633, 'Phase 5', 'Rogue', 'Assassination', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(4, 0, 2, 0, 290, 50646, 'Phase 5', 'Rogue', 'Assassination', 'Shoulders', 'Both', 'Cultist''s Bloodsoaked Spaulders'), +(4, 0, 4, 0, 290, 50656, 'Phase 5', 'Rogue', 'Assassination', 'Chest', 'Both', 'Ikfirus''s Sack of Wonder'), +(4, 0, 5, 0, 290, 47112, 'Phase 5', 'Rogue', 'Assassination', 'Waist', 'Both', 'Belt of the Merciless Killer'), +(4, 0, 6, 0, 290, 51253, 'Phase 5', 'Rogue', 'Assassination', 'Legs', 'Both', 'Sanctified Shadowblade Legplates'), +(4, 0, 7, 0, 290, 50607, 'Phase 5', 'Rogue', 'Assassination', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(4, 0, 8, 0, 290, 54580, 'Phase 5', 'Rogue', 'Assassination', 'Wrists', 'Both', 'Umbrage Armbands'), +(4, 0, 9, 0, 290, 51251, 'Phase 5', 'Rogue', 'Assassination', 'Hands', 'Both', 'Sanctified Shadowblade Gauntlets'), +(4, 0, 10, 0, 290, 50402, 'Phase 5', 'Rogue', 'Assassination', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(4, 0, 11, 0, 290, 54576, 'Phase 5', 'Rogue', 'Assassination', 'Finger2', 'Both', 'Signet of Twilight'), +(4, 0, 12, 0, 290, 54590, 'Phase 5', 'Rogue', 'Assassination', 'Trinket1', 'Both', 'Sharpened Twilight Scale'), +(4, 0, 13, 0, 290, 50706, 'Phase 5', 'Rogue', 'Assassination', 'Trinket2', 'Both', 'Tiny Abomination in a Jar'), +(4, 0, 14, 0, 290, 50653, 'Phase 5', 'Rogue', 'Assassination', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(4, 0, 15, 0, 290, 50736, 'Phase 5', 'Rogue', 'Assassination', 'MainHand', 'Both', 'Heaven''s Fall, Kryss of a Thousand Lies'), +(4, 0, 16, 0, 290, 50621, 'Phase 5', 'Rogue', 'Assassination', 'OffHand', 'Both', 'Lungbreaker'), +(4, 0, 17, 0, 290, 50733, 'Phase 5', 'Rogue', 'Assassination', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- Combat (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 66, 13404, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Head', 'Both', 'Mask of the Unforgiven'), +(4, 1, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Neck', 'Both', 'Mark of Fordring'), +(4, 1, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(4, 1, 4, 0, 66, 14637, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Chest', 'Both', 'Cadaverous Armor'), +(4, 1, 5, 0, 66, 13252, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Waist', 'Both', 'Cloudrunner Girdle'), +(4, 1, 6, 0, 66, 15062, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Legs', 'Both', 'Devilsaur Leggings'), +(4, 1, 7, 0, 66, 12553, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Feet', 'Both', 'Swiftwalker Boots'), +(4, 1, 8, 0, 66, 13120, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Wrists', 'Both', 'Deepfury Bracers'), +(4, 1, 9, 0, 66, 15063, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(4, 1, 10, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Finger1', 'Both', 'Painweaver Band'), +(4, 1, 11, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Finger2', 'Both', 'Blackstone Ring'), +(4, 1, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(4, 1, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Hand of Justice'), +(4, 1, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Back', 'Both', 'Cape of the Black Baron'), +(4, 1, 15, 0, 66, 12783, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'MainHand', 'Both', 'Heartseeker'), +(4, 1, 16, 0, 66, 14555, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'OffHand', 'Both', 'Alcor''s Sunrazor'), +(4, 1, 17, 0, 66, 12651, 'Phase 1 (Pre-Raid)', 'Rogue', 'Combat', 'Ranged', 'Both', 'Blackcrow'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 76, 13404, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Head', 'Both', 'Mask of the Unforgiven'), +(4, 1, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Neck', 'Both', 'Mark of Fordring'), +(4, 1, 2, 0, 76, 12927, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(4, 1, 4, 0, 76, 14637, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Chest', 'Both', 'Cadaverous Armor'), +(4, 1, 5, 0, 76, 18505, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Waist', 'Both', 'Mugger''s Belt'), +(4, 1, 6, 0, 76, 15062, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Legs', 'Both', 'Devilsaur Leggings'), +(4, 1, 7, 0, 76, 12553, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Feet', 'Both', 'Swiftwalker Boots'), +(4, 1, 8, 0, 76, 18375, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Wrists', 'Both', 'Bracers of the Eclipse'), +(4, 1, 9, 0, 76, 15063, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(4, 1, 10, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Finger1', 'Both', 'Tarnished Elven Ring'), +(4, 1, 11, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(4, 1, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(4, 1, 13, 0, 76, 11815, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Hand of Justice'), +(4, 1, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Back', 'Both', 'Cape of the Black Baron'), +(4, 1, 15, 0, 76, 12783, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'MainHand', 'Both', 'Heartseeker'), +(4, 1, 16, 0, 76, 14555, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'OffHand', 'Both', 'Alcor''s Sunrazor'), +(4, 1, 17, 0, 76, 18323, 'Phase 2 (Pre-Raid)', 'Rogue', 'Combat', 'Ranged', 'Both', 'Satyr''s Bow'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 78, 16908, 'Phase 2', 'Rogue', 'Combat', 'Head', 'Both', 'Bloodfang Hood'), +(4, 1, 1, 0, 78, 18404, 'Phase 2', 'Rogue', 'Combat', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(4, 1, 2, 0, 78, 16823, 'Phase 2', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Nightslayer Shoulder Pads'), +(4, 1, 4, 0, 78, 16820, 'Phase 2', 'Rogue', 'Combat', 'Chest', 'Both', 'Nightslayer Chestpiece'), +(4, 1, 5, 0, 78, 16827, 'Phase 2', 'Rogue', 'Combat', 'Waist', 'Both', 'Nightslayer Belt'), +(4, 1, 6, 0, 78, 16909, 'Phase 2', 'Rogue', 'Combat', 'Legs', 'Both', 'Bloodfang Pants'), +(4, 1, 7, 0, 78, 16824, 'Phase 2', 'Rogue', 'Combat', 'Feet', 'Both', 'Nightslayer Boots'), +(4, 1, 8, 0, 78, 18375, 'Phase 2', 'Rogue', 'Combat', 'Wrists', 'Both', 'Bracers of the Eclipse'), +(4, 1, 9, 0, 78, 18823, 'Phase 2', 'Rogue', 'Combat', 'Hands', 'Both', 'Aged Core Leather Gloves'), +(4, 1, 10, 0, 78, 17063, 'Phase 2', 'Rogue', 'Combat', 'Finger1', 'Both', 'Band of Accuria'), +(4, 1, 11, 0, 78, 18500, 'Phase 2', 'Rogue', 'Combat', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(4, 1, 12, 0, 78, 13965, 'Phase 2', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(4, 1, 13, 0, 78, 11815, 'Phase 2', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Hand of Justice'), +(4, 1, 14, 0, 78, 13340, 'Phase 2', 'Rogue', 'Combat', 'Back', 'Both', 'Cape of the Black Baron'), +(4, 1, 15, 0, 78, 18816, 'Phase 2', 'Rogue', 'Combat', 'MainHand', 'Both', 'Perdition''s Blade'), +(4, 1, 16, 0, 78, 18805, 'Phase 2', 'Rogue', 'Combat', 'OffHand', 'Both', 'Core Hound Tooth'), +(4, 1, 17, 0, 78, 17069, 'Phase 2', 'Rogue', 'Combat', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 83, 16908, 'Phase 3', 'Rogue', 'Combat', 'Head', 'Both', 'Bloodfang Hood'), +(4, 1, 1, 0, 83, 19377, 'Phase 3', 'Rogue', 'Combat', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(4, 1, 2, 0, 83, 16823, 'Phase 3', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Nightslayer Shoulder Pads'), +(4, 1, 4, 0, 83, 16905, 'Phase 3', 'Rogue', 'Combat', 'Chest', 'Both', 'Bloodfang Chestpiece'), +(4, 1, 5, 0, 83, 16910, 'Phase 3', 'Rogue', 'Combat', 'Waist', 'Both', 'Bloodfang Belt'), +(4, 1, 6, 0, 83, 16909, 'Phase 3', 'Rogue', 'Combat', 'Legs', 'Both', 'Bloodfang Pants'), +(4, 1, 7, 0, 83, 19381, 'Phase 3', 'Rogue', 'Combat', 'Feet', 'Both', 'Boots of the Shadow Flame'), +(4, 1, 8, 0, 83, 16911, 'Phase 3', 'Rogue', 'Combat', 'Wrists', 'Both', 'Bloodfang Bracers'), +(4, 1, 9, 0, 83, 18823, 'Phase 3', 'Rogue', 'Combat', 'Hands', 'Both', 'Aged Core Leather Gloves'), +(4, 1, 10, 0, 83, 17063, 'Phase 3', 'Rogue', 'Combat', 'Finger1', 'Both', 'Band of Accuria'), +(4, 1, 11, 0, 83, 19384, 'Phase 3', 'Rogue', 'Combat', 'Finger2', 'Both', 'Master Dragonslayer''s Ring'), +(4, 1, 12, 0, 83, 19406, 'Phase 3', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(4, 1, 13, 0, 83, 11815, 'Phase 3', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Hand of Justice'), +(4, 1, 14, 0, 83, 19398, 'Phase 3', 'Rogue', 'Combat', 'Back', 'Both', 'Cloak of Firemaw'), +(4, 1, 15, 0, 83, 18816, 'Phase 3', 'Rogue', 'Combat', 'MainHand', 'Both', 'Perdition''s Blade'), +(4, 1, 16, 0, 83, 18805, 'Phase 3', 'Rogue', 'Combat', 'OffHand', 'Both', 'Core Hound Tooth'), +(4, 1, 17, 0, 83, 17069, 'Phase 3', 'Rogue', 'Combat', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 88, 21360, 'Phase 5', 'Rogue', 'Combat', 'Head', 'Both', 'Deathdealer''s Helm'), +(4, 1, 1, 0, 88, 19377, 'Phase 5', 'Rogue', 'Combat', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(4, 1, 2, 0, 88, 21361, 'Phase 5', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Deathdealer''s Spaulders'), +(4, 1, 4, 0, 88, 21364, 'Phase 5', 'Rogue', 'Combat', 'Chest', 'Both', 'Deathdealer''s Vest'), +(4, 1, 5, 0, 88, 21586, 'Phase 5', 'Rogue', 'Combat', 'Waist', 'Both', 'Belt of Never-ending Agony'), +(4, 1, 6, 0, 88, 21362, 'Phase 5', 'Rogue', 'Combat', 'Legs', 'Both', 'Deathdealer''s Leggings'), +(4, 1, 7, 0, 88, 21359, 'Phase 5', 'Rogue', 'Combat', 'Feet', 'Both', 'Deathdealer''s Boots'), +(4, 1, 8, 0, 88, 21602, 'Phase 5', 'Rogue', 'Combat', 'Wrists', 'Both', 'Qiraji Execution Bracers'), +(4, 1, 9, 0, 88, 18823, 'Phase 5', 'Rogue', 'Combat', 'Hands', 'Both', 'Aged Core Leather Gloves'), +(4, 1, 10, 0, 88, 17063, 'Phase 5', 'Rogue', 'Combat', 'Finger1', 'Both', 'Band of Accuria'), +(4, 1, 11, 0, 88, 19384, 'Phase 5', 'Rogue', 'Combat', 'Finger2', 'Both', 'Master Dragonslayer''s Ring'), +(4, 1, 12, 0, 88, 19406, 'Phase 5', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(4, 1, 13, 0, 88, 23570, 'Phase 5', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Jom Gabbar'), +(4, 1, 14, 0, 88, 21701, 'Phase 5', 'Rogue', 'Combat', 'Back', 'Both', 'Cloak of Concentrated Hatred'), +(4, 1, 15, 0, 88, 21126, 'Phase 5', 'Rogue', 'Combat', 'MainHand', 'Both', 'Death''s Sting'), +(4, 1, 16, 0, 88, 21244, 'Phase 5', 'Rogue', 'Combat', 'OffHand', 'Both', 'Blessed Qiraji Pugio'), +(4, 1, 17, 0, 88, 17069, 'Phase 5', 'Rogue', 'Combat', 'Ranged', 'Both', 'Striker''s Mark'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 92, 22478, 'Phase 6', 'Rogue', 'Combat', 'Head', 'Both', 'Bonescythe Helmet'), +(4, 1, 1, 0, 92, 19377, 'Phase 6', 'Rogue', 'Combat', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(4, 1, 2, 0, 92, 22479, 'Phase 6', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Bonescythe Pauldrons'), +(4, 1, 4, 0, 92, 22476, 'Phase 6', 'Rogue', 'Combat', 'Chest', 'Both', 'Bonescythe Breastplate'), +(4, 1, 5, 0, 92, 21586, 'Phase 6', 'Rogue', 'Combat', 'Waist', 'Both', 'Belt of Never-ending Agony'), +(4, 1, 6, 0, 92, 22477, 'Phase 6', 'Rogue', 'Combat', 'Legs', 'Both', 'Bonescythe Legplates'), +(4, 1, 7, 0, 92, 22480, 'Phase 6', 'Rogue', 'Combat', 'Feet', 'Both', 'Bonescythe Sabatons'), +(4, 1, 8, 0, 92, 22483, 'Phase 6', 'Rogue', 'Combat', 'Wrists', 'Both', 'Bonescythe Bracers'), +(4, 1, 9, 0, 92, 22481, 'Phase 6', 'Rogue', 'Combat', 'Hands', 'Both', 'Bonescythe Gauntlets'), +(4, 1, 10, 0, 92, 23060, 'Phase 6', 'Rogue', 'Combat', 'Finger1', 'Both', 'Bonescythe Ring'), +(4, 1, 12, 0, 92, 23041, 'Phase 6', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Slayer''s Crest'), +(4, 1, 13, 0, 92, 22954, 'Phase 6', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Kiss of the Spider'), +(4, 1, 14, 0, 92, 23045, 'Phase 6', 'Rogue', 'Combat', 'Back', 'Both', 'Shroud of Dominion'), +(4, 1, 15, 0, 92, 22802, 'Phase 6', 'Rogue', 'Combat', 'MainHand', 'Both', 'Kingsfall'), +(4, 1, 16, 0, 92, 21126, 'Phase 6', 'Rogue', 'Combat', 'OffHand', 'Both', 'Death''s Sting'), +(4, 1, 17, 0, 92, 22812, 'Phase 6', 'Rogue', 'Combat', 'Ranged', 'Both', 'Nerubian Slavemaker'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 120, 32087, 'Pre-Raid', 'Rogue', 'Combat', 'Head', 'Both', 'Mask of the Deceiver'), +(4, 1, 1, 0, 120, 29381, 'Pre-Raid', 'Rogue', 'Combat', 'Neck', 'Both', 'Choker of Vile Intent'), +(4, 1, 2, 0, 120, 27797, 'Pre-Raid', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(4, 1, 4, 0, 120, 30730, 'Pre-Raid', 'Rogue', 'Combat', 'Chest', 'Both', 'Terrorweave Tunic'), +(4, 1, 5, 0, 120, 29247, 'Pre-Raid', 'Rogue', 'Combat', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(4, 1, 6, 0, 120, 30538, 'Pre-Raid', 'Rogue', 'Combat', 'Legs', 'Both', 'Midnight Legguards'), +(4, 1, 7, 0, 120, 25686, 'Pre-Raid', 'Rogue', 'Combat', 'Feet', 'Both', 'Fel Leather Boots'), +(4, 1, 8, 0, 120, 29246, 'Pre-Raid', 'Rogue', 'Combat', 'Wrists', 'Both', 'Nightfall Wristguards'), +(4, 1, 9, 0, 120, 27531, 'Pre-Raid', 'Rogue', 'Combat', 'Hands', 'Both', 'Wastewalker Gloves'), +(4, 1, 10, 0, 120, 30738, 'Pre-Raid', 'Rogue', 'Combat', 'Finger1', 'Both', 'Ring of Reciprocity'), +(4, 1, 12, 0, 120, 28288, 'Pre-Raid', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Abacus of Violent Odds'), +(4, 1, 13, 0, 120, 29383, 'Pre-Raid', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Bloodlust Brooch'), +(4, 1, 14, 0, 120, 24259, 'Pre-Raid', 'Rogue', 'Combat', 'Back', 'Both', 'Vengeance Wrap'), +(4, 1, 15, 0, 120, 31332, 'Pre-Raid', 'Rogue', 'Combat', 'MainHand', 'Both', 'Blinkstrike'), +(4, 1, 16, 0, 120, 28189, 'Pre-Raid', 'Rogue', 'Combat', 'OffHand', 'Both', 'Latro''s Shifting Sword'), +(4, 1, 17, 2, 120, 29152, 'Pre-Raid', 'Rogue', 'Combat', 'Ranged', 'Horde', 'Marksman''s Bow'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 125, 29044, 'Phase 1', 'Rogue', 'Combat', 'Head', 'Both', 'Netherblade Facemask'), +(4, 1, 1, 0, 125, 29381, 'Phase 1', 'Rogue', 'Combat', 'Neck', 'Both', 'Choker of Vile Intent'), +(4, 1, 2, 0, 125, 27797, 'Phase 1', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(4, 1, 4, 0, 125, 29045, 'Phase 1', 'Rogue', 'Combat', 'Chest', 'Both', 'Netherblade Chestpiece'), +(4, 1, 5, 0, 125, 29247, 'Phase 1', 'Rogue', 'Combat', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(4, 1, 6, 0, 125, 28741, 'Phase 1', 'Rogue', 'Combat', 'Legs', 'Both', 'Skulker''s Greaves'), +(4, 1, 7, 0, 125, 28545, 'Phase 1', 'Rogue', 'Combat', 'Feet', 'Both', 'Edgewalker Longboots'), +(4, 1, 8, 0, 125, 29246, 'Phase 1', 'Rogue', 'Combat', 'Wrists', 'Both', 'Nightfall Wristguards'), +(4, 1, 9, 0, 125, 27531, 'Phase 1', 'Rogue', 'Combat', 'Hands', 'Both', 'Wastewalker Gloves'), +(4, 1, 10, 0, 125, 28649, 'Phase 1', 'Rogue', 'Combat', 'Finger1', 'Both', 'Garona''s Signet Ring'), +(4, 1, 11, 0, 125, 28757, 'Phase 1', 'Rogue', 'Combat', 'Finger2', 'Both', 'Ring of a Thousand Marks'), +(4, 1, 12, 0, 125, 29383, 'Phase 1', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(4, 1, 13, 0, 125, 28830, 'Phase 1', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(4, 1, 14, 0, 125, 28672, 'Phase 1', 'Rogue', 'Combat', 'Back', 'Both', 'Drape of the Dark Reavers'), +(4, 1, 15, 0, 125, 31332, 'Phase 1', 'Rogue', 'Combat', 'MainHand', 'Both', 'Blinkstrike'), +(4, 1, 16, 0, 125, 28307, 'Phase 1', 'Rogue', 'Combat', 'OffHand', 'Both', 'Gladiator''s Quickblade'), +(4, 1, 17, 0, 125, 28772, 'Phase 1', 'Rogue', 'Combat', 'Ranged', 'Both', 'Sunfury Bow of the Phoenix'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 200, 42550, 'Pre-Raid', 'Rogue', 'Combat', 'Head', 'Both', 'Weakness Spectralizers'), +(4, 1, 1, 0, 200, 42645, 'Pre-Raid', 'Rogue', 'Combat', 'Neck', 'Both', 'Titanium Impact Choker'), +(4, 1, 2, 0, 200, 43481, 'Pre-Raid', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Trollwoven Spaulders'), +(4, 1, 4, 0, 200, 39558, 'Pre-Raid', 'Rogue', 'Combat', 'Chest', 'Both', 'Heroes'' Bonescythe Breastplate'), +(4, 1, 5, 0, 200, 43484, 'Pre-Raid', 'Rogue', 'Combat', 'Waist', 'Both', 'Trollwoven Girdle'), +(4, 1, 6, 0, 200, 39564, 'Pre-Raid', 'Rogue', 'Combat', 'Legs', 'Both', 'Heroes'' Bonescythe Legplates'), +(4, 1, 7, 0, 200, 37666, 'Pre-Raid', 'Rogue', 'Combat', 'Feet', 'Both', 'Boots of the Whirling Mist'), +(4, 1, 8, 0, 200, 37853, 'Pre-Raid', 'Rogue', 'Combat', 'Wrists', 'Both', 'Advanced Tooled-Leather Bands'), +(4, 1, 9, 0, 200, 39560, 'Pre-Raid', 'Rogue', 'Combat', 'Hands', 'Both', 'Heroes'' Bonescythe Gauntlets'), +(4, 1, 10, 0, 200, 42642, 'Pre-Raid', 'Rogue', 'Combat', 'Finger1', 'Both', 'Titanium Impact Band'), +(4, 1, 12, 0, 200, 40684, 'Pre-Raid', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Mirror of Truth'), +(4, 1, 14, 0, 200, 43566, 'Pre-Raid', 'Rogue', 'Combat', 'Back', 'Both', 'Ice Striker''s Cloak'), +(4, 1, 15, 0, 200, 37871, 'Pre-Raid', 'Rogue', 'Combat', 'MainHand', 'Both', 'The Key'), +(4, 1, 17, 0, 200, 43284, 'Pre-Raid', 'Rogue', 'Combat', 'Ranged', 'Both', 'Amanitar Skullbow'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 224, 40499, 'Phase 1', 'Rogue', 'Combat', 'Head', 'Both', 'Valorous Bonescythe Helmet'), +(4, 1, 1, 0, 224, 44664, 'Phase 1', 'Rogue', 'Combat', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(4, 1, 2, 0, 224, 40502, 'Phase 1', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Valorous Bonescythe Pauldrons'), +(4, 1, 4, 0, 224, 40539, 'Phase 1', 'Rogue', 'Combat', 'Chest', 'Both', 'Chestguard of the Recluse'), +(4, 1, 5, 0, 224, 40205, 'Phase 1', 'Rogue', 'Combat', 'Waist', 'Both', 'Stalk-Skin Belt'), +(4, 1, 6, 0, 224, 44011, 'Phase 1', 'Rogue', 'Combat', 'Legs', 'Both', 'Leggings of the Honored'), +(4, 1, 7, 0, 224, 39701, 'Phase 1', 'Rogue', 'Combat', 'Feet', 'Both', 'Dawnwalkers'), +(4, 1, 8, 0, 224, 39765, 'Phase 1', 'Rogue', 'Combat', 'Wrists', 'Both', 'Sinner''s Bindings'), +(4, 1, 9, 0, 224, 40541, 'Phase 1', 'Rogue', 'Combat', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(4, 1, 10, 0, 224, 40074, 'Phase 1', 'Rogue', 'Combat', 'Finger1', 'Both', 'Strong-Handed Ring'), +(4, 1, 12, 0, 224, 40684, 'Phase 1', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Mirror of Truth'), +(4, 1, 14, 0, 224, 40403, 'Phase 1', 'Rogue', 'Combat', 'Back', 'Both', 'Drape of the Deadly Foe'), +(4, 1, 15, 0, 224, 40383, 'Phase 1', 'Rogue', 'Combat', 'MainHand', 'Both', 'Calamity''s Grasp'), +(4, 1, 17, 0, 224, 40385, 'Phase 1', 'Rogue', 'Combat', 'Ranged', 'Both', 'Envoy of Mortality'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 245, 46125, 'Phase 2', 'Rogue', 'Combat', 'Head', 'Both', 'Conqueror''s Terrorblade Helmet'), +(4, 1, 1, 0, 245, 45517, 'Phase 2', 'Rogue', 'Combat', 'Neck', 'Both', 'Pendulum of Infinity'), +(4, 1, 2, 0, 245, 46127, 'Phase 2', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Conqueror''s Terrorblade Pauldrons'), +(4, 1, 4, 0, 245, 45473, 'Phase 2', 'Rogue', 'Combat', 'Chest', 'Both', 'Embrace of the Gladiator'), +(4, 1, 5, 0, 245, 46095, 'Phase 2', 'Rogue', 'Combat', 'Waist', 'Both', 'Soul-Devouring Cinch'), +(4, 1, 6, 0, 245, 45536, 'Phase 2', 'Rogue', 'Combat', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(4, 1, 7, 0, 245, 45564, 'Phase 2', 'Rogue', 'Combat', 'Feet', 'Both', 'Footpads of Silence'), +(4, 1, 8, 0, 245, 45611, 'Phase 2', 'Rogue', 'Combat', 'Wrists', 'Both', 'Solar Bindings'), +(4, 1, 9, 0, 245, 46124, 'Phase 2', 'Rogue', 'Combat', 'Hands', 'Both', 'Conqueror''s Terrorblade Gauntlets'), +(4, 1, 10, 0, 245, 45456, 'Phase 2', 'Rogue', 'Combat', 'Finger1', 'Both', 'Loop of the Agile'), +(4, 1, 12, 0, 245, 45609, 'Phase 2', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Comet''s Trail'), +(4, 1, 14, 0, 245, 46032, 'Phase 2', 'Rogue', 'Combat', 'Back', 'Both', 'Drape of the Faceless General'), +(4, 1, 17, 0, 245, 45296, 'Phase 2', 'Rogue', 'Combat', 'Ranged', 'Both', 'Twirling Blades'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 258, 48230, 'Phase 3', 'Rogue', 'Combat', 'Head', 'Both', 'Helmet of Triumph'), +(4, 1, 2, 0, 258, 48228, 'Phase 3', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Pauldrons of Triumph'), +(4, 1, 4, 0, 258, 48232, 'Phase 3', 'Rogue', 'Combat', 'Chest', 'Both', 'Breastplate of Triumph'), +(4, 1, 5, 1, 258, 47112, 'Phase 3', 'Rogue', 'Combat', 'Waist', 'Alliance', 'Belt of the Merciless Killer'), +(4, 1, 5, 2, 258, 47460, 'Phase 3', 'Rogue', 'Combat', 'Waist', 'Horde', 'Belt of the Pitiless Killer'), +(4, 1, 6, 1, 258, 46975, 'Phase 3', 'Rogue', 'Combat', 'Legs', 'Alliance', 'Leggings of the Broken Beast'), +(4, 1, 6, 2, 258, 47420, 'Phase 3', 'Rogue', 'Combat', 'Legs', 'Horde', 'Legwraps of the Broken Beast'), +(4, 1, 7, 1, 258, 47077, 'Phase 3', 'Rogue', 'Combat', 'Feet', 'Alliance', 'Treads of the Icewalker'), +(4, 1, 7, 2, 258, 47445, 'Phase 3', 'Rogue', 'Combat', 'Feet', 'Horde', 'Icewalker Treads'), +(4, 1, 8, 1, 258, 47155, 'Phase 3', 'Rogue', 'Combat', 'Wrists', 'Alliance', 'Bracers of Dark Determination'), +(4, 1, 8, 2, 258, 47474, 'Phase 3', 'Rogue', 'Combat', 'Wrists', 'Horde', 'Armbands of Dark Determination'), +(4, 1, 9, 0, 258, 48231, 'Phase 3', 'Rogue', 'Combat', 'Hands', 'Both', 'Gauntlets of Triumph'), +(4, 1, 10, 1, 258, 47075, 'Phase 3', 'Rogue', 'Combat', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(4, 1, 10, 2, 258, 47443, 'Phase 3', 'Rogue', 'Combat', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(4, 1, 12, 1, 258, 47131, 'Phase 3', 'Rogue', 'Combat', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(4, 1, 12, 2, 258, 47464, 'Phase 3', 'Rogue', 'Combat', 'Trinket1', 'Horde', 'Death''s Choice'), +(4, 1, 14, 1, 258, 47545, 'Phase 3', 'Rogue', 'Combat', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(4, 1, 14, 2, 258, 47546, 'Phase 3', 'Rogue', 'Combat', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(4, 1, 17, 1, 258, 47521, 'Phase 3', 'Rogue', 'Combat', 'Ranged', 'Alliance', 'BRK 1000'), +(4, 1, 17, 2, 258, 47523, 'Phase 3', 'Rogue', 'Combat', 'Ranged', 'Horde', 'Fezzik''s Autocannon'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 264, 51252, 'Phase 4', 'Rogue', 'Combat', 'Head', 'Both', 'Sanctified Shadowblade Helmet'), +(4, 1, 2, 0, 264, 51254, 'Phase 4', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Sanctified Shadowblade Pauldrons'), +(4, 1, 4, 0, 264, 50656, 'Phase 4', 'Rogue', 'Combat', 'Chest', 'Both', 'Ikfirus''s Sack of Wonder'), +(4, 1, 5, 0, 264, 50707, 'Phase 4', 'Rogue', 'Combat', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(4, 1, 6, 0, 264, 50697, 'Phase 4', 'Rogue', 'Combat', 'Legs', 'Both', 'Gangrenous Leggings'), +(4, 1, 7, 0, 264, 50607, 'Phase 4', 'Rogue', 'Combat', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(4, 1, 8, 0, 264, 50670, 'Phase 4', 'Rogue', 'Combat', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(4, 1, 9, 0, 264, 50675, 'Phase 4', 'Rogue', 'Combat', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(4, 1, 10, 0, 264, 50618, 'Phase 4', 'Rogue', 'Combat', 'Finger1', 'Both', 'Frostbrood Sapphire Ring'), +(4, 1, 12, 0, 264, 50363, 'Phase 4', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(4, 1, 14, 1, 264, 47545, 'Phase 4', 'Rogue', 'Combat', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(4, 1, 14, 2, 264, 47546, 'Phase 4', 'Rogue', 'Combat', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(4, 1, 17, 0, 264, 50733, 'Phase 4', 'Rogue', 'Combat', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 1, 0, 0, 290, 51252, 'Phase 5', 'Rogue', 'Combat', 'Head', 'Both', 'Sanctified Shadowblade Helmet'), +(4, 1, 1, 0, 290, 50633, 'Phase 5', 'Rogue', 'Combat', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(4, 1, 2, 0, 290, 51254, 'Phase 5', 'Rogue', 'Combat', 'Shoulders', 'Both', 'Sanctified Shadowblade Pauldrons'), +(4, 1, 4, 0, 290, 51250, 'Phase 5', 'Rogue', 'Combat', 'Chest', 'Both', 'Sanctified Shadowblade Breastplate'), +(4, 1, 5, 0, 290, 50707, 'Phase 5', 'Rogue', 'Combat', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(4, 1, 6, 0, 290, 51253, 'Phase 5', 'Rogue', 'Combat', 'Legs', 'Both', 'Sanctified Shadowblade Legplates'), +(4, 1, 7, 0, 290, 50607, 'Phase 5', 'Rogue', 'Combat', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(4, 1, 8, 0, 290, 50670, 'Phase 5', 'Rogue', 'Combat', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(4, 1, 9, 0, 290, 50675, 'Phase 5', 'Rogue', 'Combat', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(4, 1, 10, 0, 290, 50402, 'Phase 5', 'Rogue', 'Combat', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(4, 1, 11, 0, 290, 50618, 'Phase 5', 'Rogue', 'Combat', 'Finger2', 'Both', 'Frostbrood Sapphire Ring'), +(4, 1, 12, 0, 290, 50363, 'Phase 5', 'Rogue', 'Combat', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(4, 1, 13, 0, 290, 54590, 'Phase 5', 'Rogue', 'Combat', 'Trinket2', 'Both', 'Sharpened Twilight Scale'), +(4, 1, 14, 0, 290, 47545, 'Phase 5', 'Rogue', 'Combat', 'Back', 'Both', 'Vereesa''s Dexterity'), +(4, 1, 15, 0, 290, 50737, 'Phase 5', 'Rogue', 'Combat', 'MainHand', 'Both', 'Havoc''s Call, Blade of Lordaeron Kings'), +(4, 1, 16, 0, 290, 50654, 'Phase 5', 'Rogue', 'Combat', 'OffHand', 'Both', 'Scourgeborne Waraxe'), +(4, 1, 17, 0, 290, 50733, 'Phase 5', 'Rogue', 'Combat', 'Ranged', 'Both', 'Fal''inrush, Defender of Quel''thalas'); + +-- Subtlety (tab 2) +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 2, 0, 0, 120, 32087, 'Pre-Raid', 'Rogue', 'Subtlety', 'Head', 'Both', 'Mask of the Deceiver'), +(4, 2, 1, 0, 120, 29381, 'Pre-Raid', 'Rogue', 'Subtlety', 'Neck', 'Both', 'Choker of Vile Intent'), +(4, 2, 2, 0, 120, 27797, 'Pre-Raid', 'Rogue', 'Subtlety', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(4, 2, 4, 0, 120, 30730, 'Pre-Raid', 'Rogue', 'Subtlety', 'Chest', 'Both', 'Terrorweave Tunic'), +(4, 2, 5, 0, 120, 29247, 'Pre-Raid', 'Rogue', 'Subtlety', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(4, 2, 6, 0, 120, 30538, 'Pre-Raid', 'Rogue', 'Subtlety', 'Legs', 'Both', 'Midnight Legguards'), +(4, 2, 7, 0, 120, 25686, 'Pre-Raid', 'Rogue', 'Subtlety', 'Feet', 'Both', 'Fel Leather Boots'), +(4, 2, 8, 0, 120, 29246, 'Pre-Raid', 'Rogue', 'Subtlety', 'Wrists', 'Both', 'Nightfall Wristguards'), +(4, 2, 9, 0, 120, 27531, 'Pre-Raid', 'Rogue', 'Subtlety', 'Hands', 'Both', 'Wastewalker Gloves'), +(4, 2, 10, 0, 120, 30738, 'Pre-Raid', 'Rogue', 'Subtlety', 'Finger1', 'Both', 'Ring of Reciprocity'), +(4, 2, 12, 0, 120, 28288, 'Pre-Raid', 'Rogue', 'Subtlety', 'Trinket1', 'Both', 'Abacus of Violent Odds'), +(4, 2, 13, 0, 120, 29383, 'Pre-Raid', 'Rogue', 'Subtlety', 'Trinket2', 'Both', 'Bloodlust Brooch'), +(4, 2, 14, 0, 120, 24259, 'Pre-Raid', 'Rogue', 'Subtlety', 'Back', 'Both', 'Vengeance Wrap'), +(4, 2, 15, 0, 120, 31332, 'Pre-Raid', 'Rogue', 'Subtlety', 'MainHand', 'Both', 'Blinkstrike'), +(4, 2, 16, 0, 120, 28189, 'Pre-Raid', 'Rogue', 'Subtlety', 'OffHand', 'Both', 'Latro''s Shifting Sword'), +(4, 2, 17, 2, 120, 29152, 'Pre-Raid', 'Rogue', 'Subtlety', 'Ranged', 'Horde', 'Marksman''s Bow'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(4, 2, 0, 0, 125, 29044, 'Phase 1', 'Rogue', 'Subtlety', 'Head', 'Both', 'Netherblade Facemask'), +(4, 2, 1, 0, 125, 29381, 'Phase 1', 'Rogue', 'Subtlety', 'Neck', 'Both', 'Choker of Vile Intent'), +(4, 2, 2, 0, 125, 27797, 'Phase 1', 'Rogue', 'Subtlety', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(4, 2, 4, 0, 125, 29045, 'Phase 1', 'Rogue', 'Subtlety', 'Chest', 'Both', 'Netherblade Chestpiece'), +(4, 2, 5, 0, 125, 29247, 'Phase 1', 'Rogue', 'Subtlety', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(4, 2, 6, 0, 125, 28741, 'Phase 1', 'Rogue', 'Subtlety', 'Legs', 'Both', 'Skulker''s Greaves'), +(4, 2, 7, 0, 125, 28545, 'Phase 1', 'Rogue', 'Subtlety', 'Feet', 'Both', 'Edgewalker Longboots'), +(4, 2, 8, 0, 125, 29246, 'Phase 1', 'Rogue', 'Subtlety', 'Wrists', 'Both', 'Nightfall Wristguards'), +(4, 2, 9, 0, 125, 27531, 'Phase 1', 'Rogue', 'Subtlety', 'Hands', 'Both', 'Wastewalker Gloves'), +(4, 2, 10, 0, 125, 28649, 'Phase 1', 'Rogue', 'Subtlety', 'Finger1', 'Both', 'Garona''s Signet Ring'), +(4, 2, 11, 0, 125, 28757, 'Phase 1', 'Rogue', 'Subtlety', 'Finger2', 'Both', 'Ring of a Thousand Marks'), +(4, 2, 12, 0, 125, 29383, 'Phase 1', 'Rogue', 'Subtlety', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(4, 2, 13, 0, 125, 28830, 'Phase 1', 'Rogue', 'Subtlety', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(4, 2, 14, 0, 125, 28672, 'Phase 1', 'Rogue', 'Subtlety', 'Back', 'Both', 'Drape of the Dark Reavers'), +(4, 2, 15, 0, 125, 31332, 'Phase 1', 'Rogue', 'Subtlety', 'MainHand', 'Both', 'Blinkstrike'), +(4, 2, 16, 0, 125, 28307, 'Phase 1', 'Rogue', 'Subtlety', 'OffHand', 'Both', 'Gladiator''s Quickblade'), +(4, 2, 17, 0, 125, 28772, 'Phase 1', 'Rogue', 'Subtlety', 'Ranged', 'Both', 'Sunfury Bow of the Phoenix'); + + +-- ============================================================ +-- Priest (5) +-- ============================================================ +-- Discipline (tab 0) +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 120, 24264, 'Pre-Raid', 'Priest', 'Discipline', 'Head', 'Both', 'Whitemend Hood'), +(5, 0, 1, 0, 120, 30377, 'Pre-Raid', 'Priest', 'Discipline', 'Neck', 'Both', 'Karja''s Medallion'), +(5, 0, 2, 0, 120, 21874, 'Pre-Raid', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Primal Mooncloth Shoulders'), +(5, 0, 4, 0, 120, 21875, 'Pre-Raid', 'Priest', 'Discipline', 'Chest', 'Both', 'Primal Mooncloth Robe'), +(5, 0, 5, 0, 120, 21873, 'Pre-Raid', 'Priest', 'Discipline', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(5, 0, 6, 0, 120, 24261, 'Pre-Raid', 'Priest', 'Discipline', 'Legs', 'Both', 'Whitemend Pants'), +(5, 0, 7, 0, 120, 29251, 'Pre-Raid', 'Priest', 'Discipline', 'Feet', 'Both', 'Boots of the Pious'), +(5, 0, 8, 0, 120, 29249, 'Pre-Raid', 'Priest', 'Discipline', 'Wrists', 'Both', 'Bands of the Benevolent'), +(5, 0, 9, 0, 120, 24393, 'Pre-Raid', 'Priest', 'Discipline', 'Hands', 'Both', 'Bloody Surgeon''s Mitts'), +(5, 0, 10, 0, 120, 27780, 'Pre-Raid', 'Priest', 'Discipline', 'Finger1', 'Both', 'Ring of Fabled Hope'), +(5, 0, 10, 2, 120, 29168, 'Pre-Raid', 'Priest', 'Discipline', 'Finger1', 'Horde', 'Ancestral Band'), +(5, 0, 12, 0, 120, 19288, 'Pre-Raid', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Darkmoon Card: Blue Dragon'), +(5, 0, 13, 0, 120, 29376, 'Pre-Raid', 'Priest', 'Discipline', 'Trinket2', 'Both', 'Essence of the Martyr'), +(5, 0, 14, 0, 120, 29354, 'Pre-Raid', 'Priest', 'Discipline', 'Back', 'Both', 'Light-Touched Stole of Altruism'), +(5, 0, 15, 0, 120, 29353, 'Pre-Raid', 'Priest', 'Discipline', 'MainHand', 'Both', 'Shockwave Truncheon'), +(5, 0, 16, 0, 120, 29170, 'Pre-Raid', 'Priest', 'Discipline', 'OffHand', 'Both', 'Windcaller''s Orb'), +(5, 0, 17, 0, 120, 27885, 'Pre-Raid', 'Priest', 'Discipline', 'Ranged', 'Both', 'Soul-Wand of the Aldor'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 125, 29049, 'Phase 1', 'Priest', 'Discipline', 'Head', 'Both', 'Light-Collar of the Incarnate'), +(5, 0, 1, 0, 125, 30726, 'Phase 1', 'Priest', 'Discipline', 'Neck', 'Both', 'Archaic Charm of Presence'), +(5, 0, 2, 0, 125, 21874, 'Phase 1', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Primal Mooncloth Shoulders'), +(5, 0, 4, 0, 125, 21875, 'Phase 1', 'Priest', 'Discipline', 'Chest', 'Both', 'Primal Mooncloth Robe'), +(5, 0, 5, 0, 125, 21873, 'Phase 1', 'Priest', 'Discipline', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(5, 0, 6, 0, 125, 30727, 'Phase 1', 'Priest', 'Discipline', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(5, 0, 7, 0, 125, 28663, 'Phase 1', 'Priest', 'Discipline', 'Feet', 'Both', 'Boots of the Incorrupt'), +(5, 0, 8, 0, 125, 29249, 'Phase 1', 'Priest', 'Discipline', 'Wrists', 'Both', 'Bands of the Benevolent'), +(5, 0, 9, 0, 125, 28508, 'Phase 1', 'Priest', 'Discipline', 'Hands', 'Both', 'Gloves of Saintly Blessings'), +(5, 0, 10, 0, 125, 29290, 'Phase 1', 'Priest', 'Discipline', 'Finger1', 'Both', 'Violet Signet of the Grand Restorer'), +(5, 0, 11, 0, 125, 28763, 'Phase 1', 'Priest', 'Discipline', 'Finger2', 'Both', 'Jade Ring of the Everliving'), +(5, 0, 12, 0, 125, 29376, 'Phase 1', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 0, 13, 0, 125, 28823, 'Phase 1', 'Priest', 'Discipline', 'Trinket2', 'Both', 'Eye of Gruul'), +(5, 0, 14, 0, 125, 28765, 'Phase 1', 'Priest', 'Discipline', 'Back', 'Both', 'Stainless Cloak of the Pure Hearted'), +(5, 0, 15, 0, 125, 28771, 'Phase 1', 'Priest', 'Discipline', 'MainHand', 'Both', 'Light''s Justice'), +(5, 0, 16, 0, 125, 29170, 'Phase 1', 'Priest', 'Discipline', 'OffHand', 'Both', 'Windcaller''s Orb'), +(5, 0, 17, 0, 125, 28588, 'Phase 1', 'Priest', 'Discipline', 'Ranged', 'Both', 'Blue Diamond Witchwand'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 141, 30152, 'Phase 2', 'Priest', 'Discipline', 'Head', 'Both', 'Cowl of the Avatar'), +(5, 0, 1, 0, 141, 30018, 'Phase 2', 'Priest', 'Discipline', 'Neck', 'Both', 'Lord Sanguinar''s Claim'), +(5, 0, 2, 0, 141, 30154, 'Phase 2', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Mantle of the Avatar'), +(5, 0, 4, 0, 141, 30150, 'Phase 2', 'Priest', 'Discipline', 'Chest', 'Both', 'Vestments of the Avatar'), +(5, 0, 5, 0, 141, 30036, 'Phase 2', 'Priest', 'Discipline', 'Waist', 'Both', 'Belt of the Long Road'), +(5, 0, 6, 0, 141, 30727, 'Phase 2', 'Priest', 'Discipline', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(5, 0, 7, 0, 141, 30100, 'Phase 2', 'Priest', 'Discipline', 'Feet', 'Both', 'Soul-Strider Boots'), +(5, 0, 8, 0, 141, 32516, 'Phase 2', 'Priest', 'Discipline', 'Wrists', 'Both', 'Wraps of Purification'), +(5, 0, 9, 0, 141, 30151, 'Phase 2', 'Priest', 'Discipline', 'Hands', 'Both', 'Gloves of the Avatar'), +(5, 0, 10, 0, 141, 30110, 'Phase 2', 'Priest', 'Discipline', 'Finger1', 'Both', 'Coral Band of the Revived'), +(5, 0, 11, 0, 141, 29290, 'Phase 2', 'Priest', 'Discipline', 'Finger2', 'Both', 'Violet Signet of the Grand Restorer'), +(5, 0, 12, 0, 141, 29376, 'Phase 2', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 0, 13, 0, 141, 28823, 'Phase 2', 'Priest', 'Discipline', 'Trinket2', 'Both', 'Eye of Gruul'), +(5, 0, 14, 0, 141, 29989, 'Phase 2', 'Priest', 'Discipline', 'Back', 'Both', 'Sunshower Light Cloak'), +(5, 0, 15, 0, 141, 30108, 'Phase 2', 'Priest', 'Discipline', 'MainHand', 'Both', 'Lightfathom Scepter'), +(5, 0, 16, 0, 141, 29923, 'Phase 2', 'Priest', 'Discipline', 'OffHand', 'Both', 'Talisman of the Sun King'), +(5, 0, 17, 0, 141, 30080, 'Phase 2', 'Priest', 'Discipline', 'Ranged', 'Both', 'Luminescent Rod of the Naaru'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 156, 31063, 'Phase 3', 'Priest', 'Discipline', 'Head', 'Both', 'Cowl of Absolution'), +(5, 0, 1, 0, 156, 32370, 'Phase 3', 'Priest', 'Discipline', 'Neck', 'Both', 'Nadina''s Pendant of Purity'), +(5, 0, 2, 0, 156, 31069, 'Phase 3', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Mantle of Absolution'), +(5, 0, 4, 0, 156, 31066, 'Phase 3', 'Priest', 'Discipline', 'Chest', 'Both', 'Vestments of Absolution'), +(5, 0, 5, 0, 156, 32519, 'Phase 3', 'Priest', 'Discipline', 'Waist', 'Both', 'Belt of Divine Guidance'), +(5, 0, 6, 0, 156, 30912, 'Phase 3', 'Priest', 'Discipline', 'Legs', 'Both', 'Leggings of Eternity'), +(5, 0, 7, 0, 156, 32609, 'Phase 3', 'Priest', 'Discipline', 'Feet', 'Both', 'Boots of the Divine Light'), +(5, 0, 8, 0, 156, 30871, 'Phase 3', 'Priest', 'Discipline', 'Wrists', 'Both', 'Bracers of Martyrdom'), +(5, 0, 9, 0, 156, 31060, 'Phase 3', 'Priest', 'Discipline', 'Hands', 'Both', 'Gloves of Absolution'), +(5, 0, 10, 0, 156, 32528, 'Phase 3', 'Priest', 'Discipline', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(5, 0, 11, 0, 156, 32528, 'Phase 3', 'Priest', 'Discipline', 'Finger2', 'Both', 'Blessed Band of Karabor'), +(5, 0, 12, 0, 156, 29376, 'Phase 3', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 0, 13, 0, 156, 32496, 'Phase 3', 'Priest', 'Discipline', 'Trinket2', 'Both', 'Memento of Tyrande'), +(5, 0, 14, 0, 156, 32524, 'Phase 3', 'Priest', 'Discipline', 'Back', 'Both', 'Shroud of the Highborne'), +(5, 0, 15, 0, 156, 32500, 'Phase 3', 'Priest', 'Discipline', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(5, 0, 16, 0, 156, 30911, 'Phase 3', 'Priest', 'Discipline', 'OffHand', 'Both', 'Scepter of Purification'), +(5, 0, 17, 0, 156, 32363, 'Phase 3', 'Priest', 'Discipline', 'Ranged', 'Both', 'Naaru-Blessed Life Rod'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 164, 31063, 'Phase 4', 'Priest', 'Discipline', 'Head', 'Both', 'Cowl of Absolution'), +(5, 0, 1, 0, 164, 33281, 'Phase 4', 'Priest', 'Discipline', 'Neck', 'Both', 'Brooch of Nature''s Mercy'), +(5, 0, 2, 0, 164, 31069, 'Phase 4', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Mantle of Absolution'), +(5, 0, 4, 0, 164, 31066, 'Phase 4', 'Priest', 'Discipline', 'Chest', 'Both', 'Vestments of Absolution'), +(5, 0, 5, 0, 164, 32519, 'Phase 4', 'Priest', 'Discipline', 'Waist', 'Both', 'Belt of Divine Guidance'), +(5, 0, 6, 0, 164, 30912, 'Phase 4', 'Priest', 'Discipline', 'Legs', 'Both', 'Leggings of Eternity'), +(5, 0, 7, 0, 164, 32609, 'Phase 4', 'Priest', 'Discipline', 'Feet', 'Both', 'Boots of the Divine Light'), +(5, 0, 8, 0, 164, 30871, 'Phase 4', 'Priest', 'Discipline', 'Wrists', 'Both', 'Bracers of Martyrdom'), +(5, 0, 9, 0, 164, 31060, 'Phase 4', 'Priest', 'Discipline', 'Hands', 'Both', 'Gloves of Absolution'), +(5, 0, 10, 0, 164, 32528, 'Phase 4', 'Priest', 'Discipline', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(5, 0, 11, 0, 164, 30110, 'Phase 4', 'Priest', 'Discipline', 'Finger2', 'Both', 'Coral Band of the Revived'), +(5, 0, 12, 0, 164, 29376, 'Phase 4', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 0, 13, 0, 164, 32496, 'Phase 4', 'Priest', 'Discipline', 'Trinket2', 'Both', 'Memento of Tyrande'), +(5, 0, 14, 0, 164, 32524, 'Phase 4', 'Priest', 'Discipline', 'Back', 'Both', 'Shroud of the Highborne'), +(5, 0, 15, 0, 164, 32500, 'Phase 4', 'Priest', 'Discipline', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(5, 0, 16, 0, 164, 30911, 'Phase 4', 'Priest', 'Discipline', 'OffHand', 'Both', 'Scepter of Purification'), +(5, 0, 17, 0, 164, 32363, 'Phase 4', 'Priest', 'Discipline', 'Ranged', 'Both', 'Naaru-Blessed Life Rod'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 200, 37294, 'Pre-Raid', 'Priest', 'Discipline', 'Head', 'Both', 'Crown of Unbridled Magic'), +(5, 0, 1, 0, 200, 40681, 'Pre-Raid', 'Priest', 'Discipline', 'Neck', 'Both', 'Lattice Choker of Light'), +(5, 0, 2, 0, 200, 37673, 'Pre-Raid', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Dark Runic Mantle'), +(5, 0, 4, 0, 200, 44180, 'Pre-Raid', 'Priest', 'Discipline', 'Chest', 'Both', 'Robes of Crackling Flame'), +(5, 0, 5, 0, 200, 40697, 'Pre-Raid', 'Priest', 'Discipline', 'Waist', 'Both', 'Elegant Temple Gardens'' Girdle'), +(5, 0, 6, 0, 200, 37854, 'Pre-Raid', 'Priest', 'Discipline', 'Legs', 'Both', 'Woven Bracae Leggings'), +(5, 0, 7, 0, 200, 44202, 'Pre-Raid', 'Priest', 'Discipline', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(5, 0, 8, 0, 200, 37361, 'Pre-Raid', 'Priest', 'Discipline', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(5, 0, 9, 0, 200, 37172, 'Pre-Raid', 'Priest', 'Discipline', 'Hands', 'Both', 'Gloves of Glistening Runes'), +(5, 0, 10, 0, 200, 37694, 'Pre-Raid', 'Priest', 'Discipline', 'Finger1', 'Both', 'Band of Guile'), +(5, 0, 12, 0, 200, 37835, 'Pre-Raid', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Je''Tze''s Bell'), +(5, 0, 14, 0, 200, 41610, 'Pre-Raid', 'Priest', 'Discipline', 'Back', 'Both', 'Deathchill Cloak'), +(5, 0, 15, 0, 200, 37169, 'Pre-Raid', 'Priest', 'Discipline', 'MainHand', 'Both', 'War Mace of Unrequited Love'), +(5, 0, 17, 0, 200, 37619, 'Pre-Raid', 'Priest', 'Discipline', 'Ranged', 'Both', 'Wand of Ahn''kahet'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 224, 40447, 'Phase 1', 'Priest', 'Discipline', 'Head', 'Both', 'Valorous Crown of Faith'), +(5, 0, 1, 0, 224, 44661, 'Phase 1', 'Priest', 'Discipline', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(5, 0, 2, 0, 224, 40450, 'Phase 1', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Valorous Shoulderpads of Faith'), +(5, 0, 4, 0, 224, 44002, 'Phase 1', 'Priest', 'Discipline', 'Chest', 'Both', 'The Sanctum''s Flowing Vestments'), +(5, 0, 5, 0, 224, 40561, 'Phase 1', 'Priest', 'Discipline', 'Waist', 'Both', 'Leash of Heedless Magic'), +(5, 0, 6, 0, 224, 40448, 'Phase 1', 'Priest', 'Discipline', 'Legs', 'Both', 'Valorous Leggings of Faith'), +(5, 0, 7, 0, 224, 40558, 'Phase 1', 'Priest', 'Discipline', 'Feet', 'Both', 'Arcanic Tramplers'), +(5, 0, 8, 0, 224, 44008, 'Phase 1', 'Priest', 'Discipline', 'Wrists', 'Both', 'Unsullied Cuffs'), +(5, 0, 9, 0, 224, 40445, 'Phase 1', 'Priest', 'Discipline', 'Hands', 'Both', 'Valorous Gloves of Faith'), +(5, 0, 10, 0, 224, 40399, 'Phase 1', 'Priest', 'Discipline', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(5, 0, 12, 0, 224, 37835, 'Phase 1', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Je''Tze''s Bell'), +(5, 0, 14, 0, 224, 44005, 'Phase 1', 'Priest', 'Discipline', 'Back', 'Both', 'Pennant Cloak'), +(5, 0, 17, 0, 224, 39426, 'Phase 1', 'Priest', 'Discipline', 'Ranged', 'Both', 'Wand of the Archlich'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 245, 46197, 'Phase 2', 'Priest', 'Discipline', 'Head', 'Both', 'Conqueror''s Cowl of Sanctification'), +(5, 0, 1, 0, 245, 45443, 'Phase 2', 'Priest', 'Discipline', 'Neck', 'Both', 'Charm of Meticulous Timing'), +(5, 0, 2, 0, 245, 46190, 'Phase 2', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Conqueror''s Shoulderpads of Sanctification'), +(5, 0, 4, 0, 245, 46193, 'Phase 2', 'Priest', 'Discipline', 'Chest', 'Both', 'Conqueror''s Robe of Sanctification'), +(5, 0, 5, 0, 245, 45619, 'Phase 2', 'Priest', 'Discipline', 'Waist', 'Both', 'Starwatcher''s Binding'), +(5, 0, 6, 0, 245, 46195, 'Phase 2', 'Priest', 'Discipline', 'Legs', 'Both', 'Conqueror''s Leggings of Sanctification'), +(5, 0, 7, 0, 245, 45135, 'Phase 2', 'Priest', 'Discipline', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(5, 0, 8, 0, 245, 45446, 'Phase 2', 'Priest', 'Discipline', 'Wrists', 'Both', 'Grasps of Reason'), +(5, 0, 9, 0, 245, 45520, 'Phase 2', 'Priest', 'Discipline', 'Hands', 'Both', 'Handwraps of the Vigilant'), +(5, 0, 12, 0, 245, 45535, 'Phase 2', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Show of Faith'), +(5, 0, 14, 0, 245, 45486, 'Phase 2', 'Priest', 'Discipline', 'Back', 'Both', 'Drape of the Sullen Goddess'), +(5, 0, 15, 0, 245, 46017, 'Phase 2', 'Priest', 'Discipline', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(5, 0, 17, 0, 245, 45294, 'Phase 2', 'Priest', 'Discipline', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 258, 46197, 'Phase 3', 'Priest', 'Discipline', 'Head', 'Both', 'Conqueror''s Cowl of Sanctification'), +(5, 0, 2, 0, 258, 46190, 'Phase 3', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Conqueror''s Shoulderpads of Sanctification'), +(5, 0, 4, 0, 258, 48031, 'Phase 3', 'Priest', 'Discipline', 'Chest', 'Both', 'Robe of Triumph'), +(5, 0, 5, 1, 258, 47084, 'Phase 3', 'Priest', 'Discipline', 'Waist', 'Alliance', 'Cord of Biting Cold'), +(5, 0, 5, 2, 258, 47447, 'Phase 3', 'Priest', 'Discipline', 'Waist', 'Horde', 'Belt of Biting Cold'), +(5, 0, 6, 1, 258, 47189, 'Phase 3', 'Priest', 'Discipline', 'Legs', 'Alliance', 'Leggings of the Deepening Void'), +(5, 0, 6, 2, 258, 47478, 'Phase 3', 'Priest', 'Discipline', 'Legs', 'Horde', 'Breeches of the Deepening Void'), +(5, 0, 7, 1, 258, 47097, 'Phase 3', 'Priest', 'Discipline', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(5, 0, 7, 2, 258, 47454, 'Phase 3', 'Priest', 'Discipline', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(5, 0, 8, 1, 258, 47143, 'Phase 3', 'Priest', 'Discipline', 'Wrists', 'Alliance', 'Bindings of Dark Essence'), +(5, 0, 8, 2, 258, 47467, 'Phase 3', 'Priest', 'Discipline', 'Wrists', 'Horde', 'Dark Essence Bindings'), +(5, 0, 9, 0, 258, 46188, 'Phase 3', 'Priest', 'Discipline', 'Hands', 'Both', 'Conqueror''s Gloves of Sanctification'), +(5, 0, 10, 1, 258, 47237, 'Phase 3', 'Priest', 'Discipline', 'Finger1', 'Alliance', 'Band of Deplorable Violence'), +(5, 0, 10, 2, 258, 47489, 'Phase 3', 'Priest', 'Discipline', 'Finger1', 'Horde', 'Lurid Manifestation'), +(5, 0, 12, 1, 258, 47059, 'Phase 3', 'Priest', 'Discipline', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(5, 0, 12, 2, 258, 47432, 'Phase 3', 'Priest', 'Discipline', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(5, 0, 14, 1, 258, 47552, 'Phase 3', 'Priest', 'Discipline', 'Back', 'Alliance', 'Jaina''s Radiance'), +(5, 0, 14, 2, 258, 47551, 'Phase 3', 'Priest', 'Discipline', 'Back', 'Horde', 'Aethas'' Intensity'), +(5, 0, 17, 0, 258, 45294, 'Phase 3', 'Priest', 'Discipline', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 264, 51261, 'Phase 4', 'Priest', 'Discipline', 'Head', 'Both', 'Sanctified Crimson Acolyte Hood'), +(5, 0, 2, 0, 264, 51264, 'Phase 4', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Sanctified Crimson Acolyte Shoulderpads'), +(5, 0, 4, 0, 264, 51263, 'Phase 4', 'Priest', 'Discipline', 'Chest', 'Both', 'Sanctified Crimson Acolyte Robe'), +(5, 0, 5, 0, 264, 50702, 'Phase 4', 'Priest', 'Discipline', 'Waist', 'Both', 'Lingering Illness'), +(5, 0, 6, 0, 264, 51262, 'Phase 4', 'Priest', 'Discipline', 'Legs', 'Both', 'Sanctified Crimson Acolyte Leggings'), +(5, 0, 7, 0, 264, 50699, 'Phase 4', 'Priest', 'Discipline', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(5, 0, 8, 0, 264, 50686, 'Phase 4', 'Priest', 'Discipline', 'Wrists', 'Both', 'Death Surgeon''s Sleeves'), +(5, 0, 9, 0, 264, 50722, 'Phase 4', 'Priest', 'Discipline', 'Hands', 'Both', 'San''layn Ritualist Gloves'), +(5, 0, 10, 0, 264, 50720, 'Phase 4', 'Priest', 'Discipline', 'Finger1', 'Both', 'Incarnadine Band of Mending'), +(5, 0, 12, 1, 264, 47059, 'Phase 4', 'Priest', 'Discipline', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(5, 0, 12, 2, 264, 47432, 'Phase 4', 'Priest', 'Discipline', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(5, 0, 14, 0, 264, 50628, 'Phase 4', 'Priest', 'Discipline', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(5, 0, 15, 0, 264, 50734, 'Phase 4', 'Priest', 'Discipline', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(5, 0, 17, 0, 264, 50631, 'Phase 4', 'Priest', 'Discipline', 'Ranged', 'Both', 'Nightmare Ender'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 0, 0, 0, 290, 51261, 'Phase 5', 'Priest', 'Discipline', 'Head', 'Both', 'Sanctified Crimson Acolyte Hood'), +(5, 0, 1, 0, 290, 50182, 'Phase 5', 'Priest', 'Discipline', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(5, 0, 2, 0, 290, 51264, 'Phase 5', 'Priest', 'Discipline', 'Shoulders', 'Both', 'Sanctified Crimson Acolyte Shoulderpads'), +(5, 0, 4, 0, 290, 51263, 'Phase 5', 'Priest', 'Discipline', 'Chest', 'Both', 'Sanctified Crimson Acolyte Robe'), +(5, 0, 5, 0, 290, 50613, 'Phase 5', 'Priest', 'Discipline', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(5, 0, 6, 0, 290, 51262, 'Phase 5', 'Priest', 'Discipline', 'Legs', 'Both', 'Sanctified Crimson Acolyte Leggings'), +(5, 0, 7, 0, 290, 50699, 'Phase 5', 'Priest', 'Discipline', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(5, 0, 8, 0, 290, 54582, 'Phase 5', 'Priest', 'Discipline', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(5, 0, 9, 0, 290, 50722, 'Phase 5', 'Priest', 'Discipline', 'Hands', 'Both', 'San''layn Ritualist Gloves'), +(5, 0, 10, 0, 290, 50644, 'Phase 5', 'Priest', 'Discipline', 'Finger1', 'Both', 'Ring of Maddening Whispers'), +(5, 0, 11, 0, 290, 50636, 'Phase 5', 'Priest', 'Discipline', 'Finger2', 'Both', 'Memory of Malygos'), +(5, 0, 12, 0, 290, 50366, 'Phase 5', 'Priest', 'Discipline', 'Trinket1', 'Both', 'Althor''s Abacus'), +(5, 0, 13, 0, 290, 54589, 'Phase 5', 'Priest', 'Discipline', 'Trinket2', 'Both', 'Glowing Twilight Scale'), +(5, 0, 14, 0, 290, 54583, 'Phase 5', 'Priest', 'Discipline', 'Back', 'Both', 'Cloak of Burning Dusk'), +(5, 0, 15, 0, 290, 50734, 'Phase 5', 'Priest', 'Discipline', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(5, 0, 16, 0, 290, 50719, 'Phase 5', 'Priest', 'Discipline', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(5, 0, 17, 0, 290, 50631, 'Phase 5', 'Priest', 'Discipline', 'Ranged', 'Both', 'Nightmare Ender'); + +-- Holy (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 66, 13102, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Head', 'Both', 'Cassandra''s Grace'), +(5, 1, 1, 0, 66, 18723, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(5, 1, 2, 0, 66, 13013, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Shoulders', 'Both', 'Elder Wizard''s Mantle'), +(5, 1, 4, 0, 66, 14154, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Chest', 'Both', 'Truefaith Vestments'), +(5, 1, 5, 0, 66, 14143, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Waist', 'Both', 'Ghostweave Belt'), +(5, 1, 6, 0, 66, 11841, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Legs', 'Both', 'Senior Designer''s Pantaloons'), +(5, 1, 7, 0, 66, 11822, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Feet', 'Both', 'Omnicast Boots'), +(5, 1, 8, 0, 66, 11766, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Wrists', 'Both', 'Flameweave Cuffs'), +(5, 1, 9, 0, 66, 10787, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Hands', 'Both', 'Atal''ai Gloves'), +(5, 1, 10, 0, 66, 16058, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Finger1', 'Both', 'Fordring''s Seal'), +(5, 1, 11, 0, 66, 13178, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Finger2', 'Both', 'Rosewine Circle'), +(5, 1, 12, 0, 66, 11819, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Trinket1', 'Both', 'Second Wind'), +(5, 1, 13, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Trinket2', 'Both', 'Briarwood Reed'), +(5, 1, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Back', 'Both', 'Archivist Cape'), +(5, 1, 15, 0, 66, 11923, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'MainHand', 'Both', 'The Hammer of Grace'), +(5, 1, 16, 0, 66, 11928, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'OffHand', 'Both', 'Thaurissan''s Royal Scepter'), +(5, 1, 17, 0, 66, 16997, 'Phase 1 (Pre-Raid)', 'Priest', 'Holy', 'Ranged', 'Both', 'Stormrager'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 76, 13102, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Head', 'Both', 'Cassandra''s Grace'), +(5, 1, 1, 0, 76, 18723, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(5, 1, 2, 0, 76, 13013, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Shoulders', 'Both', 'Elder Wizard''s Mantle'), +(5, 1, 4, 0, 76, 14154, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Chest', 'Both', 'Truefaith Vestments'), +(5, 1, 5, 0, 76, 18327, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Waist', 'Both', 'Whipvine Cord'), +(5, 1, 6, 0, 76, 18386, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Legs', 'Both', 'Padre''s Trousers'), +(5, 1, 7, 0, 76, 18507, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of the Full Moon'), +(5, 1, 8, 0, 76, 11766, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Wrists', 'Both', 'Flameweave Cuffs'), +(5, 1, 9, 0, 76, 10787, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Hands', 'Both', 'Atal''ai Gloves'), +(5, 1, 10, 0, 76, 16058, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Finger1', 'Both', 'Fordring''s Seal'), +(5, 1, 11, 0, 76, 13178, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Finger2', 'Both', 'Rosewine Circle'), +(5, 1, 12, 0, 76, 18469, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Trinket1', 'Both', 'Royal Seal of Eldre''Thalas'), +(5, 1, 13, 0, 76, 18371, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Trinket2', 'Both', 'Mindtap Talisman'), +(5, 1, 14, 0, 76, 18510, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Back', 'Both', 'Hide of the Wild'), +(5, 1, 15, 0, 76, 11923, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'MainHand', 'Both', 'The Hammer of Grace'), +(5, 1, 16, 0, 76, 18523, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'OffHand', 'Both', 'Brightly Glowing Stone'), +(5, 1, 17, 0, 76, 18483, 'Phase 2 (Pre-Raid)', 'Priest', 'Holy', 'Ranged', 'Both', 'Mana Channeling Wand'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 78, 16921, 'Phase 2', 'Priest', 'Holy', 'Head', 'Both', 'Halo of Transcendence'), +(5, 1, 1, 0, 78, 18723, 'Phase 2', 'Priest', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(5, 1, 2, 0, 78, 16816, 'Phase 2', 'Priest', 'Holy', 'Shoulders', 'Both', 'Mantle of Prophecy'), +(5, 1, 4, 0, 78, 14154, 'Phase 2', 'Priest', 'Holy', 'Chest', 'Both', 'Truefaith Vestments'), +(5, 1, 5, 0, 78, 16817, 'Phase 2', 'Priest', 'Holy', 'Waist', 'Both', 'Girdle of Prophecy'), +(5, 1, 6, 0, 78, 16922, 'Phase 2', 'Priest', 'Holy', 'Legs', 'Both', 'Leggings of Transcendence'), +(5, 1, 7, 0, 78, 16811, 'Phase 2', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of Prophecy'), +(5, 1, 8, 0, 78, 16819, 'Phase 2', 'Priest', 'Holy', 'Wrists', 'Both', 'Vambraces of Prophecy'), +(5, 1, 9, 0, 78, 16812, 'Phase 2', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of Prophecy'), +(5, 1, 10, 0, 78, 19140, 'Phase 2', 'Priest', 'Holy', 'Finger1', 'Both', 'Cauterizing Band'), +(5, 1, 11, 0, 78, 19140, 'Phase 2', 'Priest', 'Holy', 'Finger2', 'Both', 'Cauterizing Band'), +(5, 1, 12, 0, 78, 18469, 'Phase 2', 'Priest', 'Holy', 'Trinket1', 'Both', 'Royal Seal of Eldre''Thalas'), +(5, 1, 13, 0, 78, 17064, 'Phase 2', 'Priest', 'Holy', 'Trinket2', 'Both', 'Shard of the Scale'), +(5, 1, 14, 0, 78, 18510, 'Phase 2', 'Priest', 'Holy', 'Back', 'Both', 'Hide of the Wild'), +(5, 1, 15, 0, 78, 18608, 'Phase 2', 'Priest', 'Holy', 'MainHand', 'Both', 'Benediction'), +(5, 1, 17, 0, 78, 18483, 'Phase 2', 'Priest', 'Holy', 'Ranged', 'Both', 'Mana Channeling Wand'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 83, 16921, 'Phase 3', 'Priest', 'Holy', 'Head', 'Both', 'Halo of Transcendence'), +(5, 1, 1, 0, 83, 18723, 'Phase 3', 'Priest', 'Holy', 'Neck', 'Both', 'Animated Chain Necklace'), +(5, 1, 2, 0, 83, 16924, 'Phase 3', 'Priest', 'Holy', 'Shoulders', 'Both', 'Pauldrons of Transcendence'), +(5, 1, 4, 0, 83, 16923, 'Phase 3', 'Priest', 'Holy', 'Chest', 'Both', 'Robes of Transcendence'), +(5, 1, 5, 0, 83, 16925, 'Phase 3', 'Priest', 'Holy', 'Waist', 'Both', 'Belt of Transcendence'), +(5, 1, 6, 0, 83, 16922, 'Phase 3', 'Priest', 'Holy', 'Legs', 'Both', 'Leggings of Transcendence'), +(5, 1, 7, 0, 83, 16919, 'Phase 3', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of Transcendence'), +(5, 1, 8, 0, 83, 16926, 'Phase 3', 'Priest', 'Holy', 'Wrists', 'Both', 'Bindings of Transcendence'), +(5, 1, 9, 0, 83, 16920, 'Phase 3', 'Priest', 'Holy', 'Hands', 'Both', 'Handguards of Transcendence'), +(5, 1, 10, 0, 83, 19382, 'Phase 3', 'Priest', 'Holy', 'Finger1', 'Both', 'Pure Elementium Band'), +(5, 1, 11, 0, 83, 19140, 'Phase 3', 'Priest', 'Holy', 'Finger2', 'Both', 'Cauterizing Band'), +(5, 1, 12, 0, 83, 19395, 'Phase 3', 'Priest', 'Holy', 'Trinket1', 'Both', 'Rejuvenating Gem'), +(5, 1, 13, 0, 83, 17064, 'Phase 3', 'Priest', 'Holy', 'Trinket2', 'Both', 'Shard of the Scale'), +(5, 1, 14, 0, 83, 19430, 'Phase 3', 'Priest', 'Holy', 'Back', 'Both', 'Shroud of Pure Thought'), +(5, 1, 15, 0, 83, 18608, 'Phase 3', 'Priest', 'Holy', 'MainHand', 'Both', 'Benediction'), +(5, 1, 17, 0, 83, 19435, 'Phase 3', 'Priest', 'Holy', 'Ranged', 'Both', 'Essence Gatherer'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 88, 21615, 'Phase 5', 'Priest', 'Holy', 'Head', 'Both', 'Don Rigoberto''s Lost Hat'), +(5, 1, 1, 0, 88, 21712, 'Phase 5', 'Priest', 'Holy', 'Neck', 'Both', 'Amulet of the Fallen God'), +(5, 1, 2, 0, 88, 16924, 'Phase 5', 'Priest', 'Holy', 'Shoulders', 'Both', 'Pauldrons of Transcendence'), +(5, 1, 4, 0, 88, 21663, 'Phase 5', 'Priest', 'Holy', 'Chest', 'Both', 'Robes of the Guardian Saint'), +(5, 1, 5, 0, 88, 21582, 'Phase 5', 'Priest', 'Holy', 'Waist', 'Both', 'Grasp of the Old God'), +(5, 1, 6, 0, 88, 16922, 'Phase 5', 'Priest', 'Holy', 'Legs', 'Both', 'Leggings of Transcendence'), +(5, 1, 7, 0, 88, 19437, 'Phase 5', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of Pure Thought'), +(5, 1, 8, 0, 88, 16926, 'Phase 5', 'Priest', 'Holy', 'Wrists', 'Both', 'Bindings of Transcendence'), +(5, 1, 9, 0, 88, 21619, 'Phase 5', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of the Messiah'), +(5, 1, 10, 0, 88, 19382, 'Phase 5', 'Priest', 'Holy', 'Finger1', 'Both', 'Pure Elementium Band'), +(5, 1, 11, 0, 88, 21620, 'Phase 5', 'Priest', 'Holy', 'Finger2', 'Both', 'Ring of the Martyr'), +(5, 1, 12, 0, 88, 19395, 'Phase 5', 'Priest', 'Holy', 'Trinket1', 'Both', 'Rejuvenating Gem'), +(5, 1, 13, 0, 88, 17064, 'Phase 5', 'Priest', 'Holy', 'Trinket2', 'Both', 'Shard of the Scale'), +(5, 1, 14, 0, 88, 21583, 'Phase 5', 'Priest', 'Holy', 'Back', 'Both', 'Cloak of Clarity'), +(5, 1, 15, 0, 88, 21839, 'Phase 5', 'Priest', 'Holy', 'MainHand', 'Both', 'Scepter of the False Prophet'), +(5, 1, 16, 0, 88, 21666, 'Phase 5', 'Priest', 'Holy', 'OffHand', 'Both', 'Sartura''s Might'), +(5, 1, 17, 0, 88, 21801, 'Phase 5', 'Priest', 'Holy', 'Ranged', 'Both', 'Antenna of Invigoration'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 92, 22514, 'Phase 6', 'Priest', 'Holy', 'Head', 'Both', 'Circlet of Faith'), +(5, 1, 1, 0, 92, 21712, 'Phase 6', 'Priest', 'Holy', 'Neck', 'Both', 'Amulet of the Fallen God'), +(5, 1, 2, 0, 92, 22515, 'Phase 6', 'Priest', 'Holy', 'Shoulders', 'Both', 'Shoulderpads of Faith'), +(5, 1, 4, 0, 92, 22512, 'Phase 6', 'Priest', 'Holy', 'Chest', 'Both', 'Robe of Faith'), +(5, 1, 5, 0, 92, 21582, 'Phase 6', 'Priest', 'Holy', 'Waist', 'Both', 'Grasp of the Old God'), +(5, 1, 6, 0, 92, 22513, 'Phase 6', 'Priest', 'Holy', 'Legs', 'Both', 'Leggings of Faith'), +(5, 1, 7, 0, 92, 22516, 'Phase 6', 'Priest', 'Holy', 'Feet', 'Both', 'Sandals of Faith'), +(5, 1, 8, 0, 92, 21604, 'Phase 6', 'Priest', 'Holy', 'Wrists', 'Both', 'Bracelets of Royal Redemption'), +(5, 1, 9, 0, 92, 22517, 'Phase 6', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of Faith'), +(5, 1, 10, 0, 92, 23061, 'Phase 6', 'Priest', 'Holy', 'Finger1', 'Both', 'Ring of Faith'), +(5, 1, 11, 0, 92, 22939, 'Phase 6', 'Priest', 'Holy', 'Finger2', 'Both', 'Band of Unanswered Prayers'), +(5, 1, 12, 0, 92, 23027, 'Phase 6', 'Priest', 'Holy', 'Trinket1', 'Both', 'Warmth of Forgiveness'), +(5, 1, 13, 0, 92, 23047, 'Phase 6', 'Priest', 'Holy', 'Trinket2', 'Both', 'Eye of the Dead'), +(5, 1, 14, 0, 92, 22960, 'Phase 6', 'Priest', 'Holy', 'Back', 'Both', 'Cloak of Suturing'), +(5, 1, 15, 0, 92, 23056, 'Phase 6', 'Priest', 'Holy', 'MainHand', 'Both', 'Hammer of the Twisting Nether'), +(5, 1, 16, 0, 92, 23048, 'Phase 6', 'Priest', 'Holy', 'OffHand', 'Both', 'Sapphiron''s Right Eye'), +(5, 1, 17, 0, 92, 23009, 'Phase 6', 'Priest', 'Holy', 'Ranged', 'Both', 'Wand of the Whispering Dead'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 120, 24264, 'Pre-Raid', 'Priest', 'Holy', 'Head', 'Both', 'Whitemend Hood'), +(5, 1, 1, 0, 120, 30377, 'Pre-Raid', 'Priest', 'Holy', 'Neck', 'Both', 'Karja''s Medallion'), +(5, 1, 2, 0, 120, 21874, 'Pre-Raid', 'Priest', 'Holy', 'Shoulders', 'Both', 'Primal Mooncloth Shoulders'), +(5, 1, 4, 0, 120, 21875, 'Pre-Raid', 'Priest', 'Holy', 'Chest', 'Both', 'Primal Mooncloth Robe'), +(5, 1, 5, 0, 120, 21873, 'Pre-Raid', 'Priest', 'Holy', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(5, 1, 6, 0, 120, 24261, 'Pre-Raid', 'Priest', 'Holy', 'Legs', 'Both', 'Whitemend Pants'), +(5, 1, 7, 0, 120, 29251, 'Pre-Raid', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of the Pious'), +(5, 1, 8, 0, 120, 29249, 'Pre-Raid', 'Priest', 'Holy', 'Wrists', 'Both', 'Bands of the Benevolent'), +(5, 1, 9, 0, 120, 24393, 'Pre-Raid', 'Priest', 'Holy', 'Hands', 'Both', 'Bloody Surgeon''s Mitts'), +(5, 1, 10, 0, 120, 27780, 'Pre-Raid', 'Priest', 'Holy', 'Finger1', 'Both', 'Ring of Fabled Hope'), +(5, 1, 10, 2, 120, 29168, 'Pre-Raid', 'Priest', 'Holy', 'Finger1', 'Horde', 'Ancestral Band'), +(5, 1, 12, 0, 120, 19288, 'Pre-Raid', 'Priest', 'Holy', 'Trinket1', 'Both', 'Darkmoon Card: Blue Dragon'), +(5, 1, 13, 0, 120, 29376, 'Pre-Raid', 'Priest', 'Holy', 'Trinket2', 'Both', 'Essence of the Martyr'), +(5, 1, 14, 0, 120, 29354, 'Pre-Raid', 'Priest', 'Holy', 'Back', 'Both', 'Light-Touched Stole of Altruism'), +(5, 1, 15, 0, 120, 29353, 'Pre-Raid', 'Priest', 'Holy', 'MainHand', 'Both', 'Shockwave Truncheon'), +(5, 1, 16, 0, 120, 29170, 'Pre-Raid', 'Priest', 'Holy', 'OffHand', 'Both', 'Windcaller''s Orb'), +(5, 1, 17, 0, 120, 27885, 'Pre-Raid', 'Priest', 'Holy', 'Ranged', 'Both', 'Soul-Wand of the Aldor'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 125, 29049, 'Phase 1', 'Priest', 'Holy', 'Head', 'Both', 'Light-Collar of the Incarnate'), +(5, 1, 1, 0, 125, 30726, 'Phase 1', 'Priest', 'Holy', 'Neck', 'Both', 'Archaic Charm of Presence'), +(5, 1, 2, 0, 125, 21874, 'Phase 1', 'Priest', 'Holy', 'Shoulders', 'Both', 'Primal Mooncloth Shoulders'), +(5, 1, 4, 0, 125, 21875, 'Phase 1', 'Priest', 'Holy', 'Chest', 'Both', 'Primal Mooncloth Robe'), +(5, 1, 5, 0, 125, 21873, 'Phase 1', 'Priest', 'Holy', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(5, 1, 6, 0, 125, 30727, 'Phase 1', 'Priest', 'Holy', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(5, 1, 7, 0, 125, 28663, 'Phase 1', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of the Incorrupt'), +(5, 1, 8, 0, 125, 29249, 'Phase 1', 'Priest', 'Holy', 'Wrists', 'Both', 'Bands of the Benevolent'), +(5, 1, 9, 0, 125, 28508, 'Phase 1', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of Saintly Blessings'), +(5, 1, 10, 0, 125, 29290, 'Phase 1', 'Priest', 'Holy', 'Finger1', 'Both', 'Violet Signet of the Grand Restorer'), +(5, 1, 11, 0, 125, 28763, 'Phase 1', 'Priest', 'Holy', 'Finger2', 'Both', 'Jade Ring of the Everliving'), +(5, 1, 12, 0, 125, 29376, 'Phase 1', 'Priest', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 1, 13, 0, 125, 28823, 'Phase 1', 'Priest', 'Holy', 'Trinket2', 'Both', 'Eye of Gruul'), +(5, 1, 14, 0, 125, 28765, 'Phase 1', 'Priest', 'Holy', 'Back', 'Both', 'Stainless Cloak of the Pure Hearted'), +(5, 1, 15, 0, 125, 28771, 'Phase 1', 'Priest', 'Holy', 'MainHand', 'Both', 'Light''s Justice'), +(5, 1, 16, 0, 125, 29170, 'Phase 1', 'Priest', 'Holy', 'OffHand', 'Both', 'Windcaller''s Orb'), +(5, 1, 17, 0, 125, 28588, 'Phase 1', 'Priest', 'Holy', 'Ranged', 'Both', 'Blue Diamond Witchwand'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 141, 30152, 'Phase 2', 'Priest', 'Holy', 'Head', 'Both', 'Cowl of the Avatar'), +(5, 1, 1, 0, 141, 30018, 'Phase 2', 'Priest', 'Holy', 'Neck', 'Both', 'Lord Sanguinar''s Claim'), +(5, 1, 2, 0, 141, 30154, 'Phase 2', 'Priest', 'Holy', 'Shoulders', 'Both', 'Mantle of the Avatar'), +(5, 1, 4, 0, 141, 30150, 'Phase 2', 'Priest', 'Holy', 'Chest', 'Both', 'Vestments of the Avatar'), +(5, 1, 5, 0, 141, 30036, 'Phase 2', 'Priest', 'Holy', 'Waist', 'Both', 'Belt of the Long Road'), +(5, 1, 6, 0, 141, 30727, 'Phase 2', 'Priest', 'Holy', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(5, 1, 7, 0, 141, 30100, 'Phase 2', 'Priest', 'Holy', 'Feet', 'Both', 'Soul-Strider Boots'), +(5, 1, 8, 0, 141, 32516, 'Phase 2', 'Priest', 'Holy', 'Wrists', 'Both', 'Wraps of Purification'), +(5, 1, 9, 0, 141, 30151, 'Phase 2', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of the Avatar'), +(5, 1, 10, 0, 141, 30110, 'Phase 2', 'Priest', 'Holy', 'Finger1', 'Both', 'Coral Band of the Revived'), +(5, 1, 11, 0, 141, 29290, 'Phase 2', 'Priest', 'Holy', 'Finger2', 'Both', 'Violet Signet of the Grand Restorer'), +(5, 1, 12, 0, 141, 29376, 'Phase 2', 'Priest', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 1, 13, 0, 141, 28823, 'Phase 2', 'Priest', 'Holy', 'Trinket2', 'Both', 'Eye of Gruul'), +(5, 1, 14, 0, 141, 29989, 'Phase 2', 'Priest', 'Holy', 'Back', 'Both', 'Sunshower Light Cloak'), +(5, 1, 15, 0, 141, 30108, 'Phase 2', 'Priest', 'Holy', 'MainHand', 'Both', 'Lightfathom Scepter'), +(5, 1, 16, 0, 141, 29923, 'Phase 2', 'Priest', 'Holy', 'OffHand', 'Both', 'Talisman of the Sun King'), +(5, 1, 17, 0, 141, 30080, 'Phase 2', 'Priest', 'Holy', 'Ranged', 'Both', 'Luminescent Rod of the Naaru'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 156, 31063, 'Phase 3', 'Priest', 'Holy', 'Head', 'Both', 'Cowl of Absolution'), +(5, 1, 1, 0, 156, 32370, 'Phase 3', 'Priest', 'Holy', 'Neck', 'Both', 'Nadina''s Pendant of Purity'), +(5, 1, 2, 0, 156, 31069, 'Phase 3', 'Priest', 'Holy', 'Shoulders', 'Both', 'Mantle of Absolution'), +(5, 1, 4, 0, 156, 31066, 'Phase 3', 'Priest', 'Holy', 'Chest', 'Both', 'Vestments of Absolution'), +(5, 1, 5, 0, 156, 32519, 'Phase 3', 'Priest', 'Holy', 'Waist', 'Both', 'Belt of Divine Guidance'), +(5, 1, 6, 0, 156, 30912, 'Phase 3', 'Priest', 'Holy', 'Legs', 'Both', 'Leggings of Eternity'), +(5, 1, 7, 0, 156, 32609, 'Phase 3', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of the Divine Light'), +(5, 1, 8, 0, 156, 30871, 'Phase 3', 'Priest', 'Holy', 'Wrists', 'Both', 'Bracers of Martyrdom'), +(5, 1, 9, 0, 156, 31060, 'Phase 3', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of Absolution'), +(5, 1, 10, 0, 156, 32528, 'Phase 3', 'Priest', 'Holy', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(5, 1, 11, 0, 156, 32528, 'Phase 3', 'Priest', 'Holy', 'Finger2', 'Both', 'Blessed Band of Karabor'), +(5, 1, 12, 0, 156, 29376, 'Phase 3', 'Priest', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 1, 13, 0, 156, 32496, 'Phase 3', 'Priest', 'Holy', 'Trinket2', 'Both', 'Memento of Tyrande'), +(5, 1, 14, 0, 156, 32524, 'Phase 3', 'Priest', 'Holy', 'Back', 'Both', 'Shroud of the Highborne'), +(5, 1, 15, 0, 156, 32500, 'Phase 3', 'Priest', 'Holy', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(5, 1, 16, 0, 156, 30911, 'Phase 3', 'Priest', 'Holy', 'OffHand', 'Both', 'Scepter of Purification'), +(5, 1, 17, 0, 156, 32363, 'Phase 3', 'Priest', 'Holy', 'Ranged', 'Both', 'Naaru-Blessed Life Rod'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 164, 31063, 'Phase 4', 'Priest', 'Holy', 'Head', 'Both', 'Cowl of Absolution'), +(5, 1, 1, 0, 164, 33281, 'Phase 4', 'Priest', 'Holy', 'Neck', 'Both', 'Brooch of Nature''s Mercy'), +(5, 1, 2, 0, 164, 31069, 'Phase 4', 'Priest', 'Holy', 'Shoulders', 'Both', 'Mantle of Absolution'), +(5, 1, 4, 0, 164, 31066, 'Phase 4', 'Priest', 'Holy', 'Chest', 'Both', 'Vestments of Absolution'), +(5, 1, 5, 0, 164, 32519, 'Phase 4', 'Priest', 'Holy', 'Waist', 'Both', 'Belt of Divine Guidance'), +(5, 1, 6, 0, 164, 30912, 'Phase 4', 'Priest', 'Holy', 'Legs', 'Both', 'Leggings of Eternity'), +(5, 1, 7, 0, 164, 32609, 'Phase 4', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of the Divine Light'), +(5, 1, 8, 0, 164, 30871, 'Phase 4', 'Priest', 'Holy', 'Wrists', 'Both', 'Bracers of Martyrdom'), +(5, 1, 9, 0, 164, 31060, 'Phase 4', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of Absolution'), +(5, 1, 10, 0, 164, 32528, 'Phase 4', 'Priest', 'Holy', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(5, 1, 11, 0, 164, 30110, 'Phase 4', 'Priest', 'Holy', 'Finger2', 'Both', 'Coral Band of the Revived'), +(5, 1, 12, 0, 164, 29376, 'Phase 4', 'Priest', 'Holy', 'Trinket1', 'Both', 'Essence of the Martyr'), +(5, 1, 13, 0, 164, 32496, 'Phase 4', 'Priest', 'Holy', 'Trinket2', 'Both', 'Memento of Tyrande'), +(5, 1, 14, 0, 164, 32524, 'Phase 4', 'Priest', 'Holy', 'Back', 'Both', 'Shroud of the Highborne'), +(5, 1, 15, 0, 164, 32500, 'Phase 4', 'Priest', 'Holy', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(5, 1, 16, 0, 164, 30911, 'Phase 4', 'Priest', 'Holy', 'OffHand', 'Both', 'Scepter of Purification'), +(5, 1, 17, 0, 164, 32363, 'Phase 4', 'Priest', 'Holy', 'Ranged', 'Both', 'Naaru-Blessed Life Rod'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 200, 37294, 'Pre-Raid', 'Priest', 'Holy', 'Head', 'Both', 'Crown of Unbridled Magic'), +(5, 1, 1, 0, 200, 40681, 'Pre-Raid', 'Priest', 'Holy', 'Neck', 'Both', 'Lattice Choker of Light'), +(5, 1, 2, 0, 200, 37673, 'Pre-Raid', 'Priest', 'Holy', 'Shoulders', 'Both', 'Dark Runic Mantle'), +(5, 1, 4, 0, 200, 44180, 'Pre-Raid', 'Priest', 'Holy', 'Chest', 'Both', 'Robes of Crackling Flame'), +(5, 1, 5, 0, 200, 40697, 'Pre-Raid', 'Priest', 'Holy', 'Waist', 'Both', 'Elegant Temple Gardens'' Girdle'), +(5, 1, 6, 0, 200, 37854, 'Pre-Raid', 'Priest', 'Holy', 'Legs', 'Both', 'Woven Bracae Leggings'), +(5, 1, 7, 0, 200, 44202, 'Pre-Raid', 'Priest', 'Holy', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(5, 1, 8, 0, 200, 37361, 'Pre-Raid', 'Priest', 'Holy', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(5, 1, 9, 0, 200, 37172, 'Pre-Raid', 'Priest', 'Holy', 'Hands', 'Both', 'Gloves of Glistening Runes'), +(5, 1, 10, 0, 200, 37694, 'Pre-Raid', 'Priest', 'Holy', 'Finger1', 'Both', 'Band of Guile'), +(5, 1, 12, 0, 200, 37835, 'Pre-Raid', 'Priest', 'Holy', 'Trinket1', 'Both', 'Je''Tze''s Bell'), +(5, 1, 14, 0, 200, 41610, 'Pre-Raid', 'Priest', 'Holy', 'Back', 'Both', 'Deathchill Cloak'), +(5, 1, 15, 0, 200, 37169, 'Pre-Raid', 'Priest', 'Holy', 'MainHand', 'Both', 'War Mace of Unrequited Love'), +(5, 1, 17, 0, 200, 37619, 'Pre-Raid', 'Priest', 'Holy', 'Ranged', 'Both', 'Wand of Ahn''kahet'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 224, 40447, 'Phase 1', 'Priest', 'Holy', 'Head', 'Both', 'Valorous Crown of Faith'), +(5, 1, 1, 0, 224, 44661, 'Phase 1', 'Priest', 'Holy', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(5, 1, 2, 0, 224, 40450, 'Phase 1', 'Priest', 'Holy', 'Shoulders', 'Both', 'Valorous Shoulderpads of Faith'), +(5, 1, 4, 0, 224, 44002, 'Phase 1', 'Priest', 'Holy', 'Chest', 'Both', 'The Sanctum''s Flowing Vestments'), +(5, 1, 5, 0, 224, 40561, 'Phase 1', 'Priest', 'Holy', 'Waist', 'Both', 'Leash of Heedless Magic'), +(5, 1, 6, 0, 224, 40448, 'Phase 1', 'Priest', 'Holy', 'Legs', 'Both', 'Valorous Leggings of Faith'), +(5, 1, 7, 0, 224, 40558, 'Phase 1', 'Priest', 'Holy', 'Feet', 'Both', 'Arcanic Tramplers'), +(5, 1, 8, 0, 224, 44008, 'Phase 1', 'Priest', 'Holy', 'Wrists', 'Both', 'Unsullied Cuffs'), +(5, 1, 9, 0, 224, 40445, 'Phase 1', 'Priest', 'Holy', 'Hands', 'Both', 'Valorous Gloves of Faith'), +(5, 1, 10, 0, 224, 40399, 'Phase 1', 'Priest', 'Holy', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(5, 1, 12, 0, 224, 37835, 'Phase 1', 'Priest', 'Holy', 'Trinket1', 'Both', 'Je''Tze''s Bell'), +(5, 1, 14, 0, 224, 44005, 'Phase 1', 'Priest', 'Holy', 'Back', 'Both', 'Pennant Cloak'), +(5, 1, 17, 0, 224, 39426, 'Phase 1', 'Priest', 'Holy', 'Ranged', 'Both', 'Wand of the Archlich'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 245, 45497, 'Phase 2', 'Priest', 'Holy', 'Head', 'Both', 'Crown of Luminescence'), +(5, 1, 1, 0, 245, 45243, 'Phase 2', 'Priest', 'Holy', 'Neck', 'Both', 'Sapphire Amulet of Renewal'), +(5, 1, 2, 0, 245, 46068, 'Phase 2', 'Priest', 'Holy', 'Shoulders', 'Both', 'Amice of Inconceivable Horror'), +(5, 1, 4, 0, 245, 45240, 'Phase 2', 'Priest', 'Holy', 'Chest', 'Both', 'Raiments of the Iron Council'), +(5, 1, 5, 0, 245, 45619, 'Phase 2', 'Priest', 'Holy', 'Waist', 'Both', 'Starwatcher''s Binding'), +(5, 1, 6, 0, 245, 46195, 'Phase 2', 'Priest', 'Holy', 'Legs', 'Both', 'Conqueror''s Leggings of Sanctification'), +(5, 1, 7, 0, 245, 45135, 'Phase 2', 'Priest', 'Holy', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(5, 1, 8, 0, 245, 45460, 'Phase 2', 'Priest', 'Holy', 'Wrists', 'Both', 'Bindings of Winter Gale'), +(5, 1, 9, 0, 245, 46188, 'Phase 2', 'Priest', 'Holy', 'Hands', 'Both', 'Conqueror''s Gloves of Sanctification'), +(5, 1, 12, 0, 245, 45535, 'Phase 2', 'Priest', 'Holy', 'Trinket1', 'Both', 'Show of Faith'), +(5, 1, 14, 0, 245, 45618, 'Phase 2', 'Priest', 'Holy', 'Back', 'Both', 'Sunglimmer Cloak'), +(5, 1, 15, 0, 245, 46017, 'Phase 2', 'Priest', 'Holy', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(5, 1, 17, 0, 245, 45170, 'Phase 2', 'Priest', 'Holy', 'Ranged', 'Both', 'Scepter of Creation'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 258, 48035, 'Phase 3', 'Priest', 'Holy', 'Head', 'Both', 'Cowl of Triumph'), +(5, 1, 2, 0, 258, 48029, 'Phase 3', 'Priest', 'Holy', 'Shoulders', 'Both', 'Shoulderpads of Triumph'), +(5, 1, 4, 1, 258, 46993, 'Phase 3', 'Priest', 'Holy', 'Chest', 'Alliance', 'Flowing Vestments of Ascent'), +(5, 1, 4, 2, 258, 47425, 'Phase 3', 'Priest', 'Holy', 'Chest', 'Horde', 'Flowing Robes of Ascent'), +(5, 1, 5, 1, 258, 46973, 'Phase 3', 'Priest', 'Holy', 'Waist', 'Alliance', 'Cord of the Tenebrous Mist'), +(5, 1, 5, 2, 258, 47419, 'Phase 3', 'Priest', 'Holy', 'Waist', 'Horde', 'Belt of the Tenebrous Mist'), +(5, 1, 6, 1, 258, 47062, 'Phase 3', 'Priest', 'Holy', 'Legs', 'Alliance', 'Leggings of the Soothing Touch'), +(5, 1, 6, 2, 258, 47435, 'Phase 3', 'Priest', 'Holy', 'Legs', 'Horde', 'Pants of the Soothing Touch'), +(5, 1, 7, 1, 258, 47097, 'Phase 3', 'Priest', 'Holy', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(5, 1, 7, 2, 258, 47454, 'Phase 3', 'Priest', 'Holy', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(5, 1, 8, 1, 258, 47208, 'Phase 3', 'Priest', 'Holy', 'Wrists', 'Alliance', 'Armbands of the Ashen Saint'), +(5, 1, 8, 2, 258, 47485, 'Phase 3', 'Priest', 'Holy', 'Wrists', 'Horde', 'Bindings of the Ashen Saint'), +(5, 1, 9, 0, 258, 45665, 'Phase 3', 'Priest', 'Holy', 'Hands', 'Both', 'Pharos Gloves'), +(5, 1, 10, 1, 258, 47224, 'Phase 3', 'Priest', 'Holy', 'Finger1', 'Alliance', 'Ring of the Darkmender'), +(5, 1, 10, 2, 258, 47439, 'Phase 3', 'Priest', 'Holy', 'Finger1', 'Horde', 'Circle of the Darkmender'), +(5, 1, 12, 1, 258, 47059, 'Phase 3', 'Priest', 'Holy', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(5, 1, 12, 2, 258, 47432, 'Phase 3', 'Priest', 'Holy', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(5, 1, 14, 1, 258, 47552, 'Phase 3', 'Priest', 'Holy', 'Back', 'Alliance', 'Jaina''s Radiance'), +(5, 1, 14, 2, 258, 47551, 'Phase 3', 'Priest', 'Holy', 'Back', 'Horde', 'Aethas'' Intensity'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 264, 51261, 'Phase 4', 'Priest', 'Holy', 'Head', 'Both', 'Sanctified Crimson Acolyte Hood'), +(5, 1, 2, 0, 264, 51264, 'Phase 4', 'Priest', 'Holy', 'Shoulders', 'Both', 'Sanctified Crimson Acolyte Shoulderpads'), +(5, 1, 4, 0, 264, 50717, 'Phase 4', 'Priest', 'Holy', 'Chest', 'Both', 'Sanguine Silk Robes'), +(5, 1, 5, 0, 264, 50613, 'Phase 4', 'Priest', 'Holy', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(5, 1, 6, 0, 264, 51262, 'Phase 4', 'Priest', 'Holy', 'Legs', 'Both', 'Sanctified Crimson Acolyte Leggings'), +(5, 1, 7, 0, 264, 50699, 'Phase 4', 'Priest', 'Holy', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(5, 1, 8, 0, 264, 50686, 'Phase 4', 'Priest', 'Holy', 'Wrists', 'Both', 'Death Surgeon''s Sleeves'), +(5, 1, 9, 0, 264, 51260, 'Phase 4', 'Priest', 'Holy', 'Hands', 'Both', 'Sanctified Crimson Acolyte Gloves'), +(5, 1, 10, 0, 264, 50636, 'Phase 4', 'Priest', 'Holy', 'Finger1', 'Both', 'Memory of Malygos'), +(5, 1, 12, 1, 264, 47059, 'Phase 4', 'Priest', 'Holy', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(5, 1, 12, 2, 264, 47432, 'Phase 4', 'Priest', 'Holy', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(5, 1, 14, 0, 264, 50668, 'Phase 4', 'Priest', 'Holy', 'Back', 'Both', 'Greatcloak of the Turned Champion'), +(5, 1, 15, 0, 264, 46017, 'Phase 4', 'Priest', 'Holy', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(5, 1, 17, 0, 264, 50684, 'Phase 4', 'Priest', 'Holy', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 1, 0, 0, 290, 51261, 'Phase 5', 'Priest', 'Holy', 'Head', 'Both', 'Sanctified Crimson Acolyte Hood'), +(5, 1, 1, 0, 290, 50609, 'Phase 5', 'Priest', 'Holy', 'Neck', 'Both', 'Bone Sentinel''s Amulet'), +(5, 1, 2, 0, 290, 51264, 'Phase 5', 'Priest', 'Holy', 'Shoulders', 'Both', 'Sanctified Crimson Acolyte Shoulderpads'), +(5, 1, 4, 0, 290, 50717, 'Phase 5', 'Priest', 'Holy', 'Chest', 'Both', 'Sanguine Silk Robes'), +(5, 1, 5, 0, 290, 50702, 'Phase 5', 'Priest', 'Holy', 'Waist', 'Both', 'Lingering Illness'), +(5, 1, 6, 0, 290, 51262, 'Phase 5', 'Priest', 'Holy', 'Legs', 'Both', 'Sanctified Crimson Acolyte Leggings'), +(5, 1, 7, 0, 290, 50699, 'Phase 5', 'Priest', 'Holy', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(5, 1, 8, 0, 290, 54582, 'Phase 5', 'Priest', 'Holy', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(5, 1, 9, 0, 290, 51260, 'Phase 5', 'Priest', 'Holy', 'Hands', 'Both', 'Sanctified Crimson Acolyte Gloves'), +(5, 1, 10, 0, 290, 50400, 'Phase 5', 'Priest', 'Holy', 'Finger1', 'Both', 'Ashen Band of Endless Wisdom'), +(5, 1, 11, 0, 290, 50636, 'Phase 5', 'Priest', 'Holy', 'Finger2', 'Both', 'Memory of Malygos'), +(5, 1, 12, 0, 290, 50366, 'Phase 5', 'Priest', 'Holy', 'Trinket1', 'Both', 'Althor''s Abacus'), +(5, 1, 13, 0, 290, 54589, 'Phase 5', 'Priest', 'Holy', 'Trinket2', 'Both', 'Glowing Twilight Scale'), +(5, 1, 14, 0, 290, 50668, 'Phase 5', 'Priest', 'Holy', 'Back', 'Both', 'Greatcloak of the Turned Champion'), +(5, 1, 15, 0, 290, 50731, 'Phase 5', 'Priest', 'Holy', 'MainHand', 'Both', 'Archus, Greatstaff of Antonidas'), +(5, 1, 17, 0, 290, 50631, 'Phase 5', 'Priest', 'Holy', 'Ranged', 'Both', 'Nightmare Ender'); + +-- Shadow (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 1, 66, 10504, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Head', 'Alliance', 'Green Lens'), +(5, 2, 0, 2, 66, 10504, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Head', 'Horde', 'Green Lens'), +(5, 2, 1, 1, 66, 18691, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(5, 2, 1, 2, 66, 18691, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(5, 2, 2, 1, 66, 14112, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Shoulders', 'Alliance', 'Felcloth Shoulders'), +(5, 2, 2, 2, 66, 14112, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Shoulders', 'Horde', 'Felcloth Shoulders'), +(5, 2, 4, 1, 66, 14136, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Chest', 'Alliance', 'Robe of Winter Night'), +(5, 2, 4, 2, 66, 14136, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Chest', 'Horde', 'Robe of Winter Night'), +(5, 2, 5, 1, 66, 11662, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Waist', 'Alliance', 'Ban''thok Sash'), +(5, 2, 5, 2, 66, 11662, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Waist', 'Horde', 'Ban''thok Sash'), +(5, 2, 6, 1, 66, 13170, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(5, 2, 6, 2, 66, 13170, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Legs', 'Horde', 'Skyshroud Leggings'), +(5, 2, 7, 1, 66, 18735, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(5, 2, 7, 2, 66, 18735, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(5, 2, 8, 1, 66, 11766, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(5, 2, 8, 2, 66, 11766, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(5, 2, 9, 1, 66, 13253, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Hands', 'Alliance', 'Hands of Power'), +(5, 2, 9, 2, 66, 13253, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Hands', 'Horde', 'Hands of Power'), +(5, 2, 10, 1, 66, 12543, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(5, 2, 10, 2, 66, 12545, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(5, 2, 11, 1, 66, 13001, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(5, 2, 11, 2, 66, 13001, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Finger2', 'Horde', 'Maiden''s Circle'), +(5, 2, 12, 1, 66, 12930, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(5, 2, 12, 2, 66, 12930, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket1', 'Horde', 'Briarwood Reed'), +(5, 2, 13, 1, 66, 11819, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket2', 'Alliance', 'Second Wind'), +(5, 2, 13, 2, 66, 11819, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket2', 'Horde', 'Second Wind'), +(5, 2, 14, 1, 66, 13386, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Back', 'Alliance', 'Archivist Cape'), +(5, 2, 14, 2, 66, 13386, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Back', 'Horde', 'Archivist Cape'), +(5, 2, 15, 1, 66, 13349, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'MainHand', 'Alliance', 'Scepter of the Unholy'), +(5, 2, 15, 2, 66, 13349, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'MainHand', 'Horde', 'Scepter of the Unholy'), +(5, 2, 16, 1, 66, 10796, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'OffHand', 'Alliance', 'Drakestone'), +(5, 2, 16, 2, 66, 10796, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'OffHand', 'Horde', 'Drakestone'), +(5, 2, 17, 1, 66, 13396, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(5, 2, 17, 2, 66, 13396, 'Phase 1 (Pre-Raid)', 'Priest', 'Shadow', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 1, 76, 10504, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Head', 'Alliance', 'Green Lens'), +(5, 2, 0, 2, 76, 10504, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Head', 'Horde', 'Green Lens'), +(5, 2, 1, 1, 76, 18691, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(5, 2, 1, 2, 76, 18691, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(5, 2, 2, 1, 76, 14112, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Shoulders', 'Alliance', 'Felcloth Shoulders'), +(5, 2, 2, 2, 76, 14112, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Shoulders', 'Horde', 'Felcloth Shoulders'), +(5, 2, 4, 1, 76, 14136, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Chest', 'Alliance', 'Robe of Winter Night'), +(5, 2, 4, 2, 76, 14136, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Chest', 'Horde', 'Robe of Winter Night'), +(5, 2, 5, 1, 76, 11662, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Waist', 'Alliance', 'Ban''thok Sash'), +(5, 2, 5, 2, 76, 11662, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Waist', 'Horde', 'Ban''thok Sash'), +(5, 2, 6, 1, 76, 13170, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(5, 2, 6, 2, 76, 13170, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Legs', 'Horde', 'Skyshroud Leggings'), +(5, 2, 7, 1, 76, 18735, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(5, 2, 7, 2, 76, 18735, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(5, 2, 8, 1, 76, 11766, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(5, 2, 8, 2, 76, 11766, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(5, 2, 9, 1, 76, 18407, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Hands', 'Alliance', 'Felcloth Gloves'), +(5, 2, 9, 2, 76, 18407, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Hands', 'Horde', 'Felcloth Gloves'), +(5, 2, 10, 1, 76, 12543, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(5, 2, 10, 2, 76, 12545, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(5, 2, 11, 1, 76, 13001, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(5, 2, 11, 2, 76, 13001, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Finger2', 'Horde', 'Maiden''s Circle'), +(5, 2, 12, 1, 76, 12930, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(5, 2, 12, 2, 76, 12930, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket1', 'Horde', 'Briarwood Reed'), +(5, 2, 13, 1, 76, 18371, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket2', 'Alliance', 'Mindtap Talisman'), +(5, 2, 13, 2, 76, 18371, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Trinket2', 'Horde', 'Mindtap Talisman'), +(5, 2, 14, 1, 76, 13386, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Back', 'Alliance', 'Archivist Cape'), +(5, 2, 14, 2, 76, 13386, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Back', 'Horde', 'Archivist Cape'), +(5, 2, 15, 1, 76, 13349, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'MainHand', 'Alliance', 'Scepter of the Unholy'), +(5, 2, 15, 2, 76, 13349, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'MainHand', 'Horde', 'Scepter of the Unholy'), +(5, 2, 16, 1, 76, 10796, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'OffHand', 'Alliance', 'Drakestone'), +(5, 2, 16, 2, 76, 10796, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'OffHand', 'Horde', 'Drakestone'), +(5, 2, 17, 1, 76, 13396, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(5, 2, 17, 2, 76, 13396, 'Phase 2 (Pre-Raid)', 'Priest', 'Shadow', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 78, 10504, 'Phase 2', 'Priest', 'Shadow', 'Head', 'Both', 'Green Lens'), +(5, 2, 1, 0, 78, 18814, 'Phase 2', 'Priest', 'Shadow', 'Neck', 'Both', 'Choker of the Fire Lord'), +(5, 2, 2, 0, 78, 14112, 'Phase 2', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Felcloth Shoulders'), +(5, 2, 4, 0, 78, 14136, 'Phase 2', 'Priest', 'Shadow', 'Chest', 'Both', 'Robe of Winter Night'), +(5, 2, 5, 0, 78, 18809, 'Phase 2', 'Priest', 'Shadow', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(5, 2, 6, 0, 78, 19133, 'Phase 2', 'Priest', 'Shadow', 'Legs', 'Both', 'Fel Infused Leggings'), +(5, 2, 7, 0, 78, 19131, 'Phase 2', 'Priest', 'Shadow', 'Feet', 'Both', 'Snowblind Shoes'), +(5, 2, 8, 0, 78, 11766, 'Phase 2', 'Priest', 'Shadow', 'Wrists', 'Both', 'Flameweave Cuffs'), +(5, 2, 9, 0, 78, 18407, 'Phase 2', 'Priest', 'Shadow', 'Hands', 'Both', 'Felcloth Gloves'), +(5, 2, 10, 0, 78, 19147, 'Phase 2', 'Priest', 'Shadow', 'Finger1', 'Both', 'Ring of Spell Power'), +(5, 2, 11, 0, 78, 19147, 'Phase 2', 'Priest', 'Shadow', 'Finger2', 'Both', 'Ring of Spell Power'), +(5, 2, 12, 0, 78, 12930, 'Phase 2', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Briarwood Reed'), +(5, 2, 13, 0, 78, 18820, 'Phase 2', 'Priest', 'Shadow', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(5, 2, 14, 0, 78, 13386, 'Phase 2', 'Priest', 'Shadow', 'Back', 'Both', 'Archivist Cape'), +(5, 2, 15, 0, 78, 18609, 'Phase 2', 'Priest', 'Shadow', 'MainHand', 'Both', 'Anathema'), +(5, 2, 17, 0, 78, 13396, 'Phase 2', 'Priest', 'Shadow', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 83, 19375, 'Phase 3', 'Priest', 'Shadow', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(5, 2, 1, 0, 83, 18814, 'Phase 3', 'Priest', 'Shadow', 'Neck', 'Both', 'Choker of the Fire Lord'), +(5, 2, 2, 0, 83, 19370, 'Phase 3', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(5, 2, 4, 0, 83, 14136, 'Phase 3', 'Priest', 'Shadow', 'Chest', 'Both', 'Robe of Winter Night'), +(5, 2, 5, 0, 83, 19400, 'Phase 3', 'Priest', 'Shadow', 'Waist', 'Both', 'Firemaw''s Clutch'), +(5, 2, 6, 0, 83, 19133, 'Phase 3', 'Priest', 'Shadow', 'Legs', 'Both', 'Fel Infused Leggings'), +(5, 2, 7, 0, 83, 19131, 'Phase 3', 'Priest', 'Shadow', 'Feet', 'Both', 'Snowblind Shoes'), +(5, 2, 8, 0, 83, 19374, 'Phase 3', 'Priest', 'Shadow', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(5, 2, 9, 0, 83, 19407, 'Phase 3', 'Priest', 'Shadow', 'Hands', 'Both', 'Ebony Flame Gloves'), +(5, 2, 11, 0, 83, 19434, 'Phase 3', 'Priest', 'Shadow', 'Finger2', 'Both', 'Band of Dark Dominion'), +(5, 2, 12, 0, 83, 19379, 'Phase 3', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(5, 2, 13, 0, 83, 18820, 'Phase 3', 'Priest', 'Shadow', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(5, 2, 14, 0, 83, 19378, 'Phase 3', 'Priest', 'Shadow', 'Back', 'Both', 'Cloak of the Brood Lord'), +(5, 2, 15, 0, 83, 19360, 'Phase 3', 'Priest', 'Shadow', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(5, 2, 16, 0, 83, 19366, 'Phase 3', 'Priest', 'Shadow', 'OffHand', 'Both', 'Master Dragonslayer''s Orb'), +(5, 2, 17, 0, 83, 13396, 'Phase 3', 'Priest', 'Shadow', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 88, 21348, 'Phase 5', 'Priest', 'Shadow', 'Head', 'Both', 'Tiara of the Oracle'), +(5, 2, 1, 0, 88, 18814, 'Phase 5', 'Priest', 'Shadow', 'Neck', 'Both', 'Choker of the Fire Lord'), +(5, 2, 2, 0, 88, 19370, 'Phase 5', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(5, 2, 4, 0, 88, 21351, 'Phase 5', 'Priest', 'Shadow', 'Chest', 'Both', 'Vestments of the Oracle'), +(5, 2, 5, 0, 88, 19400, 'Phase 5', 'Priest', 'Shadow', 'Waist', 'Both', 'Firemaw''s Clutch'), +(5, 2, 6, 0, 88, 19133, 'Phase 5', 'Priest', 'Shadow', 'Legs', 'Both', 'Fel Infused Leggings'), +(5, 2, 7, 0, 88, 19131, 'Phase 5', 'Priest', 'Shadow', 'Feet', 'Both', 'Snowblind Shoes'), +(5, 2, 8, 0, 88, 21611, 'Phase 5', 'Priest', 'Shadow', 'Wrists', 'Both', 'Burrower Bracers'), +(5, 2, 9, 0, 88, 21585, 'Phase 5', 'Priest', 'Shadow', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(5, 2, 10, 0, 88, 21709, 'Phase 5', 'Priest', 'Shadow', 'Finger1', 'Both', 'Ring of the Fallen God'), +(5, 2, 11, 0, 88, 21210, 'Phase 5', 'Priest', 'Shadow', 'Finger2', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(5, 2, 12, 0, 88, 19379, 'Phase 5', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(5, 2, 13, 0, 88, 18820, 'Phase 5', 'Priest', 'Shadow', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(5, 2, 14, 0, 88, 22731, 'Phase 5', 'Priest', 'Shadow', 'Back', 'Both', 'Cloak of the Devoured'), +(5, 2, 15, 0, 88, 19360, 'Phase 5', 'Priest', 'Shadow', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(5, 2, 16, 0, 88, 19366, 'Phase 5', 'Priest', 'Shadow', 'OffHand', 'Both', 'Master Dragonslayer''s Orb'), +(5, 2, 17, 0, 88, 21603, 'Phase 5', 'Priest', 'Shadow', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 92, 23035, 'Phase 6', 'Priest', 'Shadow', 'Head', 'Both', 'Preceptor''s Hat'), +(5, 2, 1, 0, 92, 18814, 'Phase 6', 'Priest', 'Shadow', 'Neck', 'Both', 'Choker of the Fire Lord'), +(5, 2, 2, 0, 92, 22983, 'Phase 6', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Rime Covered Mantle'), +(5, 2, 4, 0, 92, 23220, 'Phase 6', 'Priest', 'Shadow', 'Chest', 'Both', 'Crystal Webbed Robe'), +(5, 2, 5, 0, 92, 19400, 'Phase 6', 'Priest', 'Shadow', 'Waist', 'Both', 'Firemaw''s Clutch'), +(5, 2, 6, 0, 92, 19133, 'Phase 6', 'Priest', 'Shadow', 'Legs', 'Both', 'Fel Infused Leggings'), +(5, 2, 7, 0, 92, 19131, 'Phase 6', 'Priest', 'Shadow', 'Feet', 'Both', 'Snowblind Shoes'), +(5, 2, 8, 0, 92, 21611, 'Phase 6', 'Priest', 'Shadow', 'Wrists', 'Both', 'Burrower Bracers'), +(5, 2, 9, 0, 92, 21585, 'Phase 6', 'Priest', 'Shadow', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(5, 2, 10, 0, 92, 21709, 'Phase 6', 'Priest', 'Shadow', 'Finger1', 'Both', 'Ring of the Fallen God'), +(5, 2, 11, 0, 92, 21210, 'Phase 6', 'Priest', 'Shadow', 'Finger2', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(5, 2, 12, 0, 92, 19379, 'Phase 6', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(5, 2, 13, 0, 92, 23046, 'Phase 6', 'Priest', 'Shadow', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(5, 2, 14, 0, 92, 22731, 'Phase 6', 'Priest', 'Shadow', 'Back', 'Both', 'Cloak of the Devoured'), +(5, 2, 15, 0, 92, 22988, 'Phase 6', 'Priest', 'Shadow', 'MainHand', 'Both', 'The End of Dreams'), +(5, 2, 16, 0, 92, 23049, 'Phase 6', 'Priest', 'Shadow', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(5, 2, 17, 0, 92, 21603, 'Phase 6', 'Priest', 'Shadow', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 200 (Fresh 80) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 200, 43995, 'Fresh 80', 'Priest', 'Shadow', 'Head', 'Both', 'Enamored Cowl'), +(5, 2, 1, 0, 200, 44658, 'Fresh 80', 'Priest', 'Shadow', 'Neck', 'Both', 'Chain of the Ancient Wyrm'), +(5, 2, 2, 0, 200, 40459, 'Fresh 80', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Valorous Mantle of Faith'), +(5, 2, 4, 0, 200, 40526, 'Fresh 80', 'Priest', 'Shadow', 'Chest', 'Both', 'Gown of the Spell-Weaver'), +(5, 2, 5, 0, 200, 40696, 'Fresh 80', 'Priest', 'Shadow', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(5, 2, 6, 0, 200, 40457, 'Fresh 80', 'Priest', 'Shadow', 'Legs', 'Both', 'Valorous Pants of Faith'), +(5, 2, 7, 0, 200, 40751, 'Fresh 80', 'Priest', 'Shadow', 'Feet', 'Both', 'Slippers of the Holy Light'), +(5, 2, 8, 0, 200, 40740, 'Fresh 80', 'Priest', 'Shadow', 'Wrists', 'Both', 'Wraps of the Astral Traveler'), +(5, 2, 9, 0, 200, 39530, 'Fresh 80', 'Priest', 'Shadow', 'Hands', 'Both', 'Heroes'' Handwraps of Faith'), +(5, 2, 10, 0, 200, 40719, 'Fresh 80', 'Priest', 'Shadow', 'Finger1', 'Both', 'Band of Channeled Magic'), +(5, 2, 12, 0, 200, 39229, 'Fresh 80', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Embrace of the Spider'), +(5, 2, 14, 0, 200, 40723, 'Fresh 80', 'Priest', 'Shadow', 'Back', 'Both', 'Disguise of the Kumiho'), +(5, 2, 15, 0, 200, 39423, 'Fresh 80', 'Priest', 'Shadow', 'MainHand', 'Both', 'Hammer of the Astral Plane'), +(5, 2, 17, 0, 200, 39426, 'Fresh 80', 'Priest', 'Shadow', 'Ranged', 'Both', 'Wand of the Archlich'); + +-- ilvl 213 (Pre-Ulduar) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 213, 40562, 'Pre-Ulduar', 'Priest', 'Shadow', 'Head', 'Both', 'Hood of Rationality'), +(5, 2, 1, 0, 213, 44661, 'Pre-Ulduar', 'Priest', 'Shadow', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(5, 2, 2, 0, 213, 40555, 'Pre-Ulduar', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Mantle of Dissemination'), +(5, 2, 4, 0, 213, 40234, 'Pre-Ulduar', 'Priest', 'Shadow', 'Chest', 'Both', 'Heigan''s Putrid Vestments'), +(5, 2, 5, 0, 213, 40561, 'Pre-Ulduar', 'Priest', 'Shadow', 'Waist', 'Both', 'Leash of Heedless Magic'), +(5, 2, 6, 0, 213, 40560, 'Pre-Ulduar', 'Priest', 'Shadow', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(5, 2, 7, 0, 213, 40558, 'Pre-Ulduar', 'Priest', 'Shadow', 'Feet', 'Both', 'Arcanic Tramplers'), +(5, 2, 8, 0, 213, 44008, 'Pre-Ulduar', 'Priest', 'Shadow', 'Wrists', 'Both', 'Unsullied Cuffs'), +(5, 2, 9, 0, 213, 40454, 'Pre-Ulduar', 'Priest', 'Shadow', 'Hands', 'Both', 'Valorous Handwraps of Faith'), +(5, 2, 10, 0, 213, 40719, 'Pre-Ulduar', 'Priest', 'Shadow', 'Finger1', 'Both', 'Band of Channeled Magic'), +(5, 2, 12, 0, 213, 40432, 'Pre-Ulduar', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Illustration of the Dragon Soul'), +(5, 2, 14, 0, 213, 44005, 'Pre-Ulduar', 'Priest', 'Shadow', 'Back', 'Both', 'Pennant Cloak'), +(5, 2, 17, 0, 213, 39712, 'Pre-Ulduar', 'Priest', 'Shadow', 'Ranged', 'Both', 'Gemmed Wand of the Nerubians'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 245, 46172, 'Phase 2', 'Priest', 'Shadow', 'Head', 'Both', 'Conqueror''s Circlet of Sanctification'), +(5, 2, 1, 0, 245, 45243, 'Phase 2', 'Priest', 'Shadow', 'Neck', 'Both', 'Sapphire Amulet of Renewal'), +(5, 2, 2, 0, 245, 46165, 'Phase 2', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Conqueror''s Mantle of Sanctification'), +(5, 2, 4, 0, 245, 46168, 'Phase 2', 'Priest', 'Shadow', 'Chest', 'Both', 'Conqueror''s Raiments of Sanctification'), +(5, 2, 5, 0, 245, 45619, 'Phase 2', 'Priest', 'Shadow', 'Waist', 'Both', 'Starwatcher''s Binding'), +(5, 2, 6, 0, 245, 46170, 'Phase 2', 'Priest', 'Shadow', 'Legs', 'Both', 'Conqueror''s Pants of Sanctification'), +(5, 2, 7, 0, 245, 45135, 'Phase 2', 'Priest', 'Shadow', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(5, 2, 8, 0, 245, 45446, 'Phase 2', 'Priest', 'Shadow', 'Wrists', 'Both', 'Grasps of Reason'), +(5, 2, 9, 0, 245, 45665, 'Phase 2', 'Priest', 'Shadow', 'Hands', 'Both', 'Pharos Gloves'), +(5, 2, 10, 0, 245, 45495, 'Phase 2', 'Priest', 'Shadow', 'Finger1', 'Both', 'Conductive Seal'), +(5, 2, 12, 0, 245, 45466, 'Phase 2', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Scale of Fates'), +(5, 2, 14, 0, 245, 45242, 'Phase 2', 'Priest', 'Shadow', 'Back', 'Both', 'Drape of Mortal Downfall'), +(5, 2, 17, 0, 245, 45294, 'Phase 2', 'Priest', 'Shadow', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 258, 48085, 'Phase 3', 'Priest', 'Shadow', 'Head', 'Both', 'Circlet of Triumph'), +(5, 2, 1, 1, 258, 47144, 'Phase 3', 'Priest', 'Shadow', 'Neck', 'Alliance', 'Wail of the Val''kyr'), +(5, 2, 1, 2, 258, 47468, 'Phase 3', 'Priest', 'Shadow', 'Neck', 'Horde', 'Cry of the Val''kyr'), +(5, 2, 2, 1, 258, 48082, 'Phase 3', 'Priest', 'Shadow', 'Shoulders', 'Alliance', 'Velen''s Mantle of Triumph'), +(5, 2, 2, 2, 258, 48090, 'Phase 3', 'Priest', 'Shadow', 'Shoulders', 'Horde', 'Zabra''s Raiments of Triumph'), +(5, 2, 4, 0, 258, 48083, 'Phase 3', 'Priest', 'Shadow', 'Chest', 'Both', 'Raiments of Triumph'), +(5, 2, 5, 1, 258, 46973, 'Phase 3', 'Priest', 'Shadow', 'Waist', 'Alliance', 'Cord of the Tenebrous Mist'), +(5, 2, 5, 2, 258, 47419, 'Phase 3', 'Priest', 'Shadow', 'Waist', 'Horde', 'Belt of the Tenebrous Mist'), +(5, 2, 6, 0, 258, 48084, 'Phase 3', 'Priest', 'Shadow', 'Legs', 'Both', 'Pants of Triumph'), +(5, 2, 7, 1, 258, 47097, 'Phase 3', 'Priest', 'Shadow', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(5, 2, 7, 2, 258, 47454, 'Phase 3', 'Priest', 'Shadow', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(5, 2, 8, 1, 258, 47208, 'Phase 3', 'Priest', 'Shadow', 'Wrists', 'Alliance', 'Armbands of the Ashen Saint'), +(5, 2, 8, 2, 258, 47467, 'Phase 3', 'Priest', 'Shadow', 'Wrists', 'Horde', 'Dark Essence Bindings'), +(5, 2, 9, 1, 258, 45665, 'Phase 3', 'Priest', 'Shadow', 'Hands', 'Alliance', 'Pharos Gloves'), +(5, 2, 10, 1, 258, 46046, 'Phase 3', 'Priest', 'Shadow', 'Finger1', 'Alliance', 'Nebula Band'), +(5, 2, 10, 2, 258, 47237, 'Phase 3', 'Priest', 'Shadow', 'Finger1', 'Horde', 'Band of Deplorable Violence'), +(5, 2, 12, 2, 258, 45518, 'Phase 3', 'Priest', 'Shadow', 'Trinket1', 'Horde', 'Flare of the Heavens'), +(5, 2, 14, 1, 258, 47552, 'Phase 3', 'Priest', 'Shadow', 'Back', 'Alliance', 'Jaina''s Radiance'), +(5, 2, 14, 2, 258, 47551, 'Phase 3', 'Priest', 'Shadow', 'Back', 'Horde', 'Aethas'' Intensity'), +(5, 2, 17, 1, 258, 47922, 'Phase 3', 'Priest', 'Shadow', 'Ranged', 'Alliance', 'Rod of Imprisoned Souls'), +(5, 2, 17, 2, 258, 45294, 'Phase 3', 'Priest', 'Shadow', 'Ranged', 'Horde', 'Petrified Ivy Sprig'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 264, 51255, 'Phase 4', 'Priest', 'Shadow', 'Head', 'Both', 'Sanctified Crimson Acolyte Cowl'), +(5, 2, 1, 0, 264, 50724, 'Phase 4', 'Priest', 'Shadow', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(5, 2, 2, 0, 264, 51257, 'Phase 4', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Sanctified Crimson Acolyte Mantle'), +(5, 2, 4, 0, 264, 51259, 'Phase 4', 'Priest', 'Shadow', 'Chest', 'Both', 'Sanctified Crimson Acolyte Raiments'), +(5, 2, 5, 0, 264, 50613, 'Phase 4', 'Priest', 'Shadow', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(5, 2, 6, 0, 264, 50694, 'Phase 4', 'Priest', 'Shadow', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(5, 2, 7, 0, 264, 50699, 'Phase 4', 'Priest', 'Shadow', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(5, 2, 8, 0, 264, 50651, 'Phase 4', 'Priest', 'Shadow', 'Wrists', 'Both', 'The Lady''s Brittle Bracers'), +(5, 2, 9, 0, 264, 51256, 'Phase 4', 'Priest', 'Shadow', 'Hands', 'Both', 'Sanctified Crimson Acolyte Handwraps'), +(5, 2, 10, 0, 264, 50398, 'Phase 4', 'Priest', 'Shadow', 'Finger1', 'Both', 'Ashen Band of Endless Destruction'), +(5, 2, 12, 0, 264, 50348, 'Phase 4', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Dislodged Foreign Object'), +(5, 2, 14, 0, 264, 50628, 'Phase 4', 'Priest', 'Shadow', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(5, 2, 15, 0, 264, 50734, 'Phase 4', 'Priest', 'Shadow', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(5, 2, 17, 0, 264, 50684, 'Phase 4', 'Priest', 'Shadow', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(5, 2, 0, 0, 290, 51255, 'Phase 5', 'Priest', 'Shadow', 'Head', 'Both', 'Sanctified Crimson Acolyte Cowl'), +(5, 2, 1, 0, 290, 50182, 'Phase 5', 'Priest', 'Shadow', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(5, 2, 2, 0, 290, 51257, 'Phase 5', 'Priest', 'Shadow', 'Shoulders', 'Both', 'Sanctified Crimson Acolyte Mantle'), +(5, 2, 4, 0, 290, 51259, 'Phase 5', 'Priest', 'Shadow', 'Chest', 'Both', 'Sanctified Crimson Acolyte Raiments'), +(5, 2, 5, 0, 290, 50613, 'Phase 5', 'Priest', 'Shadow', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(5, 2, 6, 0, 290, 50694, 'Phase 5', 'Priest', 'Shadow', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(5, 2, 7, 0, 290, 50699, 'Phase 5', 'Priest', 'Shadow', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(5, 2, 8, 0, 290, 54582, 'Phase 5', 'Priest', 'Shadow', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(5, 2, 9, 0, 290, 51256, 'Phase 5', 'Priest', 'Shadow', 'Hands', 'Both', 'Sanctified Crimson Acolyte Handwraps'), +(5, 2, 10, 0, 290, 50664, 'Phase 5', 'Priest', 'Shadow', 'Finger1', 'Both', 'Ring of Rapid Ascent'), +(5, 2, 11, 0, 290, 50398, 'Phase 5', 'Priest', 'Shadow', 'Finger2', 'Both', 'Ashen Band of Endless Destruction'), +(5, 2, 12, 0, 290, 54588, 'Phase 5', 'Priest', 'Shadow', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(5, 2, 13, 0, 290, 50348, 'Phase 5', 'Priest', 'Shadow', 'Trinket2', 'Both', 'Dislodged Foreign Object'), +(5, 2, 14, 0, 290, 54583, 'Phase 5', 'Priest', 'Shadow', 'Back', 'Both', 'Cloak of Burning Dusk'), +(5, 2, 15, 0, 290, 50734, 'Phase 5', 'Priest', 'Shadow', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(5, 2, 16, 0, 290, 50719, 'Phase 5', 'Priest', 'Shadow', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(5, 2, 17, 0, 290, 50684, 'Phase 5', 'Priest', 'Shadow', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + + +-- ============================================================ +-- Death Knight (6) +-- ============================================================ +-- Blood Tank (tab 0) +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 0, 0, 0, 200, 41387, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Head', 'Both', 'Tempered Titansteel Helm'), +(6, 0, 1, 0, 200, 42646, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Neck', 'Both', 'Titanium Earthguard Chain'), +(6, 0, 2, 0, 200, 37814, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Shoulders', 'Both', 'Iron Dwarf Smith Pauldrons'), +(6, 0, 4, 0, 200, 37735, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Chest', 'Both', 'Ziggurat Imprinted Chestguard'), +(6, 0, 5, 0, 200, 37241, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Waist', 'Both', 'Ancient Aligned Girdle'), +(6, 0, 6, 0, 200, 37193, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Legs', 'Both', 'Staggering Legplates'), +(6, 0, 7, 0, 200, 41392, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Feet', 'Both', 'Tempered Titansteel Treads'), +(6, 0, 8, 0, 200, 37620, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Wrists', 'Both', 'Bracers of the Herald'), +(6, 0, 9, 0, 200, 37645, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Hands', 'Both', 'Horn-Tipped Gauntlets'), +(6, 0, 10, 0, 200, 42643, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Finger1', 'Both', 'Titanium Earthguard Ring'), +(6, 0, 12, 0, 200, 37220, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Trinket1', 'Both', 'Essence of Gossamer'), +(6, 0, 14, 0, 200, 43565, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Back', 'Both', 'Durable Nerubhide Cape'), +(6, 0, 15, 0, 200, 41257, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'MainHand', 'Both', 'Titansteel Destroyer'), +(6, 0, 17, 0, 200, 40714, 'Pre-Raid', 'Death Knight', 'Blood Tank', 'Ranged', 'Both', 'Sigil of the Unfaltering Knight'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 0, 0, 0, 224, 40565, 'Phase 1', 'Death Knight', 'Blood Tank', 'Head', 'Both', 'Valorous Scourgeborne Faceguard'), +(6, 0, 1, 0, 224, 40387, 'Phase 1', 'Death Knight', 'Blood Tank', 'Neck', 'Both', 'Boundless Ambition'), +(6, 0, 2, 0, 224, 40568, 'Phase 1', 'Death Knight', 'Blood Tank', 'Shoulders', 'Both', 'Valorous Scourgeborne Pauldrons'), +(6, 0, 4, 0, 224, 40559, 'Phase 1', 'Death Knight', 'Blood Tank', 'Chest', 'Both', 'Valorous Scourgeborne Chestguard'), +(6, 0, 5, 0, 224, 39759, 'Phase 1', 'Death Knight', 'Blood Tank', 'Waist', 'Both', 'Ablative Chitin Girdle'), +(6, 0, 6, 0, 224, 40589, 'Phase 1', 'Death Knight', 'Blood Tank', 'Legs', 'Both', 'Legplates of Sovereignty'), +(6, 0, 7, 0, 224, 40297, 'Phase 1', 'Death Knight', 'Blood Tank', 'Feet', 'Both', 'Sabatons of Endurance'), +(6, 0, 8, 0, 224, 40306, 'Phase 1', 'Death Knight', 'Blood Tank', 'Wrists', 'Both', 'Bracers of the Unholy Knight'), +(6, 0, 9, 0, 224, 40563, 'Phase 1', 'Death Knight', 'Blood Tank', 'Hands', 'Both', 'Valorous Scourgeborne Handguards'), +(6, 0, 10, 0, 224, 40107, 'Phase 1', 'Death Knight', 'Blood Tank', 'Finger1', 'Both', 'Sand-Worn Band'), +(6, 0, 12, 0, 224, 37220, 'Phase 1', 'Death Knight', 'Blood Tank', 'Trinket1', 'Both', 'Essence of Gossamer'), +(6, 0, 14, 0, 224, 40252, 'Phase 1', 'Death Knight', 'Blood Tank', 'Back', 'Both', 'Cloak of the Shadowed Sun'), +(6, 0, 15, 0, 224, 40406, 'Phase 1', 'Death Knight', 'Blood Tank', 'MainHand', 'Both', 'Inevitable Defeat'), +(6, 0, 17, 0, 224, 40714, 'Phase 1', 'Death Knight', 'Blood Tank', 'Ranged', 'Both', 'Sigil of the Unfaltering Knight'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 0, 0, 0, 245, 46120, 'Phase 2', 'Death Knight', 'Blood Tank', 'Head', 'Both', 'Conqueror''s Darkruned Faceguard'), +(6, 0, 1, 0, 245, 45485, 'Phase 2', 'Death Knight', 'Blood Tank', 'Neck', 'Both', 'Bronze Pendant of the Vanir'), +(6, 0, 2, 0, 245, 46122, 'Phase 2', 'Death Knight', 'Blood Tank', 'Shoulders', 'Both', 'Conqueror''s Darkruned Pauldrons'), +(6, 0, 4, 0, 245, 46039, 'Phase 2', 'Death Knight', 'Blood Tank', 'Chest', 'Both', 'Breastplate of the Timeless'), +(6, 0, 5, 0, 245, 45825, 'Phase 2', 'Death Knight', 'Blood Tank', 'Waist', 'Both', 'Shieldwarder Girdle'), +(6, 0, 6, 0, 245, 45594, 'Phase 2', 'Death Knight', 'Blood Tank', 'Legs', 'Both', 'Legplates of the Endless Void'), +(6, 0, 7, 0, 245, 45988, 'Phase 2', 'Death Knight', 'Blood Tank', 'Feet', 'Both', 'Greaves of the Iron Army'), +(6, 0, 8, 0, 245, 45111, 'Phase 2', 'Death Knight', 'Blood Tank', 'Wrists', 'Both', 'Mimiron''s Inferno Couplings'), +(6, 0, 9, 0, 245, 45487, 'Phase 2', 'Death Knight', 'Blood Tank', 'Hands', 'Both', 'Handguards of Revitalization'), +(6, 0, 10, 0, 245, 45471, 'Phase 2', 'Death Knight', 'Blood Tank', 'Finger1', 'Both', 'Fate''s Clutch'), +(6, 0, 12, 0, 245, 45158, 'Phase 2', 'Death Knight', 'Blood Tank', 'Trinket1', 'Both', 'Heart of Iron'), +(6, 0, 14, 0, 245, 45496, 'Phase 2', 'Death Knight', 'Blood Tank', 'Back', 'Both', 'Titanskin Cloak'), +(6, 0, 15, 0, 245, 45516, 'Phase 2', 'Death Knight', 'Blood', 'MainHand', 'Both', 'Voldrethar, Dark Blade of Oblivion'), +(6, 0, 17, 0, 245, 40207, 'Phase 2', 'Death Knight', 'Blood Tank', 'Ranged', 'Both', 'Sigil of Awareness'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 0, 0, 0, 258, 48545, 'Phase 3', 'Death Knight', 'Blood Tank', 'Head', 'Both', 'Faceguard of Triumph'), +(6, 0, 0, 1, 258, 48488, 'Phase 3', 'Death Knight', 'Blood', 'Head', 'Alliance', 'Thassarian''s Helmet of Triumph'), +(6, 0, 0, 2, 258, 48493, 'Phase 3', 'Death Knight', 'Blood', 'Head', 'Horde', 'Koltira''s Helmet of Triumph'), +(6, 0, 1, 1, 258, 47110, 'Phase 3', 'Death Knight', 'Blood', 'Neck', 'Alliance', 'The Executioner''s Malice'), +(6, 0, 1, 2, 258, 47458, 'Phase 3', 'Death Knight', 'Blood', 'Neck', 'Horde', 'The Executioner''s Vice'), +(6, 0, 2, 0, 258, 48543, 'Phase 3', 'Death Knight', 'Blood Tank', 'Shoulders', 'Both', 'Pauldrons of Triumph'), +(6, 0, 2, 1, 258, 48486, 'Phase 3', 'Death Knight', 'Blood', 'Shoulders', 'Alliance', 'Thassarian''s Shoulderplates of Triumph'), +(6, 0, 2, 2, 258, 48495, 'Phase 3', 'Death Knight', 'Blood', 'Shoulders', 'Horde', 'Koltira''s Shoulderplates of Triumph'), +(6, 0, 4, 1, 258, 46968, 'Phase 3', 'Death Knight', 'Blood Tank', 'Chest', 'Alliance', 'Chestplate of the Towering Monstrosity'), +(6, 0, 4, 2, 258, 47415, 'Phase 3', 'Death Knight', 'Blood Tank', 'Chest', 'Horde', 'Hauberk of the Towering Monstrosity'), +(6, 0, 5, 1, 258, 47076, 'Phase 3', 'Death Knight', 'Blood Tank', 'Waist', 'Alliance', 'Girdle of Bloodied Scars'), +(6, 0, 5, 2, 258, 47444, 'Phase 3', 'Death Knight', 'Blood Tank', 'Waist', 'Horde', 'Belt of Bloodied Scars'), +(6, 0, 6, 0, 258, 48544, 'Phase 3', 'Death Knight', 'Blood Tank', 'Legs', 'Both', 'Legguards of Triumph'), +(6, 0, 6, 1, 258, 48487, 'Phase 3', 'Death Knight', 'Blood', 'Legs', 'Alliance', 'Thassarian''s Legplates of Triumph'), +(6, 0, 6, 2, 258, 48494, 'Phase 3', 'Death Knight', 'Blood', 'Legs', 'Horde', 'Koltira''s Legplates of Triumph'), +(6, 0, 7, 0, 258, 45599, 'Phase 3', 'Death Knight', 'Blood', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(6, 0, 7, 1, 258, 47003, 'Phase 3', 'Death Knight', 'Blood Tank', 'Feet', 'Alliance', 'Dawnbreaker Greaves'), +(6, 0, 7, 2, 258, 47430, 'Phase 3', 'Death Knight', 'Blood Tank', 'Feet', 'Horde', 'Dawnbreaker Sabatons'), +(6, 0, 8, 0, 258, 45663, 'Phase 3', 'Death Knight', 'Blood', 'Wrists', 'Both', 'Armbands of Bedlam'), +(6, 0, 8, 1, 258, 47111, 'Phase 3', 'Death Knight', 'Blood Tank', 'Wrists', 'Alliance', 'Bracers of the Shieldmaiden'), +(6, 0, 8, 2, 258, 47459, 'Phase 3', 'Death Knight', 'Blood Tank', 'Wrists', 'Horde', 'Armguards of the Shieldmaiden'), +(6, 0, 9, 0, 258, 48546, 'Phase 3', 'Death Knight', 'Blood Tank', 'Hands', 'Both', 'Handguards of Triumph'), +(6, 0, 9, 1, 258, 47240, 'Phase 3', 'Death Knight', 'Blood', 'Hands', 'Alliance', 'Gloves of Bitter Reprisal'), +(6, 0, 9, 2, 258, 47492, 'Phase 3', 'Death Knight', 'Blood', 'Hands', 'Horde', 'Gauntlets of Bitter Reprisal'), +(6, 0, 10, 1, 258, 45471, 'Phase 3', 'Death Knight', 'Blood Tank', 'Finger1', 'Alliance', 'Fate''s Clutch'), +(6, 0, 10, 2, 258, 47413, 'Phase 3', 'Death Knight', 'Blood', 'Finger1', 'Horde', 'Ring of the Violent Temperament'), +(6, 0, 12, 0, 258, 45931, 'Phase 3', 'Death Knight', 'Blood', 'Trinket1', 'Both', 'Mjolnir Runestone'), +(6, 0, 12, 1, 258, 47216, 'Phase 3', 'Death Knight', 'Blood Tank', 'Trinket1', 'Alliance', 'The Black Heart'), +(6, 0, 14, 1, 258, 47549, 'Phase 3', 'Death Knight', 'Blood Tank', 'Back', 'Alliance', 'Magni''s Resolution'), +(6, 0, 14, 2, 258, 47550, 'Phase 3', 'Death Knight', 'Blood Tank', 'Back', 'Horde', 'Cairne''s Endurance'), +(6, 0, 15, 0, 258, 47156, 'Phase 3', 'Death Knight', 'Blood', 'MainHand', 'Both', 'Stormpike Cleaver'), +(6, 0, 15, 2, 258, 47475, 'Phase 3', 'Death Knight', 'Blood', 'MainHand', 'Horde', 'Hellscream Slicer'), +(6, 0, 17, 0, 258, 47672, 'Phase 3', 'Death Knight', 'Blood Tank', 'Ranged', 'Both', 'Sigil of Insolence'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 0, 0, 0, 264, 51306, 'Phase 4', 'Death Knight', 'Blood Tank', 'Head', 'Both', 'Sanctified Scourgelord Faceguard'), +(6, 0, 1, 0, 264, 54581, 'Phase 4', 'Death Knight', 'Blood', 'Neck', 'Both', 'Penumbra Pendant'), +(6, 0, 2, 0, 264, 51309, 'Phase 4', 'Death Knight', 'Blood Tank', 'Shoulders', 'Both', 'Sanctified Scourgelord Pauldrons'), +(6, 0, 4, 0, 264, 51305, 'Phase 4', 'Death Knight', 'Blood Tank', 'Chest', 'Both', 'Sanctified Scourgelord Chestguard'), +(6, 0, 5, 0, 264, 50691, 'Phase 4', 'Death Knight', 'Blood Tank', 'Waist', 'Both', 'Belt of Broken Bones'), +(6, 0, 6, 0, 264, 50612, 'Phase 4', 'Death Knight', 'Blood Tank', 'Legs', 'Both', 'Legguards of Lost Hope'), +(6, 0, 7, 0, 264, 50625, 'Phase 4', 'Death Knight', 'Blood Tank', 'Feet', 'Both', 'Grinning Skull Greatboots'), +(6, 0, 8, 0, 264, 50611, 'Phase 4', 'Death Knight', 'Blood Tank', 'Wrists', 'Both', 'Bracers of Dark Reckoning'), +(6, 0, 9, 0, 264, 51307, 'Phase 4', 'Death Knight', 'Blood Tank', 'Hands', 'Both', 'Sanctified Scourgelord Handguards'), +(6, 0, 10, 0, 264, 50622, 'Phase 4', 'Death Knight', 'Blood Tank', 'Finger1', 'Both', 'Devium''s Eternally Cold Ring'), +(6, 0, 12, 0, 264, 50364, 'Phase 4', 'Death Knight', 'Blood Tank', 'Trinket1', 'Both', 'Sindragosa''s Flawless Fang'), +(6, 0, 14, 0, 264, 50718, 'Phase 4', 'Death Knight', 'Blood Tank', 'Back', 'Both', 'Royal Crimson Cloak'), +(6, 0, 15, 0, 264, 49623, 'Phase 4', 'Death Knight', 'Blood', 'MainHand', 'Both', 'Shadowmourne'), +(6, 0, 17, 0, 264, 50462, 'Phase 4', 'Death Knight', 'Blood Tank', 'Ranged', 'Both', 'Sigil of the Bone Gryphon'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 0, 0, 0, 290, 51306, 'Phase 5', 'DeathKnight', 'BloodTank', 'Head', 'Both', 'Sanctified Scourgelord Faceguard'), +(6, 0, 1, 0, 290, 50682, 'Phase 5', 'DeathKnight', 'BloodTank', 'Neck', 'Both', 'Bile-Encrusted Medallion'), +(6, 0, 2, 0, 290, 51309, 'Phase 5', 'DeathKnight', 'BloodTank', 'Shoulders', 'Both', 'Sanctified Scourgelord Pauldrons'), +(6, 0, 4, 0, 290, 51305, 'Phase 5', 'DeathKnight', 'BloodTank', 'Chest', 'Both', 'Sanctified Scourgelord Chestguard'), +(6, 0, 5, 0, 290, 50991, 'Phase 5', 'DeathKnight', 'BloodTank', 'Waist', 'Both', 'Verdigris Chain Belt'), +(6, 0, 6, 0, 290, 50612, 'Phase 5', 'DeathKnight', 'BloodTank', 'Legs', 'Both', 'Legguards of Lost Hope'), +(6, 0, 7, 0, 290, 54579, 'Phase 5', 'DeathKnight', 'BloodTank', 'Feet', 'Both', 'Treads of Impending Resurrection'), +(6, 0, 8, 0, 290, 51901, 'Phase 5', 'DeathKnight', 'BloodTank', 'Wrists', 'Both', 'Gargoyle Spit Bracers'), +(6, 0, 9, 0, 290, 51307, 'Phase 5', 'DeathKnight', 'BloodTank', 'Hands', 'Both', 'Sanctified Scourgelord Handguards'), +(6, 0, 10, 0, 290, 50622, 'Phase 5', 'DeathKnight', 'BloodTank', 'Finger1', 'Both', 'Devium''s Eternally Cold Ring'), +(6, 0, 11, 0, 290, 50404, 'Phase 5', 'DeathKnight', 'BloodTank', 'Finger2', 'Both', 'Ashen Band of Endless Courage'), +(6, 0, 12, 0, 290, 54591, 'Phase 5', 'DeathKnight', 'BloodTank', 'Trinket1', 'Both', 'Petrified Twilight Scale'), +(6, 0, 13, 0, 290, 50364, 'Phase 5', 'DeathKnight', 'BloodTank', 'Trinket2', 'Both', 'Sindragosa''s Flawless Fang'), +(6, 0, 14, 0, 290, 50466, 'Phase 5', 'DeathKnight', 'BloodTank', 'Back', 'Both', 'Sentinel''s Winter Cloak'), +(6, 0, 15, 0, 290, 49623, 'Phase 5', 'DeathKnight', 'BloodTank', 'MainHand', 'Both', 'Shadowmourne'), +(6, 0, 17, 0, 290, 50462, 'Phase 5', 'DeathKnight', 'BloodTank', 'Ranged', 'Both', 'Sigil of the Bone Gryphon'); + +-- Frost (tab 1) +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 1, 0, 0, 200, 41386, 'Pre-Raid', 'Death Knight', 'Frost', 'Head', 'Both', 'Spiked Titansteel Helm'), +(6, 1, 1, 0, 200, 42645, 'Pre-Raid', 'Death Knight', 'Frost', 'Neck', 'Both', 'Titanium Impact Choker'), +(6, 1, 2, 0, 200, 37627, 'Pre-Raid', 'Death Knight', 'Frost', 'Shoulders', 'Both', 'Snake Den Spaulders'), +(6, 1, 4, 0, 200, 37612, 'Pre-Raid', 'Death Knight', 'Frost', 'Chest', 'Both', 'Bonegrinder Breastplate'), +(6, 1, 5, 0, 200, 37171, 'Pre-Raid', 'Death Knight', 'Frost', 'Waist', 'Both', 'Flame-Bathed Steel Girdle'), +(6, 1, 6, 0, 200, 37193, 'Pre-Raid', 'Death Knight', 'Frost', 'Legs', 'Both', 'Staggering Legplates'), +(6, 1, 7, 0, 200, 41391, 'Pre-Raid', 'Death Knight', 'Frost', 'Feet', 'Both', 'Spiked Titansteel Treads'), +(6, 1, 8, 0, 200, 37668, 'Pre-Raid', 'Death Knight', 'Frost', 'Wrists', 'Both', 'Bands of the Stoneforge'), +(6, 1, 9, 0, 200, 37363, 'Pre-Raid', 'Death Knight', 'Frost', 'Hands', 'Both', 'Gauntlets of Dragon Wrath'), +(6, 1, 10, 0, 200, 37642, 'Pre-Raid', 'Death Knight', 'Frost', 'Finger1', 'Both', 'Hemorrhaging Circle'), +(6, 1, 12, 0, 200, 40684, 'Pre-Raid', 'Death Knight', 'Frost', 'Trinket1', 'Both', 'Mirror of Truth'), +(6, 1, 14, 0, 200, 43566, 'Pre-Raid', 'Death Knight', 'Frost', 'Back', 'Both', 'Ice Striker''s Cloak'), +(6, 1, 15, 0, 200, 41383, 'Pre-Raid', 'Death Knight', 'Frost', 'MainHand', 'Both', 'Titansteel Bonecrusher'), +(6, 1, 17, 0, 200, 40715, 'Pre-Raid', 'Death Knight', 'Frost', 'Ranged', 'Both', 'Sigil of Haunted Dreams'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 1, 0, 0, 224, 44006, 'Phase 1', 'Death Knight', 'Frost', 'Head', 'Both', 'Obsidian Greathelm'), +(6, 1, 1, 0, 224, 44664, 'Phase 1', 'Death Knight', 'Frost', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(6, 1, 2, 0, 224, 40557, 'Phase 1', 'Death Knight', 'Frost', 'Shoulders', 'Both', 'Valorous Scourgeborne Shoulderplates'), +(6, 1, 4, 0, 224, 40550, 'Phase 1', 'Death Knight', 'Frost', 'Chest', 'Both', 'Valorous Scourgeborne Battleplate'), +(6, 1, 5, 0, 224, 40317, 'Phase 1', 'Death Knight', 'Frost', 'Waist', 'Both', 'Girdle of Razuvious'), +(6, 1, 6, 0, 224, 40556, 'Phase 1', 'Death Knight', 'Frost', 'Legs', 'Both', 'Valorous Scourgeborne Legplates'), +(6, 1, 7, 0, 224, 40591, 'Phase 1', 'Death Knight', 'Frost', 'Feet', 'Both', 'Melancholy Sabatons'), +(6, 1, 8, 0, 224, 40330, 'Phase 1', 'Death Knight', 'Frost', 'Wrists', 'Both', 'Bracers of Unrelenting Attack'), +(6, 1, 9, 0, 224, 40552, 'Phase 1', 'Death Knight', 'Frost', 'Hands', 'Both', 'Valorous Scourgeborne Gauntlets'), +(6, 1, 10, 0, 224, 40075, 'Phase 1', 'Death Knight', 'Frost', 'Finger1', 'Both', 'Ruthlessness'), +(6, 1, 12, 0, 224, 40256, 'Phase 1', 'Death Knight', 'Frost', 'Trinket1', 'Both', 'Grim Toll'), +(6, 1, 14, 0, 224, 40403, 'Phase 1', 'Death Knight', 'Frost', 'Back', 'Both', 'Drape of the Deadly Foe'), +(6, 1, 15, 0, 224, 40189, 'Phase 1', 'Death Knight', 'Frost', 'MainHand', 'Both', 'Angry Dread'), +(6, 1, 17, 0, 224, 40207, 'Phase 1', 'Death Knight', 'Frost', 'Ranged', 'Both', 'Sigil of Awareness'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 1, 0, 0, 245, 46115, 'Phase 2', 'Death Knight', 'Frost', 'Head', 'Both', 'Conqueror''s Darkruned Helmet'), +(6, 1, 1, 0, 245, 45459, 'Phase 2', 'Death Knight', 'Frost', 'Neck', 'Both', 'Frigid Strength of Hodir'), +(6, 1, 2, 0, 245, 46117, 'Phase 2', 'Death Knight', 'Frost', 'Shoulders', 'Both', 'Conqueror''s Darkruned Shoulderplates'), +(6, 1, 4, 0, 245, 46111, 'Phase 2', 'Death Knight', 'Frost', 'Chest', 'Both', 'Conqueror''s Darkruned Battleplate'), +(6, 1, 5, 0, 245, 45241, 'Phase 2', 'Death Knight', 'Frost', 'Waist', 'Both', 'Belt of Colossal Rage'), +(6, 1, 6, 0, 245, 45134, 'Phase 2', 'Death Knight', 'Frost', 'Legs', 'Both', 'Plated Leggings of Ruination'), +(6, 1, 7, 0, 245, 45599, 'Phase 2', 'Death Knight', 'Frost', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(6, 1, 8, 0, 245, 45663, 'Phase 2', 'Death Knight', 'Frost', 'Wrists', 'Both', 'Armbands of Bedlam'), +(6, 1, 9, 0, 245, 46113, 'Phase 2', 'Death Knight', 'Frost', 'Hands', 'Both', 'Conqueror''s Darkruned Gauntlets'), +(6, 1, 10, 0, 245, 45534, 'Phase 2', 'Death Knight', 'Frost', 'Finger1', 'Both', 'Seal of the Betrayed King'), +(6, 1, 12, 0, 245, 45931, 'Phase 2', 'Death Knight', 'Frost', 'Trinket1', 'Both', 'Mjolnir Runestone'), +(6, 1, 14, 0, 245, 46032, 'Phase 2', 'Death Knight', 'Frost', 'Back', 'Both', 'Drape of the Faceless General'), +(6, 1, 15, 0, 245, 46097, 'Phase 2', 'Death Knight', 'Frost', 'MainHand', 'Both', 'Caress of Insanity'), +(6, 1, 17, 0, 245, 40207, 'Phase 2', 'Death Knight', 'Frost', 'Ranged', 'Both', 'Sigil of Awareness'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 1, 0, 1, 258, 48488, 'Phase 3', 'Death Knight', 'Frost', 'Head', 'Alliance', 'Thassarian''s Helmet of Triumph'), +(6, 1, 0, 2, 258, 48493, 'Phase 3', 'Death Knight', 'Frost', 'Head', 'Horde', 'Koltira''s Helmet of Triumph'), +(6, 1, 1, 1, 258, 47110, 'Phase 3', 'Death Knight', 'Frost', 'Neck', 'Alliance', 'The Executioner''s Malice'), +(6, 1, 1, 2, 258, 47458, 'Phase 3', 'Death Knight', 'Frost', 'Neck', 'Horde', 'The Executioner''s Vice'), +(6, 1, 2, 1, 258, 48486, 'Phase 3', 'Death Knight', 'Frost', 'Shoulders', 'Alliance', 'Thassarian''s Shoulderplates of Triumph'), +(6, 1, 2, 2, 258, 48495, 'Phase 3', 'Death Knight', 'Frost', 'Shoulders', 'Horde', 'Koltira''s Shoulderplates of Triumph'), +(6, 1, 4, 1, 258, 48490, 'Phase 3', 'Death Knight', 'Frost', 'Chest', 'Alliance', 'Thassarian''s Battleplate of Triumph'), +(6, 1, 4, 2, 258, 48491, 'Phase 3', 'Death Knight', 'Frost', 'Chest', 'Horde', 'Koltira''s Battleplate of Triumph'), +(6, 1, 5, 0, 258, 45241, 'Phase 3', 'Death Knight', 'Frost', 'Waist', 'Both', 'Belt of Colossal Rage'), +(6, 1, 6, 1, 258, 48487, 'Phase 3', 'Death Knight', 'Frost', 'Legs', 'Alliance', 'Thassarian''s Legplates of Triumph'), +(6, 1, 6, 2, 258, 48494, 'Phase 3', 'Death Knight', 'Frost', 'Legs', 'Horde', 'Koltira''s Legplates of Triumph'), +(6, 1, 7, 0, 258, 45599, 'Phase 3', 'Death Knight', 'Frost', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(6, 1, 8, 0, 258, 45663, 'Phase 3', 'Death Knight', 'Frost', 'Wrists', 'Both', 'Armbands of Bedlam'), +(6, 1, 9, 1, 258, 47240, 'Phase 3', 'Death Knight', 'Frost', 'Hands', 'Alliance', 'Gloves of Bitter Reprisal'), +(6, 1, 9, 2, 258, 47492, 'Phase 3', 'Death Knight', 'Frost', 'Hands', 'Horde', 'Gauntlets of Bitter Reprisal'), +(6, 1, 10, 1, 258, 46966, 'Phase 3', 'Death Knight', 'Frost', 'Finger1', 'Alliance', 'Band of the Violent Temperment'), +(6, 1, 10, 2, 258, 47413, 'Phase 3', 'Death Knight', 'Frost', 'Finger1', 'Horde', 'Ring of the Violent Temperament'), +(6, 1, 12, 0, 258, 45931, 'Phase 3', 'Death Knight', 'Frost', 'Trinket1', 'Both', 'Mjolnir Runestone'), +(6, 1, 14, 1, 258, 47547, 'Phase 3', 'Death Knight', 'Frost', 'Back', 'Alliance', 'Varian''s Furor'), +(6, 1, 14, 2, 258, 47548, 'Phase 3', 'Death Knight', 'Frost', 'Back', 'Horde', 'Garrosh''s Rage'), +(6, 1, 15, 0, 258, 47156, 'Phase 3', 'Death Knight', 'Frost', 'MainHand', 'Both', 'Stormpike Cleaver'), +(6, 1, 15, 2, 258, 47475, 'Phase 3', 'Death Knight', 'Frost', 'MainHand', 'Horde', 'Hellscream Slicer'), +(6, 1, 17, 0, 258, 40207, 'Phase 3', 'Death Knight', 'Frost', 'Ranged', 'Both', 'Sigil of Awareness'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 1, 0, 0, 264, 51312, 'Phase 4', 'Death Knight', 'Frost', 'Head', 'Both', 'Sanctified Scourgelord Helmet'), +(6, 1, 1, 0, 264, 50728, 'Phase 4', 'Death Knight', 'Frost', 'Neck', 'Both', 'Lana''thel''s Chain of Flagellation'), +(6, 1, 2, 0, 264, 51314, 'Phase 4', 'Death Knight', 'Frost', 'Shoulders', 'Both', 'Sanctified Scourgelord Shoulderplates'), +(6, 1, 4, 0, 264, 51310, 'Phase 4', 'Death Knight', 'Frost', 'Chest', 'Both', 'Sanctified Scourgelord Battleplate'), +(6, 1, 5, 0, 264, 50620, 'Phase 4', 'Death Knight', 'Frost', 'Waist', 'Both', 'Coldwraith Links'), +(6, 1, 6, 0, 264, 51817, 'Phase 4', 'Death Knight', 'Frost', 'Legs', 'Both', 'Legplates of Aetheric Strife'), +(6, 1, 7, 0, 264, 50639, 'Phase 4', 'Death Knight', 'Frost', 'Feet', 'Both', 'Blood-Soaked Saronite Stompers'), +(6, 1, 8, 0, 264, 50659, 'Phase 4', 'Death Knight', 'Frost', 'Wrists', 'Both', 'Polar Bear Claw Bracers'), +(6, 1, 9, 0, 264, 51311, 'Phase 4', 'Death Knight', 'Frost', 'Hands', 'Both', 'Sanctified Scourgelord Gauntlets'), +(6, 1, 10, 0, 264, 52572, 'Phase 4', 'Death Knight', 'Frost', 'Finger1', 'Both', 'Ashen Band of Endless Might'), +(6, 1, 12, 0, 264, 50363, 'Phase 4', 'Death Knight', 'Frost', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(6, 1, 14, 1, 264, 47547, 'Phase 4', 'Death Knight', 'Frost', 'Back', 'Alliance', 'Varian''s Furor'), +(6, 1, 14, 2, 264, 47548, 'Phase 4', 'Death Knight', 'Frost', 'Back', 'Horde', 'Garrosh''s Rage'), +(6, 1, 15, 0, 264, 50737, 'Phase 4', 'Death Knight', 'Frost', 'MainHand', 'Both', 'Havoc''s Call, Blade of Lordaeron Kings'), +(6, 1, 17, 0, 264, 40207, 'Phase 4', 'Death Knight', 'Frost', 'Ranged', 'Both', 'Sigil of Awareness'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 1, 0, 0, 290, 51312, 'Phase 5', 'DeathKnight', 'Frost', 'Head', 'Both', 'Sanctified Scourgelord Helmet'), +(6, 1, 1, 0, 290, 54581, 'Phase 5', 'DeathKnight', 'Frost', 'Neck', 'Both', 'Penumbra Pendant'), +(6, 1, 2, 0, 290, 51314, 'Phase 5', 'DeathKnight', 'Frost', 'Shoulders', 'Both', 'Sanctified Scourgelord Shoulderplates'), +(6, 1, 4, 0, 290, 51310, 'Phase 5', 'DeathKnight', 'Frost', 'Chest', 'Both', 'Sanctified Scourgelord Battleplate'), +(6, 1, 5, 0, 290, 50620, 'Phase 5', 'DeathKnight', 'Frost', 'Waist', 'Both', 'Coldwraith Links'), +(6, 1, 6, 0, 290, 51313, 'Phase 5', 'DeathKnight', 'Frost', 'Legs', 'Both', 'Sanctified Scourgelord Legplates'), +(6, 1, 7, 0, 290, 54578, 'Phase 5', 'DeathKnight', 'Frost', 'Feet', 'Both', 'Apocalypse''s Advance'), +(6, 1, 8, 0, 290, 50670, 'Phase 5', 'DeathKnight', 'Frost', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(6, 1, 9, 0, 290, 50690, 'Phase 5', 'DeathKnight', 'Frost', 'Hands', 'Both', 'Fleshrending Gauntlets'), +(6, 1, 10, 0, 290, 50693, 'Phase 5', 'DeathKnight', 'Frost', 'Finger1', 'Both', 'Might of Blight'), +(6, 1, 11, 0, 290, 52572, 'Phase 5', 'DeathKnight', 'Frost', 'Finger2', 'Both', 'Ashen Band of Endless Might'), +(6, 1, 12, 0, 290, 54590, 'Phase 5', 'DeathKnight', 'Frost', 'Trinket1', 'Both', 'Sharpened Twilight Scale'), +(6, 1, 13, 0, 290, 50363, 'Phase 5', 'DeathKnight', 'Frost', 'Trinket2', 'Both', 'Deathbringer''s Will'), +(6, 1, 14, 0, 290, 50677, 'Phase 5', 'DeathKnight', 'Frost', 'Back', 'Both', 'Winding Sheet'), +(6, 1, 15, 0, 290, 50737, 'Phase 5', 'DeathKnight', 'Frost', 'MainHand', 'Both', 'Havoc''s Call, Blade of Lordaeron Kings'), +(6, 1, 16, 0, 290, 50737, 'Phase 5', 'DeathKnight', 'Frost', 'OffHand', 'Both', 'Havoc''s Call, Blade of Lordaeron Kings'), +(6, 1, 17, 0, 290, 50459, 'Phase 5', 'DeathKnight', 'Frost', 'Ranged', 'Both', 'Sigil of the Hanged Man'); + +-- Unholy (tab 2) +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 2, 0, 0, 200, 41386, 'Pre-Raid', 'Death Knight', 'Unholy', 'Head', 'Both', 'Spiked Titansteel Helm'), +(6, 2, 1, 0, 200, 42645, 'Pre-Raid', 'Death Knight', 'Unholy', 'Neck', 'Both', 'Titanium Impact Choker'), +(6, 2, 2, 0, 200, 37627, 'Pre-Raid', 'Death Knight', 'Unholy', 'Shoulders', 'Both', 'Snake Den Spaulders'), +(6, 2, 4, 0, 200, 39617, 'Pre-Raid', 'Death Knight', 'Unholy', 'Chest', 'Both', 'Heroes'' Scourgeborne Battleplate'), +(6, 2, 5, 0, 200, 37178, 'Pre-Raid', 'Death Knight', 'Unholy', 'Waist', 'Both', 'Strategist''s Belt'), +(6, 2, 6, 0, 200, 39620, 'Pre-Raid', 'Death Knight', 'Unholy', 'Legs', 'Both', 'Heroes'' Scourgeborne Legplates'), +(6, 2, 7, 0, 200, 43402, 'Pre-Raid', 'Death Knight', 'Unholy', 'Feet', 'Both', 'The Obliterator Greaves'), +(6, 2, 8, 0, 200, 37668, 'Pre-Raid', 'Death Knight', 'Unholy', 'Wrists', 'Both', 'Bands of the Stoneforge'), +(6, 2, 9, 0, 200, 39618, 'Pre-Raid', 'Death Knight', 'Unholy', 'Hands', 'Both', 'Heroes'' Scourgeborne Gauntlets'), +(6, 2, 10, 0, 200, 37151, 'Pre-Raid', 'Death Knight', 'Unholy', 'Finger1', 'Both', 'Band of Frosted Thorns'), +(6, 2, 12, 0, 200, 40684, 'Pre-Raid', 'Death Knight', 'Unholy', 'Trinket1', 'Both', 'Mirror of Truth'), +(6, 2, 14, 0, 200, 43566, 'Pre-Raid', 'Death Knight', 'Unholy', 'Back', 'Both', 'Ice Striker''s Cloak'), +(6, 2, 15, 0, 200, 41257, 'Pre-Raid', 'Death Knight', 'Unholy', 'MainHand', 'Both', 'Titansteel Destroyer'), +(6, 2, 17, 0, 200, 40715, 'Pre-Raid', 'Death Knight', 'Unholy', 'Ranged', 'Both', 'Sigil of Haunted Dreams'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 2, 0, 0, 224, 44006, 'Phase 1', 'Death Knight', 'Unholy', 'Head', 'Both', 'Obsidian Greathelm'), +(6, 2, 1, 0, 224, 44664, 'Phase 1', 'Death Knight', 'Unholy', 'Neck', 'Both', 'Favor of the Dragon Queen'), +(6, 2, 2, 0, 224, 40557, 'Phase 1', 'Death Knight', 'Unholy', 'Shoulders', 'Both', 'Valorous Scourgeborne Shoulderplates'), +(6, 2, 4, 0, 224, 40550, 'Phase 1', 'Death Knight', 'Unholy', 'Chest', 'Both', 'Valorous Scourgeborne Battleplate'), +(6, 2, 5, 0, 224, 40317, 'Phase 1', 'Death Knight', 'Unholy', 'Waist', 'Both', 'Girdle of Razuvious'), +(6, 2, 6, 0, 224, 40556, 'Phase 1', 'Death Knight', 'Unholy', 'Legs', 'Both', 'Valorous Scourgeborne Legplates'), +(6, 2, 7, 0, 224, 40206, 'Phase 1', 'Death Knight', 'Unholy', 'Feet', 'Both', 'Iron-Spring Jumpers'), +(6, 2, 8, 0, 224, 40330, 'Phase 1', 'Death Knight', 'Unholy', 'Wrists', 'Both', 'Bracers of Unrelenting Attack'), +(6, 2, 9, 0, 224, 40552, 'Phase 1', 'Death Knight', 'Unholy', 'Hands', 'Both', 'Valorous Scourgeborne Gauntlets'), +(6, 2, 10, 0, 224, 40074, 'Phase 1', 'Death Knight', 'Unholy', 'Finger1', 'Both', 'Strong-Handed Ring'), +(6, 2, 12, 0, 224, 42987, 'Phase 1', 'Death Knight', 'Unholy', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(6, 2, 14, 0, 224, 40250, 'Phase 1', 'Death Knight', 'Unholy', 'Back', 'Both', 'Aged Winter Cloak'), +(6, 2, 17, 0, 224, 40207, 'Phase 1', 'Death Knight', 'Unholy', 'Ranged', 'Both', 'Sigil of Awareness'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 2, 0, 1, 245, 51312, 'Phase 2', 'Death Knight', 'Unholy', 'Head', 'Alliance', 'Sanctified Scourgelord Helmet'), +(6, 2, 0, 2, 245, 45472, 'Phase 2', 'Death Knight', 'Unholy', 'Head', 'Horde', 'Warhelm of the Champion'), +(6, 2, 1, 1, 245, 54581, 'Phase 2', 'Death Knight', 'Unholy', 'Neck', 'Alliance', 'Penumbra Pendant'), +(6, 2, 1, 2, 245, 46040, 'Phase 2', 'Death Knight', 'Unholy', 'Neck', 'Horde', 'Strength of the Heavens'), +(6, 2, 2, 1, 245, 51314, 'Phase 2', 'Death Knight', 'Unholy', 'Shoulders', 'Alliance', 'Sanctified Scourgelord Shoulderplates'), +(6, 2, 2, 2, 245, 46117, 'Phase 2', 'Death Knight', 'Unholy', 'Shoulders', 'Horde', 'Conqueror''s Darkruned Shoulderplates'), +(6, 2, 4, 1, 245, 51310, 'Phase 2', 'Death Knight', 'Unholy', 'Chest', 'Alliance', 'Sanctified Scourgelord Battleplate'), +(6, 2, 4, 2, 245, 46111, 'Phase 2', 'Death Knight', 'Unholy', 'Chest', 'Horde', 'Conqueror''s Darkruned Battleplate'), +(6, 2, 5, 1, 245, 50620, 'Phase 2', 'Death Knight', 'Unholy', 'Waist', 'Alliance', 'Coldwraith Links'), +(6, 2, 5, 2, 245, 45241, 'Phase 2', 'Death Knight', 'Unholy', 'Waist', 'Horde', 'Belt of Colossal Rage'), +(6, 2, 6, 1, 245, 50624, 'Phase 2', 'Death Knight', 'Unholy', 'Legs', 'Alliance', 'Scourge Reaver''s Legplates'), +(6, 2, 6, 2, 245, 46116, 'Phase 2', 'Death Knight', 'Unholy', 'Legs', 'Horde', 'Conqueror''s Darkruned Legplates'), +(6, 2, 7, 1, 245, 54578, 'Phase 2', 'Death Knight', 'Unholy', 'Feet', 'Alliance', 'Apocalypse''s Advance'), +(6, 2, 7, 2, 245, 45599, 'Phase 2', 'Death Knight', 'Unholy', 'Feet', 'Horde', 'Sabatons of Lifeless Night'), +(6, 2, 8, 1, 245, 51842, 'Phase 2', 'Death Knight', 'Unholy', 'Wrists', 'Alliance', 'Collar of Haughty Disdain'), +(6, 2, 8, 2, 245, 45663, 'Phase 2', 'Death Knight', 'Unholy', 'Wrists', 'Horde', 'Armbands of Bedlam'), +(6, 2, 9, 1, 245, 51311, 'Phase 2', 'Death Knight', 'Unholy', 'Hands', 'Alliance', 'Sanctified Scourgelord Gauntlets'), +(6, 2, 9, 2, 245, 46113, 'Phase 2', 'Death Knight', 'Unholy', 'Hands', 'Horde', 'Conqueror''s Darkruned Gauntlets'), +(6, 2, 10, 1, 245, 50693, 'Phase 2', 'Death Knight', 'Unholy', 'Finger1', 'Alliance', 'Might of Blight'), +(6, 2, 10, 2, 245, 45469, 'Phase 2', 'Death Knight', 'Unholy', 'Finger1', 'Horde', 'Sif''s Promise'), +(6, 2, 12, 1, 245, 54590, 'Phase 2', 'Death Knight', 'Unholy', 'Trinket1', 'Alliance', 'Sharpened Twilight Scale'), +(6, 2, 12, 2, 245, 45609, 'Phase 2', 'Death Knight', 'Unholy', 'Trinket1', 'Horde', 'Comet''s Trail'), +(6, 2, 14, 1, 245, 50677, 'Phase 2', 'Death Knight', 'Unholy', 'Back', 'Alliance', 'Winding Sheet'), +(6, 2, 14, 2, 245, 46032, 'Phase 2', 'Death Knight', 'Unholy', 'Back', 'Horde', 'Drape of the Faceless General'), +(6, 2, 17, 1, 245, 50459, 'Phase 2', 'Death Knight', 'Unholy', 'Ranged', 'Alliance', 'Sigil of the Hanged Man'), +(6, 2, 17, 2, 245, 45254, 'Phase 2', 'Death Knight', 'Unholy', 'Ranged', 'Horde', 'Sigil of the Vengeful Heart'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 2, 0, 0, 258, 45472, 'Phase 3', 'Death Knight', 'Unholy', 'Head', 'Both', 'Warhelm of the Champion'), +(6, 2, 1, 0, 258, 46040, 'Phase 3', 'Death Knight', 'Unholy', 'Neck', 'Both', 'Strength of the Heavens'), +(6, 2, 2, 0, 258, 46117, 'Phase 3', 'Death Knight', 'Unholy', 'Shoulders', 'Both', 'Conqueror''s Darkruned Shoulderplates'), +(6, 2, 4, 0, 258, 45712, 'Phase 3', 'Death Knight', 'Unholy', 'Chest', 'Both', 'Chestplate of Titanic Fury'), +(6, 2, 5, 0, 258, 45241, 'Phase 3', 'Death Knight', 'Unholy', 'Waist', 'Both', 'Belt of Colossal Rage'), +(6, 2, 6, 0, 258, 46116, 'Phase 3', 'Death Knight', 'Unholy', 'Legs', 'Both', 'Conqueror''s Darkruned Legplates'), +(6, 2, 7, 0, 258, 45599, 'Phase 3', 'Death Knight', 'Unholy', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(6, 2, 8, 0, 258, 45663, 'Phase 3', 'Death Knight', 'Unholy', 'Wrists', 'Both', 'Armbands of Bedlam'), +(6, 2, 9, 0, 258, 46113, 'Phase 3', 'Death Knight', 'Unholy', 'Hands', 'Both', 'Conqueror''s Darkruned Gauntlets'), +(6, 2, 10, 0, 258, 45250, 'Phase 3', 'Death Knight', 'Unholy', 'Finger1', 'Both', 'Crazed Construct Ring'), +(6, 2, 12, 0, 258, 40531, 'Phase 3', 'Death Knight', 'Unholy', 'Trinket1', 'Both', 'Mark of Norgannon'), +(6, 2, 14, 0, 258, 45588, 'Phase 3', 'Death Knight', 'Unholy', 'Back', 'Both', 'Drape of the Skyborn'), +(6, 2, 17, 0, 258, 45254, 'Phase 3', 'Death Knight', 'Unholy', 'Ranged', 'Both', 'Sigil of the Vengeful Heart'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 2, 0, 1, 264, 48488, 'Phase 4', 'Death Knight', 'Unholy', 'Head', 'Alliance', 'Thassarian''s Helmet of Triumph'), +(6, 2, 0, 2, 264, 48493, 'Phase 4', 'Death Knight', 'Unholy', 'Head', 'Horde', 'Koltira''s Helmet of Triumph'), +(6, 2, 1, 1, 264, 47110, 'Phase 4', 'Death Knight', 'Unholy', 'Neck', 'Alliance', 'The Executioner''s Malice'), +(6, 2, 1, 2, 264, 47458, 'Phase 4', 'Death Knight', 'Unholy', 'Neck', 'Horde', 'The Executioner''s Vice'), +(6, 2, 2, 1, 264, 48486, 'Phase 4', 'Death Knight', 'Unholy', 'Shoulders', 'Alliance', 'Thassarian''s Shoulderplates of Triumph'), +(6, 2, 2, 2, 264, 48495, 'Phase 4', 'Death Knight', 'Unholy', 'Shoulders', 'Horde', 'Koltira''s Shoulderplates of Triumph'), +(6, 2, 4, 1, 264, 48490, 'Phase 4', 'Death Knight', 'Unholy', 'Chest', 'Alliance', 'Thassarian''s Battleplate of Triumph'), +(6, 2, 4, 2, 264, 48491, 'Phase 4', 'Death Knight', 'Unholy', 'Chest', 'Horde', 'Koltira''s Battleplate of Triumph'), +(6, 2, 5, 1, 264, 47002, 'Phase 4', 'Death Knight', 'Unholy', 'Waist', 'Alliance', 'Bloodbath Belt'), +(6, 2, 5, 2, 264, 47429, 'Phase 4', 'Death Knight', 'Unholy', 'Waist', 'Horde', 'Bloodbath Girdle'), +(6, 2, 6, 1, 264, 47132, 'Phase 4', 'Death Knight', 'Unholy', 'Legs', 'Alliance', 'Legguards of Ascension'), +(6, 2, 6, 2, 264, 47465, 'Phase 4', 'Death Knight', 'Unholy', 'Legs', 'Horde', 'Legplates of Ascension'), +(6, 2, 7, 0, 264, 45599, 'Phase 4', 'Death Knight', 'Unholy', 'Feet', 'Both', 'Sabatons of Lifeless Night'), +(6, 2, 8, 0, 264, 45663, 'Phase 4', 'Death Knight', 'Unholy', 'Wrists', 'Both', 'Armbands of Bedlam'), +(6, 2, 9, 1, 264, 48489, 'Phase 4', 'Death Knight', 'Unholy', 'Hands', 'Alliance', 'Thassarian''s Gauntlets of Triumph'), +(6, 2, 9, 2, 264, 48492, 'Phase 4', 'Death Knight', 'Unholy', 'Hands', 'Horde', 'Koltira''s Gauntlets of Triumph'), +(6, 2, 10, 1, 264, 47920, 'Phase 4', 'Death Knight', 'Unholy', 'Finger1', 'Alliance', 'Carnivorous Band'), +(6, 2, 10, 2, 264, 47993, 'Phase 4', 'Death Knight', 'Unholy', 'Finger1', 'Horde', 'Gormok''s Band'), +(6, 2, 12, 1, 264, 47131, 'Phase 4', 'Death Knight', 'Unholy', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(6, 2, 12, 2, 264, 47464, 'Phase 4', 'Death Knight', 'Unholy', 'Trinket1', 'Horde', 'Death''s Choice'), +(6, 2, 14, 1, 264, 47547, 'Phase 4', 'Death Knight', 'Unholy', 'Back', 'Alliance', 'Varian''s Furor'), +(6, 2, 14, 2, 264, 47548, 'Phase 4', 'Death Knight', 'Unholy', 'Back', 'Horde', 'Garrosh''s Rage'), +(6, 2, 17, 0, 264, 47673, 'Phase 4', 'Death Knight', 'Unholy', 'Ranged', 'Both', 'Sigil of Virulence'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(6, 2, 0, 0, 290, 51312, 'Phase 5', 'DeathKnight', 'Unholy', 'Head', 'Both', 'Sanctified Scourgelord Helmet'), +(6, 2, 1, 0, 290, 54581, 'Phase 5', 'DeathKnight', 'Unholy', 'Neck', 'Both', 'Penumbra Pendant'), +(6, 2, 2, 0, 290, 51314, 'Phase 5', 'DeathKnight', 'Unholy', 'Shoulders', 'Both', 'Sanctified Scourgelord Shoulderplates'), +(6, 2, 4, 0, 290, 51310, 'Phase 5', 'DeathKnight', 'Unholy', 'Chest', 'Both', 'Sanctified Scourgelord Battleplate'), +(6, 2, 5, 0, 290, 50620, 'Phase 5', 'DeathKnight', 'Unholy', 'Waist', 'Both', 'Coldwraith Links'), +(6, 2, 6, 0, 290, 51313, 'Phase 5', 'DeathKnight', 'Unholy', 'Legs', 'Both', 'Sanctified Scourgelord Legplates'), +(6, 2, 7, 0, 290, 54578, 'Phase 5', 'DeathKnight', 'Unholy', 'Feet', 'Both', 'Apocalypse''s Advance'), +(6, 2, 8, 0, 290, 50670, 'Phase 5', 'DeathKnight', 'Unholy', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(6, 2, 9, 0, 290, 50690, 'Phase 5', 'DeathKnight', 'Unholy', 'Hands', 'Both', 'Fleshrending Gauntlets'), +(6, 2, 10, 0, 290, 50693, 'Phase 5', 'DeathKnight', 'Unholy', 'Finger1', 'Both', 'Might of Blight'), +(6, 2, 11, 0, 290, 52572, 'Phase 5', 'DeathKnight', 'Unholy', 'Finger2', 'Both', 'Ashen Band of Endless Might'), +(6, 2, 12, 0, 290, 54590, 'Phase 5', 'DeathKnight', 'Unholy', 'Trinket1', 'Both', 'Sharpened Twilight Scale'), +(6, 2, 13, 0, 290, 50363, 'Phase 5', 'DeathKnight', 'Unholy', 'Trinket2', 'Both', 'Deathbringer''s Will'), +(6, 2, 14, 0, 290, 50677, 'Phase 5', 'DeathKnight', 'Unholy', 'Back', 'Both', 'Winding Sheet'), +(6, 2, 15, 0, 290, 49623, 'Phase 5', 'DeathKnight', 'Unholy', 'MainHand', 'Both', 'Shadowmourne'), +(6, 2, 17, 0, 290, 50459, 'Phase 5', 'DeathKnight', 'Unholy', 'Ranged', 'Both', 'Sigil of the Hanged Man'); + + +-- ============================================================ +-- Shaman (7) +-- ============================================================ +-- Elemental (tab 0) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 66, 10504, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Head', 'Both', 'Green Lens'), +(7, 0, 1, 0, 66, 12103, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Neck', 'Both', 'Star of Mystaria'), +(7, 0, 2, 0, 66, 13013, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Elder Wizard''s Mantle'), +(7, 0, 4, 0, 66, 11924, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Chest', 'Both', 'Robes of the Royal Crown'), +(7, 0, 5, 0, 66, 11662, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Waist', 'Both', 'Ban''thok Sash'), +(7, 0, 6, 0, 66, 13170, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Legs', 'Both', 'Skyshroud Leggings'), +(7, 0, 7, 0, 66, 13954, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Feet', 'Both', 'Verdant Footpads'), +(7, 0, 8, 0, 66, 11765, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Pyremail Wristguards'), +(7, 0, 9, 0, 66, 13253, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Hands', 'Both', 'Hands of Power'), +(7, 0, 10, 0, 66, 12545, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Eye of Orgrimmar'), +(7, 0, 11, 0, 66, 13001, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Maiden''s Circle'), +(7, 0, 12, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Briarwood Reed'), +(7, 0, 13, 0, 66, 13968, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Eye of the Beast'), +(7, 0, 14, 0, 66, 15421, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'Back', 'Both', 'Shroud of the Exile'), +(7, 0, 15, 0, 66, 13964, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Witchblade'), +(7, 0, 16, 0, 66, 11904, 'Phase 1 (Pre-Raid)', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Spirit of Aquementas'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 76, 10504, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Head', 'Both', 'Green Lens'), +(7, 0, 1, 0, 76, 12103, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Neck', 'Both', 'Star of Mystaria'), +(7, 0, 2, 0, 76, 23260, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Champion''s Mail Pauldrons'), +(7, 0, 4, 0, 76, 18385, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Chest', 'Both', 'Robe of Everlasting Night'), +(7, 0, 5, 0, 76, 11662, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Waist', 'Both', 'Ban''thok Sash'), +(7, 0, 6, 0, 76, 13170, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Legs', 'Both', 'Skyshroud Leggings'), +(7, 0, 7, 0, 76, 18322, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Feet', 'Both', 'Waterspout Boots'), +(7, 0, 8, 0, 76, 11765, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Pyremail Wristguards'), +(7, 0, 9, 0, 76, 13253, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Hands', 'Both', 'Hands of Power'), +(7, 0, 10, 0, 76, 12545, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Eye of Orgrimmar'), +(7, 0, 11, 0, 76, 13001, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Maiden''s Circle'), +(7, 0, 12, 0, 76, 12930, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Briarwood Reed'), +(7, 0, 13, 0, 76, 13968, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Eye of the Beast'), +(7, 0, 14, 0, 76, 18496, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'Back', 'Both', 'Heliotrope Cloak'), +(7, 0, 15, 0, 76, 18534, 'Phase 2 (Pre-Raid)', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Rod of the Ogre Magi'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 78, 10504, 'Phase 2', 'Shaman', 'Elemental', 'Head', 'Both', 'Green Lens'), +(7, 0, 1, 0, 78, 18814, 'Phase 2', 'Shaman', 'Elemental', 'Neck', 'Both', 'Choker of the Fire Lord'), +(7, 0, 2, 0, 78, 18829, 'Phase 2', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Deep Earth Spaulders'), +(7, 0, 4, 0, 78, 19145, 'Phase 2', 'Shaman', 'Elemental', 'Chest', 'Both', 'Robe of Volatile Power'), +(7, 0, 5, 0, 78, 19136, 'Phase 2', 'Shaman', 'Elemental', 'Waist', 'Both', 'Mana Igniting Cord'), +(7, 0, 6, 0, 78, 16946, 'Phase 2', 'Shaman', 'Elemental', 'Legs', 'Both', 'Legplates of Ten Storms'), +(7, 0, 7, 0, 78, 18322, 'Phase 2', 'Shaman', 'Elemental', 'Feet', 'Both', 'Waterspout Boots'), +(7, 0, 8, 0, 78, 11765, 'Phase 2', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Pyremail Wristguards'), +(7, 0, 9, 0, 78, 16839, 'Phase 2', 'Shaman', 'Elemental', 'Hands', 'Both', 'Earthfury Gauntlets'), +(7, 0, 10, 0, 78, 19147, 'Phase 2', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Spell Power'), +(7, 0, 11, 0, 78, 19147, 'Phase 2', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of Spell Power'), +(7, 0, 12, 0, 78, 12930, 'Phase 2', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Briarwood Reed'), +(7, 0, 13, 0, 78, 18820, 'Phase 2', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(7, 0, 14, 0, 78, 18496, 'Phase 2', 'Shaman', 'Elemental', 'Back', 'Both', 'Heliotrope Cloak'), +(7, 0, 15, 0, 78, 18842, 'Phase 2', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Staff of Dominance'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 83, 19375, 'Phase 3', 'Shaman', 'Elemental', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(7, 0, 1, 0, 83, 18814, 'Phase 3', 'Shaman', 'Elemental', 'Neck', 'Both', 'Choker of the Fire Lord'), +(7, 0, 2, 0, 83, 18829, 'Phase 3', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Deep Earth Spaulders'), +(7, 0, 4, 0, 83, 19145, 'Phase 3', 'Shaman', 'Elemental', 'Chest', 'Both', 'Robe of Volatile Power'), +(7, 0, 5, 0, 83, 19400, 'Phase 3', 'Shaman', 'Elemental', 'Waist', 'Both', 'Firemaw''s Clutch'), +(7, 0, 6, 0, 83, 16946, 'Phase 3', 'Shaman', 'Elemental', 'Legs', 'Both', 'Legplates of Ten Storms'), +(7, 0, 7, 0, 83, 18322, 'Phase 3', 'Shaman', 'Elemental', 'Feet', 'Both', 'Waterspout Boots'), +(7, 0, 8, 0, 83, 19374, 'Phase 3', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(7, 0, 9, 0, 83, 16839, 'Phase 3', 'Shaman', 'Elemental', 'Hands', 'Both', 'Earthfury Gauntlets'), +(7, 0, 10, 0, 83, 19397, 'Phase 3', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Blackrock'), +(7, 0, 12, 0, 83, 19379, 'Phase 3', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(7, 0, 13, 0, 83, 19344, 'Phase 3', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Natural Alignment Crystal'), +(7, 0, 14, 0, 83, 19378, 'Phase 3', 'Shaman', 'Elemental', 'Back', 'Both', 'Cloak of the Brood Lord'), +(7, 0, 15, 0, 83, 19360, 'Phase 3', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(7, 0, 16, 0, 83, 19366, 'Phase 3', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Master Dragonslayer''s Orb'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 88, 19375, 'Phase 5', 'Shaman', 'Elemental', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(7, 0, 1, 0, 88, 21608, 'Phase 5', 'Shaman', 'Elemental', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(7, 0, 2, 0, 88, 21376, 'Phase 5', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Stormcaller''s Pauldrons'), +(7, 0, 4, 0, 88, 21671, 'Phase 5', 'Shaman', 'Elemental', 'Chest', 'Both', 'Robes of the Battleguard'), +(7, 0, 5, 0, 88, 22730, 'Phase 5', 'Shaman', 'Elemental', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(7, 0, 6, 0, 88, 21375, 'Phase 5', 'Shaman', 'Elemental', 'Legs', 'Both', 'Stormcaller''s Leggings'), +(7, 0, 7, 0, 88, 21373, 'Phase 5', 'Shaman', 'Elemental', 'Feet', 'Both', 'Stormcaller''s Footguards'), +(7, 0, 8, 0, 88, 21464, 'Phase 5', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Shackles of the Unscarred'), +(7, 0, 9, 0, 88, 21585, 'Phase 5', 'Shaman', 'Elemental', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(7, 0, 10, 0, 88, 21707, 'Phase 5', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Swarming Thought'), +(7, 0, 11, 0, 88, 21709, 'Phase 5', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of the Fallen God'), +(7, 0, 12, 0, 88, 19379, 'Phase 5', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(7, 0, 13, 0, 88, 19344, 'Phase 5', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Natural Alignment Crystal'), +(7, 0, 14, 0, 88, 22731, 'Phase 5', 'Shaman', 'Elemental', 'Back', 'Both', 'Cloak of the Devoured'), +(7, 0, 15, 0, 88, 19360, 'Phase 5', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(7, 0, 16, 0, 88, 19366, 'Phase 5', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Master Dragonslayer''s Orb'), +(7, 0, 17, 0, 88, 23199, 'Phase 5', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of the Storm'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 92, 19375, 'Phase 6', 'Shaman', 'Elemental', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(7, 0, 1, 0, 92, 22943, 'Phase 6', 'Shaman', 'Elemental', 'Neck', 'Both', 'Malice Stone Pendant'), +(7, 0, 2, 0, 92, 21376, 'Phase 6', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Stormcaller''s Pauldrons'), +(7, 0, 4, 0, 92, 21671, 'Phase 6', 'Shaman', 'Elemental', 'Chest', 'Both', 'Robes of the Battleguard'), +(7, 0, 5, 0, 92, 22730, 'Phase 6', 'Shaman', 'Elemental', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(7, 0, 6, 0, 92, 21375, 'Phase 6', 'Shaman', 'Elemental', 'Legs', 'Both', 'Stormcaller''s Leggings'), +(7, 0, 7, 0, 92, 21373, 'Phase 6', 'Shaman', 'Elemental', 'Feet', 'Both', 'Stormcaller''s Footguards'), +(7, 0, 8, 0, 92, 21464, 'Phase 6', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Shackles of the Unscarred'), +(7, 0, 9, 0, 92, 21585, 'Phase 6', 'Shaman', 'Elemental', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(7, 0, 10, 0, 92, 21707, 'Phase 6', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Swarming Thought'), +(7, 0, 11, 0, 92, 21709, 'Phase 6', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of the Fallen God'), +(7, 0, 12, 0, 92, 19379, 'Phase 6', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(7, 0, 13, 0, 92, 23046, 'Phase 6', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(7, 0, 14, 0, 92, 23050, 'Phase 6', 'Shaman', 'Elemental', 'Back', 'Both', 'Cloak of the Necropolis'), +(7, 0, 15, 0, 92, 22988, 'Phase 6', 'Shaman', 'Elemental', 'MainHand', 'Both', 'The End of Dreams'), +(7, 0, 16, 0, 92, 23049, 'Phase 6', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(7, 0, 17, 0, 92, 23199, 'Phase 6', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of the Storm'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 120, 32086, 'Pre-Raid', 'Shaman', 'Elemental', 'Head', 'Both', 'Storm Master''s Helmet'), +(7, 0, 1, 0, 120, 31692, 'Pre-Raid', 'Shaman', 'Elemental', 'Neck', 'Both', 'Natasha''s Ember Necklace'), +(7, 0, 2, 0, 120, 27796, 'Pre-Raid', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Mana-Etched Spaulders'), +(7, 0, 4, 0, 120, 29519, 'Pre-Raid', 'Shaman', 'Elemental', 'Chest', 'Both', 'Netherstrike Breastplate'), +(7, 0, 5, 0, 120, 29520, 'Pre-Raid', 'Shaman', 'Elemental', 'Waist', 'Both', 'Netherstrike Belt'), +(7, 0, 6, 0, 120, 24262, 'Pre-Raid', 'Shaman', 'Elemental', 'Legs', 'Both', 'Spellstrike Pants'), +(7, 0, 7, 0, 120, 28406, 'Pre-Raid', 'Shaman', 'Elemental', 'Feet', 'Both', 'Sigil-Laced Boots'), +(7, 0, 8, 0, 120, 29521, 'Pre-Raid', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Netherstrike Bracers'), +(7, 0, 9, 0, 120, 27465, 'Pre-Raid', 'Shaman', 'Elemental', 'Hands', 'Both', 'Mana-Etched Gloves'), +(7, 0, 10, 0, 120, 29367, 'Pre-Raid', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Cryptic Dreams'), +(7, 0, 11, 0, 120, 29126, 'Pre-Raid', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Seer''s Signet'), +(7, 0, 12, 0, 120, 29370, 'Pre-Raid', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(7, 0, 13, 0, 120, 27683, 'Pre-Raid', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(7, 0, 14, 0, 120, 29369, 'Pre-Raid', 'Shaman', 'Elemental', 'Back', 'Both', 'Shawl of Shifting Probabilities'), +(7, 0, 15, 0, 120, 23554, 'Pre-Raid', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Eternium Runed Blade'), +(7, 0, 16, 0, 120, 29268, 'Pre-Raid', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Mazthoril Honor Shield'), +(7, 0, 17, 0, 120, 28248, 'Pre-Raid', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of the Void'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 125, 29035, 'Phase 1', 'Shaman', 'Elemental', 'Head', 'Both', 'Cyclone Faceguard'), +(7, 0, 1, 0, 125, 28762, 'Phase 1', 'Shaman', 'Elemental', 'Neck', 'Both', 'Adornment of Stolen Souls'), +(7, 0, 2, 0, 125, 29037, 'Phase 1', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Cyclone Shoulderguards'), +(7, 0, 4, 0, 125, 29519, 'Phase 1', 'Shaman', 'Elemental', 'Chest', 'Both', 'Netherstrike Breastplate'), +(7, 0, 5, 0, 125, 29520, 'Phase 1', 'Shaman', 'Elemental', 'Waist', 'Both', 'Netherstrike Belt'), +(7, 0, 6, 0, 125, 30734, 'Phase 1', 'Shaman', 'Elemental', 'Legs', 'Both', 'Leggings of the Seventh Circle'), +(7, 0, 7, 0, 125, 28517, 'Phase 1', 'Shaman', 'Elemental', 'Feet', 'Both', 'Boots of Foretelling'), +(7, 0, 8, 0, 125, 29521, 'Phase 1', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Netherstrike Bracers'), +(7, 0, 9, 0, 125, 30725, 'Phase 1', 'Shaman', 'Elemental', 'Hands', 'Both', 'Anger-Spark Gloves'), +(7, 0, 10, 0, 125, 28753, 'Phase 1', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Recurrence'), +(7, 0, 11, 0, 125, 30667, 'Phase 1', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of Unrelenting Storms'), +(7, 0, 12, 0, 125, 29370, 'Phase 1', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(7, 0, 13, 0, 125, 28785, 'Phase 1', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'The Lightning Capacitor'), +(7, 0, 14, 0, 125, 30735, 'Phase 1', 'Shaman', 'Elemental', 'Back', 'Both', 'Ancient Spellcloak of the Highborne'), +(7, 0, 16, 0, 125, 29268, 'Phase 1', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Mazthoril Honor Shield'), +(7, 0, 17, 0, 125, 28248, 'Phase 1', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of the Void'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 141, 29035, 'Phase 2', 'Shaman', 'Elemental', 'Head', 'Both', 'Cyclone Faceguard'), +(7, 0, 1, 0, 141, 30015, 'Phase 2', 'Shaman', 'Elemental', 'Neck', 'Both', 'The Sun King''s Talisman'), +(7, 0, 2, 0, 141, 29037, 'Phase 2', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Cyclone Shoulderguards'), +(7, 0, 4, 0, 141, 30107, 'Phase 2', 'Shaman', 'Elemental', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(7, 0, 5, 0, 141, 30038, 'Phase 2', 'Shaman', 'Elemental', 'Waist', 'Both', 'Belt of Blasting'), +(7, 0, 6, 0, 141, 30734, 'Phase 2', 'Shaman', 'Elemental', 'Legs', 'Both', 'Leggings of the Seventh Circle'), +(7, 0, 7, 0, 141, 30067, 'Phase 2', 'Shaman', 'Elemental', 'Feet', 'Both', 'Velvet Boots of the Guardian'), +(7, 0, 8, 0, 141, 29918, 'Phase 2', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Mindstorm Wristbands'), +(7, 0, 9, 0, 141, 28780, 'Phase 2', 'Shaman', 'Elemental', 'Hands', 'Both', 'Soul-Eater''s Handwraps'), +(7, 0, 10, 0, 141, 30109, 'Phase 2', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Endless Coils'), +(7, 0, 11, 0, 141, 30667, 'Phase 2', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of Unrelenting Storms'), +(7, 0, 12, 0, 141, 29370, 'Phase 2', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(7, 0, 13, 0, 141, 28785, 'Phase 2', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'The Lightning Capacitor'), +(7, 0, 14, 0, 141, 30735, 'Phase 2', 'Shaman', 'Elemental', 'Back', 'Both', 'Ancient Spellcloak of the Highborne'), +(7, 0, 15, 0, 141, 29988, 'Phase 2', 'Shaman', 'Elemental', 'MainHand', 'Both', 'The Nexus Key'), +(7, 0, 17, 0, 141, 28248, 'Phase 2', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of the Void'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 156, 31014, 'Phase 3', 'Shaman', 'Elemental', 'Head', 'Both', 'Skyshatter Headguard'), +(7, 0, 1, 0, 156, 32349, 'Phase 3', 'Shaman', 'Elemental', 'Neck', 'Both', 'Translucent Spellthread Necklace'), +(7, 0, 2, 0, 156, 31023, 'Phase 3', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Skyshatter Mantle'), +(7, 0, 4, 0, 156, 31017, 'Phase 3', 'Shaman', 'Elemental', 'Chest', 'Both', 'Skyshatter Breastplate'), +(7, 0, 5, 0, 156, 32276, 'Phase 3', 'Shaman', 'Elemental', 'Waist', 'Both', 'Flashfire Girdle'), +(7, 0, 6, 0, 156, 30916, 'Phase 3', 'Shaman', 'Elemental', 'Legs', 'Both', 'Leggings of Channeled Elements'), +(7, 0, 7, 0, 156, 32239, 'Phase 3', 'Shaman', 'Elemental', 'Feet', 'Both', 'Slippers of the Seacaller'), +(7, 0, 8, 0, 156, 32586, 'Phase 3', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(7, 0, 9, 0, 156, 31008, 'Phase 3', 'Shaman', 'Elemental', 'Hands', 'Both', 'Skyshatter Gauntlets'), +(7, 0, 10, 0, 156, 32527, 'Phase 3', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(7, 0, 11, 0, 156, 32527, 'Phase 3', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of Ancient Knowledge'), +(7, 0, 12, 0, 156, 28785, 'Phase 3', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'The Lightning Capacitor'), +(7, 0, 13, 0, 156, 32483, 'Phase 3', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(7, 0, 14, 0, 156, 32524, 'Phase 3', 'Shaman', 'Elemental', 'Back', 'Both', 'Shroud of the Highborne'), +(7, 0, 15, 0, 156, 32374, 'Phase 3', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(7, 0, 17, 0, 156, 32330, 'Phase 3', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of Ancestral Guidance'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 164, 31014, 'Phase 4', 'Shaman', 'Elemental', 'Head', 'Both', 'Skyshatter Headguard'), +(7, 0, 1, 0, 164, 33281, 'Phase 4', 'Shaman', 'Elemental', 'Neck', 'Both', 'Brooch of Nature''s Mercy'), +(7, 0, 2, 0, 164, 31023, 'Phase 4', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Skyshatter Mantle'), +(7, 0, 4, 0, 164, 31017, 'Phase 4', 'Shaman', 'Elemental', 'Chest', 'Both', 'Skyshatter Breastplate'), +(7, 0, 5, 0, 164, 32276, 'Phase 4', 'Shaman', 'Elemental', 'Waist', 'Both', 'Flashfire Girdle'), +(7, 0, 6, 0, 164, 33584, 'Phase 4', 'Shaman', 'Elemental', 'Legs', 'Both', 'Pantaloons of Arcane Annihilation'), +(7, 0, 7, 0, 164, 33357, 'Phase 4', 'Shaman', 'Elemental', 'Feet', 'Both', 'Footpads of Madness'), +(7, 0, 8, 0, 164, 32586, 'Phase 4', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(7, 0, 9, 0, 164, 31008, 'Phase 4', 'Shaman', 'Elemental', 'Hands', 'Both', 'Skyshatter Gauntlets'), +(7, 0, 10, 0, 164, 32527, 'Phase 4', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(7, 0, 11, 0, 164, 32527, 'Phase 4', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ring of Ancient Knowledge'), +(7, 0, 12, 0, 164, 33829, 'Phase 4', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Hex Shrunken Head'), +(7, 0, 13, 0, 164, 32483, 'Phase 4', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(7, 0, 14, 0, 164, 32524, 'Phase 4', 'Shaman', 'Elemental', 'Back', 'Both', 'Shroud of the Highborne'), +(7, 0, 15, 0, 164, 32374, 'Phase 4', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(7, 0, 16, 0, 164, 30909, 'Phase 5', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Antonidas''s Aegis of Rapt Concentration'), +(7, 0, 17, 0, 164, 32330, 'Phase 4', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of Ancestral Guidance'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 200, 42555, 'Pre-Raid', 'Shaman', 'Elemental', 'Head', 'Both', 'Electroflux Sight Enhancers'), +(7, 0, 1, 0, 200, 37595, 'Pre-Raid', 'Shaman', 'Elemental', 'Neck', 'Both', 'Necklace of Taldaram'), +(7, 0, 2, 0, 200, 37398, 'Pre-Raid', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Mantle of Discarded Ways'), +(7, 0, 4, 0, 200, 42101, 'Pre-Raid', 'Shaman', 'Elemental', 'Chest', 'Both', 'Ebonweave Robe'), +(7, 0, 5, 0, 200, 37855, 'Pre-Raid', 'Shaman', 'Elemental', 'Waist', 'Both', 'Mail Girdle of the Audient Earth'), +(7, 0, 6, 0, 200, 37695, 'Pre-Raid', 'Shaman', 'Elemental', 'Legs', 'Both', 'Legguards of Nature''s Power'), +(7, 0, 7, 0, 200, 43469, 'Pre-Raid', 'Shaman', 'Elemental', 'Feet', 'Both', 'Revenant''s Treads'), +(7, 0, 8, 0, 200, 37788, 'Pre-Raid', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Limb Regeneration Bracers'), +(7, 0, 9, 0, 200, 37623, 'Pre-Raid', 'Shaman', 'Elemental', 'Hands', 'Both', 'Fiery Obelisk Handguards'), +(7, 0, 10, 0, 200, 37192, 'Pre-Raid', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Annhylde''s Ring'), +(7, 0, 12, 0, 200, 40682, 'Pre-Raid', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Sundial of the Exiled'), +(7, 0, 14, 0, 200, 41610, 'Pre-Raid', 'Shaman', 'Elemental', 'Back', 'Both', 'Deathchill Cloak'), +(7, 0, 15, 0, 200, 41384, 'Pre-Raid', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Titansteel Guardian'), +(7, 0, 17, 0, 200, 40708, 'Pre-Raid', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of the Elemental Plane'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 224, 40516, 'Phase 1', 'Shaman', 'Elemental', 'Head', 'Both', 'Valorous Earthshatter Helm'), +(7, 0, 1, 0, 224, 44661, 'Phase 1', 'Shaman', 'Elemental', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(7, 0, 2, 0, 224, 40518, 'Phase 1', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Valorous Earthshatter Shoulderpads'), +(7, 0, 4, 0, 224, 40283, 'Phase 1', 'Shaman', 'Elemental', 'Chest', 'Both', 'Fallout Impervious Tunic'), +(7, 0, 5, 0, 224, 40327, 'Phase 1', 'Shaman', 'Elemental', 'Waist', 'Both', 'Girdle of Recuperation'), +(7, 0, 6, 0, 224, 40560, 'Phase 1', 'Shaman', 'Elemental', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(7, 0, 7, 0, 224, 40237, 'Phase 1', 'Shaman', 'Elemental', 'Feet', 'Both', 'Eruption-Scarred Boots'), +(7, 0, 8, 0, 224, 40324, 'Phase 1', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Bands of Mutual Respect'), +(7, 0, 9, 0, 224, 40302, 'Phase 1', 'Shaman', 'Elemental', 'Hands', 'Both', 'Benefactor''s Gauntlets'), +(7, 0, 10, 0, 224, 40399, 'Phase 1', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(7, 0, 12, 0, 224, 40255, 'Phase 1', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Dying Curse'), +(7, 0, 14, 0, 224, 44005, 'Phase 1', 'Shaman', 'Elemental', 'Back', 'Both', 'Pennant Cloak'), +(7, 0, 17, 0, 224, 40267, 'Phase 1', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of Hex'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 245, 46209, 'Phase 2', 'Shaman', 'Elemental', 'Head', 'Both', 'Conqueror''s Worldbreaker Helm'), +(7, 0, 2, 0, 245, 46211, 'Phase 2', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Conqueror''s Worldbreaker Shoulderpads'), +(7, 0, 4, 0, 245, 46206, 'Phase 2', 'Shaman', 'Elemental', 'Chest', 'Both', 'Conqueror''s Worldbreaker Hauberk'), +(7, 0, 6, 0, 245, 46210, 'Phase 2', 'Shaman', 'Elemental', 'Legs', 'Both', 'Conqueror''s Worldbreaker Kilt'), +(7, 0, 7, 0, 245, 45537, 'Phase 2', 'Shaman', 'Elemental', 'Feet', 'Both', 'Treads of the False Oracle'), +(7, 0, 8, 1, 245, 45616, 'Phase 2', 'Shaman', 'Elemental', 'Wrists', 'Alliance', 'Star-beaded Clutch'), +(7, 0, 8, 2, 245, 45460, 'Phase 2', 'Shaman', 'Elemental', 'Wrists', 'Horde', 'Bindings of Winter Gale'), +(7, 0, 9, 0, 245, 45665, 'Phase 2', 'Shaman', 'Elemental', 'Hands', 'Both', 'Pharos Gloves'), +(7, 0, 10, 0, 245, 45495, 'Phase 2', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Conductive Seal'), +(7, 0, 12, 0, 245, 45518, 'Phase 2', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Flare of the Heavens'), +(7, 0, 14, 0, 245, 45242, 'Phase 2', 'Shaman', 'Elemental', 'Back', 'Both', 'Drape of Mortal Downfall'), +(7, 0, 17, 0, 245, 40267, 'Phase 2', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Totem of Hex'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 264, 51237, 'Phase 4', 'Shaman', 'Elemental', 'Head', 'Both', 'Sanctified Frost Witch''s Helm'), +(7, 0, 2, 0, 264, 50698, 'Phase 4', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Horrific Flesh Epaulets'), +(7, 0, 4, 0, 264, 51239, 'Phase 4', 'Shaman', 'Elemental', 'Chest', 'Both', 'Sanctified Frost Witch''s Hauberk'), +(7, 0, 5, 0, 264, 50613, 'Phase 4', 'Shaman', 'Elemental', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(7, 0, 6, 0, 264, 51236, 'Phase 4', 'Shaman', 'Elemental', 'Legs', 'Both', 'Sanctified Frost Witch''s Kilt'), +(7, 0, 7, 0, 264, 50699, 'Phase 4', 'Shaman', 'Elemental', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(7, 0, 8, 0, 264, 50687, 'Phase 4', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Bloodsunder''s Bracers'), +(7, 0, 9, 0, 264, 51238, 'Phase 4', 'Shaman', 'Elemental', 'Hands', 'Both', 'Sanctified Frost Witch''s Gloves'), +(7, 0, 10, 0, 264, 50664, 'Phase 4', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Rapid Ascent'), +(7, 0, 12, 0, 264, 50348, 'Phase 4', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Dislodged Foreign Object'), +(7, 0, 14, 0, 264, 50628, 'Phase 4', 'Shaman', 'Elemental', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(7, 0, 15, 0, 264, 50734, 'Phase 4', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(7, 0, 17, 0, 264, 50458, 'Phase 4', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Bizuri''s Totem of Shattered Ice'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 0, 0, 0, 290, 51237, 'Phase 5', 'Shaman', 'Elemental', 'Head', 'Both', 'Sanctified Frost Witch''s Helm'), +(7, 0, 1, 0, 290, 50182, 'Phase 5', 'Shaman', 'Elemental', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(7, 0, 2, 0, 290, 51235, 'Phase 5', 'Shaman', 'Elemental', 'Shoulders', 'Both', 'Sanctified Frost Witch''s Shoulderpads'), +(7, 0, 4, 0, 290, 51239, 'Phase 5', 'Shaman', 'Elemental', 'Chest', 'Both', 'Sanctified Frost Witch''s Hauberk'), +(7, 0, 5, 0, 290, 54587, 'Phase 5', 'Shaman', 'Elemental', 'Waist', 'Both', 'Split Shape Belt'), +(7, 0, 6, 0, 290, 50694, 'Phase 5', 'Shaman', 'Elemental', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(7, 0, 7, 0, 290, 50699, 'Phase 5', 'Shaman', 'Elemental', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(7, 0, 8, 0, 290, 54582, 'Phase 5', 'Shaman', 'Elemental', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(7, 0, 9, 0, 290, 51238, 'Phase 5', 'Shaman', 'Elemental', 'Hands', 'Both', 'Sanctified Frost Witch''s Gloves'), +(7, 0, 10, 0, 290, 50664, 'Phase 5', 'Shaman', 'Elemental', 'Finger1', 'Both', 'Ring of Rapid Ascent'), +(7, 0, 11, 0, 290, 50398, 'Phase 5', 'Shaman', 'Elemental', 'Finger2', 'Both', 'Ashen Band of Endless Destruction'), +(7, 0, 12, 0, 290, 54588, 'Phase 5', 'Shaman', 'Elemental', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(7, 0, 13, 0, 290, 50365, 'Phase 5', 'Shaman', 'Elemental', 'Trinket2', 'Both', 'Phylactery of the Nameless Lich'), +(7, 0, 14, 0, 290, 54583, 'Phase 5', 'Shaman', 'Elemental', 'Back', 'Both', 'Cloak of Burning Dusk'), +(7, 0, 15, 0, 290, 50734, 'Phase 5', 'Shaman', 'Elemental', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(7, 0, 16, 0, 290, 50616, 'Phase 5', 'Shaman', 'Elemental', 'OffHand', 'Both', 'Bulwark of Smouldering Steel'), +(7, 0, 17, 0, 290, 50458, 'Phase 5', 'Shaman', 'Elemental', 'Ranged', 'Both', 'Bizuri''s Totem of Shattered Ice'); + +-- Enhancement (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 66, 12587, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Head', 'Both', 'Eye of Rend'), +(7, 1, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Mark of Fordring'), +(7, 1, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(7, 1, 4, 0, 66, 11726, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Savage Gladiator Chain'), +(7, 1, 5, 0, 66, 22232, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Marksman''s Girdle'), +(7, 1, 6, 0, 66, 15062, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Devilsaur Leggings'), +(7, 1, 7, 0, 66, 14616, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Bloodmail Boots'), +(7, 1, 8, 0, 66, 13211, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Slashclaw Bracers'), +(7, 1, 9, 0, 66, 13957, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Gargoyle Slashers'), +(7, 1, 10, 0, 66, 13098, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Painweaver Band'), +(7, 1, 11, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Blackstone Ring'), +(7, 1, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(7, 1, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Hand of Justice'), +(7, 1, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'Back', 'Both', 'Cape of the Black Baron'), +(7, 1, 15, 0, 66, 12784, 'Phase 1 (Pre-Raid)', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Arcanite Reaper'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 76, 12587, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Head', 'Both', 'Eye of Rend'), +(7, 1, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Mark of Fordring'), +(7, 1, 2, 0, 76, 12927, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(7, 1, 4, 0, 76, 11726, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Savage Gladiator Chain'), +(7, 1, 5, 0, 76, 22232, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Marksman''s Girdle'), +(7, 1, 6, 0, 76, 15062, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Devilsaur Leggings'), +(7, 1, 7, 0, 76, 14616, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Bloodmail Boots'), +(7, 1, 8, 0, 76, 13211, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Slashclaw Bracers'), +(7, 1, 9, 0, 76, 13957, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Gargoyle Slashers'), +(7, 1, 10, 0, 76, 13098, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Painweaver Band'), +(7, 1, 11, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(7, 1, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(7, 1, 13, 0, 76, 11815, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Hand of Justice'), +(7, 1, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'Back', 'Both', 'Cape of the Black Baron'), +(7, 1, 15, 0, 76, 12784, 'Phase 2 (Pre-Raid)', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Arcanite Reaper'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 78, 18817, 'Phase 2', 'Shaman', 'Enhancement', 'Head', 'Both', 'Crown of Destruction'), +(7, 1, 1, 0, 78, 18404, 'Phase 2', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(7, 1, 2, 0, 78, 12927, 'Phase 2', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(7, 1, 4, 0, 78, 11726, 'Phase 2', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Savage Gladiator Chain'), +(7, 1, 5, 0, 78, 18393, 'Phase 2', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Warpwood Binding'), +(7, 1, 6, 0, 78, 15062, 'Phase 2', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Devilsaur Leggings'), +(7, 1, 7, 0, 78, 14616, 'Phase 2', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Bloodmail Boots'), +(7, 1, 8, 0, 78, 19146, 'Phase 2', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Wristguards of Stability'), +(7, 1, 9, 0, 78, 13957, 'Phase 2', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Gargoyle Slashers'), +(7, 1, 10, 0, 78, 17063, 'Phase 2', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Band of Accuria'), +(7, 1, 11, 0, 78, 18821, 'Phase 2', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Quick Strike Ring'), +(7, 1, 12, 0, 78, 13965, 'Phase 2', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(7, 1, 13, 0, 78, 11815, 'Phase 2', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Hand of Justice'), +(7, 1, 14, 0, 78, 13340, 'Phase 2', 'Shaman', 'Enhancement', 'Back', 'Both', 'Cape of the Black Baron'), +(7, 1, 15, 0, 78, 17104, 'Phase 2', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Spinal Reaper'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 83, 18817, 'Phase 3', 'Shaman', 'Enhancement', 'Head', 'Both', 'Crown of Destruction'), +(7, 1, 1, 0, 83, 18404, 'Phase 3', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(7, 1, 2, 0, 83, 12927, 'Phase 3', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(7, 1, 4, 0, 83, 11726, 'Phase 3', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Savage Gladiator Chain'), +(7, 1, 5, 0, 83, 19380, 'Phase 3', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Therazane''s Link'), +(7, 1, 6, 0, 83, 15062, 'Phase 3', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Devilsaur Leggings'), +(7, 1, 7, 0, 83, 19381, 'Phase 3', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Boots of the Shadow Flame'), +(7, 1, 8, 0, 83, 19146, 'Phase 3', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Wristguards of Stability'), +(7, 1, 9, 0, 83, 19157, 'Phase 3', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Chromatic Gauntlets'), +(7, 1, 10, 0, 83, 19384, 'Phase 3', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Master Dragonslayer''s Ring'), +(7, 1, 11, 0, 83, 18821, 'Phase 3', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Quick Strike Ring'), +(7, 1, 12, 0, 83, 13965, 'Phase 3', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(7, 1, 13, 0, 83, 11815, 'Phase 3', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Hand of Justice'), +(7, 1, 14, 0, 83, 19436, 'Phase 3', 'Shaman', 'Enhancement', 'Back', 'Both', 'Cloak of Draconic Might'), +(7, 1, 15, 0, 83, 17104, 'Phase 3', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Spinal Reaper'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 88, 18817, 'Phase 5', 'Shaman', 'Enhancement', 'Head', 'Both', 'Crown of Destruction'), +(7, 1, 1, 0, 88, 18404, 'Phase 5', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(7, 1, 2, 0, 88, 21684, 'Phase 5', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Mantle of the Desert''s Fury'), +(7, 1, 4, 0, 88, 21374, 'Phase 5', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Stormcaller''s Hauberk'), +(7, 1, 5, 0, 88, 21586, 'Phase 5', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Belt of Never-ending Agony'), +(7, 1, 6, 0, 88, 21651, 'Phase 5', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Scaled Sand Reaver Leggings'), +(7, 1, 7, 0, 88, 21705, 'Phase 5', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Boots of the Fallen Prophet'), +(7, 1, 8, 0, 88, 21602, 'Phase 5', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Qiraji Execution Bracers'), +(7, 1, 9, 0, 88, 21624, 'Phase 5', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Gauntlets of Kalimdor'), +(7, 1, 10, 0, 88, 17063, 'Phase 5', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Band of Accuria'), +(7, 1, 11, 0, 88, 18821, 'Phase 5', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Quick Strike Ring'), +(7, 1, 12, 0, 88, 22321, 'Phase 5', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Heart of Wyrmthalak'), +(7, 1, 13, 0, 88, 19289, 'Phase 5', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Darkmoon Card: Maelstrom'), +(7, 1, 14, 0, 88, 21701, 'Phase 5', 'Shaman', 'Enhancement', 'Back', 'Both', 'Cloak of Concentrated Hatred'), +(7, 1, 15, 0, 88, 21134, 'Phase 5', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Dark Edge of Insanity'), +(7, 1, 17, 0, 88, 22395, 'Phase 5', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of Rage'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 92, 18817, 'Phase 6', 'Shaman', 'Enhancement', 'Head', 'Both', 'Crown of Destruction'), +(7, 1, 1, 0, 92, 18404, 'Phase 6', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(7, 1, 2, 0, 92, 21684, 'Phase 6', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Mantle of the Desert''s Fury'), +(7, 1, 4, 0, 92, 21374, 'Phase 6', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Stormcaller''s Hauberk'), +(7, 1, 5, 0, 92, 21586, 'Phase 6', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Belt of Never-ending Agony'), +(7, 1, 6, 0, 92, 21651, 'Phase 6', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Scaled Sand Reaver Leggings'), +(7, 1, 7, 0, 92, 21705, 'Phase 6', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Boots of the Fallen Prophet'), +(7, 1, 8, 0, 92, 21602, 'Phase 6', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Qiraji Execution Bracers'), +(7, 1, 9, 0, 92, 21624, 'Phase 6', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Gauntlets of Kalimdor'), +(7, 1, 10, 0, 92, 17063, 'Phase 6', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Band of Accuria'), +(7, 1, 12, 0, 92, 22321, 'Phase 6', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Heart of Wyrmthalak'), +(7, 1, 13, 0, 92, 19289, 'Phase 6', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Darkmoon Card: Maelstrom'), +(7, 1, 14, 0, 92, 23045, 'Phase 6', 'Shaman', 'Enhancement', 'Back', 'Both', 'Shroud of Dominion'), +(7, 1, 15, 0, 92, 22798, 'Phase 6', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Might of Menethil'), +(7, 1, 17, 0, 92, 22395, 'Phase 6', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of Rage'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 120, 28224, 'Pre-Raid', 'Shaman', 'Enhancement', 'Head', 'Both', 'Wastewalker Helm'), +(7, 1, 1, 0, 120, 29381, 'Pre-Raid', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Choker of Vile Intent'), +(7, 1, 2, 0, 120, 27797, 'Pre-Raid', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(7, 1, 4, 0, 120, 29515, 'Pre-Raid', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Ebon Netherscale Breastplate'), +(7, 1, 5, 0, 120, 29516, 'Pre-Raid', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Ebon Netherscale Belt'), +(7, 1, 6, 0, 120, 31544, 'Pre-Raid', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Clefthoof Hide Leggings'), +(7, 1, 7, 0, 120, 25686, 'Pre-Raid', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Fel Leather Boots'), +(7, 1, 8, 0, 120, 29517, 'Pre-Raid', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Ebon Netherscale Bracers'), +(7, 1, 9, 0, 120, 25685, 'Pre-Raid', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Fel Leather Gloves'), +(7, 1, 10, 0, 120, 30365, 'Pre-Raid', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Overseer''s Signet'), +(7, 1, 11, 0, 120, 30834, 'Pre-Raid', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(7, 1, 12, 0, 120, 28288, 'Pre-Raid', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Abacus of Violent Odds'), +(7, 1, 13, 0, 120, 29383, 'Pre-Raid', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Bloodlust Brooch'), +(7, 1, 14, 0, 120, 24259, 'Pre-Raid', 'Shaman', 'Enhancement', 'Back', 'Both', 'Vengeance Wrap'), +(7, 1, 15, 0, 120, 27872, 'Pre-Raid', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'The Harvester of Souls'), +(7, 1, 16, 0, 120, 27872, 'Pre-Raid', 'Shaman', 'Enhancement', 'OffHand', 'Both', 'The Harvester of Souls'), +(7, 1, 17, 0, 120, 27815, 'Pre-Raid', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of the Astral Winds'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 125, 29040, 'Phase 1', 'Shaman', 'Enhancement', 'Head', 'Both', 'Cyclone Helm'), +(7, 1, 1, 0, 125, 29381, 'Phase 1', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Choker of Vile Intent'), +(7, 1, 2, 0, 125, 29043, 'Phase 1', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Cyclone Shoulderplates'), +(7, 1, 4, 0, 125, 29515, 'Phase 1', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Ebon Netherscale Breastplate'), +(7, 1, 5, 0, 125, 29516, 'Phase 1', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Ebon Netherscale Belt'), +(7, 1, 6, 0, 125, 30739, 'Phase 1', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Scaled Greaves of the Marksman'), +(7, 1, 7, 0, 125, 28545, 'Phase 1', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Edgewalker Longboots'), +(7, 1, 8, 0, 125, 29517, 'Phase 1', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Ebon Netherscale Bracers'), +(7, 1, 9, 0, 125, 28776, 'Phase 1', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Liar''s Tongue Gloves'), +(7, 1, 10, 0, 125, 28757, 'Phase 1', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Ring of a Thousand Marks'), +(7, 1, 11, 0, 125, 30834, 'Phase 1', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(7, 1, 12, 0, 125, 29383, 'Phase 1', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(7, 1, 13, 0, 125, 28830, 'Phase 1', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(7, 1, 14, 0, 125, 24259, 'Phase 1', 'Shaman', 'Enhancement', 'Back', 'Both', 'Vengeance Wrap'), +(7, 1, 15, 0, 125, 28308, 'Phase 1', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Gladiator''s Cleaver'), +(7, 1, 16, 0, 125, 28308, 'Phase 1', 'Shaman', 'Enhancement', 'OffHand', 'Both', 'Gladiator''s Cleaver'), +(7, 1, 17, 0, 125, 27815, 'Phase 1', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of the Astral Winds'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 141, 30190, 'Phase 2', 'Shaman', 'Enhancement', 'Head', 'Both', 'Cataclysm Helm'), +(7, 1, 1, 0, 141, 30017, 'Phase 2', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Telonicus''s Pendant of Mayhem'), +(7, 1, 2, 0, 141, 30055, 'Phase 2', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Shoulderpads of the Stranger'), +(7, 1, 4, 0, 141, 30185, 'Phase 2', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Cataclysm Chestplate'), +(7, 1, 5, 0, 141, 30106, 'Phase 2', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(7, 1, 6, 0, 141, 30192, 'Phase 2', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Cataclysm Legplates'), +(7, 1, 7, 0, 141, 30039, 'Phase 2', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Boots of Utter Darkness'), +(7, 1, 8, 0, 141, 30091, 'Phase 2', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'True-Aim Stalker Bands'), +(7, 1, 9, 0, 141, 30189, 'Phase 2', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Cataclysm Gauntlets'), +(7, 1, 10, 0, 141, 29997, 'Phase 2', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Band of the Ranger-General'), +(7, 1, 11, 0, 141, 30052, 'Phase 2', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Ring of Lethality'), +(7, 1, 12, 0, 141, 29383, 'Phase 2', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(7, 1, 13, 0, 141, 28830, 'Phase 2', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(7, 1, 14, 0, 141, 29994, 'Phase 2', 'Shaman', 'Enhancement', 'Back', 'Both', 'Thalassian Wildercloak'), +(7, 1, 16, 0, 141, 28433, 'Phase 2', 'Shaman', 'Enhancement', 'OffHand', 'Both', 'Wicked Edge of the Planes'), +(7, 1, 17, 0, 141, 27815, 'Phase 2', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of the Astral Winds'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 156, 32235, 'Phase 3', 'Shaman', 'Enhancement', 'Head', 'Both', 'Cursed Vision of Sargeras'), +(7, 1, 1, 0, 156, 32260, 'Phase 3', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Choker of Endless Nightmares'), +(7, 1, 2, 0, 156, 32581, 'Phase 3', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Swiftstrike Shoulders'), +(7, 1, 4, 0, 156, 30905, 'Phase 3', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Midnight Chestguard'), +(7, 1, 5, 0, 156, 30106, 'Phase 3', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(7, 1, 6, 0, 156, 30900, 'Phase 3', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Bow-Stitched Leggings'), +(7, 1, 7, 0, 156, 32366, 'Phase 3', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Shadowmaster''s Boots'), +(7, 1, 8, 0, 156, 32574, 'Phase 3', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Bindings of Lightning Reflexes'), +(7, 1, 9, 0, 156, 32347, 'Phase 3', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Grips of Damnation'), +(7, 1, 10, 0, 156, 29301, 'Phase 3', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Band of the Eternal Champion'), +(7, 1, 11, 0, 156, 32497, 'Phase 3', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Stormrage Signet Ring'), +(7, 1, 12, 0, 156, 28830, 'Phase 3', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Dragonspine Trophy'), +(7, 1, 13, 0, 156, 32505, 'Phase 3', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Madness of the Betrayer'), +(7, 1, 14, 0, 156, 32323, 'Phase 3', 'Shaman', 'Enhancement', 'Back', 'Both', 'Shadowmoon Destroyer''s Drape'), +(7, 1, 17, 0, 156, 27815, 'Phase 3', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of the Astral Winds'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 164, 32235, 'Phase 4', 'Shaman', 'Enhancement', 'Head', 'Both', 'Cursed Vision of Sargeras'), +(7, 1, 1, 0, 164, 32260, 'Phase 4', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Choker of Endless Nightmares'), +(7, 1, 2, 0, 164, 32581, 'Phase 4', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Swiftstrike Shoulders'), +(7, 1, 4, 0, 164, 30905, 'Phase 4', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Midnight Chestguard'), +(7, 1, 5, 0, 164, 30106, 'Phase 4', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Belt of One-Hundred Deaths'), +(7, 1, 6, 0, 164, 30900, 'Phase 4', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Bow-Stitched Leggings'), +(7, 1, 7, 0, 164, 32366, 'Phase 4', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Shadowmaster''s Boots'), +(7, 1, 8, 0, 164, 32574, 'Phase 4', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Bindings of Lightning Reflexes'), +(7, 1, 9, 0, 164, 32347, 'Phase 4', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Grips of Damnation'), +(7, 1, 10, 0, 164, 33496, 'Phase 4', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Signet of Primal Wrath'), +(7, 1, 11, 0, 164, 32497, 'Phase 4', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Stormrage Signet Ring'), +(7, 1, 12, 0, 164, 28830, 'Phase 4', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Dragonspine Trophy'), +(7, 1, 13, 0, 164, 32505, 'Phase 4', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Madness of the Betrayer'), +(7, 1, 14, 0, 164, 32323, 'Phase 4', 'Shaman', 'Enhancement', 'Back', 'Both', 'Shadowmoon Destroyer''s Drape'), +(7, 1, 15, 0, 164, 34331, 'Phase 5', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Hand of the Deceiver'), +(7, 1, 16, 0, 164, 34346, 'Phase 5', 'Shaman', 'Enhancement', 'OffHand', 'Both', 'Mounting Vengeance'), +(7, 1, 17, 0, 164, 33507, 'Phase 4', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Stonebreaker''s Totem'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 224, 39602, 'Phase 1', 'Shaman', 'Enhancement', 'Head', 'Both', 'Heroes'' Earthshatter Faceguard'), +(7, 1, 1, 0, 224, 39146, 'Phase 1', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Collar of Dissolution'), +(7, 1, 2, 0, 224, 40524, 'Phase 1', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Valorous Earthshatter Shoulderguards'), +(7, 1, 4, 0, 224, 39597, 'Phase 1', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Heroes'' Earthshatter Chestguard'), +(7, 1, 5, 0, 224, 39762, 'Phase 1', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Torn Web Wrapping'), +(7, 1, 6, 0, 224, 40522, 'Phase 1', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Valorous Earthshatter War-Kilt'), +(7, 1, 7, 0, 224, 40746, 'Phase 1', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Pack-Ice Striders'), +(7, 1, 8, 0, 224, 40282, 'Phase 1', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Slime Stream Bands'), +(7, 1, 9, 0, 224, 39601, 'Phase 1', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Heroes'' Earthshatter Grips'), +(7, 1, 10, 0, 224, 45688, 'Phase 1', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Inscribed Band of the Kirin Tor'), +(7, 1, 12, 1, 224, 40322, 'Phase 1', 'Shaman', 'Enhancement', 'Trinket1', 'Alliance', 'Totem of Dueling'), +(7, 1, 12, 2, 224, 37390, 'Phase 1', 'Shaman', 'Enhancement', 'Trinket1', 'Horde', 'Meteorite Whetstone'), +(7, 1, 14, 0, 224, 40721, 'Phase 1', 'Shaman', 'Enhancement', 'Back', 'Both', 'Hammerhead Sharkskin Cloak'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 1, 245, 51242, 'Phase 2', 'Shaman', 'Enhancement', 'Head', 'Alliance', 'Sanctified Frost Witch''s Faceguard'), +(7, 1, 0, 2, 245, 40543, 'Phase 2', 'Shaman', 'Enhancement', 'Head', 'Horde', 'Blue Aspect Helm'), +(7, 1, 1, 1, 245, 50658, 'Phase 2', 'Shaman', 'Enhancement', 'Neck', 'Alliance', 'Amulet of the Silent Eulogy'), +(7, 1, 1, 2, 245, 44661, 'Phase 2', 'Shaman', 'Enhancement', 'Neck', 'Horde', 'Wyrmrest Necklace of Power'), +(7, 1, 2, 1, 245, 51240, 'Phase 2', 'Shaman', 'Enhancement', 'Shoulders', 'Alliance', 'Sanctified Frost Witch''s Shoulderguards'), +(7, 1, 2, 2, 245, 40524, 'Phase 2', 'Shaman', 'Enhancement', 'Shoulders', 'Horde', 'Valorous Earthshatter Shoulderguards'), +(7, 1, 4, 1, 245, 50689, 'Phase 2', 'Shaman', 'Enhancement', 'Chest', 'Alliance', 'Carapace of Forgotten Kings'), +(7, 1, 4, 2, 245, 40523, 'Phase 2', 'Shaman', 'Enhancement', 'Chest', 'Horde', 'Valorous Earthshatter Chestguard'), +(7, 1, 5, 1, 245, 54587, 'Phase 2', 'Shaman', 'Enhancement', 'Waist', 'Alliance', 'Split Shape Belt'), +(7, 1, 5, 2, 245, 39762, 'Phase 2', 'Shaman', 'Enhancement', 'Waist', 'Horde', 'Torn Web Wrapping'), +(7, 1, 6, 1, 245, 51241, 'Phase 2', 'Shaman', 'Enhancement', 'Legs', 'Alliance', 'Sanctified Frost Witch''s War-Kilt'), +(7, 1, 6, 2, 245, 40522, 'Phase 2', 'Shaman', 'Enhancement', 'Legs', 'Horde', 'Valorous Earthshatter War-Kilt'), +(7, 1, 7, 1, 245, 50711, 'Phase 2', 'Shaman', 'Enhancement', 'Feet', 'Alliance', 'Treads of the Wasteland'), +(7, 1, 7, 2, 245, 40367, 'Phase 2', 'Shaman', 'Enhancement', 'Feet', 'Horde', 'Boots of the Great Construct'), +(7, 1, 8, 1, 245, 54582, 'Phase 2', 'Shaman', 'Enhancement', 'Wrists', 'Alliance', 'Bracers of Fiery Night'), +(7, 1, 8, 2, 245, 40282, 'Phase 2', 'Shaman', 'Enhancement', 'Wrists', 'Horde', 'Slime Stream Bands'), +(7, 1, 9, 1, 245, 51243, 'Phase 2', 'Shaman', 'Enhancement', 'Hands', 'Alliance', 'Sanctified Frost Witch''s Grips'), +(7, 1, 9, 2, 245, 40520, 'Phase 2', 'Shaman', 'Enhancement', 'Hands', 'Horde', 'Valorous Earthshatter Grips'), +(7, 1, 10, 1, 245, 54576, 'Phase 2', 'Shaman', 'Enhancement', 'Finger1', 'Alliance', 'Signet of Twilight'), +(7, 1, 10, 2, 245, 40074, 'Phase 2', 'Shaman', 'Enhancement', 'Finger1', 'Horde', 'Strong-Handed Ring'), +(7, 1, 12, 1, 245, 50365, 'Phase 2', 'Shaman', 'Enhancement', 'Trinket1', 'Alliance', 'Phylactery of the Nameless Lich'), +(7, 1, 12, 2, 245, 40684, 'Phase 2', 'Shaman', 'Enhancement', 'Trinket1', 'Horde', 'Mirror of Truth'), +(7, 1, 14, 1, 245, 54583, 'Phase 2', 'Shaman', 'Enhancement', 'Back', 'Alliance', 'Cloak of Burning Dusk'), +(7, 1, 14, 2, 245, 40403, 'Phase 2', 'Shaman', 'Enhancement', 'Back', 'Horde', 'Drape of the Deadly Foe'), +(7, 1, 15, 0, 245, 50734, 'Phase 2', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(7, 1, 17, 1, 245, 50458, 'Phase 2', 'Shaman', 'Enhancement', 'Ranged', 'Alliance', 'Bizuri''s Totem of Shattered Ice'), +(7, 1, 17, 2, 245, 40322, 'Phase 2', 'Shaman', 'Enhancement', 'Ranged', 'Horde', 'Totem of Dueling'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 258, 45610, 'Phase 3', 'Shaman', 'Enhancement', 'Head', 'Both', 'Boundless Gaze'), +(7, 1, 1, 0, 258, 45517, 'Phase 3', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Pendulum of Infinity'), +(7, 1, 2, 0, 258, 46203, 'Phase 3', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Conqueror''s Worldbreaker Shoulderguards'), +(7, 1, 4, 0, 258, 46205, 'Phase 3', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Conqueror''s Worldbreaker Chestguard'), +(7, 1, 5, 0, 258, 45553, 'Phase 3', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Belt of Dragons'), +(7, 1, 6, 0, 258, 46208, 'Phase 3', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Conqueror''s Worldbreaker War-Kilt'), +(7, 1, 7, 0, 258, 45244, 'Phase 3', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Greaves of Swift Vengeance'), +(7, 1, 8, 0, 258, 45611, 'Phase 3', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Solar Bindings'), +(7, 1, 9, 0, 258, 46200, 'Phase 3', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Conqueror''s Worldbreaker Grips'), +(7, 1, 12, 0, 258, 45609, 'Phase 3', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Comet''s Trail'), +(7, 1, 14, 0, 258, 45461, 'Phase 3', 'Shaman', 'Enhancement', 'Back', 'Both', 'Drape of Icy Intent'), +(7, 1, 15, 0, 258, 45132, 'Phase 3', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Golden Saronite Dragon'), +(7, 1, 17, 0, 258, 42608, 'Phase 3', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Furious Gladiator''s Totem of Indomitability'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 264, 48353, 'Phase 4', 'Shaman', 'Enhancement', 'Head', 'Both', 'Faceguard of Triumph'), +(7, 1, 2, 0, 264, 48351, 'Phase 4', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Shoulderguards of Triumph'), +(7, 1, 4, 1, 264, 46965, 'Phase 4', 'Shaman', 'Enhancement', 'Chest', 'Alliance', 'Breastplate of Cruel Intent'), +(7, 1, 4, 2, 264, 47412, 'Phase 4', 'Shaman', 'Enhancement', 'Chest', 'Horde', 'Cuirass of Cruel Intent'), +(7, 1, 5, 1, 264, 47112, 'Phase 4', 'Shaman', 'Enhancement', 'Waist', 'Alliance', 'Belt of the Merciless Killer'), +(7, 1, 5, 2, 264, 47460, 'Phase 4', 'Shaman', 'Enhancement', 'Waist', 'Horde', 'Belt of the Pitiless Killer'), +(7, 1, 6, 0, 264, 48352, 'Phase 4', 'Shaman', 'Enhancement', 'Legs', 'Both', 'War-Kilt of Triumph'), +(7, 1, 7, 1, 264, 47099, 'Phase 4', 'Shaman', 'Enhancement', 'Feet', 'Alliance', 'Boots of Tremoring Earth'), +(7, 1, 7, 2, 264, 47456, 'Phase 4', 'Shaman', 'Enhancement', 'Feet', 'Horde', 'Sabatons of Tremoring Earth'), +(7, 1, 8, 1, 264, 47143, 'Phase 4', 'Shaman', 'Enhancement', 'Wrists', 'Alliance', 'Bindings of Dark Essence'), +(7, 1, 8, 2, 264, 47467, 'Phase 4', 'Shaman', 'Enhancement', 'Wrists', 'Horde', 'Dark Essence Bindings'), +(7, 1, 9, 0, 264, 48354, 'Phase 4', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Grips of Triumph'), +(7, 1, 10, 1, 264, 47075, 'Phase 4', 'Shaman', 'Enhancement', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(7, 1, 10, 2, 264, 47443, 'Phase 4', 'Shaman', 'Enhancement', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(7, 1, 12, 1, 264, 45609, 'Phase 4', 'Shaman', 'Enhancement', 'Trinket1', 'Alliance', 'Comet''s Trail'), +(7, 1, 14, 1, 264, 47552, 'Phase 4', 'Shaman', 'Enhancement', 'Back', 'Alliance', 'Jaina''s Radiance'), +(7, 1, 14, 2, 264, 47551, 'Phase 4', 'Shaman', 'Enhancement', 'Back', 'Horde', 'Aethas'' Intensity'), +(7, 1, 17, 1, 264, 47666, 'Phase 4', 'Shaman', 'Enhancement', 'Ranged', 'Alliance', 'Totem of Electrifying Wind'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 1, 0, 0, 290, 51242, 'Phase 5', 'Shaman', 'Enhancement', 'Head', 'Both', 'Sanctified Frost Witch''s Faceguard'), +(7, 1, 1, 0, 290, 50633, 'Phase 5', 'Shaman', 'Enhancement', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(7, 1, 2, 0, 290, 51240, 'Phase 5', 'Shaman', 'Enhancement', 'Shoulders', 'Both', 'Sanctified Frost Witch''s Shoulderguards'), +(7, 1, 4, 0, 290, 50689, 'Phase 5', 'Shaman', 'Enhancement', 'Chest', 'Both', 'Carapace of Forgotten Kings'), +(7, 1, 5, 0, 290, 50688, 'Phase 5', 'Shaman', 'Enhancement', 'Waist', 'Both', 'Nerub''ar Stalker''s Cord'), +(7, 1, 6, 0, 290, 51241, 'Phase 5', 'Shaman', 'Enhancement', 'Legs', 'Both', 'Sanctified Frost Witch''s War-Kilt'), +(7, 1, 7, 0, 290, 54577, 'Phase 5', 'Shaman', 'Enhancement', 'Feet', 'Both', 'Returning Footfalls'), +(7, 1, 8, 0, 290, 50655, 'Phase 5', 'Shaman', 'Enhancement', 'Wrists', 'Both', 'Scourge Hunter''s Vambraces'), +(7, 1, 9, 0, 290, 51243, 'Phase 5', 'Shaman', 'Enhancement', 'Hands', 'Both', 'Sanctified Frost Witch''s Grips'), +(7, 1, 10, 0, 290, 50678, 'Phase 5', 'Shaman', 'Enhancement', 'Finger1', 'Both', 'Seal of Many Mouths'), +(7, 1, 11, 0, 290, 50402, 'Phase 5', 'Shaman', 'Enhancement', 'Finger2', 'Both', 'Ashen Band of Endless Vengeance'), +(7, 1, 12, 0, 290, 50706, 'Phase 5', 'Shaman', 'Enhancement', 'Trinket1', 'Both', 'Tiny Abomination in a Jar'), +(7, 1, 13, 0, 290, 54590, 'Phase 5', 'Shaman', 'Enhancement', 'Trinket2', 'Both', 'Sharpened Twilight Scale'), +(7, 1, 14, 0, 290, 50653, 'Phase 5', 'Shaman', 'Enhancement', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(7, 1, 15, 0, 290, 50737, 'Phase 5', 'Shaman', 'Enhancement', 'MainHand', 'Both', 'Havoc''s Call, Blade of Lordaeron Kings'), +(7, 1, 16, 0, 290, 50737, 'Phase 5', 'Shaman', 'Enhancement', 'OffHand', 'Both', 'Havoc''s Call, Blade of Lordaeron Kings'), +(7, 1, 17, 0, 290, 50463, 'Phase 5', 'Shaman', 'Enhancement', 'Ranged', 'Both', 'Totem of the Avalanche'); + +-- Restoration (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 66, 13216, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Head', 'Both', 'Crown of the Penitent'), +(7, 2, 1, 0, 66, 18723, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(7, 2, 2, 0, 66, 15061, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Living Shoulders'), +(7, 2, 4, 0, 66, 13346, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(7, 2, 5, 0, 66, 14553, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Waist', 'Both', 'Sash of Mercy'), +(7, 2, 6, 0, 66, 11841, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Legs', 'Both', 'Senior Designer''s Pantaloons'), +(7, 2, 7, 0, 66, 13954, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Feet', 'Both', 'Verdant Footpads'), +(7, 2, 8, 0, 66, 13969, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Loomguard Armbraces'), +(7, 2, 9, 0, 66, 10787, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Hands', 'Both', 'Atal''ai Gloves'), +(7, 2, 10, 0, 66, 16058, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Fordring''s Seal'), +(7, 2, 11, 0, 66, 13178, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Rosewine Circle'), +(7, 2, 12, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Briarwood Reed'), +(7, 2, 13, 0, 66, 11819, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Second Wind'), +(7, 2, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'Back', 'Both', 'Archivist Cape'), +(7, 2, 15, 0, 66, 11923, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'MainHand', 'Both', 'The Hammer of Grace'), +(7, 2, 16, 0, 66, 11928, 'Phase 1 (Pre-Raid)', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Thaurissan''s Royal Scepter'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 76, 13216, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Head', 'Both', 'Crown of the Penitent'), +(7, 2, 1, 0, 76, 18723, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(7, 2, 2, 0, 76, 18757, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Diabolic Mantle'), +(7, 2, 4, 0, 76, 13346, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(7, 2, 5, 0, 76, 14553, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Waist', 'Both', 'Sash of Mercy'), +(7, 2, 6, 0, 76, 18386, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Legs', 'Both', 'Padre''s Trousers'), +(7, 2, 7, 0, 76, 13954, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Feet', 'Both', 'Verdant Footpads'), +(7, 2, 8, 0, 76, 13969, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Loomguard Armbraces'), +(7, 2, 9, 0, 76, 18527, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Hands', 'Both', 'Harmonious Gauntlets'), +(7, 2, 10, 0, 76, 16058, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Fordring''s Seal'), +(7, 2, 11, 0, 76, 13178, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Rosewine Circle'), +(7, 2, 12, 0, 76, 18371, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Mindtap Talisman'), +(7, 2, 13, 0, 76, 18371, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Mindtap Talisman'), +(7, 2, 14, 0, 76, 13386, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'Back', 'Both', 'Archivist Cape'), +(7, 2, 15, 0, 76, 11923, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'MainHand', 'Both', 'The Hammer of Grace'), +(7, 2, 16, 0, 76, 18523, 'Phase 2 (Pre-Raid)', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Brightly Glowing Stone'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 78, 16842, 'Phase 2', 'Shaman', 'Restoration', 'Head', 'Both', 'Earthfury Helmet'), +(7, 2, 1, 0, 78, 18723, 'Phase 2', 'Shaman', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(7, 2, 2, 0, 78, 18810, 'Phase 2', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(7, 2, 4, 0, 78, 13346, 'Phase 2', 'Shaman', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(7, 2, 5, 0, 78, 19162, 'Phase 2', 'Shaman', 'Restoration', 'Waist', 'Both', 'Corehound Belt'), +(7, 2, 6, 0, 78, 18875, 'Phase 2', 'Shaman', 'Restoration', 'Legs', 'Both', 'Salamander Scale Pants'), +(7, 2, 7, 0, 78, 13954, 'Phase 2', 'Shaman', 'Restoration', 'Feet', 'Both', 'Verdant Footpads'), +(7, 2, 8, 0, 78, 13969, 'Phase 2', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Loomguard Armbraces'), +(7, 2, 9, 0, 78, 18527, 'Phase 2', 'Shaman', 'Restoration', 'Hands', 'Both', 'Harmonious Gauntlets'), +(7, 2, 10, 0, 78, 19140, 'Phase 2', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Cauterizing Band'), +(7, 2, 11, 0, 78, 13178, 'Phase 2', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Rosewine Circle'), +(7, 2, 12, 0, 78, 17064, 'Phase 2', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Shard of the Scale'), +(7, 2, 13, 0, 78, 18371, 'Phase 2', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Mindtap Talisman'), +(7, 2, 14, 0, 78, 13386, 'Phase 2', 'Shaman', 'Restoration', 'Back', 'Both', 'Archivist Cape'), +(7, 2, 15, 0, 78, 17070, 'Phase 2', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Fang of the Mystics'), +(7, 2, 16, 0, 78, 18523, 'Phase 2', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Brightly Glowing Stone'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 83, 16947, 'Phase 3', 'Shaman', 'Restoration', 'Head', 'Both', 'Helmet of Ten Storms'), +(7, 2, 1, 0, 83, 19371, 'Phase 3', 'Shaman', 'Restoration', 'Neck', 'Both', 'Pendant of the Fallen Dragon'), +(7, 2, 2, 0, 83, 18810, 'Phase 3', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(7, 2, 4, 0, 83, 13346, 'Phase 3', 'Shaman', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(7, 2, 5, 0, 83, 19162, 'Phase 3', 'Shaman', 'Restoration', 'Waist', 'Both', 'Corehound Belt'), +(7, 2, 6, 0, 83, 18875, 'Phase 3', 'Shaman', 'Restoration', 'Legs', 'Both', 'Salamander Scale Pants'), +(7, 2, 7, 0, 83, 19391, 'Phase 3', 'Shaman', 'Restoration', 'Feet', 'Both', 'Shimmering Geta'), +(7, 2, 8, 0, 83, 16943, 'Phase 3', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bracers of Ten Storms'), +(7, 2, 9, 0, 83, 16948, 'Phase 3', 'Shaman', 'Restoration', 'Hands', 'Both', 'Gauntlets of Ten Storms'), +(7, 2, 10, 0, 83, 19397, 'Phase 3', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Ring of Blackrock'), +(7, 2, 11, 0, 83, 19382, 'Phase 3', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Pure Elementium Band'), +(7, 2, 12, 0, 83, 17064, 'Phase 3', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Shard of the Scale'), +(7, 2, 13, 0, 83, 19395, 'Phase 3', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Rejuvenating Gem'), +(7, 2, 14, 0, 83, 19430, 'Phase 3', 'Shaman', 'Restoration', 'Back', 'Both', 'Shroud of Pure Thought'), +(7, 2, 15, 0, 83, 19347, 'Phase 3', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Claw of Chromaggus'), +(7, 2, 16, 0, 83, 19312, 'Phase 3', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Lei of the Lifegiver'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 88, 21372, 'Phase 5', 'Shaman', 'Restoration', 'Head', 'Both', 'Stormcaller''s Diadem'), +(7, 2, 1, 0, 88, 21712, 'Phase 5', 'Shaman', 'Restoration', 'Neck', 'Both', 'Amulet of the Fallen God'), +(7, 2, 2, 0, 88, 21376, 'Phase 5', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Stormcaller''s Pauldrons'), +(7, 2, 4, 0, 88, 21374, 'Phase 5', 'Shaman', 'Restoration', 'Chest', 'Both', 'Stormcaller''s Hauberk'), +(7, 2, 5, 0, 88, 16944, 'Phase 5', 'Shaman', 'Restoration', 'Waist', 'Both', 'Belt of Ten Storms'), +(7, 2, 6, 0, 88, 21375, 'Phase 5', 'Shaman', 'Restoration', 'Legs', 'Both', 'Stormcaller''s Leggings'), +(7, 2, 7, 0, 88, 21373, 'Phase 5', 'Shaman', 'Restoration', 'Feet', 'Both', 'Stormcaller''s Footguards'), +(7, 2, 8, 0, 88, 16943, 'Phase 5', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bracers of Ten Storms'), +(7, 2, 9, 0, 88, 16948, 'Phase 5', 'Shaman', 'Restoration', 'Hands', 'Both', 'Gauntlets of Ten Storms'), +(7, 2, 10, 0, 88, 21620, 'Phase 5', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Ring of the Martyr'), +(7, 2, 11, 0, 88, 21681, 'Phase 5', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Ring of the Devoured'), +(7, 2, 12, 0, 88, 17064, 'Phase 5', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Shard of the Scale'), +(7, 2, 13, 0, 88, 19395, 'Phase 5', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Rejuvenating Gem'), +(7, 2, 14, 0, 88, 21583, 'Phase 5', 'Shaman', 'Restoration', 'Back', 'Both', 'Cloak of Clarity'), +(7, 2, 15, 0, 88, 21839, 'Phase 5', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Scepter of the False Prophet'), +(7, 2, 16, 0, 88, 21610, 'Phase 5', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Wormscale Blocker'), +(7, 2, 17, 0, 88, 22396, 'Phase 5', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Life'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 92, 22466, 'Phase 6', 'Shaman', 'Restoration', 'Head', 'Both', 'Earthshatter Headpiece'), +(7, 2, 1, 0, 92, 21712, 'Phase 6', 'Shaman', 'Restoration', 'Neck', 'Both', 'Amulet of the Fallen God'), +(7, 2, 2, 0, 92, 22467, 'Phase 6', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Earthshatter Spaulders'), +(7, 2, 4, 0, 92, 22464, 'Phase 6', 'Shaman', 'Restoration', 'Chest', 'Both', 'Earthshatter Tunic'), +(7, 2, 5, 0, 92, 22470, 'Phase 6', 'Shaman', 'Restoration', 'Waist', 'Both', 'Earthshatter Girdle'), +(7, 2, 6, 0, 92, 22465, 'Phase 6', 'Shaman', 'Restoration', 'Legs', 'Both', 'Earthshatter Legguards'), +(7, 2, 7, 0, 92, 22468, 'Phase 6', 'Shaman', 'Restoration', 'Feet', 'Both', 'Earthshatter Boots'), +(7, 2, 8, 0, 92, 22471, 'Phase 6', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Earthshatter Wristguards'), +(7, 2, 9, 0, 92, 21619, 'Phase 6', 'Shaman', 'Restoration', 'Hands', 'Both', 'Gloves of the Messiah'), +(7, 2, 10, 0, 92, 21620, 'Phase 6', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Ring of the Martyr'), +(7, 2, 11, 0, 92, 23065, 'Phase 6', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Ring of the Earthshatterer'), +(7, 2, 12, 0, 92, 23027, 'Phase 6', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Warmth of Forgiveness'), +(7, 2, 13, 0, 92, 19395, 'Phase 6', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Rejuvenating Gem'), +(7, 2, 14, 0, 92, 21583, 'Phase 6', 'Shaman', 'Restoration', 'Back', 'Both', 'Cloak of Clarity'), +(7, 2, 15, 0, 92, 23056, 'Phase 6', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Hammer of the Twisting Nether'), +(7, 2, 16, 0, 92, 22819, 'Phase 6', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Shield of Condemnation'), +(7, 2, 17, 0, 92, 22396, 'Phase 6', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Life'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 120, 24264, 'Pre-Raid', 'Shaman', 'Restoration', 'Head', 'Both', 'Whitemend Hood'), +(7, 2, 1, 0, 120, 31691, 'Pre-Raid', 'Shaman', 'Restoration', 'Neck', 'Both', 'Natasha''s Guardian Cord'), +(7, 2, 2, 0, 120, 35395, 'Pre-Raid', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Seer''s Ringmail Shoulderpads'), +(7, 2, 4, 0, 120, 29522, 'Pre-Raid', 'Shaman', 'Restoration', 'Chest', 'Both', 'Windhawk Hauberk'), +(7, 2, 5, 0, 120, 29524, 'Pre-Raid', 'Shaman', 'Restoration', 'Waist', 'Both', 'Windhawk Belt'), +(7, 2, 6, 0, 120, 24261, 'Pre-Raid', 'Shaman', 'Restoration', 'Legs', 'Both', 'Whitemend Pants'), +(7, 2, 7, 0, 120, 27549, 'Pre-Raid', 'Shaman', 'Restoration', 'Feet', 'Both', 'Wavefury Boots'), +(7, 2, 8, 0, 120, 29523, 'Pre-Raid', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Windhawk Bracers'), +(7, 2, 9, 0, 120, 29506, 'Pre-Raid', 'Shaman', 'Restoration', 'Hands', 'Both', 'Gloves of the Living Touch'), +(7, 2, 10, 0, 120, 28259, 'Pre-Raid', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Cosmic Lifeband'), +(7, 2, 10, 2, 120, 29168, 'Pre-Raid', 'Shaman', 'Restoration', 'Finger1', 'Horde', 'Ancestral Band'), +(7, 2, 12, 0, 120, 29376, 'Pre-Raid', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(7, 2, 13, 0, 120, 30841, 'Pre-Raid', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Lower City Prayerbook'), +(7, 2, 14, 0, 120, 24254, 'Pre-Raid', 'Shaman', 'Restoration', 'Back', 'Both', 'White Remedy Cape'), +(7, 2, 15, 0, 120, 23556, 'Pre-Raid', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Hand of Eternity'), +(7, 2, 16, 0, 120, 29267, 'Pre-Raid', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Light-Bearer''s Faith Shield'), +(7, 2, 17, 0, 120, 27544, 'Pre-Raid', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Spontaneous Regrowth'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 125, 29035, 'Phase 1', 'Shaman', 'Restoration', 'Head', 'Both', 'Cyclone Faceguard'), +(7, 2, 1, 0, 125, 28609, 'Phase 1', 'Shaman', 'Restoration', 'Neck', 'Both', 'Emberspur Talisman'), +(7, 2, 2, 0, 125, 29037, 'Phase 1', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Cyclone Shoulderguards'), +(7, 2, 4, 0, 125, 29522, 'Phase 1', 'Shaman', 'Restoration', 'Chest', 'Both', 'Windhawk Hauberk'), +(7, 2, 5, 0, 125, 29524, 'Phase 1', 'Shaman', 'Restoration', 'Waist', 'Both', 'Windhawk Belt'), +(7, 2, 6, 0, 125, 30727, 'Phase 1', 'Shaman', 'Restoration', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(7, 2, 7, 0, 125, 30737, 'Phase 1', 'Shaman', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(7, 2, 8, 0, 125, 29523, 'Phase 1', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Windhawk Bracers'), +(7, 2, 9, 0, 125, 28520, 'Phase 1', 'Shaman', 'Restoration', 'Hands', 'Both', 'Gloves of Centering'), +(7, 2, 10, 0, 125, 28763, 'Phase 1', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Jade Ring of the Everliving'), +(7, 2, 11, 0, 125, 28790, 'Phase 1', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Naaru Lightwarden''s Band'), +(7, 2, 12, 0, 125, 29376, 'Phase 1', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(7, 2, 13, 0, 125, 28590, 'Phase 1', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Ribbon of Sacrifice'), +(7, 2, 14, 0, 125, 28765, 'Phase 1', 'Shaman', 'Restoration', 'Back', 'Both', 'Stainless Cloak of the Pure Hearted'), +(7, 2, 15, 0, 125, 28771, 'Phase 1', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Light''s Justice'), +(7, 2, 16, 0, 125, 29458, 'Phase 1', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Aegis of the Vindicator'), +(7, 2, 17, 0, 125, 28523, 'Phase 1', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Healing Rains'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 141, 32475, 'Phase 2', 'Shaman', 'Restoration', 'Head', 'Both', 'Living Replicator Specs'), +(7, 2, 1, 0, 141, 30018, 'Phase 2', 'Shaman', 'Restoration', 'Neck', 'Both', 'Lord Sanguinar''s Claim'), +(7, 2, 2, 0, 141, 30168, 'Phase 2', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Cataclysm Shoulderguards'), +(7, 2, 4, 0, 141, 30164, 'Phase 2', 'Shaman', 'Restoration', 'Chest', 'Both', 'Cataclysm Chestguard'), +(7, 2, 5, 0, 141, 21873, 'Phase 2', 'Shaman', 'Restoration', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(7, 2, 6, 0, 141, 30727, 'Phase 2', 'Shaman', 'Restoration', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(7, 2, 7, 0, 141, 30737, 'Phase 2', 'Shaman', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(7, 2, 8, 0, 141, 30047, 'Phase 2', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Blackfathom Warbands'), +(7, 2, 9, 0, 141, 29976, 'Phase 2', 'Shaman', 'Restoration', 'Hands', 'Both', 'Worldstorm Gauntlets'), +(7, 2, 10, 0, 141, 28763, 'Phase 2', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Jade Ring of the Everliving'), +(7, 2, 11, 0, 141, 30736, 'Phase 2', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Ring of Flowing Light'), +(7, 2, 12, 0, 141, 29376, 'Phase 2', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(7, 2, 13, 0, 141, 28590, 'Phase 2', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Ribbon of Sacrifice'), +(7, 2, 14, 0, 141, 29989, 'Phase 2', 'Shaman', 'Restoration', 'Back', 'Both', 'Sunshower Light Cloak'), +(7, 2, 15, 0, 141, 30108, 'Phase 2', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Lightfathom Scepter'), +(7, 2, 16, 0, 141, 29458, 'Phase 2', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Aegis of the Vindicator'), +(7, 2, 17, 0, 141, 28523, 'Phase 2', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Healing Rains'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 156, 31012, 'Phase 3', 'Shaman', 'Restoration', 'Head', 'Both', 'Skyshatter Helmet'), +(7, 2, 1, 0, 156, 32370, 'Phase 3', 'Shaman', 'Restoration', 'Neck', 'Both', 'Nadina''s Pendant of Purity'), +(7, 2, 2, 0, 156, 31022, 'Phase 3', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Skyshatter Shoulderpads'), +(7, 2, 4, 0, 156, 31016, 'Phase 3', 'Shaman', 'Restoration', 'Chest', 'Both', 'Skyshatter Chestguard'), +(7, 2, 5, 0, 156, 32258, 'Phase 3', 'Shaman', 'Restoration', 'Waist', 'Both', 'Naturalist''s Preserving Cinch'), +(7, 2, 6, 0, 156, 31019, 'Phase 3', 'Shaman', 'Restoration', 'Legs', 'Both', 'Skyshatter Leggings'), +(7, 2, 7, 0, 156, 30737, 'Phase 3', 'Shaman', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(7, 2, 8, 0, 156, 32584, 'Phase 3', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Swiftheal Wraps'), +(7, 2, 9, 0, 156, 32328, 'Phase 3', 'Shaman', 'Restoration', 'Hands', 'Both', 'Botanist''s Gloves of Growth'), +(7, 2, 10, 0, 156, 32528, 'Phase 3', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(7, 2, 11, 0, 156, 32528, 'Phase 3', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Blessed Band of Karabor'), +(7, 2, 12, 0, 156, 29376, 'Phase 3', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(7, 2, 13, 0, 156, 32496, 'Phase 3', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Memento of Tyrande'), +(7, 2, 14, 0, 156, 32524, 'Phase 3', 'Shaman', 'Restoration', 'Back', 'Both', 'Shroud of the Highborne'), +(7, 2, 15, 0, 156, 32500, 'Phase 3', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(7, 2, 16, 0, 156, 30882, 'Phase 3', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Bastion of Light'), +(7, 2, 17, 0, 156, 28523, 'Phase 3', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Healing Rains'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 164, 31012, 'Phase 4', 'Shaman', 'Restoration', 'Head', 'Both', 'Skyshatter Helmet'), +(7, 2, 1, 0, 164, 33281, 'Phase 4', 'Shaman', 'Restoration', 'Neck', 'Both', 'Brooch of Nature''s Mercy'), +(7, 2, 2, 0, 164, 31022, 'Phase 4', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Skyshatter Shoulderpads'), +(7, 2, 4, 0, 164, 31016, 'Phase 4', 'Shaman', 'Restoration', 'Chest', 'Both', 'Skyshatter Chestguard'), +(7, 2, 5, 0, 164, 32258, 'Phase 4', 'Shaman', 'Restoration', 'Waist', 'Both', 'Naturalist''s Preserving Cinch'), +(7, 2, 6, 0, 164, 31019, 'Phase 4', 'Shaman', 'Restoration', 'Legs', 'Both', 'Skyshatter Leggings'), +(7, 2, 7, 0, 164, 33471, 'Phase 4', 'Shaman', 'Restoration', 'Feet', 'Both', 'Two-Toed Sandals'), +(7, 2, 8, 0, 164, 32584, 'Phase 4', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Swiftheal Wraps'), +(7, 2, 9, 0, 164, 32328, 'Phase 4', 'Shaman', 'Restoration', 'Hands', 'Both', 'Botanist''s Gloves of Growth'), +(7, 2, 10, 0, 164, 32528, 'Phase 4', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(7, 2, 11, 0, 164, 32528, 'Phase 4', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Blessed Band of Karabor'), +(7, 2, 12, 0, 164, 29376, 'Phase 4', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(7, 2, 13, 0, 164, 32496, 'Phase 4', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Memento of Tyrande'), +(7, 2, 14, 0, 164, 32524, 'Phase 4', 'Shaman', 'Restoration', 'Back', 'Both', 'Shroud of the Highborne'), +(7, 2, 15, 0, 164, 32500, 'Phase 4', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(7, 2, 16, 0, 164, 30882, 'Phase 4', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Bastion of Light'), +(7, 2, 17, 0, 164, 28523, 'Phase 4', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Healing Rains'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 200, 42555, 'Pre-Raid', 'Shaman', 'Restoration', 'Head', 'Both', 'Electroflux Sight Enhancers'), +(7, 2, 1, 0, 200, 42647, 'Pre-Raid', 'Shaman', 'Restoration', 'Neck', 'Both', 'Titanium Spellshock Necklace'), +(7, 2, 2, 0, 200, 37875, 'Pre-Raid', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Spaulders of the Violet Hold'), +(7, 2, 4, 0, 200, 43461, 'Pre-Raid', 'Shaman', 'Restoration', 'Chest', 'Both', 'Revenant''s Breastplate'), +(7, 2, 5, 0, 200, 37855, 'Pre-Raid', 'Shaman', 'Restoration', 'Waist', 'Both', 'Mail Girdle of the Audient Earth'), +(7, 2, 6, 0, 200, 44305, 'Pre-Raid', 'Shaman', 'Restoration', 'Legs', 'Both', 'Kilt of Dark Mercy'), +(7, 2, 7, 0, 200, 43469, 'Pre-Raid', 'Shaman', 'Restoration', 'Feet', 'Both', 'Revenant''s Treads'), +(7, 2, 8, 0, 200, 37788, 'Pre-Raid', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Limb Regeneration Bracers'), +(7, 2, 9, 0, 200, 44204, 'Pre-Raid', 'Shaman', 'Restoration', 'Hands', 'Both', 'Grips of Fierce Pronouncements'), +(7, 2, 10, 0, 200, 37192, 'Pre-Raid', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Annhylde''s Ring'), +(7, 2, 12, 0, 200, 40685, 'Pre-Raid', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'The Egg of Mortal Essence'), +(7, 2, 14, 0, 200, 41609, 'Pre-Raid', 'Shaman', 'Restoration', 'Back', 'Both', 'Wispcloak'), +(7, 2, 15, 0, 200, 37169, 'Pre-Raid', 'Shaman', 'Restoration', 'MainHand', 'Both', 'War Mace of Unrequited Love'), +(7, 2, 17, 0, 200, 40709, 'Pre-Raid', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Forest Growth'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 224, 40510, 'Phase 1', 'Shaman', 'Restoration', 'Head', 'Both', 'Valorous Earthshatter Headpiece'), +(7, 2, 1, 0, 224, 44662, 'Phase 1', 'Shaman', 'Restoration', 'Neck', 'Both', 'Life-Binder''s Locket'), +(7, 2, 2, 0, 224, 40513, 'Phase 1', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Valorous Earthshatter Spaulders'), +(7, 2, 4, 0, 224, 40508, 'Phase 1', 'Shaman', 'Restoration', 'Chest', 'Both', 'Valorous Earthshatter Tunic'), +(7, 2, 5, 0, 224, 40327, 'Phase 1', 'Shaman', 'Restoration', 'Waist', 'Both', 'Girdle of Recuperation'), +(7, 2, 6, 0, 224, 40512, 'Phase 1', 'Shaman', 'Restoration', 'Legs', 'Both', 'Valorous Earthshatter Legguards'), +(7, 2, 7, 0, 224, 40519, 'Phase 1', 'Shaman', 'Restoration', 'Feet', 'Both', 'Footsteps of Malygos'), +(7, 2, 8, 0, 224, 40324, 'Phase 1', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bands of Mutual Respect'), +(7, 2, 9, 0, 224, 40564, 'Phase 1', 'Shaman', 'Restoration', 'Hands', 'Both', 'Winter Spectacle Gloves'), +(7, 2, 10, 0, 224, 40375, 'Phase 1', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Ring of Decaying Beauty'), +(7, 2, 12, 0, 224, 40258, 'Phase 1', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Forethought Talisman'), +(7, 2, 14, 0, 224, 44005, 'Phase 1', 'Shaman', 'Restoration', 'Back', 'Both', 'Pennant Cloak'), +(7, 2, 17, 0, 224, 39728, 'Phase 1', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of Misery'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 245, 46201, 'Phase 2', 'Shaman', 'Restoration', 'Head', 'Both', 'Conqueror''s Worldbreaker Headpiece'), +(7, 2, 1, 0, 245, 45443, 'Phase 2', 'Shaman', 'Restoration', 'Neck', 'Both', 'Charm of Meticulous Timing'), +(7, 2, 2, 0, 245, 46204, 'Phase 2', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Conqueror''s Worldbreaker Spaulders'), +(7, 2, 4, 0, 245, 45867, 'Phase 2', 'Shaman', 'Restoration', 'Chest', 'Both', 'Breastplate of the Stoneshaper'), +(7, 2, 5, 0, 245, 45151, 'Phase 2', 'Shaman', 'Restoration', 'Waist', 'Both', 'Belt of the Fallen Wyrm'), +(7, 2, 6, 0, 245, 46202, 'Phase 2', 'Shaman', 'Restoration', 'Legs', 'Both', 'Conqueror''s Worldbreaker Legguards'), +(7, 2, 7, 0, 245, 45537, 'Phase 2', 'Shaman', 'Restoration', 'Feet', 'Both', 'Treads of the False Oracle'), +(7, 2, 8, 0, 245, 45460, 'Phase 2', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bindings of Winter Gale'), +(7, 2, 9, 0, 245, 46199, 'Phase 2', 'Shaman', 'Restoration', 'Hands', 'Both', 'Conqueror''s Worldbreaker Handguards'), +(7, 2, 10, 0, 245, 45614, 'Phase 2', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Starshine Circle'), +(7, 2, 12, 0, 245, 40432, 'Phase 2', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Illustration of the Dragon Soul'), +(7, 2, 14, 0, 245, 44005, 'Phase 2', 'Shaman', 'Restoration', 'Back', 'Both', 'Pennant Cloak'), +(7, 2, 15, 0, 245, 46017, 'Phase 2', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(7, 2, 17, 0, 245, 45114, 'Phase 2', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Steamcaller''s Totem'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 258, 46201, 'Phase 3', 'Shaman', 'Restoration', 'Head', 'Both', 'Conqueror''s Worldbreaker Headpiece'), +(7, 2, 1, 1, 258, 47144, 'Phase 3', 'Shaman', 'Restoration', 'Neck', 'Alliance', 'Wail of the Val''kyr'), +(7, 2, 1, 2, 258, 47468, 'Phase 3', 'Shaman', 'Restoration', 'Neck', 'Horde', 'Cry of the Val''kyr'), +(7, 2, 2, 0, 258, 46204, 'Phase 3', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Conqueror''s Worldbreaker Spaulders'), +(7, 2, 4, 0, 258, 46198, 'Phase 3', 'Shaman', 'Restoration', 'Chest', 'Both', 'Conqueror''s Worldbreaker Tunic'), +(7, 2, 5, 0, 258, 46991, 'Phase 3', 'Shaman', 'Restoration', 'Waist', 'Both', 'Belt of the Ice Burrower'), +(7, 2, 6, 1, 258, 47190, 'Phase 3', 'Shaman', 'Restoration', 'Legs', 'Alliance', 'Legwraps of the Awakening'), +(7, 2, 6, 2, 258, 47479, 'Phase 3', 'Shaman', 'Restoration', 'Legs', 'Horde', 'Leggings of the Awakening'), +(7, 2, 7, 1, 258, 47099, 'Phase 3', 'Shaman', 'Restoration', 'Feet', 'Alliance', 'Boots of Tremoring Earth'), +(7, 2, 7, 2, 258, 47456, 'Phase 3', 'Shaman', 'Restoration', 'Feet', 'Horde', 'Sabatons of Tremoring Earth'), +(7, 2, 8, 0, 258, 45460, 'Phase 3', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bindings of Winter Gale'), +(7, 2, 9, 0, 258, 46199, 'Phase 3', 'Shaman', 'Restoration', 'Hands', 'Both', 'Conqueror''s Worldbreaker Handguards'), +(7, 2, 10, 1, 258, 47224, 'Phase 3', 'Shaman', 'Restoration', 'Finger1', 'Alliance', 'Ring of the Darkmender'), +(7, 2, 10, 2, 258, 47439, 'Phase 3', 'Shaman', 'Restoration', 'Finger1', 'Horde', 'Circle of the Darkmender'), +(7, 2, 12, 1, 258, 47059, 'Phase 3', 'Shaman', 'Restoration', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(7, 2, 12, 2, 258, 47432, 'Phase 3', 'Shaman', 'Restoration', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(7, 2, 14, 1, 258, 47552, 'Phase 3', 'Shaman', 'Restoration', 'Back', 'Alliance', 'Jaina''s Radiance'), +(7, 2, 14, 2, 258, 47551, 'Phase 3', 'Shaman', 'Restoration', 'Back', 'Horde', 'Aethas'' Intensity'), +(7, 2, 15, 0, 258, 46017, 'Phase 3', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 264, 51247, 'Phase 4', 'Shaman', 'Restoration', 'Head', 'Both', 'Sanctified Frost Witch''s Headpiece'), +(7, 2, 1, 0, 264, 50724, 'Phase 4', 'Shaman', 'Restoration', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(7, 2, 2, 0, 264, 51245, 'Phase 4', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Sanctified Frost Witch''s Spaulders'), +(7, 2, 4, 0, 264, 51249, 'Phase 4', 'Shaman', 'Restoration', 'Chest', 'Both', 'Sanctified Frost Witch''s Tunic'), +(7, 2, 5, 0, 264, 50613, 'Phase 4', 'Shaman', 'Restoration', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(7, 2, 6, 0, 264, 51246, 'Phase 4', 'Shaman', 'Restoration', 'Legs', 'Both', 'Sanctified Frost Witch''s Legguards'), +(7, 2, 7, 0, 264, 50699, 'Phase 4', 'Shaman', 'Restoration', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(7, 2, 8, 0, 264, 50687, 'Phase 4', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bloodsunder''s Bracers'), +(7, 2, 9, 0, 264, 50703, 'Phase 4', 'Shaman', 'Restoration', 'Hands', 'Both', 'Unclean Surgical Gloves'), +(7, 2, 10, 0, 264, 50400, 'Phase 4', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Ashen Band of Endless Wisdom'), +(7, 2, 12, 1, 264, 47059, 'Phase 4', 'Shaman', 'Restoration', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(7, 2, 12, 2, 264, 47432, 'Phase 4', 'Shaman', 'Restoration', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(7, 2, 14, 0, 264, 50628, 'Phase 4', 'Shaman', 'Restoration', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(7, 2, 15, 0, 264, 46017, 'Phase 4', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(7, 2, 17, 0, 264, 50464, 'Phase 4', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Totem of the Surging Sea'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(7, 2, 0, 0, 290, 51247, 'Phase 5', 'Shaman', 'Restoration', 'Head', 'Both', 'Sanctified Frost Witch''s Headpiece'), +(7, 2, 1, 0, 290, 50182, 'Phase 5', 'Shaman', 'Restoration', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(7, 2, 2, 0, 290, 51245, 'Phase 5', 'Shaman', 'Restoration', 'Shoulders', 'Both', 'Sanctified Frost Witch''s Spaulders'), +(7, 2, 4, 0, 290, 51249, 'Phase 5', 'Shaman', 'Restoration', 'Chest', 'Both', 'Sanctified Frost Witch''s Tunic'), +(7, 2, 5, 0, 290, 54587, 'Phase 5', 'Shaman', 'Restoration', 'Waist', 'Both', 'Split Shape Belt'), +(7, 2, 6, 0, 290, 51246, 'Phase 5', 'Shaman', 'Restoration', 'Legs', 'Both', 'Sanctified Frost Witch''s Legguards'), +(7, 2, 7, 0, 290, 50699, 'Phase 5', 'Shaman', 'Restoration', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(7, 2, 8, 0, 290, 50687, 'Phase 5', 'Shaman', 'Restoration', 'Wrists', 'Both', 'Bloodsunder''s Bracers'), +(7, 2, 9, 0, 290, 50703, 'Phase 5', 'Shaman', 'Restoration', 'Hands', 'Both', 'Unclean Surgical Gloves'), +(7, 2, 10, 0, 290, 50664, 'Phase 5', 'Shaman', 'Restoration', 'Finger1', 'Both', 'Ring of Rapid Ascent'), +(7, 2, 11, 0, 290, 50400, 'Phase 5', 'Shaman', 'Restoration', 'Finger2', 'Both', 'Ashen Band of Endless Wisdom'), +(7, 2, 12, 0, 290, 50366, 'Phase 5', 'Shaman', 'Restoration', 'Trinket1', 'Both', 'Althor''s Abacus'), +(7, 2, 13, 0, 290, 54589, 'Phase 5', 'Shaman', 'Restoration', 'Trinket2', 'Both', 'Glowing Twilight Scale'), +(7, 2, 14, 0, 290, 54583, 'Phase 5', 'Shaman', 'Restoration', 'Back', 'Both', 'Cloak of Burning Dusk'), +(7, 2, 15, 0, 290, 46017, 'Phase 5', 'Shaman', 'Restoration', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(7, 2, 16, 0, 290, 50616, 'Phase 5', 'Shaman', 'Restoration', 'OffHand', 'Both', 'Bulwark of Smouldering Steel'), +(7, 2, 17, 0, 290, 50458, 'Phase 5', 'Shaman', 'Restoration', 'Ranged', 'Both', 'Bizuri''s Totem of Shattered Ice'); + + +-- ============================================================ +-- Mage (8) +-- ============================================================ +-- Arcane (tab 0) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 66, 10504, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Head', 'Both', 'Green Lens'), +(8, 0, 1, 0, 66, 12103, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Neck', 'Both', 'Star of Mystaria'), +(8, 0, 2, 0, 66, 11782, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Boreal Mantle'), +(8, 0, 4, 0, 66, 14152, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 0, 5, 0, 66, 11662, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Waist', 'Both', 'Ban''thok Sash'), +(8, 0, 6, 0, 66, 13170, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Legs', 'Both', 'Skyshroud Leggings'), +(8, 0, 7, 0, 66, 11822, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Feet', 'Both', 'Omnicast Boots'), +(8, 0, 8, 0, 66, 11766, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 0, 9, 0, 66, 13253, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Hands', 'Both', 'Hands of Power'), +(8, 0, 10, 0, 66, 942, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Finger1', 'Both', 'Freezing Band'), +(8, 0, 11, 0, 66, 942, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Finger2', 'Both', 'Freezing Band'), +(8, 0, 12, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 0, 13, 0, 66, 13968, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Eye of the Beast'), +(8, 0, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Back', 'Both', 'Archivist Cape'), +(8, 0, 15, 0, 66, 13964, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'MainHand', 'Both', 'Witchblade'), +(8, 0, 16, 0, 66, 10796, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'OffHand', 'Both', 'Drakestone'), +(8, 0, 17, 0, 66, 13938, 'Phase 1 (Pre-Raid)', 'Mage', 'Arcane', 'Ranged', 'Both', 'Bonecreeper Stylus'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 76, 23318, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Head', 'Both', 'Lieutenant Commander''s Silk Cowl'), +(8, 0, 1, 0, 76, 12103, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Neck', 'Both', 'Star of Mystaria'), +(8, 0, 2, 0, 76, 23319, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Lieutenant Commander''s Silk Mantle'), +(8, 0, 4, 0, 76, 14152, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 0, 5, 0, 76, 11662, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Waist', 'Both', 'Ban''thok Sash'), +(8, 0, 6, 0, 76, 23304, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Legs', 'Both', 'Knight-Captain''s Silk Legguards'), +(8, 0, 7, 0, 76, 23291, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Feet', 'Both', 'Knight-Lieutenant''s Silk Walkers'), +(8, 0, 8, 0, 76, 11766, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 0, 9, 0, 76, 13253, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Hands', 'Both', 'Hands of Power'), +(8, 0, 10, 0, 76, 942, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Finger1', 'Both', 'Freezing Band'), +(8, 0, 11, 0, 76, 942, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Finger2', 'Both', 'Freezing Band'), +(8, 0, 12, 0, 76, 12930, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 0, 13, 0, 76, 13968, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Eye of the Beast'), +(8, 0, 14, 0, 76, 13386, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Back', 'Both', 'Archivist Cape'), +(8, 0, 15, 0, 76, 13964, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'MainHand', 'Both', 'Witchblade'), +(8, 0, 16, 0, 76, 10796, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'OffHand', 'Both', 'Drakestone'), +(8, 0, 17, 0, 76, 13938, 'Phase 2 (Pre-Raid)', 'Mage', 'Arcane', 'Ranged', 'Both', 'Bonecreeper Stylus'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 78, 16914, 'Phase 2', 'Mage', 'Arcane', 'Head', 'Both', 'Netherwind Crown'), +(8, 0, 1, 0, 78, 18814, 'Phase 2', 'Mage', 'Arcane', 'Neck', 'Both', 'Choker of the Fire Lord'), +(8, 0, 2, 0, 78, 23319, 'Phase 2', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Lieutenant Commander''s Silk Mantle'), +(8, 0, 4, 0, 78, 14152, 'Phase 2', 'Mage', 'Arcane', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 0, 5, 0, 78, 19136, 'Phase 2', 'Mage', 'Arcane', 'Waist', 'Both', 'Mana Igniting Cord'), +(8, 0, 6, 0, 78, 16915, 'Phase 2', 'Mage', 'Arcane', 'Legs', 'Both', 'Netherwind Pants'), +(8, 0, 7, 0, 78, 23291, 'Phase 2', 'Mage', 'Arcane', 'Feet', 'Both', 'Knight-Lieutenant''s Silk Walkers'), +(8, 0, 8, 0, 78, 11766, 'Phase 2', 'Mage', 'Arcane', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 0, 9, 0, 78, 13253, 'Phase 2', 'Mage', 'Arcane', 'Hands', 'Both', 'Hands of Power'), +(8, 0, 10, 0, 78, 19147, 'Phase 2', 'Mage', 'Arcane', 'Finger1', 'Both', 'Ring of Spell Power'), +(8, 0, 11, 0, 78, 19147, 'Phase 2', 'Mage', 'Arcane', 'Finger2', 'Both', 'Ring of Spell Power'), +(8, 0, 12, 0, 78, 12930, 'Phase 2', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 0, 13, 0, 78, 18820, 'Phase 2', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(8, 0, 14, 0, 78, 13386, 'Phase 2', 'Mage', 'Arcane', 'Back', 'Both', 'Archivist Cape'), +(8, 0, 15, 0, 78, 17103, 'Phase 2', 'Mage', 'Arcane', 'MainHand', 'Both', 'Azuresong Mageblade'), +(8, 0, 16, 0, 78, 10796, 'Phase 2', 'Mage', 'Arcane', 'OffHand', 'Both', 'Drakestone'), +(8, 0, 17, 0, 78, 19130, 'Phase 2', 'Mage', 'Arcane', 'Ranged', 'Both', 'Cold Snap'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 83, 19375, 'Phase 3', 'Mage', 'Arcane', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(8, 0, 1, 0, 83, 18814, 'Phase 3', 'Mage', 'Arcane', 'Neck', 'Both', 'Choker of the Fire Lord'), +(8, 0, 2, 0, 83, 19370, 'Phase 3', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(8, 0, 4, 0, 83, 14152, 'Phase 3', 'Mage', 'Arcane', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 0, 5, 0, 83, 19136, 'Phase 3', 'Mage', 'Arcane', 'Waist', 'Both', 'Mana Igniting Cord'), +(8, 0, 6, 0, 83, 16915, 'Phase 3', 'Mage', 'Arcane', 'Legs', 'Both', 'Netherwind Pants'), +(8, 0, 7, 0, 83, 19438, 'Phase 3', 'Mage', 'Arcane', 'Feet', 'Both', 'Ringo''s Blizzard Boots'), +(8, 0, 8, 0, 83, 19374, 'Phase 3', 'Mage', 'Arcane', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(8, 0, 9, 0, 83, 16913, 'Phase 3', 'Mage', 'Arcane', 'Hands', 'Both', 'Netherwind Gloves'), +(8, 0, 11, 0, 83, 19147, 'Phase 3', 'Mage', 'Arcane', 'Finger2', 'Both', 'Ring of Spell Power'), +(8, 0, 12, 0, 83, 19379, 'Phase 3', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 0, 13, 0, 83, 19339, 'Phase 3', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Mind Quickening Gem'), +(8, 0, 14, 0, 83, 19378, 'Phase 3', 'Mage', 'Arcane', 'Back', 'Both', 'Cloak of the Brood Lord'), +(8, 0, 15, 0, 83, 19356, 'Phase 3', 'Mage', 'Arcane', 'MainHand', 'Both', 'Staff of the Shadow Flame'), +(8, 0, 17, 0, 83, 19130, 'Phase 3', 'Mage', 'Arcane', 'Ranged', 'Both', 'Cold Snap'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 88, 19375, 'Phase 5', 'Mage', 'Arcane', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(8, 0, 1, 0, 88, 21608, 'Phase 5', 'Mage', 'Arcane', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(8, 0, 2, 0, 88, 19370, 'Phase 5', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(8, 0, 4, 0, 88, 19145, 'Phase 5', 'Mage', 'Arcane', 'Chest', 'Both', 'Robe of Volatile Power'), +(8, 0, 5, 0, 88, 22730, 'Phase 5', 'Mage', 'Arcane', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(8, 0, 6, 0, 88, 21461, 'Phase 5', 'Mage', 'Arcane', 'Legs', 'Both', 'Leggings of the Black Blizzard'), +(8, 0, 7, 0, 88, 21344, 'Phase 5', 'Mage', 'Arcane', 'Feet', 'Both', 'Enigma Boots'), +(8, 0, 8, 0, 88, 21186, 'Phase 5', 'Mage', 'Arcane', 'Wrists', 'Both', 'Rockfury Bracers'), +(8, 0, 9, 0, 88, 21585, 'Phase 5', 'Mage', 'Arcane', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(8, 0, 10, 0, 88, 21836, 'Phase 5', 'Mage', 'Arcane', 'Finger1', 'Both', 'Ritssyn''s Ring of Chaos'), +(8, 0, 11, 0, 88, 21709, 'Phase 5', 'Mage', 'Arcane', 'Finger2', 'Both', 'Ring of the Fallen God'), +(8, 0, 12, 0, 88, 19379, 'Phase 5', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 0, 13, 0, 88, 19339, 'Phase 5', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Mind Quickening Gem'), +(8, 0, 14, 0, 88, 22731, 'Phase 5', 'Mage', 'Arcane', 'Back', 'Both', 'Cloak of the Devoured'), +(8, 0, 15, 0, 88, 21622, 'Phase 5', 'Mage', 'Arcane', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(8, 0, 16, 0, 88, 21597, 'Phase 5', 'Mage', 'Arcane', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(8, 0, 17, 0, 88, 21603, 'Phase 5', 'Mage', 'Arcane', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 92, 22498, 'Phase 6', 'Mage', 'Arcane', 'Head', 'Both', 'Frostfire Circlet'), +(8, 0, 1, 0, 92, 23057, 'Phase 6', 'Mage', 'Arcane', 'Neck', 'Both', 'Gem of Trapped Innocents'), +(8, 0, 2, 0, 92, 22983, 'Phase 6', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Rime Covered Mantle'), +(8, 0, 4, 0, 92, 22496, 'Phase 6', 'Mage', 'Arcane', 'Chest', 'Both', 'Frostfire Robe'), +(8, 0, 5, 0, 92, 22730, 'Phase 6', 'Mage', 'Arcane', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(8, 0, 6, 0, 92, 23070, 'Phase 6', 'Mage', 'Arcane', 'Legs', 'Both', 'Leggings of Polarity'), +(8, 0, 7, 0, 92, 22500, 'Phase 6', 'Mage', 'Arcane', 'Feet', 'Both', 'Frostfire Sandals'), +(8, 0, 8, 0, 92, 23021, 'Phase 6', 'Mage', 'Arcane', 'Wrists', 'Both', 'The Soul Harvester''s Bindings'), +(8, 0, 9, 0, 92, 21585, 'Phase 6', 'Mage', 'Arcane', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(8, 0, 10, 0, 92, 23237, 'Phase 6', 'Mage', 'Arcane', 'Finger1', 'Both', 'Ring of the Eternal Flame'), +(8, 0, 11, 0, 92, 23062, 'Phase 6', 'Mage', 'Arcane', 'Finger2', 'Both', 'Frostfire Ring'), +(8, 0, 12, 0, 92, 19379, 'Phase 6', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 0, 13, 0, 92, 23046, 'Phase 6', 'Mage', 'Arcane', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(8, 0, 14, 0, 92, 23050, 'Phase 6', 'Mage', 'Arcane', 'Back', 'Both', 'Cloak of the Necropolis'), +(8, 0, 15, 0, 92, 22807, 'Phase 6', 'Mage', 'Arcane', 'MainHand', 'Both', 'Wraith Blade'), +(8, 0, 16, 0, 92, 23049, 'Phase 6', 'Mage', 'Arcane', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(8, 0, 17, 0, 92, 22821, 'Phase 6', 'Mage', 'Arcane', 'Ranged', 'Both', 'Doomfinger'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 120, 28278, 'Pre-Raid', 'Mage', 'Arcane', 'Head', 'Both', 'Incanter''s Cowl'), +(8, 0, 1, 0, 120, 28134, 'Pre-Raid', 'Mage', 'Arcane', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(8, 0, 2, 0, 120, 27796, 'Pre-Raid', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Mana-Etched Spaulders'), +(8, 0, 4, 0, 120, 21848, 'Pre-Raid', 'Mage', 'Arcane', 'Chest', 'Both', 'Spellfire Robe'), +(8, 0, 5, 0, 120, 21846, 'Pre-Raid', 'Mage', 'Arcane', 'Waist', 'Both', 'Spellfire Belt'), +(8, 0, 6, 0, 120, 30532, 'Pre-Raid', 'Mage', 'Arcane', 'Legs', 'Both', 'Kirin Tor Master''s Trousers'), +(8, 0, 7, 0, 120, 28406, 'Pre-Raid', 'Mage', 'Arcane', 'Feet', 'Both', 'Sigil-Laced Boots'), +(8, 0, 8, 0, 120, 29240, 'Pre-Raid', 'Mage', 'Arcane', 'Wrists', 'Both', 'Bands of Negation'), +(8, 0, 9, 0, 120, 21847, 'Pre-Raid', 'Mage', 'Arcane', 'Hands', 'Both', 'Spellfire Gloves'), +(8, 0, 10, 0, 120, 29172, 'Pre-Raid', 'Mage', 'Arcane', 'Finger1', 'Both', 'Ashyen''s Gift'), +(8, 0, 11, 0, 120, 28227, 'Pre-Raid', 'Mage', 'Arcane', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(8, 0, 12, 0, 120, 29370, 'Pre-Raid', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 0, 13, 0, 120, 27683, 'Pre-Raid', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(8, 0, 14, 0, 120, 27981, 'Pre-Raid', 'Mage', 'Arcane', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(8, 0, 15, 0, 120, 23554, 'Pre-Raid', 'Mage', 'Arcane', 'MainHand', 'Both', 'Eternium Runed Blade'), +(8, 0, 16, 0, 120, 29271, 'Pre-Raid', 'Mage', 'Arcane', 'OffHand', 'Both', 'Talisman of Kalecgos'), +(8, 0, 17, 0, 120, 29350, 'Pre-Raid', 'Mage', 'Arcane', 'Ranged', 'Both', 'The Black Stalk'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 125, 29076, 'Phase 1', 'Mage', 'Arcane', 'Head', 'Both', 'Collar of the Aldor'), +(8, 0, 1, 0, 125, 28762, 'Phase 1', 'Mage', 'Arcane', 'Neck', 'Both', 'Adornment of Stolen Souls'), +(8, 0, 2, 0, 125, 29079, 'Phase 1', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Pauldrons of the Aldor'), +(8, 0, 4, 0, 125, 21848, 'Phase 1', 'Mage', 'Arcane', 'Chest', 'Both', 'Spellfire Robe'), +(8, 0, 5, 0, 125, 21846, 'Phase 1', 'Mage', 'Arcane', 'Waist', 'Both', 'Spellfire Belt'), +(8, 0, 6, 0, 125, 29078, 'Phase 1', 'Mage', 'Arcane', 'Legs', 'Both', 'Legwraps of the Aldor'), +(8, 0, 7, 0, 125, 28517, 'Phase 1', 'Mage', 'Arcane', 'Feet', 'Both', 'Boots of Foretelling'), +(8, 0, 8, 0, 125, 28515, 'Phase 1', 'Mage', 'Arcane', 'Wrists', 'Both', 'Bands of Nefarious Deeds'), +(8, 0, 9, 0, 125, 21847, 'Phase 1', 'Mage', 'Arcane', 'Hands', 'Both', 'Spellfire Gloves'), +(8, 0, 10, 0, 125, 28793, 'Phase 1', 'Mage', 'Arcane', 'Finger1', 'Both', 'Band of Crimson Fury'), +(8, 0, 11, 0, 125, 29287, 'Phase 1', 'Mage', 'Arcane', 'Finger2', 'Both', 'Violet Signet of the Archmage'), +(8, 0, 12, 0, 125, 29370, 'Phase 1', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 0, 13, 0, 125, 28785, 'Phase 1', 'Mage', 'Arcane', 'Trinket2', 'Both', 'The Lightning Capacitor'), +(8, 0, 14, 0, 125, 28766, 'Phase 1', 'Mage', 'Arcane', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(8, 0, 15, 0, 125, 28770, 'Phase 1', 'Mage', 'Arcane', 'MainHand', 'Both', 'Nathrezim Mindblade'), +(8, 0, 16, 0, 125, 29271, 'Phase 1', 'Mage', 'Arcane', 'OffHand', 'Both', 'Talisman of Kalecgos'), +(8, 0, 17, 0, 125, 28783, 'Phase 1', 'Mage', 'Arcane', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 141, 30206, 'Phase 2', 'Mage', 'Arcane', 'Head', 'Both', 'Cowl of Tirisfal'), +(8, 0, 1, 0, 141, 30015, 'Phase 2', 'Mage', 'Arcane', 'Neck', 'Both', 'The Sun King''s Talisman'), +(8, 0, 2, 0, 141, 30210, 'Phase 2', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Mantle of Tirisfal'), +(8, 0, 4, 0, 141, 30196, 'Phase 2', 'Mage', 'Arcane', 'Chest', 'Both', 'Robes of Tirisfal'), +(8, 0, 5, 0, 141, 30038, 'Phase 2', 'Mage', 'Arcane', 'Waist', 'Both', 'Belt of Blasting'), +(8, 0, 6, 0, 141, 30207, 'Phase 2', 'Mage', 'Arcane', 'Legs', 'Both', 'Leggings of Tirisfal'), +(8, 0, 7, 0, 141, 30067, 'Phase 2', 'Mage', 'Arcane', 'Feet', 'Both', 'Velvet Boots of the Guardian'), +(8, 0, 8, 0, 141, 29918, 'Phase 2', 'Mage', 'Arcane', 'Wrists', 'Both', 'Mindstorm Wristbands'), +(8, 0, 9, 0, 141, 29987, 'Phase 2', 'Mage', 'Arcane', 'Hands', 'Both', 'Gauntlets of the Sun King'), +(8, 0, 10, 0, 141, 29302, 'Phase 2', 'Mage', 'Arcane', 'Finger1', 'Both', 'Band of Eternity'), +(8, 0, 11, 0, 141, 29287, 'Phase 2', 'Mage', 'Arcane', 'Finger2', 'Both', 'Violet Signet of the Archmage'), +(8, 0, 12, 0, 141, 29370, 'Phase 2', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 0, 13, 0, 141, 30720, 'Phase 2', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Serpent-Coil Braid'), +(8, 0, 14, 0, 141, 29992, 'Phase 2', 'Mage', 'Arcane', 'Back', 'Both', 'Royal Cloak of the Sunstriders'), +(8, 0, 15, 0, 141, 30095, 'Phase 2', 'Mage', 'Arcane', 'MainHand', 'Both', 'Fang of the Leviathan'), +(8, 0, 16, 0, 141, 30049, 'Phase 2', 'Mage', 'Arcane', 'OffHand', 'Both', 'Fathomstone'), +(8, 0, 17, 0, 141, 28783, 'Phase 2', 'Mage', 'Arcane', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 156, 30206, 'Phase 3', 'Mage', 'Arcane', 'Head', 'Both', 'Cowl of Tirisfal'), +(8, 0, 1, 0, 156, 30015, 'Phase 3', 'Mage', 'Arcane', 'Neck', 'Both', 'The Sun King''s Talisman'), +(8, 0, 2, 0, 156, 30210, 'Phase 3', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Mantle of Tirisfal'), +(8, 0, 4, 0, 156, 30196, 'Phase 3', 'Mage', 'Arcane', 'Chest', 'Both', 'Robes of Tirisfal'), +(8, 0, 5, 0, 156, 30888, 'Phase 3', 'Mage', 'Arcane', 'Waist', 'Both', 'Anetheron''s Noose'), +(8, 0, 6, 0, 156, 31058, 'Phase 3', 'Mage', 'Arcane', 'Legs', 'Both', 'Leggings of the Tempest'), +(8, 0, 7, 0, 156, 32239, 'Phase 3', 'Mage', 'Arcane', 'Feet', 'Both', 'Slippers of the Seacaller'), +(8, 0, 8, 0, 156, 30870, 'Phase 3', 'Mage', 'Arcane', 'Wrists', 'Both', 'Cuffs of Devastation'), +(8, 0, 9, 0, 156, 30205, 'Phase 3', 'Mage', 'Arcane', 'Hands', 'Both', 'Gloves of Tirisfal'), +(8, 0, 10, 0, 156, 29305, 'Phase 3', 'Mage', 'Arcane', 'Finger1', 'Both', 'Band of the Eternal Sage'), +(8, 0, 11, 0, 156, 32527, 'Phase 3', 'Mage', 'Arcane', 'Finger2', 'Both', 'Ring of Ancient Knowledge'), +(8, 0, 12, 0, 156, 32488, 'Phase 3', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Ashtongue Talisman of Insight'), +(8, 0, 13, 0, 156, 32483, 'Phase 3', 'Mage', 'Arcane', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(8, 0, 14, 0, 156, 32331, 'Phase 3', 'Mage', 'Arcane', 'Back', 'Both', 'Cloak of the Illidari Council'), +(8, 0, 15, 0, 156, 30910, 'Phase 3', 'Mage', 'Arcane', 'MainHand', 'Both', 'Tempest of Chaos'), +(8, 0, 16, 0, 156, 30872, 'Phase 3', 'Mage', 'Arcane', 'OffHand', 'Both', 'Chronicle of Dark Secrets'), +(8, 0, 17, 0, 156, 28783, 'Phase 3', 'Mage', 'Arcane', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 164, 30206, 'Phase 4', 'Mage', 'Arcane', 'Head', 'Both', 'Cowl of Tirisfal'), +(8, 0, 1, 0, 164, 30015, 'Phase 4', 'Mage', 'Arcane', 'Neck', 'Both', 'The Sun King''s Talisman'), +(8, 0, 2, 0, 164, 30210, 'Phase 4', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Mantle of Tirisfal'), +(8, 0, 4, 0, 164, 30196, 'Phase 4', 'Mage', 'Arcane', 'Chest', 'Both', 'Robes of Tirisfal'), +(8, 0, 5, 0, 164, 30888, 'Phase 4', 'Mage', 'Arcane', 'Waist', 'Both', 'Anetheron''s Noose'), +(8, 0, 6, 0, 164, 31058, 'Phase 4', 'Mage', 'Arcane', 'Legs', 'Both', 'Leggings of the Tempest'), +(8, 0, 7, 0, 164, 32239, 'Phase 4', 'Mage', 'Arcane', 'Feet', 'Both', 'Slippers of the Seacaller'), +(8, 0, 8, 0, 164, 30870, 'Phase 4', 'Mage', 'Arcane', 'Wrists', 'Both', 'Cuffs of Devastation'), +(8, 0, 9, 0, 164, 30205, 'Phase 4', 'Mage', 'Arcane', 'Hands', 'Both', 'Gloves of Tirisfal'), +(8, 0, 10, 0, 164, 29305, 'Phase 4', 'Mage', 'Arcane', 'Finger1', 'Both', 'Band of the Eternal Sage'), +(8, 0, 11, 0, 164, 32527, 'Phase 4', 'Mage', 'Arcane', 'Finger2', 'Both', 'Ring of Ancient Knowledge'), +(8, 0, 12, 0, 164, 32488, 'Phase 4', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Ashtongue Talisman of Insight'), +(8, 0, 13, 0, 164, 32483, 'Phase 4', 'Mage', 'Arcane', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(8, 0, 14, 0, 164, 32331, 'Phase 4', 'Mage', 'Arcane', 'Back', 'Both', 'Cloak of the Illidari Council'), +(8, 0, 15, 0, 164, 30910, 'Phase 4', 'Mage', 'Arcane', 'MainHand', 'Both', 'Tempest of Chaos'), +(8, 0, 16, 0, 164, 30872, 'Phase 4', 'Mage', 'Arcane', 'OffHand', 'Both', 'Chronicle of Dark Secrets'), +(8, 0, 17, 0, 164, 33192, 'Phase 4', 'Mage', 'Arcane', 'Ranged', 'Both', 'Carved Witch Doctor''s Stick'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 200, 37294, 'Pre-Raid', 'Mage', 'Arcane', 'Head', 'Both', 'Crown of Unbridled Magic'), +(8, 0, 1, 0, 200, 39472, 'Pre-Raid', 'Mage', 'Arcane', 'Neck', 'Both', 'Chain of Latent Energies'), +(8, 0, 2, 0, 200, 37673, 'Pre-Raid', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Dark Runic Mantle'), +(8, 0, 4, 0, 200, 39492, 'Pre-Raid', 'Mage', 'Arcane', 'Chest', 'Both', 'Heroes'' Frostfire Robe'), +(8, 0, 5, 0, 200, 40696, 'Pre-Raid', 'Mage', 'Arcane', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(8, 0, 6, 0, 200, 37854, 'Pre-Raid', 'Mage', 'Arcane', 'Legs', 'Both', 'Woven Bracae Leggings'), +(8, 0, 7, 0, 200, 44202, 'Pre-Raid', 'Mage', 'Arcane', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(8, 0, 8, 0, 200, 37361, 'Pre-Raid', 'Mage', 'Arcane', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(8, 0, 9, 0, 200, 39495, 'Pre-Raid', 'Mage', 'Arcane', 'Hands', 'Both', 'Heroes'' Frostfire Gloves'), +(8, 0, 10, 0, 200, 37694, 'Pre-Raid', 'Mage', 'Arcane', 'Finger1', 'Both', 'Band of Guile'), +(8, 0, 12, 0, 200, 37660, 'Pre-Raid', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Forge Ember'), +(8, 0, 14, 0, 200, 41610, 'Pre-Raid', 'Mage', 'Arcane', 'Back', 'Both', 'Deathchill Cloak'), +(8, 0, 15, 0, 200, 37360, 'Pre-Raid', 'Mage', 'Arcane', 'MainHand', 'Both', 'Staff of Draconic Combat'), +(8, 0, 17, 0, 200, 37177, 'Pre-Raid', 'Mage', 'Arcane', 'Ranged', 'Both', 'Wand of the San''layn'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 1, 0, 224, 44661, 'Phase 1', 'Mage', 'Arcane', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(8, 0, 2, 0, 224, 40419, 'Phase 1', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Valorous Frostfire Shoulderpads'), +(8, 0, 4, 0, 224, 44002, 'Phase 1', 'Mage', 'Arcane', 'Chest', 'Both', 'The Sanctum''s Flowing Vestments'), +(8, 0, 5, 0, 224, 40561, 'Phase 1', 'Mage', 'Arcane', 'Waist', 'Both', 'Leash of Heedless Magic'), +(8, 0, 6, 0, 224, 40417, 'Phase 1', 'Mage', 'Arcane', 'Legs', 'Both', 'Valorous Frostfire Leggings'), +(8, 0, 7, 0, 224, 40558, 'Phase 1', 'Mage', 'Arcane', 'Feet', 'Both', 'Arcanic Tramplers'), +(8, 0, 8, 0, 224, 44008, 'Phase 1', 'Mage', 'Arcane', 'Wrists', 'Both', 'Unsullied Cuffs'), +(8, 0, 9, 0, 224, 40415, 'Phase 1', 'Mage', 'Arcane', 'Hands', 'Both', 'Valorous Frostfire Gloves'), +(8, 0, 10, 0, 224, 40719, 'Phase 1', 'Mage', 'Arcane', 'Finger1', 'Both', 'Band of Channeled Magic'), +(8, 0, 12, 0, 224, 40255, 'Phase 1', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Dying Curse'), +(8, 0, 14, 0, 224, 44005, 'Phase 1', 'Mage', 'Arcane', 'Back', 'Both', 'Pennant Cloak'), +(8, 0, 15, 0, 224, 40396, 'Phase 1', 'Mage', 'Arcane', 'MainHand', 'Both', 'The Turning Tide'), +(8, 0, 17, 0, 224, 39426, 'Phase 1', 'Mage', 'Arcane', 'Ranged', 'Both', 'Wand of the Archlich'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 245, 46129, 'Phase 2', 'Mage', 'Arcane', 'Head', 'Both', 'Conqueror''s Kirin Tor Hood'), +(8, 0, 1, 0, 245, 45243, 'Phase 2', 'Mage', 'Arcane', 'Neck', 'Both', 'Sapphire Amulet of Renewal'), +(8, 0, 2, 0, 245, 46134, 'Phase 2', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Conqueror''s Kirin Tor Shoulderpads'), +(8, 0, 4, 0, 245, 46130, 'Phase 2', 'Mage', 'Arcane', 'Chest', 'Both', 'Conqueror''s Kirin Tor Tunic'), +(8, 0, 5, 0, 245, 45557, 'Phase 2', 'Mage', 'Arcane', 'Waist', 'Both', 'Sash of Ancient Power'), +(8, 0, 6, 0, 245, 46133, 'Phase 2', 'Mage', 'Arcane', 'Legs', 'Both', 'Conqueror''s Kirin Tor Leggings'), +(8, 0, 7, 0, 245, 45135, 'Phase 2', 'Mage', 'Arcane', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(8, 0, 8, 0, 245, 45446, 'Phase 2', 'Mage', 'Arcane', 'Wrists', 'Both', 'Grasps of Reason'), +(8, 0, 9, 0, 245, 45665, 'Phase 2', 'Mage', 'Arcane', 'Hands', 'Both', 'Pharos Gloves'), +(8, 0, 10, 0, 245, 46046, 'Phase 2', 'Mage', 'Arcane', 'Finger1', 'Both', 'Nebula Band'), +(8, 0, 12, 0, 245, 45518, 'Phase 2', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Flare of the Heavens'), +(8, 0, 14, 0, 245, 45618, 'Phase 2', 'Mage', 'Arcane', 'Back', 'Both', 'Sunglimmer Cloak'), +(8, 0, 15, 0, 245, 45457, 'Phase 2', 'Mage', 'Arcane', 'MainHand', 'Both', 'Staff of Endless Winter'), +(8, 0, 17, 0, 245, 45511, 'Phase 2', 'Mage', 'Arcane', 'Ranged', 'Both', 'Scepter of Lost Souls'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 258, 47761, 'Phase 3', 'Mage', 'Arcane', 'Head', 'Both', 'Hood of Triumph'), +(8, 0, 2, 0, 258, 47758, 'Phase 3', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Shoulderpads of Triumph'), +(8, 0, 4, 1, 258, 46993, 'Phase 3', 'Mage', 'Arcane', 'Chest', 'Alliance', 'Flowing Vestments of Ascent'), +(8, 0, 4, 2, 258, 47425, 'Phase 3', 'Mage', 'Arcane', 'Chest', 'Horde', 'Flowing Robes of Ascent'), +(8, 0, 5, 1, 258, 47419, 'Phase 3', 'Mage', 'Arcane', 'Waist', 'Alliance', 'Belt of the Tenebrous Mist'), +(8, 0, 5, 2, 258, 46973, 'Phase 3', 'Mage', 'Arcane', 'Waist', 'Horde', 'Cord of the Tenebrous Mist'), +(8, 0, 6, 0, 258, 47760, 'Phase 3', 'Mage', 'Arcane', 'Legs', 'Both', 'Leggings of Triumph'), +(8, 0, 7, 1, 258, 47097, 'Phase 3', 'Mage', 'Arcane', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(8, 0, 7, 2, 258, 47454, 'Phase 3', 'Mage', 'Arcane', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(8, 0, 8, 1, 258, 47208, 'Phase 3', 'Mage', 'Arcane', 'Wrists', 'Alliance', 'Armbands of the Ashen Saint'), +(8, 0, 8, 2, 258, 47485, 'Phase 3', 'Mage', 'Arcane', 'Wrists', 'Horde', 'Bindings of the Ashen Saint'), +(8, 0, 9, 0, 258, 47762, 'Phase 3', 'Mage', 'Arcane', 'Hands', 'Both', 'Gauntlets of Triumph'), +(8, 0, 10, 1, 258, 47489, 'Phase 3', 'Mage', 'Arcane', 'Finger1', 'Alliance', 'Lurid Manifestation'), +(8, 0, 10, 2, 258, 47237, 'Phase 3', 'Mage', 'Arcane', 'Finger1', 'Horde', 'Band of Deplorable Violence'), +(8, 0, 12, 1, 258, 47188, 'Phase 3', 'Mage', 'Arcane', 'Trinket1', 'Alliance', 'Reign of the Unliving'), +(8, 0, 12, 2, 258, 47477, 'Phase 3', 'Mage', 'Arcane', 'Trinket1', 'Horde', 'Reign of the Dead'), +(8, 0, 14, 1, 258, 47552, 'Phase 3', 'Mage', 'Arcane', 'Back', 'Alliance', 'Jaina''s Radiance'), +(8, 0, 14, 2, 258, 47551, 'Phase 3', 'Mage', 'Arcane', 'Back', 'Horde', 'Aethas'' Intensity'), +(8, 0, 17, 0, 258, 45294, 'Phase 3', 'Mage', 'Arcane', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 264, 51281, 'Phase 4', 'Mage', 'Arcane', 'Head', 'Both', 'Sanctified Bloodmage Hood'), +(8, 0, 1, 0, 264, 50724, 'Phase 4', 'Mage', 'Arcane', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(8, 0, 2, 0, 264, 51284, 'Phase 4', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Sanctified Bloodmage Shoulderpads'), +(8, 0, 4, 0, 264, 51283, 'Phase 4', 'Mage', 'Arcane', 'Chest', 'Both', 'Sanctified Bloodmage Robe'), +(8, 0, 5, 0, 264, 50613, 'Phase 4', 'Mage', 'Arcane', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(8, 0, 6, 0, 264, 51282, 'Phase 4', 'Mage', 'Arcane', 'Legs', 'Both', 'Sanctified Bloodmage Leggings'), +(8, 0, 7, 0, 264, 50699, 'Phase 4', 'Mage', 'Arcane', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(8, 0, 8, 0, 264, 50651, 'Phase 4', 'Mage', 'Arcane', 'Wrists', 'Both', 'The Lady''s Brittle Bracers'), +(8, 0, 9, 0, 264, 50722, 'Phase 4', 'Mage', 'Arcane', 'Hands', 'Both', 'San''layn Ritualist Gloves'), +(8, 0, 10, 0, 264, 50398, 'Phase 4', 'Mage', 'Arcane', 'Finger1', 'Both', 'Ashen Band of Endless Destruction'), +(8, 0, 12, 1, 264, 47188, 'Phase 4', 'Mage', 'Arcane', 'Trinket1', 'Alliance', 'Reign of the Unliving'), +(8, 0, 12, 2, 264, 47477, 'Phase 4', 'Mage', 'Arcane', 'Trinket1', 'Horde', 'Reign of the Dead'), +(8, 0, 14, 0, 264, 50628, 'Phase 4', 'Mage', 'Arcane', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(8, 0, 15, 0, 264, 50732, 'Phase 4', 'Mage', 'Arcane', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(8, 0, 17, 0, 264, 50684, 'Phase 4', 'Mage', 'Arcane', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 0, 0, 0, 290, 51281, 'Phase 5', 'Mage', 'Arcane', 'Head', 'Both', 'Sanctified Bloodmage Hood'), +(8, 0, 1, 0, 290, 50182, 'Phase 5', 'Mage', 'Arcane', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(8, 0, 2, 0, 290, 51284, 'Phase 5', 'Mage', 'Arcane', 'Shoulders', 'Both', 'Sanctified Bloodmage Shoulderpads'), +(8, 0, 4, 0, 290, 51283, 'Phase 5', 'Mage', 'Arcane', 'Chest', 'Both', 'Sanctified Bloodmage Robe'), +(8, 0, 5, 0, 290, 50613, 'Phase 5', 'Mage', 'Arcane', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(8, 0, 6, 0, 290, 50694, 'Phase 5', 'Mage', 'Arcane', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(8, 0, 7, 0, 290, 50699, 'Phase 5', 'Mage', 'Arcane', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(8, 0, 8, 0, 290, 54582, 'Phase 5', 'Mage', 'Arcane', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(8, 0, 9, 0, 290, 51280, 'Phase 5', 'Mage', 'Arcane', 'Hands', 'Both', 'Sanctified Bloodmage Gloves'), +(8, 0, 10, 0, 290, 50614, 'Phase 5', 'Mage', 'Arcane', 'Finger1', 'Both', 'Loop of the Endless Labyrinth'), +(8, 0, 11, 0, 290, 50398, 'Phase 5', 'Mage', 'Arcane', 'Finger2', 'Both', 'Ashen Band of Endless Destruction'), +(8, 0, 12, 0, 290, 54588, 'Phase 5', 'Mage', 'Arcane', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(8, 0, 13, 0, 290, 50348, 'Phase 5', 'Mage', 'Arcane', 'Trinket2', 'Both', 'Dislodged Foreign Object'), +(8, 0, 14, 0, 290, 54583, 'Phase 5', 'Mage', 'Arcane', 'Back', 'Both', 'Cloak of Burning Dusk'), +(8, 0, 15, 0, 290, 50732, 'Phase 5', 'Mage', 'Arcane', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(8, 0, 16, 0, 290, 50719, 'Phase 5', 'Mage', 'Arcane', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(8, 0, 17, 0, 290, 50684, 'Phase 5', 'Mage', 'Arcane', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- Fire (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 66, 10504, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Head', 'Both', 'Green Lens'), +(8, 1, 1, 0, 66, 12103, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Neck', 'Both', 'Star of Mystaria'), +(8, 1, 2, 0, 66, 11782, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Shoulders', 'Both', 'Boreal Mantle'), +(8, 1, 4, 0, 66, 14152, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 1, 5, 0, 66, 11662, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Waist', 'Both', 'Ban''thok Sash'), +(8, 1, 6, 0, 66, 13170, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Legs', 'Both', 'Skyshroud Leggings'), +(8, 1, 7, 0, 66, 11822, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Feet', 'Both', 'Omnicast Boots'), +(8, 1, 8, 0, 66, 11766, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 1, 9, 0, 66, 13253, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Hands', 'Both', 'Hands of Power'), +(8, 1, 10, 0, 66, 942, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Finger1', 'Both', 'Freezing Band'), +(8, 1, 11, 0, 66, 942, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Finger2', 'Both', 'Freezing Band'), +(8, 1, 12, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 1, 13, 0, 66, 13968, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Trinket2', 'Both', 'Eye of the Beast'), +(8, 1, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Back', 'Both', 'Archivist Cape'), +(8, 1, 15, 0, 66, 13964, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'MainHand', 'Both', 'Witchblade'), +(8, 1, 16, 0, 66, 10796, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'OffHand', 'Both', 'Drakestone'), +(8, 1, 17, 0, 66, 13938, 'Phase 1 (Pre-Raid)', 'Mage', 'Fire', 'Ranged', 'Both', 'Bonecreeper Stylus'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 76, 23318, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Head', 'Both', 'Lieutenant Commander''s Silk Cowl'), +(8, 1, 1, 0, 76, 12103, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Neck', 'Both', 'Star of Mystaria'), +(8, 1, 2, 0, 76, 23319, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Shoulders', 'Both', 'Lieutenant Commander''s Silk Mantle'), +(8, 1, 4, 0, 76, 14152, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 1, 5, 0, 76, 11662, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Waist', 'Both', 'Ban''thok Sash'), +(8, 1, 6, 0, 76, 23304, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Legs', 'Both', 'Knight-Captain''s Silk Legguards'), +(8, 1, 7, 0, 76, 23291, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Feet', 'Both', 'Knight-Lieutenant''s Silk Walkers'), +(8, 1, 8, 0, 76, 11766, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 1, 9, 0, 76, 13253, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Hands', 'Both', 'Hands of Power'), +(8, 1, 10, 0, 76, 942, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Finger1', 'Both', 'Freezing Band'), +(8, 1, 11, 0, 76, 942, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Finger2', 'Both', 'Freezing Band'), +(8, 1, 12, 0, 76, 12930, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 1, 13, 0, 76, 13968, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Trinket2', 'Both', 'Eye of the Beast'), +(8, 1, 14, 0, 76, 13386, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Back', 'Both', 'Archivist Cape'), +(8, 1, 15, 0, 76, 13964, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'MainHand', 'Both', 'Witchblade'), +(8, 1, 16, 0, 76, 10796, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'OffHand', 'Both', 'Drakestone'), +(8, 1, 17, 0, 76, 13938, 'Phase 2 (Pre-Raid)', 'Mage', 'Fire', 'Ranged', 'Both', 'Bonecreeper Stylus'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 78, 16914, 'Phase 2', 'Mage', 'Fire', 'Head', 'Both', 'Netherwind Crown'), +(8, 1, 1, 0, 78, 18814, 'Phase 2', 'Mage', 'Fire', 'Neck', 'Both', 'Choker of the Fire Lord'), +(8, 1, 2, 0, 78, 23319, 'Phase 2', 'Mage', 'Fire', 'Shoulders', 'Both', 'Lieutenant Commander''s Silk Mantle'), +(8, 1, 4, 0, 78, 14152, 'Phase 2', 'Mage', 'Fire', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 1, 5, 0, 78, 19136, 'Phase 2', 'Mage', 'Fire', 'Waist', 'Both', 'Mana Igniting Cord'), +(8, 1, 6, 0, 78, 16915, 'Phase 2', 'Mage', 'Fire', 'Legs', 'Both', 'Netherwind Pants'), +(8, 1, 7, 0, 78, 23291, 'Phase 2', 'Mage', 'Fire', 'Feet', 'Both', 'Knight-Lieutenant''s Silk Walkers'), +(8, 1, 8, 0, 78, 11766, 'Phase 2', 'Mage', 'Fire', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 1, 9, 0, 78, 13253, 'Phase 2', 'Mage', 'Fire', 'Hands', 'Both', 'Hands of Power'), +(8, 1, 10, 0, 78, 19147, 'Phase 2', 'Mage', 'Fire', 'Finger1', 'Both', 'Ring of Spell Power'), +(8, 1, 11, 0, 78, 19147, 'Phase 2', 'Mage', 'Fire', 'Finger2', 'Both', 'Ring of Spell Power'), +(8, 1, 12, 0, 78, 12930, 'Phase 2', 'Mage', 'Fire', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 1, 13, 0, 78, 18820, 'Phase 2', 'Mage', 'Fire', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(8, 1, 14, 0, 78, 13386, 'Phase 2', 'Mage', 'Fire', 'Back', 'Both', 'Archivist Cape'), +(8, 1, 15, 0, 78, 17103, 'Phase 2', 'Mage', 'Fire', 'MainHand', 'Both', 'Azuresong Mageblade'), +(8, 1, 16, 0, 78, 10796, 'Phase 2', 'Mage', 'Fire', 'OffHand', 'Both', 'Drakestone'), +(8, 1, 17, 0, 78, 19130, 'Phase 2', 'Mage', 'Fire', 'Ranged', 'Both', 'Cold Snap'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 83, 19375, 'Phase 3', 'Mage', 'Fire', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(8, 1, 1, 0, 83, 18814, 'Phase 3', 'Mage', 'Fire', 'Neck', 'Both', 'Choker of the Fire Lord'), +(8, 1, 2, 0, 83, 19370, 'Phase 3', 'Mage', 'Fire', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(8, 1, 4, 0, 83, 14152, 'Phase 3', 'Mage', 'Fire', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 1, 5, 0, 83, 19136, 'Phase 3', 'Mage', 'Fire', 'Waist', 'Both', 'Mana Igniting Cord'), +(8, 1, 6, 0, 83, 16915, 'Phase 3', 'Mage', 'Fire', 'Legs', 'Both', 'Netherwind Pants'), +(8, 1, 7, 0, 83, 19438, 'Phase 3', 'Mage', 'Fire', 'Feet', 'Both', 'Ringo''s Blizzard Boots'), +(8, 1, 8, 0, 83, 19374, 'Phase 3', 'Mage', 'Fire', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(8, 1, 9, 0, 83, 16913, 'Phase 3', 'Mage', 'Fire', 'Hands', 'Both', 'Netherwind Gloves'), +(8, 1, 11, 0, 83, 19147, 'Phase 3', 'Mage', 'Fire', 'Finger2', 'Both', 'Ring of Spell Power'), +(8, 1, 12, 0, 83, 19379, 'Phase 3', 'Mage', 'Fire', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 1, 13, 0, 83, 19339, 'Phase 3', 'Mage', 'Fire', 'Trinket2', 'Both', 'Mind Quickening Gem'), +(8, 1, 14, 0, 83, 19378, 'Phase 3', 'Mage', 'Fire', 'Back', 'Both', 'Cloak of the Brood Lord'), +(8, 1, 15, 0, 83, 19356, 'Phase 3', 'Mage', 'Fire', 'MainHand', 'Both', 'Staff of the Shadow Flame'), +(8, 1, 17, 0, 83, 19130, 'Phase 3', 'Mage', 'Fire', 'Ranged', 'Both', 'Cold Snap'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 88, 19375, 'Phase 5', 'Mage', 'Fire', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(8, 1, 1, 0, 88, 21608, 'Phase 5', 'Mage', 'Fire', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(8, 1, 2, 0, 88, 19370, 'Phase 5', 'Mage', 'Fire', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(8, 1, 4, 0, 88, 19145, 'Phase 5', 'Mage', 'Fire', 'Chest', 'Both', 'Robe of Volatile Power'), +(8, 1, 5, 0, 88, 22730, 'Phase 5', 'Mage', 'Fire', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(8, 1, 6, 0, 88, 21461, 'Phase 5', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of the Black Blizzard'), +(8, 1, 7, 0, 88, 21344, 'Phase 5', 'Mage', 'Fire', 'Feet', 'Both', 'Enigma Boots'), +(8, 1, 8, 0, 88, 21186, 'Phase 5', 'Mage', 'Fire', 'Wrists', 'Both', 'Rockfury Bracers'), +(8, 1, 9, 0, 88, 21585, 'Phase 5', 'Mage', 'Fire', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(8, 1, 10, 0, 88, 21836, 'Phase 5', 'Mage', 'Fire', 'Finger1', 'Both', 'Ritssyn''s Ring of Chaos'), +(8, 1, 11, 0, 88, 21709, 'Phase 5', 'Mage', 'Fire', 'Finger2', 'Both', 'Ring of the Fallen God'), +(8, 1, 12, 0, 88, 19379, 'Phase 5', 'Mage', 'Fire', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 1, 13, 0, 88, 19339, 'Phase 5', 'Mage', 'Fire', 'Trinket2', 'Both', 'Mind Quickening Gem'), +(8, 1, 14, 0, 88, 22731, 'Phase 5', 'Mage', 'Fire', 'Back', 'Both', 'Cloak of the Devoured'), +(8, 1, 15, 0, 88, 21622, 'Phase 5', 'Mage', 'Fire', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(8, 1, 16, 0, 88, 21597, 'Phase 5', 'Mage', 'Fire', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(8, 1, 17, 0, 88, 21603, 'Phase 5', 'Mage', 'Fire', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 92, 22498, 'Phase 6', 'Mage', 'Fire', 'Head', 'Both', 'Frostfire Circlet'), +(8, 1, 1, 0, 92, 23057, 'Phase 6', 'Mage', 'Fire', 'Neck', 'Both', 'Gem of Trapped Innocents'), +(8, 1, 2, 0, 92, 22983, 'Phase 6', 'Mage', 'Fire', 'Shoulders', 'Both', 'Rime Covered Mantle'), +(8, 1, 4, 0, 92, 22496, 'Phase 6', 'Mage', 'Fire', 'Chest', 'Both', 'Frostfire Robe'), +(8, 1, 5, 0, 92, 22730, 'Phase 6', 'Mage', 'Fire', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(8, 1, 6, 0, 92, 23070, 'Phase 6', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of Polarity'), +(8, 1, 7, 0, 92, 22500, 'Phase 6', 'Mage', 'Fire', 'Feet', 'Both', 'Frostfire Sandals'), +(8, 1, 8, 0, 92, 23021, 'Phase 6', 'Mage', 'Fire', 'Wrists', 'Both', 'The Soul Harvester''s Bindings'), +(8, 1, 9, 0, 92, 21585, 'Phase 6', 'Mage', 'Fire', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(8, 1, 10, 0, 92, 23237, 'Phase 6', 'Mage', 'Fire', 'Finger1', 'Both', 'Ring of the Eternal Flame'), +(8, 1, 11, 0, 92, 23062, 'Phase 6', 'Mage', 'Fire', 'Finger2', 'Both', 'Frostfire Ring'), +(8, 1, 12, 0, 92, 19379, 'Phase 6', 'Mage', 'Fire', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 1, 13, 0, 92, 23046, 'Phase 6', 'Mage', 'Fire', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(8, 1, 14, 0, 92, 23050, 'Phase 6', 'Mage', 'Fire', 'Back', 'Both', 'Cloak of the Necropolis'), +(8, 1, 15, 0, 92, 22807, 'Phase 6', 'Mage', 'Fire', 'MainHand', 'Both', 'Wraith Blade'), +(8, 1, 16, 0, 92, 23049, 'Phase 6', 'Mage', 'Fire', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(8, 1, 17, 0, 92, 22821, 'Phase 6', 'Mage', 'Fire', 'Ranged', 'Both', 'Doomfinger'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 120, 24266, 'Pre-Raid', 'Mage', 'Fire', 'Head', 'Both', 'Spellstrike Hood'), +(8, 1, 1, 0, 120, 28134, 'Pre-Raid', 'Mage', 'Fire', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(8, 1, 2, 0, 120, 27994, 'Pre-Raid', 'Mage', 'Fire', 'Shoulders', 'Both', 'Mantle of Three Terrors'), +(8, 1, 4, 0, 120, 21848, 'Pre-Raid', 'Mage', 'Fire', 'Chest', 'Both', 'Spellfire Robe'), +(8, 1, 5, 0, 120, 21846, 'Pre-Raid', 'Mage', 'Fire', 'Waist', 'Both', 'Spellfire Belt'), +(8, 1, 6, 0, 120, 24262, 'Pre-Raid', 'Mage', 'Fire', 'Legs', 'Both', 'Spellstrike Pants'), +(8, 1, 7, 0, 120, 28406, 'Pre-Raid', 'Mage', 'Fire', 'Feet', 'Both', 'Sigil-Laced Boots'), +(8, 1, 8, 0, 120, 29240, 'Pre-Raid', 'Mage', 'Fire', 'Wrists', 'Both', 'Bands of Negation'), +(8, 1, 9, 0, 120, 21847, 'Pre-Raid', 'Mage', 'Fire', 'Hands', 'Both', 'Spellfire Gloves'), +(8, 1, 10, 0, 120, 29172, 'Pre-Raid', 'Mage', 'Fire', 'Finger1', 'Both', 'Ashyen''s Gift'), +(8, 1, 11, 0, 120, 28227, 'Pre-Raid', 'Mage', 'Fire', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(8, 1, 12, 0, 120, 29370, 'Pre-Raid', 'Mage', 'Fire', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 1, 13, 0, 120, 27683, 'Pre-Raid', 'Mage', 'Fire', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(8, 1, 14, 0, 120, 27981, 'Pre-Raid', 'Mage', 'Fire', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(8, 1, 15, 0, 120, 23554, 'Pre-Raid', 'Mage', 'Fire', 'MainHand', 'Both', 'Eternium Runed Blade'), +(8, 1, 16, 0, 120, 29270, 'Pre-Raid', 'Mage', 'Fire', 'OffHand', 'Both', 'Flametongue Seal'), +(8, 1, 17, 0, 120, 28386, 'Pre-Raid', 'Mage', 'Fire', 'Ranged', 'Both', 'Nether Core''s Control Rod'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 125, 29076, 'Phase 1', 'Mage', 'Fire', 'Head', 'Both', 'Collar of the Aldor'), +(8, 1, 1, 0, 125, 28530, 'Phase 1', 'Mage', 'Fire', 'Neck', 'Both', 'Brooch of Unquenchable Fury'), +(8, 1, 2, 0, 125, 29079, 'Phase 1', 'Mage', 'Fire', 'Shoulders', 'Both', 'Pauldrons of the Aldor'), +(8, 1, 4, 0, 125, 21848, 'Phase 1', 'Mage', 'Fire', 'Chest', 'Both', 'Spellfire Robe'), +(8, 1, 5, 0, 125, 21846, 'Phase 1', 'Mage', 'Fire', 'Waist', 'Both', 'Spellfire Belt'), +(8, 1, 6, 0, 125, 29078, 'Phase 1', 'Mage', 'Fire', 'Legs', 'Both', 'Legwraps of the Aldor'), +(8, 1, 7, 0, 125, 28517, 'Phase 1', 'Mage', 'Fire', 'Feet', 'Both', 'Boots of Foretelling'), +(8, 1, 8, 0, 125, 28515, 'Phase 1', 'Mage', 'Fire', 'Wrists', 'Both', 'Bands of Nefarious Deeds'), +(8, 1, 9, 0, 125, 21847, 'Phase 1', 'Mage', 'Fire', 'Hands', 'Both', 'Spellfire Gloves'), +(8, 1, 10, 0, 125, 28793, 'Phase 1', 'Mage', 'Fire', 'Finger1', 'Both', 'Band of Crimson Fury'), +(8, 1, 11, 0, 125, 29287, 'Phase 1', 'Mage', 'Fire', 'Finger2', 'Both', 'Violet Signet of the Archmage'), +(8, 1, 12, 0, 125, 29370, 'Phase 1', 'Mage', 'Fire', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 1, 13, 0, 125, 27683, 'Phase 1', 'Mage', 'Fire', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(8, 1, 14, 0, 125, 28766, 'Phase 1', 'Mage', 'Fire', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(8, 1, 15, 0, 125, 28770, 'Phase 1', 'Mage', 'Fire', 'MainHand', 'Both', 'Nathrezim Mindblade'), +(8, 1, 16, 0, 125, 28734, 'Phase 1', 'Mage', 'Fire', 'OffHand', 'Both', 'Jewel of Infinite Possibilities'), +(8, 1, 17, 0, 125, 28673, 'Phase 1', 'Mage', 'Fire', 'Ranged', 'Both', 'Tirisfal Wand of Ascendancy'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 141, 32494, 'Phase 2', 'Mage', 'Fire', 'Head', 'Both', 'Destruction Holo-gogs'), +(8, 1, 1, 0, 141, 30015, 'Phase 2', 'Mage', 'Fire', 'Neck', 'Both', 'The Sun King''s Talisman'), +(8, 1, 2, 0, 141, 30024, 'Phase 2', 'Mage', 'Fire', 'Shoulders', 'Both', 'Mantle of the Elven Kings'), +(8, 1, 4, 0, 141, 30107, 'Phase 2', 'Mage', 'Fire', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(8, 1, 5, 0, 141, 30038, 'Phase 2', 'Mage', 'Fire', 'Waist', 'Both', 'Belt of Blasting'), +(8, 1, 6, 0, 141, 30207, 'Phase 2', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of Tirisfal'), +(8, 1, 7, 0, 141, 30067, 'Phase 2', 'Mage', 'Fire', 'Feet', 'Both', 'Velvet Boots of the Guardian'), +(8, 1, 8, 0, 141, 29918, 'Phase 2', 'Mage', 'Fire', 'Wrists', 'Both', 'Mindstorm Wristbands'), +(8, 1, 9, 0, 141, 29987, 'Phase 2', 'Mage', 'Fire', 'Hands', 'Both', 'Gauntlets of the Sun King'), +(8, 1, 10, 0, 141, 29302, 'Phase 2', 'Mage', 'Fire', 'Finger1', 'Both', 'Band of Eternity'), +(8, 1, 11, 0, 141, 29287, 'Phase 2', 'Mage', 'Fire', 'Finger2', 'Both', 'Violet Signet of the Archmage'), +(8, 1, 12, 0, 141, 27683, 'Phase 2', 'Mage', 'Fire', 'Trinket1', 'Both', 'Quagmirran''s Eye'), +(8, 1, 13, 0, 141, 30626, 'Phase 2', 'Mage', 'Fire', 'Trinket2', 'Both', 'Sextant of Unstable Currents'), +(8, 1, 14, 0, 141, 28766, 'Phase 2', 'Mage', 'Fire', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(8, 1, 15, 0, 141, 30095, 'Phase 2', 'Mage', 'Fire', 'MainHand', 'Both', 'Fang of the Leviathan'), +(8, 1, 16, 0, 141, 29270, 'Phase 2', 'Mage', 'Fire', 'OffHand', 'Both', 'Flametongue Seal'), +(8, 1, 17, 0, 141, 29982, 'Phase 2', 'Mage', 'Fire', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 156, 32525, 'Phase 3', 'Mage', 'Fire', 'Head', 'Both', 'Cowl of the Illidari High Lord'), +(8, 1, 1, 0, 156, 32589, 'Phase 3', 'Mage', 'Fire', 'Neck', 'Both', 'Hellfire-Encased Pendant'), +(8, 1, 2, 0, 156, 31059, 'Phase 3', 'Mage', 'Fire', 'Shoulders', 'Both', 'Mantle of the Tempest'), +(8, 1, 4, 0, 156, 31057, 'Phase 3', 'Mage', 'Fire', 'Chest', 'Both', 'Robes of the Tempest'), +(8, 1, 5, 0, 156, 30038, 'Phase 3', 'Mage', 'Fire', 'Waist', 'Both', 'Belt of Blasting'), +(8, 1, 6, 0, 156, 31058, 'Phase 3', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of the Tempest'), +(8, 1, 7, 0, 156, 32239, 'Phase 3', 'Mage', 'Fire', 'Feet', 'Both', 'Slippers of the Seacaller'), +(8, 1, 8, 0, 156, 32586, 'Phase 3', 'Mage', 'Fire', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(8, 1, 9, 0, 156, 31055, 'Phase 3', 'Mage', 'Fire', 'Hands', 'Both', 'Gloves of the Tempest'), +(8, 1, 10, 0, 156, 32527, 'Phase 3', 'Mage', 'Fire', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(8, 1, 11, 0, 156, 32527, 'Phase 3', 'Mage', 'Fire', 'Finger2', 'Both', 'Ring of Ancient Knowledge'), +(8, 1, 12, 0, 156, 32488, 'Phase 3', 'Mage', 'Fire', 'Trinket1', 'Both', 'Ashtongue Talisman of Insight'), +(8, 1, 13, 0, 156, 32483, 'Phase 3', 'Mage', 'Fire', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(8, 1, 14, 0, 156, 32331, 'Phase 3', 'Mage', 'Fire', 'Back', 'Both', 'Cloak of the Illidari Council'), +(8, 1, 15, 0, 156, 30910, 'Phase 3', 'Mage', 'Fire', 'MainHand', 'Both', 'Tempest of Chaos'), +(8, 1, 16, 0, 156, 30872, 'Phase 3', 'Mage', 'Fire', 'OffHand', 'Both', 'Chronicle of Dark Secrets'), +(8, 1, 17, 0, 156, 29982, 'Phase 3', 'Mage', 'Fire', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 164, 32525, 'Phase 4', 'Mage', 'Fire', 'Head', 'Both', 'Cowl of the Illidari High Lord'), +(8, 1, 1, 0, 164, 32589, 'Phase 4', 'Mage', 'Fire', 'Neck', 'Both', 'Hellfire-Encased Pendant'), +(8, 1, 2, 0, 164, 31059, 'Phase 4', 'Mage', 'Fire', 'Shoulders', 'Both', 'Mantle of the Tempest'), +(8, 1, 4, 0, 164, 31057, 'Phase 4', 'Mage', 'Fire', 'Chest', 'Both', 'Robes of the Tempest'), +(8, 1, 5, 0, 164, 30038, 'Phase 4', 'Mage', 'Fire', 'Waist', 'Both', 'Belt of Blasting'), +(8, 1, 6, 0, 164, 31058, 'Phase 4', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of the Tempest'), +(8, 1, 7, 0, 164, 32239, 'Phase 4', 'Mage', 'Fire', 'Feet', 'Both', 'Slippers of the Seacaller'), +(8, 1, 8, 0, 164, 32586, 'Phase 4', 'Mage', 'Fire', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(8, 1, 9, 0, 164, 31055, 'Phase 4', 'Mage', 'Fire', 'Hands', 'Both', 'Gloves of the Tempest'), +(8, 1, 10, 0, 164, 32527, 'Phase 4', 'Mage', 'Fire', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(8, 1, 11, 0, 164, 32527, 'Phase 4', 'Mage', 'Fire', 'Finger2', 'Both', 'Ring of Ancient Knowledge'), +(8, 1, 12, 0, 164, 33829, 'Phase 4', 'Mage', 'Fire', 'Trinket1', 'Both', 'Hex Shrunken Head'), +(8, 1, 13, 0, 164, 32483, 'Phase 4', 'Mage', 'Fire', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(8, 1, 14, 0, 164, 32331, 'Phase 4', 'Mage', 'Fire', 'Back', 'Both', 'Cloak of the Illidari Council'), +(8, 1, 15, 0, 164, 30910, 'Phase 4', 'Mage', 'Fire', 'MainHand', 'Both', 'Tempest of Chaos'), +(8, 1, 16, 0, 164, 30872, 'Phase 4', 'Mage', 'Fire', 'OffHand', 'Both', 'Chronicle of Dark Secrets'), +(8, 1, 17, 0, 164, 29982, 'Phase 4', 'Mage', 'Fire', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 200, 42553, 'Pre-Raid', 'Mage', 'Fire', 'Head', 'Both', 'Visage Liquification Goggles'), +(8, 1, 1, 0, 200, 39472, 'Pre-Raid', 'Mage', 'Fire', 'Neck', 'Both', 'Chain of Latent Energies'), +(8, 1, 2, 0, 200, 37673, 'Pre-Raid', 'Mage', 'Fire', 'Shoulders', 'Both', 'Dark Runic Mantle'), +(8, 1, 4, 0, 200, 39492, 'Pre-Raid', 'Mage', 'Fire', 'Chest', 'Both', 'Heroes'' Frostfire Robe'), +(8, 1, 5, 0, 200, 40696, 'Pre-Raid', 'Mage', 'Fire', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(8, 1, 6, 0, 200, 37854, 'Pre-Raid', 'Mage', 'Fire', 'Legs', 'Both', 'Woven Bracae Leggings'), +(8, 1, 7, 0, 200, 44202, 'Pre-Raid', 'Mage', 'Fire', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(8, 1, 8, 0, 200, 37884, 'Pre-Raid', 'Mage', 'Fire', 'Wrists', 'Both', 'Azure Cloth Bindings'), +(8, 1, 9, 0, 200, 39495, 'Pre-Raid', 'Mage', 'Fire', 'Hands', 'Both', 'Heroes'' Frostfire Gloves'), +(8, 1, 10, 0, 200, 42644, 'Pre-Raid', 'Mage', 'Fire', 'Finger1', 'Both', 'Titanium Spellshock Ring'), +(8, 1, 12, 0, 200, 37873, 'Pre-Raid', 'Mage', 'Fire', 'Trinket1', 'Both', 'Mark of the War Prisoner'), +(8, 1, 14, 0, 200, 41610, 'Pre-Raid', 'Mage', 'Fire', 'Back', 'Both', 'Deathchill Cloak'), +(8, 1, 15, 0, 200, 45085, 'Pre-Raid', 'Mage', 'Fire', 'MainHand', 'Both', 'Titansteel Spellblade'), +(8, 1, 17, 0, 200, 37238, 'Pre-Raid', 'Mage', 'Fire', 'Ranged', 'Both', 'Rod of the Fallen Monarch'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 224, 40416, 'Phase 1', 'Mage', 'Fire', 'Head', 'Both', 'Valorous Frostfire Circlet'), +(8, 1, 1, 0, 224, 44661, 'Phase 1', 'Mage', 'Fire', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(8, 1, 2, 0, 224, 40419, 'Phase 1', 'Mage', 'Fire', 'Shoulders', 'Both', 'Valorous Frostfire Shoulderpads'), +(8, 1, 4, 0, 224, 40418, 'Phase 1', 'Mage', 'Fire', 'Chest', 'Both', 'Valorous Frostfire Robe'), +(8, 1, 5, 0, 224, 40301, 'Phase 1', 'Mage', 'Fire', 'Waist', 'Both', 'Cincture of Polarity'), +(8, 1, 6, 0, 224, 40560, 'Phase 1', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(8, 1, 7, 0, 224, 40246, 'Phase 1', 'Mage', 'Fire', 'Feet', 'Both', 'Boots of Impetuous Ideals'), +(8, 1, 8, 0, 224, 44008, 'Phase 1', 'Mage', 'Fire', 'Wrists', 'Both', 'Unsullied Cuffs'), +(8, 1, 9, 0, 224, 40415, 'Phase 1', 'Mage', 'Fire', 'Hands', 'Both', 'Valorous Frostfire Gloves'), +(8, 1, 10, 0, 224, 40399, 'Phase 1', 'Mage', 'Fire', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(8, 1, 12, 0, 224, 40255, 'Phase 1', 'Mage', 'Fire', 'Trinket1', 'Both', 'Dying Curse'), +(8, 1, 14, 0, 224, 44005, 'Phase 1', 'Mage', 'Fire', 'Back', 'Both', 'Pennant Cloak'), +(8, 1, 15, 0, 224, 40396, 'Phase 1', 'Mage', 'Fire', 'MainHand', 'Both', 'The Turning Tide'), +(8, 1, 17, 0, 224, 39712, 'Phase 1', 'Mage', 'Fire', 'Ranged', 'Both', 'Gemmed Wand of the Nerubians'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 245, 46129, 'Phase 2', 'Mage', 'Fire', 'Head', 'Both', 'Conqueror''s Kirin Tor Hood'), +(8, 1, 1, 0, 245, 45133, 'Phase 2', 'Mage', 'Fire', 'Neck', 'Both', 'Pendant of Fiery Havoc'), +(8, 1, 2, 0, 245, 46134, 'Phase 2', 'Mage', 'Fire', 'Shoulders', 'Both', 'Conqueror''s Kirin Tor Shoulderpads'), +(8, 1, 4, 0, 245, 46130, 'Phase 2', 'Mage', 'Fire', 'Chest', 'Both', 'Conqueror''s Kirin Tor Tunic'), +(8, 1, 5, 0, 245, 45619, 'Phase 2', 'Mage', 'Fire', 'Waist', 'Both', 'Starwatcher''s Binding'), +(8, 1, 6, 0, 245, 46133, 'Phase 2', 'Mage', 'Fire', 'Legs', 'Both', 'Conqueror''s Kirin Tor Leggings'), +(8, 1, 7, 0, 245, 45135, 'Phase 2', 'Mage', 'Fire', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(8, 1, 8, 0, 245, 45446, 'Phase 2', 'Mage', 'Fire', 'Wrists', 'Both', 'Grasps of Reason'), +(8, 1, 9, 0, 245, 45665, 'Phase 2', 'Mage', 'Fire', 'Hands', 'Both', 'Pharos Gloves'), +(8, 1, 10, 0, 245, 46046, 'Phase 2', 'Mage', 'Fire', 'Finger1', 'Both', 'Nebula Band'), +(8, 1, 12, 0, 245, 45518, 'Phase 2', 'Mage', 'Fire', 'Trinket1', 'Both', 'Flare of the Heavens'), +(8, 1, 14, 0, 245, 45242, 'Phase 2', 'Mage', 'Fire', 'Back', 'Both', 'Drape of Mortal Downfall'), +(8, 1, 15, 0, 245, 45620, 'Phase 2', 'Mage', 'Fire', 'MainHand', 'Both', 'Starshard Edge'), +(8, 1, 17, 0, 245, 45511, 'Phase 2', 'Mage', 'Fire', 'Ranged', 'Both', 'Scepter of Lost Souls'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 258, 47761, 'Phase 3', 'Mage', 'Fire', 'Head', 'Both', 'Hood of Triumph'), +(8, 1, 2, 0, 258, 47758, 'Phase 3', 'Mage', 'Fire', 'Shoulders', 'Both', 'Shoulderpads of Triumph'), +(8, 1, 4, 1, 258, 47129, 'Phase 3', 'Mage', 'Fire', 'Chest', 'Alliance', 'Skyweaver Robes'), +(8, 1, 4, 2, 258, 47462, 'Phase 3', 'Mage', 'Fire', 'Chest', 'Horde', 'Skyweaver Vestments'), +(8, 1, 5, 1, 258, 47084, 'Phase 3', 'Mage', 'Fire', 'Waist', 'Alliance', 'Cord of Biting Cold'), +(8, 1, 5, 2, 258, 47447, 'Phase 3', 'Mage', 'Fire', 'Waist', 'Horde', 'Belt of Biting Cold'), +(8, 1, 6, 0, 258, 47760, 'Phase 3', 'Mage', 'Fire', 'Legs', 'Both', 'Leggings of Triumph'), +(8, 1, 7, 1, 258, 47097, 'Phase 3', 'Mage', 'Fire', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(8, 1, 7, 2, 258, 47454, 'Phase 3', 'Mage', 'Fire', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(8, 1, 8, 1, 258, 47143, 'Phase 3', 'Mage', 'Fire', 'Wrists', 'Alliance', 'Bindings of Dark Essence'), +(8, 1, 8, 2, 258, 47467, 'Phase 3', 'Mage', 'Fire', 'Wrists', 'Horde', 'Dark Essence Bindings'), +(8, 1, 9, 0, 258, 47762, 'Phase 3', 'Mage', 'Fire', 'Hands', 'Both', 'Gauntlets of Triumph'), +(8, 1, 10, 1, 258, 47489, 'Phase 3', 'Mage', 'Fire', 'Finger1', 'Alliance', 'Lurid Manifestation'), +(8, 1, 10, 2, 258, 47237, 'Phase 3', 'Mage', 'Fire', 'Finger1', 'Horde', 'Band of Deplorable Violence'), +(8, 1, 12, 1, 258, 47188, 'Phase 3', 'Mage', 'Fire', 'Trinket1', 'Alliance', 'Reign of the Unliving'), +(8, 1, 12, 2, 258, 47477, 'Phase 3', 'Mage', 'Fire', 'Trinket1', 'Horde', 'Reign of the Dead'), +(8, 1, 14, 1, 258, 47552, 'Phase 3', 'Mage', 'Fire', 'Back', 'Alliance', 'Jaina''s Radiance'), +(8, 1, 14, 2, 258, 47551, 'Phase 3', 'Mage', 'Fire', 'Back', 'Horde', 'Aethas'' Intensity'), +(8, 1, 17, 0, 258, 45294, 'Phase 3', 'Mage', 'Fire', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 264, 51281, 'Phase 4', 'Mage', 'Fire', 'Head', 'Both', 'Sanctified Bloodmage Hood'), +(8, 1, 1, 0, 264, 50724, 'Phase 4', 'Mage', 'Fire', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(8, 1, 2, 0, 264, 51284, 'Phase 4', 'Mage', 'Fire', 'Shoulders', 'Both', 'Sanctified Bloodmage Shoulderpads'), +(8, 1, 4, 0, 264, 50629, 'Phase 4', 'Mage', 'Fire', 'Chest', 'Both', 'Robe of the Waking Nightmare'), +(8, 1, 5, 0, 264, 50613, 'Phase 4', 'Mage', 'Fire', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(8, 1, 6, 0, 264, 51282, 'Phase 4', 'Mage', 'Fire', 'Legs', 'Both', 'Sanctified Bloodmage Leggings'), +(8, 1, 7, 0, 264, 50699, 'Phase 4', 'Mage', 'Fire', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(8, 1, 8, 0, 264, 50651, 'Phase 4', 'Mage', 'Fire', 'Wrists', 'Both', 'The Lady''s Brittle Bracers'), +(8, 1, 9, 0, 264, 51280, 'Phase 4', 'Mage', 'Fire', 'Hands', 'Both', 'Sanctified Bloodmage Gloves'), +(8, 1, 10, 0, 264, 50398, 'Phase 4', 'Mage', 'Fire', 'Finger1', 'Both', 'Ashen Band of Endless Destruction'), +(8, 1, 12, 0, 264, 50365, 'Phase 4', 'Mage', 'Fire', 'Trinket1', 'Both', 'Phylactery of the Nameless Lich'), +(8, 1, 14, 0, 264, 50628, 'Phase 4', 'Mage', 'Fire', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(8, 1, 15, 0, 264, 50732, 'Phase 4', 'Mage', 'Fire', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(8, 1, 17, 0, 264, 50684, 'Phase 4', 'Mage', 'Fire', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 1, 0, 0, 290, 51281, 'Phase 5', 'Mage', 'Fire', 'Head', 'Both', 'Sanctified Bloodmage Hood'), +(8, 1, 1, 0, 290, 50182, 'Phase 5', 'Mage', 'Fire', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(8, 1, 2, 0, 290, 51284, 'Phase 5', 'Mage', 'Fire', 'Shoulders', 'Both', 'Sanctified Bloodmage Shoulderpads'), +(8, 1, 4, 0, 290, 51283, 'Phase 5', 'Mage', 'Fire', 'Chest', 'Both', 'Sanctified Bloodmage Robe'), +(8, 1, 5, 0, 290, 50613, 'Phase 5', 'Mage', 'Fire', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(8, 1, 6, 0, 290, 50694, 'Phase 5', 'Mage', 'Fire', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(8, 1, 7, 0, 290, 50699, 'Phase 5', 'Mage', 'Fire', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(8, 1, 8, 0, 290, 54582, 'Phase 5', 'Mage', 'Fire', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(8, 1, 9, 0, 290, 51280, 'Phase 5', 'Mage', 'Fire', 'Hands', 'Both', 'Sanctified Bloodmage Gloves'), +(8, 1, 10, 0, 290, 50614, 'Phase 5', 'Mage', 'Fire', 'Finger1', 'Both', 'Loop of the Endless Labyrinth'), +(8, 1, 11, 0, 290, 50398, 'Phase 5', 'Mage', 'Fire', 'Finger2', 'Both', 'Ashen Band of Endless Destruction'), +(8, 1, 12, 0, 290, 54588, 'Phase 5', 'Mage', 'Fire', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(8, 1, 13, 0, 290, 50365, 'Phase 5', 'Mage', 'Fire', 'Trinket2', 'Both', 'Phylactery of the Nameless Lich'), +(8, 1, 14, 0, 290, 54583, 'Phase 5', 'Mage', 'Fire', 'Back', 'Both', 'Cloak of Burning Dusk'), +(8, 1, 15, 0, 290, 50732, 'Phase 5', 'Mage', 'Fire', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(8, 1, 16, 0, 290, 50719, 'Phase 5', 'Mage', 'Fire', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(8, 1, 17, 0, 290, 50684, 'Phase 5', 'Mage', 'Fire', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- Frost (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 66, 10504, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Head', 'Both', 'Green Lens'), +(8, 2, 1, 0, 66, 12103, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Neck', 'Both', 'Star of Mystaria'), +(8, 2, 2, 0, 66, 11782, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Shoulders', 'Both', 'Boreal Mantle'), +(8, 2, 4, 0, 66, 14152, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 2, 5, 0, 66, 11662, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Waist', 'Both', 'Ban''thok Sash'), +(8, 2, 6, 0, 66, 13170, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Legs', 'Both', 'Skyshroud Leggings'), +(8, 2, 7, 0, 66, 11822, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Feet', 'Both', 'Omnicast Boots'), +(8, 2, 8, 0, 66, 11766, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 2, 9, 0, 66, 13253, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Hands', 'Both', 'Hands of Power'), +(8, 2, 10, 0, 66, 942, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Finger1', 'Both', 'Freezing Band'), +(8, 2, 11, 0, 66, 942, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Finger2', 'Both', 'Freezing Band'), +(8, 2, 12, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 2, 13, 0, 66, 13968, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Trinket2', 'Both', 'Eye of the Beast'), +(8, 2, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Back', 'Both', 'Archivist Cape'), +(8, 2, 15, 0, 66, 13964, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'MainHand', 'Both', 'Witchblade'), +(8, 2, 16, 0, 66, 10796, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'OffHand', 'Both', 'Drakestone'), +(8, 2, 17, 0, 66, 13938, 'Phase 1 (Pre-Raid)', 'Mage', 'Frost', 'Ranged', 'Both', 'Bonecreeper Stylus'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 76, 23318, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Head', 'Both', 'Lieutenant Commander''s Silk Cowl'), +(8, 2, 1, 0, 76, 12103, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Neck', 'Both', 'Star of Mystaria'), +(8, 2, 2, 0, 76, 23319, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Shoulders', 'Both', 'Lieutenant Commander''s Silk Mantle'), +(8, 2, 4, 0, 76, 14152, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 2, 5, 0, 76, 11662, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Waist', 'Both', 'Ban''thok Sash'), +(8, 2, 6, 0, 76, 23304, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Legs', 'Both', 'Knight-Captain''s Silk Legguards'), +(8, 2, 7, 0, 76, 23291, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Feet', 'Both', 'Knight-Lieutenant''s Silk Walkers'), +(8, 2, 8, 0, 76, 11766, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 2, 9, 0, 76, 13253, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Hands', 'Both', 'Hands of Power'), +(8, 2, 10, 0, 76, 942, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Finger1', 'Both', 'Freezing Band'), +(8, 2, 11, 0, 76, 942, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Finger2', 'Both', 'Freezing Band'), +(8, 2, 12, 0, 76, 12930, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 2, 13, 0, 76, 13968, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Trinket2', 'Both', 'Eye of the Beast'), +(8, 2, 14, 0, 76, 13386, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Back', 'Both', 'Archivist Cape'), +(8, 2, 15, 0, 76, 13964, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'MainHand', 'Both', 'Witchblade'), +(8, 2, 16, 0, 76, 10796, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'OffHand', 'Both', 'Drakestone'), +(8, 2, 17, 0, 76, 13938, 'Phase 2 (Pre-Raid)', 'Mage', 'Frost', 'Ranged', 'Both', 'Bonecreeper Stylus'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 78, 16914, 'Phase 2', 'Mage', 'Frost', 'Head', 'Both', 'Netherwind Crown'), +(8, 2, 1, 0, 78, 18814, 'Phase 2', 'Mage', 'Frost', 'Neck', 'Both', 'Choker of the Fire Lord'), +(8, 2, 2, 0, 78, 23319, 'Phase 2', 'Mage', 'Frost', 'Shoulders', 'Both', 'Lieutenant Commander''s Silk Mantle'), +(8, 2, 4, 0, 78, 14152, 'Phase 2', 'Mage', 'Frost', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 2, 5, 0, 78, 19136, 'Phase 2', 'Mage', 'Frost', 'Waist', 'Both', 'Mana Igniting Cord'), +(8, 2, 6, 0, 78, 16915, 'Phase 2', 'Mage', 'Frost', 'Legs', 'Both', 'Netherwind Pants'), +(8, 2, 7, 0, 78, 23291, 'Phase 2', 'Mage', 'Frost', 'Feet', 'Both', 'Knight-Lieutenant''s Silk Walkers'), +(8, 2, 8, 0, 78, 11766, 'Phase 2', 'Mage', 'Frost', 'Wrists', 'Both', 'Flameweave Cuffs'), +(8, 2, 9, 0, 78, 13253, 'Phase 2', 'Mage', 'Frost', 'Hands', 'Both', 'Hands of Power'), +(8, 2, 10, 0, 78, 19147, 'Phase 2', 'Mage', 'Frost', 'Finger1', 'Both', 'Ring of Spell Power'), +(8, 2, 11, 0, 78, 19147, 'Phase 2', 'Mage', 'Frost', 'Finger2', 'Both', 'Ring of Spell Power'), +(8, 2, 12, 0, 78, 12930, 'Phase 2', 'Mage', 'Frost', 'Trinket1', 'Both', 'Briarwood Reed'), +(8, 2, 13, 0, 78, 18820, 'Phase 2', 'Mage', 'Frost', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(8, 2, 14, 0, 78, 13386, 'Phase 2', 'Mage', 'Frost', 'Back', 'Both', 'Archivist Cape'), +(8, 2, 15, 0, 78, 17103, 'Phase 2', 'Mage', 'Frost', 'MainHand', 'Both', 'Azuresong Mageblade'), +(8, 2, 16, 0, 78, 10796, 'Phase 2', 'Mage', 'Frost', 'OffHand', 'Both', 'Drakestone'), +(8, 2, 17, 0, 78, 19130, 'Phase 2', 'Mage', 'Frost', 'Ranged', 'Both', 'Cold Snap'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 83, 19375, 'Phase 3', 'Mage', 'Frost', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(8, 2, 1, 0, 83, 18814, 'Phase 3', 'Mage', 'Frost', 'Neck', 'Both', 'Choker of the Fire Lord'), +(8, 2, 2, 0, 83, 19370, 'Phase 3', 'Mage', 'Frost', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(8, 2, 4, 0, 83, 14152, 'Phase 3', 'Mage', 'Frost', 'Chest', 'Both', 'Robe of the Archmage'), +(8, 2, 5, 0, 83, 19136, 'Phase 3', 'Mage', 'Frost', 'Waist', 'Both', 'Mana Igniting Cord'), +(8, 2, 6, 0, 83, 16915, 'Phase 3', 'Mage', 'Frost', 'Legs', 'Both', 'Netherwind Pants'), +(8, 2, 7, 0, 83, 19438, 'Phase 3', 'Mage', 'Frost', 'Feet', 'Both', 'Ringo''s Blizzard Boots'), +(8, 2, 8, 0, 83, 19374, 'Phase 3', 'Mage', 'Frost', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(8, 2, 9, 0, 83, 16913, 'Phase 3', 'Mage', 'Frost', 'Hands', 'Both', 'Netherwind Gloves'), +(8, 2, 11, 0, 83, 19147, 'Phase 3', 'Mage', 'Frost', 'Finger2', 'Both', 'Ring of Spell Power'), +(8, 2, 12, 0, 83, 19379, 'Phase 3', 'Mage', 'Frost', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 2, 13, 0, 83, 19339, 'Phase 3', 'Mage', 'Frost', 'Trinket2', 'Both', 'Mind Quickening Gem'), +(8, 2, 14, 0, 83, 19378, 'Phase 3', 'Mage', 'Frost', 'Back', 'Both', 'Cloak of the Brood Lord'), +(8, 2, 15, 0, 83, 19356, 'Phase 3', 'Mage', 'Frost', 'MainHand', 'Both', 'Staff of the Shadow Flame'), +(8, 2, 17, 0, 83, 19130, 'Phase 3', 'Mage', 'Frost', 'Ranged', 'Both', 'Cold Snap'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 88, 19375, 'Phase 5', 'Mage', 'Frost', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(8, 2, 1, 0, 88, 21608, 'Phase 5', 'Mage', 'Frost', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(8, 2, 2, 0, 88, 19370, 'Phase 5', 'Mage', 'Frost', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(8, 2, 4, 0, 88, 19145, 'Phase 5', 'Mage', 'Frost', 'Chest', 'Both', 'Robe of Volatile Power'), +(8, 2, 5, 0, 88, 22730, 'Phase 5', 'Mage', 'Frost', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(8, 2, 6, 0, 88, 21461, 'Phase 5', 'Mage', 'Frost', 'Legs', 'Both', 'Leggings of the Black Blizzard'), +(8, 2, 7, 0, 88, 21344, 'Phase 5', 'Mage', 'Frost', 'Feet', 'Both', 'Enigma Boots'), +(8, 2, 8, 0, 88, 21186, 'Phase 5', 'Mage', 'Frost', 'Wrists', 'Both', 'Rockfury Bracers'), +(8, 2, 9, 0, 88, 21585, 'Phase 5', 'Mage', 'Frost', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(8, 2, 10, 0, 88, 21836, 'Phase 5', 'Mage', 'Frost', 'Finger1', 'Both', 'Ritssyn''s Ring of Chaos'), +(8, 2, 11, 0, 88, 21709, 'Phase 5', 'Mage', 'Frost', 'Finger2', 'Both', 'Ring of the Fallen God'), +(8, 2, 12, 0, 88, 19379, 'Phase 5', 'Mage', 'Frost', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 2, 13, 0, 88, 19339, 'Phase 5', 'Mage', 'Frost', 'Trinket2', 'Both', 'Mind Quickening Gem'), +(8, 2, 14, 0, 88, 22731, 'Phase 5', 'Mage', 'Frost', 'Back', 'Both', 'Cloak of the Devoured'), +(8, 2, 15, 0, 88, 21622, 'Phase 5', 'Mage', 'Frost', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(8, 2, 16, 0, 88, 21597, 'Phase 5', 'Mage', 'Frost', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(8, 2, 17, 0, 88, 21603, 'Phase 5', 'Mage', 'Frost', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 92, 22498, 'Phase 6', 'Mage', 'Frost', 'Head', 'Both', 'Frostfire Circlet'), +(8, 2, 1, 0, 92, 23057, 'Phase 6', 'Mage', 'Frost', 'Neck', 'Both', 'Gem of Trapped Innocents'), +(8, 2, 2, 0, 92, 22983, 'Phase 6', 'Mage', 'Frost', 'Shoulders', 'Both', 'Rime Covered Mantle'), +(8, 2, 4, 0, 92, 22496, 'Phase 6', 'Mage', 'Frost', 'Chest', 'Both', 'Frostfire Robe'), +(8, 2, 5, 0, 92, 22730, 'Phase 6', 'Mage', 'Frost', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(8, 2, 6, 0, 92, 23070, 'Phase 6', 'Mage', 'Frost', 'Legs', 'Both', 'Leggings of Polarity'), +(8, 2, 7, 0, 92, 22500, 'Phase 6', 'Mage', 'Frost', 'Feet', 'Both', 'Frostfire Sandals'), +(8, 2, 8, 0, 92, 23021, 'Phase 6', 'Mage', 'Frost', 'Wrists', 'Both', 'The Soul Harvester''s Bindings'), +(8, 2, 9, 0, 92, 21585, 'Phase 6', 'Mage', 'Frost', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(8, 2, 10, 0, 92, 23237, 'Phase 6', 'Mage', 'Frost', 'Finger1', 'Both', 'Ring of the Eternal Flame'), +(8, 2, 11, 0, 92, 23062, 'Phase 6', 'Mage', 'Frost', 'Finger2', 'Both', 'Frostfire Ring'), +(8, 2, 12, 0, 92, 19379, 'Phase 6', 'Mage', 'Frost', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(8, 2, 13, 0, 92, 23046, 'Phase 6', 'Mage', 'Frost', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(8, 2, 14, 0, 92, 23050, 'Phase 6', 'Mage', 'Frost', 'Back', 'Both', 'Cloak of the Necropolis'), +(8, 2, 15, 0, 92, 22807, 'Phase 6', 'Mage', 'Frost', 'MainHand', 'Both', 'Wraith Blade'), +(8, 2, 16, 0, 92, 23049, 'Phase 6', 'Mage', 'Frost', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(8, 2, 17, 0, 92, 22821, 'Phase 6', 'Mage', 'Frost', 'Ranged', 'Both', 'Doomfinger'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 120, 24266, 'Pre-Raid', 'Mage', 'Frost', 'Head', 'Both', 'Spellstrike Hood'), +(8, 2, 1, 0, 120, 28134, 'Pre-Raid', 'Mage', 'Frost', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(8, 2, 2, 0, 120, 21869, 'Pre-Raid', 'Mage', 'Frost', 'Shoulders', 'Both', 'Frozen Shadoweave Shoulders'), +(8, 2, 4, 0, 120, 21871, 'Pre-Raid', 'Mage', 'Frost', 'Chest', 'Both', 'Frozen Shadoweave Robe'), +(8, 2, 5, 0, 120, 24256, 'Pre-Raid', 'Mage', 'Frost', 'Waist', 'Both', 'Girdle of Ruination'), +(8, 2, 6, 0, 120, 24262, 'Pre-Raid', 'Mage', 'Frost', 'Legs', 'Both', 'Spellstrike Pants'), +(8, 2, 7, 0, 120, 21870, 'Pre-Raid', 'Mage', 'Frost', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(8, 2, 8, 0, 120, 24250, 'Pre-Raid', 'Mage', 'Frost', 'Wrists', 'Both', 'Bracers of Havok'), +(8, 2, 9, 0, 120, 30725, 'Pre-Raid', 'Mage', 'Frost', 'Hands', 'Both', 'Anger-Spark Gloves'), +(8, 2, 10, 0, 120, 29172, 'Pre-Raid', 'Mage', 'Frost', 'Finger1', 'Both', 'Ashyen''s Gift'), +(8, 2, 11, 0, 120, 28227, 'Pre-Raid', 'Mage', 'Frost', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(8, 2, 12, 0, 120, 29370, 'Pre-Raid', 'Mage', 'Frost', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 2, 13, 0, 120, 27683, 'Pre-Raid', 'Mage', 'Frost', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(8, 2, 14, 0, 120, 27981, 'Pre-Raid', 'Mage', 'Frost', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(8, 2, 15, 0, 120, 23554, 'Pre-Raid', 'Mage', 'Frost', 'MainHand', 'Both', 'Eternium Runed Blade'), +(8, 2, 16, 0, 120, 29273, 'Pre-Raid', 'Mage', 'Frost', 'OffHand', 'Both', 'Khadgar''s Knapsack'), +(8, 2, 17, 0, 120, 29350, 'Pre-Raid', 'Mage', 'Frost', 'Ranged', 'Both', 'The Black Stalk'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 125, 29076, 'Phase 1', 'Mage', 'Frost', 'Head', 'Both', 'Collar of the Aldor'), +(8, 2, 1, 0, 125, 28530, 'Phase 1', 'Mage', 'Frost', 'Neck', 'Both', 'Brooch of Unquenchable Fury'), +(8, 2, 2, 0, 125, 21869, 'Phase 1', 'Mage', 'Frost', 'Shoulders', 'Both', 'Frozen Shadoweave Shoulders'), +(8, 2, 4, 0, 125, 21871, 'Phase 1', 'Mage', 'Frost', 'Chest', 'Both', 'Frozen Shadoweave Robe'), +(8, 2, 5, 0, 125, 24256, 'Phase 1', 'Mage', 'Frost', 'Waist', 'Both', 'Girdle of Ruination'), +(8, 2, 6, 0, 125, 29078, 'Phase 1', 'Mage', 'Frost', 'Legs', 'Both', 'Legwraps of the Aldor'), +(8, 2, 7, 0, 125, 21870, 'Phase 1', 'Mage', 'Frost', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(8, 2, 8, 0, 125, 28515, 'Phase 1', 'Mage', 'Frost', 'Wrists', 'Both', 'Bands of Nefarious Deeds'), +(8, 2, 9, 0, 125, 30725, 'Phase 1', 'Mage', 'Frost', 'Hands', 'Both', 'Anger-Spark Gloves'), +(8, 2, 10, 0, 125, 28793, 'Phase 1', 'Mage', 'Frost', 'Finger1', 'Both', 'Band of Crimson Fury'), +(8, 2, 11, 0, 125, 29287, 'Phase 1', 'Mage', 'Frost', 'Finger2', 'Both', 'Violet Signet of the Archmage'), +(8, 2, 12, 0, 125, 29370, 'Phase 1', 'Mage', 'Frost', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(8, 2, 13, 0, 125, 27683, 'Phase 1', 'Mage', 'Frost', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(8, 2, 14, 0, 125, 28766, 'Phase 1', 'Mage', 'Frost', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(8, 2, 15, 0, 125, 28770, 'Phase 1', 'Mage', 'Frost', 'MainHand', 'Both', 'Nathrezim Mindblade'), +(8, 2, 16, 0, 125, 29269, 'Phase 1', 'Mage', 'Frost', 'OffHand', 'Both', 'Sapphiron''s Wing Bone'), +(8, 2, 17, 0, 125, 28783, 'Phase 1', 'Mage', 'Frost', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 200, 37294, 'Pre-Raid', 'Mage', 'Frost', 'Head', 'Both', 'Crown of Unbridled Magic'), +(8, 2, 1, 0, 200, 39472, 'Pre-Raid', 'Mage', 'Frost', 'Neck', 'Both', 'Chain of Latent Energies'), +(8, 2, 2, 0, 200, 37673, 'Pre-Raid', 'Mage', 'Frost', 'Shoulders', 'Both', 'Dark Runic Mantle'), +(8, 2, 4, 0, 200, 39492, 'Pre-Raid', 'Mage', 'Frost', 'Chest', 'Both', 'Heroes'' Frostfire Robe'), +(8, 2, 5, 0, 200, 40696, 'Pre-Raid', 'Mage', 'Frost', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(8, 2, 6, 0, 200, 37854, 'Pre-Raid', 'Mage', 'Frost', 'Legs', 'Both', 'Woven Bracae Leggings'), +(8, 2, 7, 0, 200, 44202, 'Pre-Raid', 'Mage', 'Frost', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(8, 2, 8, 0, 200, 37361, 'Pre-Raid', 'Mage', 'Frost', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(8, 2, 9, 0, 200, 39495, 'Pre-Raid', 'Mage', 'Frost', 'Hands', 'Both', 'Heroes'' Frostfire Gloves'), +(8, 2, 10, 0, 200, 40585, 'Pre-Raid', 'Mage', 'Frost', 'Finger1', 'Both', 'Signet of the Kirin Tor'), +(8, 2, 12, 0, 200, 37660, 'Pre-Raid', 'Mage', 'Frost', 'Trinket1', 'Both', 'Forge Ember'), +(8, 2, 14, 0, 200, 41610, 'Pre-Raid', 'Mage', 'Frost', 'Back', 'Both', 'Deathchill Cloak'), +(8, 2, 15, 0, 200, 45085, 'Pre-Raid', 'Mage', 'Frost', 'MainHand', 'Both', 'Titansteel Spellblade'), +(8, 2, 17, 0, 200, 37177, 'Pre-Raid', 'Mage', 'Frost', 'Ranged', 'Both', 'Wand of the San''layn'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 1, 0, 224, 44661, 'Phase 1', 'Mage', 'Frost', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(8, 2, 2, 0, 224, 40419, 'Phase 1', 'Mage', 'Frost', 'Shoulders', 'Both', 'Valorous Frostfire Shoulderpads'), +(8, 2, 4, 0, 224, 40418, 'Phase 1', 'Mage', 'Frost', 'Chest', 'Both', 'Valorous Frostfire Robe'), +(8, 2, 5, 0, 224, 40561, 'Phase 1', 'Mage', 'Frost', 'Waist', 'Both', 'Leash of Heedless Magic'), +(8, 2, 6, 0, 224, 40560, 'Phase 1', 'Mage', 'Frost', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(8, 2, 7, 0, 224, 40558, 'Phase 1', 'Mage', 'Frost', 'Feet', 'Both', 'Arcanic Tramplers'), +(8, 2, 8, 0, 224, 44008, 'Phase 1', 'Mage', 'Frost', 'Wrists', 'Both', 'Unsullied Cuffs'), +(8, 2, 9, 0, 224, 40415, 'Phase 1', 'Mage', 'Frost', 'Hands', 'Both', 'Valorous Frostfire Gloves'), +(8, 2, 10, 0, 224, 40719, 'Phase 1', 'Mage', 'Frost', 'Finger1', 'Both', 'Band of Channeled Magic'), +(8, 2, 12, 0, 224, 40255, 'Phase 1', 'Mage', 'Frost', 'Trinket1', 'Both', 'Dying Curse'), +(8, 2, 14, 0, 224, 44005, 'Phase 1', 'Mage', 'Frost', 'Back', 'Both', 'Pennant Cloak'), +(8, 2, 15, 0, 224, 40396, 'Phase 1', 'Mage', 'Frost', 'MainHand', 'Both', 'The Turning Tide'), +(8, 2, 17, 0, 224, 39426, 'Phase 1', 'Mage', 'Frost', 'Ranged', 'Both', 'Wand of the Archlich'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 245, 46129, 'Phase 2', 'Mage', 'Frost', 'Head', 'Both', 'Conqueror''s Kirin Tor Hood'), +(8, 2, 1, 0, 245, 45133, 'Phase 2', 'Mage', 'Frost', 'Neck', 'Both', 'Pendant of Fiery Havoc'), +(8, 2, 2, 0, 245, 46134, 'Phase 2', 'Mage', 'Frost', 'Shoulders', 'Both', 'Conqueror''s Kirin Tor Shoulderpads'), +(8, 2, 4, 0, 245, 46130, 'Phase 2', 'Mage', 'Frost', 'Chest', 'Both', 'Conqueror''s Kirin Tor Tunic'), +(8, 2, 5, 0, 245, 45619, 'Phase 2', 'Mage', 'Frost', 'Waist', 'Both', 'Starwatcher''s Binding'), +(8, 2, 6, 0, 245, 46133, 'Phase 2', 'Mage', 'Frost', 'Legs', 'Both', 'Conqueror''s Kirin Tor Leggings'), +(8, 2, 7, 0, 245, 45135, 'Phase 2', 'Mage', 'Frost', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(8, 2, 8, 0, 245, 45446, 'Phase 2', 'Mage', 'Frost', 'Wrists', 'Both', 'Grasps of Reason'), +(8, 2, 9, 0, 245, 45665, 'Phase 2', 'Mage', 'Frost', 'Hands', 'Both', 'Pharos Gloves'), +(8, 2, 10, 0, 245, 46046, 'Phase 2', 'Mage', 'Frost', 'Finger1', 'Both', 'Nebula Band'), +(8, 2, 12, 0, 245, 45466, 'Phase 2', 'Mage', 'Frost', 'Trinket1', 'Both', 'Scale of Fates'), +(8, 2, 14, 0, 245, 45242, 'Phase 2', 'Mage', 'Frost', 'Back', 'Both', 'Drape of Mortal Downfall'), +(8, 2, 15, 0, 245, 45620, 'Phase 2', 'Mage', 'Frost', 'MainHand', 'Both', 'Starshard Edge'), +(8, 2, 17, 0, 245, 45511, 'Phase 2', 'Mage', 'Frost', 'Ranged', 'Both', 'Scepter of Lost Souls'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 1, 258, 47761, 'Phase 3', 'Mage', 'Frost', 'Head', 'Alliance', 'Khadgar''s Hood of Triumph'), +(8, 2, 2, 1, 258, 47758, 'Phase 3', 'Mage', 'Frost', 'Shoulders', 'Alliance', 'Khadgar''s Shoulderpads of Triumph'), +(8, 2, 4, 1, 258, 47129, 'Phase 3', 'Mage', 'Frost', 'Chest', 'Alliance', 'Skyweaver Robes'), +(8, 2, 5, 1, 258, 47419, 'Phase 3', 'Mage', 'Frost', 'Waist', 'Alliance', 'Belt of the Tenebrous Mist'), +(8, 2, 6, 1, 258, 47760, 'Phase 3', 'Mage', 'Frost', 'Legs', 'Alliance', 'Khadgar''s Leggings of Triumph'), +(8, 2, 7, 1, 258, 47097, 'Phase 3', 'Mage', 'Frost', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(8, 2, 8, 1, 258, 47143, 'Phase 3', 'Mage', 'Frost', 'Wrists', 'Alliance', 'Bindings of Dark Essence'), +(8, 2, 9, 1, 258, 47762, 'Phase 3', 'Mage', 'Frost', 'Hands', 'Alliance', 'Khadgar''s Gauntlets of Triumph'), +(8, 2, 10, 1, 258, 47489, 'Phase 3', 'Mage', 'Frost', 'Finger1', 'Alliance', 'Lurid Manifestation'), +(8, 2, 12, 1, 258, 47188, 'Phase 3', 'Mage', 'Frost', 'Trinket1', 'Alliance', 'Reign of the Unliving'), +(8, 2, 14, 1, 258, 47552, 'Phase 3', 'Mage', 'Frost', 'Back', 'Alliance', 'Jaina''s Radiance'), +(8, 2, 17, 0, 258, 45294, 'Phase 3', 'Mage', 'Frost', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 264, 51281, 'Phase 4', 'Mage', 'Frost', 'Head', 'Both', 'Sanctified Bloodmage Hood'), +(8, 2, 1, 0, 264, 50724, 'Phase 4', 'Mage', 'Frost', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(8, 2, 2, 0, 264, 51284, 'Phase 4', 'Mage', 'Frost', 'Shoulders', 'Both', 'Sanctified Bloodmage Shoulderpads'), +(8, 2, 4, 0, 264, 50717, 'Phase 4', 'Mage', 'Frost', 'Chest', 'Both', 'Sanguine Silk Robes'), +(8, 2, 5, 0, 264, 50613, 'Phase 4', 'Mage', 'Frost', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(8, 2, 6, 0, 264, 51282, 'Phase 4', 'Mage', 'Frost', 'Legs', 'Both', 'Sanctified Bloodmage Leggings'), +(8, 2, 7, 0, 264, 50699, 'Phase 4', 'Mage', 'Frost', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(8, 2, 8, 0, 264, 50651, 'Phase 4', 'Mage', 'Frost', 'Wrists', 'Both', 'The Lady''s Brittle Bracers'), +(8, 2, 9, 0, 264, 51280, 'Phase 4', 'Mage', 'Frost', 'Hands', 'Both', 'Sanctified Bloodmage Gloves'), +(8, 2, 10, 0, 264, 50398, 'Phase 4', 'Mage', 'Frost', 'Finger1', 'Both', 'Ashen Band of Endless Destruction'), +(8, 2, 12, 0, 264, 50365, 'Phase 4', 'Mage', 'Frost', 'Trinket1', 'Both', 'Phylactery of the Nameless Lich'), +(8, 2, 14, 0, 264, 50628, 'Phase 4', 'Mage', 'Frost', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(8, 2, 15, 0, 264, 50732, 'Phase 4', 'Mage', 'Frost', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(8, 2, 17, 0, 264, 50684, 'Phase 4', 'Mage', 'Frost', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(8, 2, 0, 0, 290, 51281, 'Phase 5', 'Mage', 'Frost', 'Head', 'Both', 'Sanctified Bloodmage Hood'), +(8, 2, 1, 0, 290, 50182, 'Phase 5', 'Mage', 'Frost', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(8, 2, 2, 0, 290, 51284, 'Phase 5', 'Mage', 'Frost', 'Shoulders', 'Both', 'Sanctified Bloodmage Shoulderpads'), +(8, 2, 4, 0, 290, 51283, 'Phase 5', 'Mage', 'Frost', 'Chest', 'Both', 'Sanctified Bloodmage Robe'), +(8, 2, 5, 0, 290, 50613, 'Phase 5', 'Mage', 'Frost', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(8, 2, 6, 0, 290, 50694, 'Phase 5', 'Mage', 'Frost', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(8, 2, 7, 0, 290, 50699, 'Phase 5', 'Mage', 'Frost', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(8, 2, 8, 0, 290, 54582, 'Phase 5', 'Mage', 'Frost', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(8, 2, 9, 0, 290, 51280, 'Phase 5', 'Mage', 'Frost', 'Hands', 'Both', 'Sanctified Bloodmage Gloves'), +(8, 2, 10, 0, 290, 50614, 'Phase 5', 'Mage', 'Frost', 'Finger1', 'Both', 'Loop of the Endless Labyrinth'), +(8, 2, 11, 0, 290, 50398, 'Phase 5', 'Mage', 'Frost', 'Finger2', 'Both', 'Ashen Band of Endless Destruction'), +(8, 2, 12, 0, 290, 54588, 'Phase 5', 'Mage', 'Frost', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(8, 2, 13, 0, 290, 50348, 'Phase 5', 'Mage', 'Frost', 'Trinket2', 'Both', 'Dislodged Foreign Object'), +(8, 2, 14, 0, 290, 54583, 'Phase 5', 'Mage', 'Frost', 'Back', 'Both', 'Cloak of Burning Dusk'), +(8, 2, 15, 0, 290, 50732, 'Phase 5', 'Mage', 'Frost', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(8, 2, 16, 0, 290, 50719, 'Phase 5', 'Mage', 'Frost', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(8, 2, 17, 0, 290, 50684, 'Phase 5', 'Mage', 'Frost', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + + +-- ============================================================ +-- Warlock (9) +-- ============================================================ +-- Affliction (tab 0) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 1, 66, 10504, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Head', 'Alliance', 'Green Lens'), +(9, 0, 0, 2, 66, 10504, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Head', 'Horde', 'Green Lens'), +(9, 0, 1, 1, 66, 18691, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(9, 0, 1, 2, 66, 18691, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(9, 0, 2, 1, 66, 14112, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Shoulders', 'Alliance', 'Felcloth Shoulders'), +(9, 0, 2, 2, 66, 14112, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Shoulders', 'Horde', 'Felcloth Shoulders'), +(9, 0, 4, 1, 66, 14153, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Chest', 'Alliance', 'Robe of the Void'), +(9, 0, 4, 2, 66, 14153, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Chest', 'Horde', 'Robe of the Void'), +(9, 0, 5, 1, 66, 11662, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Waist', 'Alliance', 'Ban''thok Sash'), +(9, 0, 5, 2, 66, 11662, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Waist', 'Horde', 'Ban''thok Sash'), +(9, 0, 6, 1, 66, 13170, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(9, 0, 6, 2, 66, 13170, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Legs', 'Horde', 'Skyshroud Leggings'), +(9, 0, 7, 1, 66, 18735, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(9, 0, 7, 2, 66, 18735, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(9, 0, 8, 1, 66, 11766, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(9, 0, 8, 2, 66, 11766, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(9, 0, 9, 1, 66, 13253, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Hands', 'Alliance', 'Hands of Power'), +(9, 0, 9, 2, 66, 13253, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Hands', 'Horde', 'Hands of Power'), +(9, 0, 10, 1, 66, 12543, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(9, 0, 10, 2, 66, 12545, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(9, 0, 11, 1, 66, 13001, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(9, 0, 11, 2, 66, 13001, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger2', 'Horde', 'Maiden''s Circle'), +(9, 0, 12, 1, 66, 12930, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(9, 0, 12, 2, 66, 12930, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket1', 'Horde', 'Briarwood Reed'), +(9, 0, 13, 1, 66, 13968, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket2', 'Alliance', 'Eye of the Beast'), +(9, 0, 13, 2, 66, 13968, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket2', 'Horde', 'Eye of the Beast'), +(9, 0, 14, 1, 66, 13386, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Back', 'Alliance', 'Archivist Cape'), +(9, 0, 14, 2, 66, 13386, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Back', 'Horde', 'Archivist Cape'), +(9, 0, 15, 1, 66, 13964, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'MainHand', 'Alliance', 'Witchblade'), +(9, 0, 15, 2, 66, 13964, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'MainHand', 'Horde', 'Witchblade'), +(9, 0, 16, 1, 66, 10796, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'OffHand', 'Alliance', 'Drakestone'), +(9, 0, 16, 2, 66, 10796, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'OffHand', 'Horde', 'Drakestone'), +(9, 0, 17, 1, 66, 13396, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(9, 0, 17, 2, 66, 13396, 'Phase 1 (Pre-Raid)', 'Warlock', 'Affliction', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 1, 76, 23310, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Head', 'Alliance', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 0, 0, 2, 76, 23310, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Head', 'Horde', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 0, 1, 1, 76, 18691, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(9, 0, 1, 2, 76, 18691, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(9, 0, 2, 1, 76, 23311, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Shoulders', 'Alliance', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 0, 2, 2, 76, 23311, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Shoulders', 'Horde', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 0, 4, 1, 76, 14153, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Chest', 'Alliance', 'Robe of the Void'), +(9, 0, 4, 2, 76, 14153, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Chest', 'Horde', 'Robe of the Void'), +(9, 0, 5, 1, 76, 11662, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Waist', 'Alliance', 'Ban''thok Sash'), +(9, 0, 5, 2, 76, 11662, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Waist', 'Horde', 'Ban''thok Sash'), +(9, 0, 6, 1, 76, 13170, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(9, 0, 6, 2, 76, 13170, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Legs', 'Horde', 'Skyshroud Leggings'), +(9, 0, 7, 1, 76, 18735, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(9, 0, 7, 2, 76, 18735, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(9, 0, 8, 1, 76, 11766, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(9, 0, 8, 2, 76, 11766, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(9, 0, 9, 1, 76, 18407, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Hands', 'Alliance', 'Felcloth Gloves'), +(9, 0, 9, 2, 76, 18407, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Hands', 'Horde', 'Felcloth Gloves'), +(9, 0, 10, 1, 76, 12543, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(9, 0, 10, 2, 76, 12545, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(9, 0, 11, 1, 76, 13001, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(9, 0, 11, 2, 76, 13001, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Finger2', 'Horde', 'Maiden''s Circle'), +(9, 0, 12, 1, 76, 12930, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(9, 0, 12, 2, 76, 12930, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket1', 'Horde', 'Briarwood Reed'), +(9, 0, 13, 1, 76, 18467, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket2', 'Alliance', 'Royal Seal of Eldre''Thalas'), +(9, 0, 13, 2, 76, 18467, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Trinket2', 'Horde', 'Royal Seal of Eldre''Thalas'), +(9, 0, 14, 1, 76, 13386, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Back', 'Alliance', 'Archivist Cape'), +(9, 0, 14, 2, 76, 13386, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Back', 'Horde', 'Archivist Cape'), +(9, 0, 15, 1, 76, 18372, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'MainHand', 'Alliance', 'Blade of the New Moon'), +(9, 0, 15, 2, 76, 18372, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'MainHand', 'Horde', 'Blade of the New Moon'), +(9, 0, 16, 1, 76, 10796, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'OffHand', 'Alliance', 'Drakestone'), +(9, 0, 16, 2, 76, 10796, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'OffHand', 'Horde', 'Drakestone'), +(9, 0, 17, 1, 76, 13396, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(9, 0, 17, 2, 76, 13396, 'Phase 2 (Pre-Raid)', 'Warlock', 'Affliction', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 78, 23310, 'Phase 2', 'Warlock', 'Affliction', 'Head', 'Both', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 0, 1, 0, 78, 18814, 'Phase 2', 'Warlock', 'Affliction', 'Neck', 'Both', 'Choker of the Fire Lord'), +(9, 0, 2, 0, 78, 23311, 'Phase 2', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 0, 4, 0, 78, 19145, 'Phase 2', 'Warlock', 'Affliction', 'Chest', 'Both', 'Robe of Volatile Power'), +(9, 0, 5, 0, 78, 18809, 'Phase 2', 'Warlock', 'Affliction', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(9, 0, 6, 0, 78, 19133, 'Phase 2', 'Warlock', 'Affliction', 'Legs', 'Both', 'Fel Infused Leggings'), +(9, 0, 7, 0, 78, 19131, 'Phase 2', 'Warlock', 'Affliction', 'Feet', 'Both', 'Snowblind Shoes'), +(9, 0, 8, 0, 78, 11766, 'Phase 2', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Flameweave Cuffs'), +(9, 0, 9, 0, 78, 18407, 'Phase 2', 'Warlock', 'Affliction', 'Hands', 'Both', 'Felcloth Gloves'), +(9, 0, 10, 0, 78, 19147, 'Phase 2', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Ring of Spell Power'), +(9, 0, 11, 0, 78, 19147, 'Phase 2', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of Spell Power'), +(9, 0, 12, 0, 78, 12930, 'Phase 2', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Briarwood Reed'), +(9, 0, 13, 0, 78, 18820, 'Phase 2', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 0, 14, 0, 78, 13386, 'Phase 2', 'Warlock', 'Affliction', 'Back', 'Both', 'Archivist Cape'), +(9, 0, 15, 0, 78, 17103, 'Phase 2', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Azuresong Mageblade'), +(9, 0, 16, 0, 78, 10796, 'Phase 2', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Drakestone'), +(9, 0, 17, 0, 78, 13396, 'Phase 2', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 83, 19375, 'Phase 3', 'Warlock', 'Affliction', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(9, 0, 1, 0, 83, 18814, 'Phase 3', 'Warlock', 'Affliction', 'Neck', 'Both', 'Choker of the Fire Lord'), +(9, 0, 2, 0, 83, 19370, 'Phase 3', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(9, 0, 4, 0, 83, 19145, 'Phase 3', 'Warlock', 'Affliction', 'Chest', 'Both', 'Robe of Volatile Power'), +(9, 0, 5, 0, 83, 18809, 'Phase 3', 'Warlock', 'Affliction', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(9, 0, 6, 0, 83, 19133, 'Phase 3', 'Warlock', 'Affliction', 'Legs', 'Both', 'Fel Infused Leggings'), +(9, 0, 7, 0, 83, 19131, 'Phase 3', 'Warlock', 'Affliction', 'Feet', 'Both', 'Snowblind Shoes'), +(9, 0, 8, 0, 83, 19374, 'Phase 3', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(9, 0, 9, 0, 83, 19407, 'Phase 3', 'Warlock', 'Affliction', 'Hands', 'Both', 'Ebony Flame Gloves'), +(9, 0, 11, 0, 83, 19434, 'Phase 3', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Band of Dark Dominion'), +(9, 0, 12, 0, 83, 19379, 'Phase 3', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 0, 13, 0, 83, 18820, 'Phase 3', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 0, 14, 0, 83, 19378, 'Phase 3', 'Warlock', 'Affliction', 'Back', 'Both', 'Cloak of the Brood Lord'), +(9, 0, 15, 0, 83, 19356, 'Phase 3', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Staff of the Shadow Flame'), +(9, 0, 17, 0, 83, 13396, 'Phase 3', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 88, 21337, 'Phase 5', 'Warlock', 'Affliction', 'Head', 'Both', 'Doomcaller''s Circlet'), +(9, 0, 1, 0, 88, 21608, 'Phase 5', 'Warlock', 'Affliction', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(9, 0, 2, 0, 88, 21335, 'Phase 5', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Doomcaller''s Mantle'), +(9, 0, 4, 0, 88, 19682, 'Phase 5', 'Warlock', 'Affliction', 'Chest', 'Both', 'Bloodvine Vest'), +(9, 0, 5, 0, 88, 22730, 'Phase 5', 'Warlock', 'Affliction', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(9, 0, 6, 0, 88, 19683, 'Phase 5', 'Warlock', 'Affliction', 'Legs', 'Both', 'Bloodvine Leggings'), +(9, 0, 7, 0, 88, 19684, 'Phase 5', 'Warlock', 'Affliction', 'Feet', 'Both', 'Bloodvine Boots'), +(9, 0, 8, 0, 88, 21186, 'Phase 5', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Rockfury Bracers'), +(9, 0, 9, 0, 88, 21585, 'Phase 5', 'Warlock', 'Affliction', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(9, 0, 10, 0, 88, 21417, 'Phase 5', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Ring of Unspoken Names'), +(9, 0, 11, 0, 88, 21709, 'Phase 5', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of the Fallen God'), +(9, 0, 12, 0, 88, 19379, 'Phase 5', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 0, 13, 0, 88, 18820, 'Phase 5', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 0, 14, 0, 88, 22731, 'Phase 5', 'Warlock', 'Affliction', 'Back', 'Both', 'Cloak of the Devoured'), +(9, 0, 15, 0, 88, 21622, 'Phase 5', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(9, 0, 16, 0, 88, 21597, 'Phase 5', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(9, 0, 17, 0, 88, 21603, 'Phase 5', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 92, 22506, 'Phase 6', 'Warlock', 'Affliction', 'Head', 'Both', 'Plagueheart Circlet'), +(9, 0, 1, 0, 92, 21608, 'Phase 6', 'Warlock', 'Affliction', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(9, 0, 2, 0, 92, 22507, 'Phase 6', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Plagueheart Shoulderpads'), +(9, 0, 4, 0, 92, 22504, 'Phase 6', 'Warlock', 'Affliction', 'Chest', 'Both', 'Plagueheart Robe'), +(9, 0, 5, 0, 92, 22730, 'Phase 6', 'Warlock', 'Affliction', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(9, 0, 6, 0, 92, 23070, 'Phase 6', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of Polarity'), +(9, 0, 7, 0, 92, 22508, 'Phase 6', 'Warlock', 'Affliction', 'Feet', 'Both', 'Plagueheart Sandals'), +(9, 0, 8, 0, 92, 21186, 'Phase 6', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Rockfury Bracers'), +(9, 0, 9, 0, 92, 21585, 'Phase 6', 'Warlock', 'Affliction', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(9, 0, 10, 0, 92, 23031, 'Phase 6', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Band of the Inevitable'), +(9, 0, 11, 0, 92, 21709, 'Phase 6', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of the Fallen God'), +(9, 0, 12, 0, 92, 19379, 'Phase 6', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 0, 13, 0, 92, 23046, 'Phase 6', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(9, 0, 14, 0, 92, 23050, 'Phase 6', 'Warlock', 'Affliction', 'Back', 'Both', 'Cloak of the Necropolis'), +(9, 0, 15, 0, 92, 22807, 'Phase 6', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Wraith Blade'), +(9, 0, 16, 0, 92, 23049, 'Phase 6', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(9, 0, 17, 0, 92, 22820, 'Phase 6', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Wand of Fates'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 120, 24266, 'Pre-Raid', 'Warlock', 'Affliction', 'Head', 'Both', 'Spellstrike Hood'), +(9, 0, 1, 0, 120, 28134, 'Pre-Raid', 'Warlock', 'Affliction', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(9, 0, 2, 0, 120, 21869, 'Pre-Raid', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Frozen Shadoweave Shoulders'), +(9, 0, 4, 0, 120, 21871, 'Pre-Raid', 'Warlock', 'Affliction', 'Chest', 'Both', 'Frozen Shadoweave Robe'), +(9, 0, 5, 0, 120, 24256, 'Pre-Raid', 'Warlock', 'Affliction', 'Waist', 'Both', 'Girdle of Ruination'), +(9, 0, 6, 0, 120, 24262, 'Pre-Raid', 'Warlock', 'Affliction', 'Legs', 'Both', 'Spellstrike Pants'), +(9, 0, 7, 0, 120, 21870, 'Pre-Raid', 'Warlock', 'Affliction', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(9, 0, 8, 0, 120, 24250, 'Pre-Raid', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Bracers of Havok'), +(9, 0, 9, 0, 120, 24450, 'Pre-Raid', 'Warlock', 'Affliction', 'Hands', 'Both', 'Manaspark Gloves'), +(9, 0, 10, 0, 120, 29172, 'Pre-Raid', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Ashyen''s Gift'), +(9, 0, 11, 0, 120, 28227, 'Pre-Raid', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(9, 0, 12, 0, 120, 29370, 'Pre-Raid', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 0, 13, 0, 120, 29132, 'Pre-Raid', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Scryer''s Bloodgem'), +(9, 0, 14, 0, 120, 27981, 'Pre-Raid', 'Warlock', 'Affliction', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(9, 0, 15, 0, 120, 23554, 'Pre-Raid', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Eternium Runed Blade'), +(9, 0, 16, 0, 120, 29273, 'Pre-Raid', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Khadgar''s Knapsack'), +(9, 0, 17, 0, 120, 29350, 'Pre-Raid', 'Warlock', 'Affliction', 'Ranged', 'Both', 'The Black Stalk'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 125, 28963, 'Phase 1', 'Warlock', 'Affliction', 'Head', 'Both', 'Voidheart Crown'), +(9, 0, 1, 0, 125, 28530, 'Phase 1', 'Warlock', 'Affliction', 'Neck', 'Both', 'Brooch of Unquenchable Fury'), +(9, 0, 2, 0, 125, 28967, 'Phase 1', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Voidheart Mantle'), +(9, 0, 4, 0, 125, 28964, 'Phase 1', 'Warlock', 'Affliction', 'Chest', 'Both', 'Voidheart Robe'), +(9, 0, 5, 0, 125, 24256, 'Phase 1', 'Warlock', 'Affliction', 'Waist', 'Both', 'Girdle of Ruination'), +(9, 0, 6, 0, 125, 30734, 'Phase 1', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of the Seventh Circle'), +(9, 0, 7, 0, 125, 21870, 'Phase 1', 'Warlock', 'Affliction', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(9, 0, 8, 0, 125, 24250, 'Phase 1', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Bracers of Havok'), +(9, 0, 9, 0, 125, 28968, 'Phase 1', 'Warlock', 'Affliction', 'Hands', 'Both', 'Voidheart Gloves'), +(9, 0, 10, 0, 125, 29172, 'Phase 1', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Ashyen''s Gift'), +(9, 0, 11, 0, 125, 28793, 'Phase 1', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Band of Crimson Fury'), +(9, 0, 12, 0, 125, 29370, 'Phase 1', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 0, 13, 0, 125, 27683, 'Phase 1', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(9, 0, 14, 0, 125, 28766, 'Phase 1', 'Warlock', 'Affliction', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(9, 0, 15, 0, 125, 30723, 'Phase 1', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Talon of the Tempest'), +(9, 0, 16, 0, 125, 29272, 'Phase 1', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Orb of the Soul-Eater'), +(9, 0, 17, 0, 125, 28783, 'Phase 1', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 141, 32494, 'Phase 2', 'Warlock', 'Affliction', 'Head', 'Both', 'Destruction Holo-gogs'), +(9, 0, 1, 0, 141, 30015, 'Phase 2', 'Warlock', 'Affliction', 'Neck', 'Both', 'The Sun King''s Talisman'), +(9, 0, 2, 0, 141, 28967, 'Phase 2', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Voidheart Mantle'), +(9, 0, 4, 0, 141, 30107, 'Phase 2', 'Warlock', 'Affliction', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 0, 5, 0, 141, 30038, 'Phase 2', 'Warlock', 'Affliction', 'Waist', 'Both', 'Belt of Blasting'), +(9, 0, 6, 0, 141, 30213, 'Phase 2', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of the Corruptor'), +(9, 0, 7, 0, 141, 30037, 'Phase 2', 'Warlock', 'Affliction', 'Feet', 'Both', 'Boots of Blasting'), +(9, 0, 8, 0, 141, 29918, 'Phase 2', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Mindstorm Wristbands'), +(9, 0, 9, 0, 141, 28968, 'Phase 2', 'Warlock', 'Affliction', 'Hands', 'Both', 'Voidheart Gloves'), +(9, 0, 10, 0, 141, 29302, 'Phase 2', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Band of Eternity'), +(9, 0, 11, 0, 141, 30109, 'Phase 2', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of Endless Coils'), +(9, 0, 12, 0, 141, 29370, 'Phase 2', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 0, 13, 0, 141, 27683, 'Phase 2', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(9, 0, 14, 0, 141, 28766, 'Phase 2', 'Warlock', 'Affliction', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(9, 0, 15, 0, 141, 30095, 'Phase 2', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Fang of the Leviathan'), +(9, 0, 16, 0, 141, 30049, 'Phase 2', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Fathomstone'), +(9, 0, 17, 0, 141, 29982, 'Phase 2', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 156, 31051, 'Phase 3', 'Warlock', 'Affliction', 'Head', 'Both', 'Hood of the Malefic'), +(9, 0, 1, 0, 156, 30015, 'Phase 3', 'Warlock', 'Affliction', 'Neck', 'Both', 'Sun King''s Talisman'), +(9, 0, 2, 0, 156, 31054, 'Phase 3', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Mantle of the Malefic'), +(9, 0, 4, 0, 156, 30107, 'Phase 3', 'Warlock', 'Affliction', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 0, 5, 0, 156, 30888, 'Phase 3', 'Warlock', 'Affliction', 'Waist', 'Both', 'Anetheron''s Noose'), +(9, 0, 6, 0, 156, 31053, 'Phase 3', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of the Malefic'), +(9, 0, 7, 0, 156, 32239, 'Phase 3', 'Warlock', 'Affliction', 'Feet', 'Both', 'Slippers of the Seacaller'), +(9, 0, 8, 0, 156, 32586, 'Phase 3', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(9, 0, 9, 0, 156, 31050, 'Phase 3', 'Warlock', 'Affliction', 'Hands', 'Both', 'Gloves of the Malefic'), +(9, 0, 10, 0, 156, 32527, 'Phase 3', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(9, 0, 11, 0, 156, 32247, 'Phase 3', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of Captured Storms'), +(9, 0, 12, 0, 156, 29370, 'Phase 3', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 0, 13, 0, 156, 32483, 'Phase 3', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(9, 0, 14, 0, 156, 32590, 'Phase 3', 'Warlock', 'Affliction', 'Back', 'Both', 'Nethervoid Cloak'), +(9, 0, 15, 0, 156, 32374, 'Phase 3', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(9, 0, 17, 0, 156, 29982, 'Phase 3', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 164, 31051, 'Phase 4', 'Warlock', 'Affliction', 'Head', 'Both', 'Hood of the Malefic'), +(9, 0, 1, 0, 164, 30015, 'Phase 4', 'Warlock', 'Affliction', 'Neck', 'Both', 'Sun King''s Talisman'), +(9, 0, 2, 0, 164, 31054, 'Phase 4', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Mantle of the Malefic'), +(9, 0, 4, 0, 164, 30107, 'Phase 4', 'Warlock', 'Affliction', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 0, 5, 0, 164, 30888, 'Phase 4', 'Warlock', 'Affliction', 'Waist', 'Both', 'Anetheron''s Noose'), +(9, 0, 6, 0, 164, 31053, 'Phase 4', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of the Malefic'), +(9, 0, 7, 0, 164, 32239, 'Phase 4', 'Warlock', 'Affliction', 'Feet', 'Both', 'Slippers of the Seacaller'), +(9, 0, 8, 0, 164, 32586, 'Phase 4', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(9, 0, 9, 0, 164, 31050, 'Phase 4', 'Warlock', 'Affliction', 'Hands', 'Both', 'Gloves of the Malefic'), +(9, 0, 10, 0, 164, 33497, 'Phase 4', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Mana Attuned Band'), +(9, 0, 11, 0, 164, 32247, 'Phase 4', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of Captured Storms'), +(9, 0, 12, 0, 164, 33829, 'Phase 4', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Hex Shrunken Head'), +(9, 0, 13, 0, 164, 32483, 'Phase 4', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(9, 0, 14, 0, 164, 32590, 'Phase 4', 'Warlock', 'Affliction', 'Back', 'Both', 'Nethervoid Cloak'), +(9, 0, 15, 0, 164, 32374, 'Phase 4', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(9, 0, 16, 0, 164, 34179, 'Phase 5', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Heart of the Pit'), +(9, 0, 17, 0, 164, 33192, 'Phase 4', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Carved Witch Doctor''s Stick'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 200, 44910, 'Pre-Raid', 'Warlock', 'Affliction', 'Head', 'Both', 'Titan-Forged Hood of Dominance'), +(9, 0, 1, 0, 200, 40680, 'Pre-Raid', 'Warlock', 'Affliction', 'Neck', 'Both', 'Encircling Burnished Gold Chains'), +(9, 0, 2, 0, 200, 34210, 'Pre-Raid', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Amice of the Convoker'), +(9, 0, 4, 0, 200, 39497, 'Pre-Raid', 'Warlock', 'Affliction', 'Chest', 'Both', 'Heroes'' Plagueheart Robe'), +(9, 0, 5, 0, 200, 40696, 'Pre-Raid', 'Warlock', 'Affliction', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(9, 0, 6, 0, 200, 34181, 'Pre-Raid', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of Calamity'), +(9, 0, 7, 0, 200, 44202, 'Pre-Raid', 'Warlock', 'Affliction', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(9, 0, 8, 0, 200, 37361, 'Pre-Raid', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(9, 0, 9, 0, 200, 42113, 'Pre-Raid', 'Warlock', 'Affliction', 'Hands', 'Both', 'Spellweave Gloves'), +(9, 0, 10, 0, 200, 43253, 'Pre-Raid', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Ring of Northern Tears'), +(9, 0, 12, 0, 200, 40682, 'Pre-Raid', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Sundial of the Exiled'), +(9, 0, 14, 0, 200, 41610, 'Pre-Raid', 'Warlock', 'Affliction', 'Back', 'Both', 'Deathchill Cloak'), +(9, 0, 17, 0, 200, 37177, 'Pre-Raid', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Wand of the San''layn'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 224, 40421, 'Phase 1', 'Warlock', 'Affliction', 'Head', 'Both', 'Valorous Plagueheart Circlet'), +(9, 0, 1, 0, 224, 44661, 'Phase 1', 'Warlock', 'Affliction', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(9, 0, 2, 0, 224, 40424, 'Phase 1', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Valorous Plagueheart Shoulderpads'), +(9, 0, 4, 0, 224, 40423, 'Phase 1', 'Warlock', 'Affliction', 'Chest', 'Both', 'Valorous Plagueheart Robe'), +(9, 0, 5, 0, 224, 40561, 'Phase 1', 'Warlock', 'Affliction', 'Waist', 'Both', 'Leash of Heedless Magic'), +(9, 0, 6, 0, 224, 40560, 'Phase 1', 'Warlock', 'Affliction', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(9, 0, 7, 0, 224, 40558, 'Phase 1', 'Warlock', 'Affliction', 'Feet', 'Both', 'Arcanic Tramplers'), +(9, 0, 8, 0, 224, 44008, 'Phase 1', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Unsullied Cuffs'), +(9, 0, 9, 0, 224, 40420, 'Phase 1', 'Warlock', 'Affliction', 'Hands', 'Both', 'Valorous Plagueheart Gloves'), +(9, 0, 10, 0, 224, 40399, 'Phase 1', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(9, 0, 12, 0, 224, 40432, 'Phase 1', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Illustration of the Dragon Soul'), +(9, 0, 14, 0, 224, 44005, 'Phase 1', 'Warlock', 'Affliction', 'Back', 'Both', 'Pennant Cloak'), +(9, 0, 15, 0, 224, 40396, 'Phase 1', 'Warlock', 'Affliction', 'MainHand', 'Both', 'The Turning Tide'), +(9, 0, 17, 0, 224, 39712, 'Phase 1', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Gemmed Wand of the Nerubians'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 245, 45497, 'Phase 2', 'Warlock', 'Affliction', 'Head', 'Both', 'Crown of Luminescence'), +(9, 0, 1, 0, 245, 45133, 'Phase 2', 'Warlock', 'Affliction', 'Neck', 'Both', 'Pendant of Fiery Havoc'), +(9, 0, 2, 0, 245, 46068, 'Phase 2', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Amice of Inconceivable Horror'), +(9, 0, 4, 0, 245, 46137, 'Phase 2', 'Warlock', 'Affliction', 'Chest', 'Both', 'Conqueror''s Deathbringer Robe'), +(9, 0, 9, 0, 245, 45665, 'Phase 2', 'Warlock', 'Affliction', 'Hands', 'Both', 'Pharos Gloves'), +(9, 0, 10, 0, 245, 45495, 'Phase 2', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Conductive Seal'), +(9, 0, 14, 0, 245, 45618, 'Phase 2', 'Warlock', 'Affliction', 'Back', 'Both', 'Sunglimmer Cloak'), +(9, 0, 15, 0, 245, 45620, 'Phase 2', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Starshard Edge'), +(9, 0, 17, 0, 245, 45294, 'Phase 2', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 10, 0, 258, 45495, 'Phase 3', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Conductive Seal'), +(9, 0, 17, 0, 258, 45294, 'Phase 3', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 0, 0, 0, 290, 51231, 'Phase 5', 'Warlock', 'Affliction', 'Head', 'Both', 'Sanctified Dark Coven Hood'), +(9, 0, 1, 0, 290, 50182, 'Phase 5', 'Warlock', 'Affliction', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(9, 0, 2, 0, 290, 51234, 'Phase 5', 'Warlock', 'Affliction', 'Shoulders', 'Both', 'Sanctified Dark Coven Shoulderpads'), +(9, 0, 4, 0, 290, 51233, 'Phase 5', 'Warlock', 'Affliction', 'Chest', 'Both', 'Sanctified Dark Coven Robe'), +(9, 0, 5, 0, 290, 50613, 'Phase 5', 'Warlock', 'Affliction', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(9, 0, 6, 0, 290, 50694, 'Phase 5', 'Warlock', 'Affliction', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(9, 0, 7, 0, 290, 50699, 'Phase 5', 'Warlock', 'Affliction', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(9, 0, 8, 0, 290, 54582, 'Phase 5', 'Warlock', 'Affliction', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(9, 0, 9, 0, 290, 51230, 'Phase 5', 'Warlock', 'Affliction', 'Hands', 'Both', 'Sanctified Dark Coven Gloves'), +(9, 0, 10, 0, 290, 50614, 'Phase 5', 'Warlock', 'Affliction', 'Finger1', 'Both', 'Loop of the Endless Labyrinth'), +(9, 0, 11, 0, 290, 50664, 'Phase 5', 'Warlock', 'Affliction', 'Finger2', 'Both', 'Ring of Rapid Ascent'), +(9, 0, 12, 0, 290, 54588, 'Phase 5', 'Warlock', 'Affliction', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(9, 0, 13, 0, 290, 50365, 'Phase 5', 'Warlock', 'Affliction', 'Trinket2', 'Both', 'Phylactery of the Nameless Lich'), +(9, 0, 14, 0, 290, 54583, 'Phase 5', 'Warlock', 'Affliction', 'Back', 'Both', 'Cloak of Burning Dusk'), +(9, 0, 15, 0, 290, 50732, 'Phase 5', 'Warlock', 'Affliction', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(9, 0, 16, 0, 290, 50719, 'Phase 5', 'Warlock', 'Affliction', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(9, 0, 17, 0, 290, 50684, 'Phase 5', 'Warlock', 'Affliction', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- Demonology (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 1, 66, 10504, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Head', 'Alliance', 'Green Lens'), +(9, 1, 0, 2, 66, 10504, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Head', 'Horde', 'Green Lens'), +(9, 1, 1, 1, 66, 18691, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(9, 1, 1, 2, 66, 18691, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(9, 1, 2, 1, 66, 14112, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Shoulders', 'Alliance', 'Felcloth Shoulders'), +(9, 1, 2, 2, 66, 14112, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Shoulders', 'Horde', 'Felcloth Shoulders'), +(9, 1, 4, 1, 66, 14153, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Chest', 'Alliance', 'Robe of the Void'), +(9, 1, 4, 2, 66, 14153, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Chest', 'Horde', 'Robe of the Void'), +(9, 1, 5, 1, 66, 11662, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Waist', 'Alliance', 'Ban''thok Sash'), +(9, 1, 5, 2, 66, 11662, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Waist', 'Horde', 'Ban''thok Sash'), +(9, 1, 6, 1, 66, 13170, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(9, 1, 6, 2, 66, 13170, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Legs', 'Horde', 'Skyshroud Leggings'), +(9, 1, 7, 1, 66, 18735, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(9, 1, 7, 2, 66, 18735, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(9, 1, 8, 1, 66, 11766, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(9, 1, 8, 2, 66, 11766, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(9, 1, 9, 1, 66, 13253, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Hands', 'Alliance', 'Hands of Power'), +(9, 1, 9, 2, 66, 13253, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Hands', 'Horde', 'Hands of Power'), +(9, 1, 10, 1, 66, 12543, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(9, 1, 10, 2, 66, 12545, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(9, 1, 11, 1, 66, 13001, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(9, 1, 11, 2, 66, 13001, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger2', 'Horde', 'Maiden''s Circle'), +(9, 1, 12, 1, 66, 12930, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(9, 1, 12, 2, 66, 12930, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket1', 'Horde', 'Briarwood Reed'), +(9, 1, 13, 1, 66, 13968, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket2', 'Alliance', 'Eye of the Beast'), +(9, 1, 13, 2, 66, 13968, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket2', 'Horde', 'Eye of the Beast'), +(9, 1, 14, 1, 66, 13386, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Back', 'Alliance', 'Archivist Cape'), +(9, 1, 14, 2, 66, 13386, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Back', 'Horde', 'Archivist Cape'), +(9, 1, 15, 1, 66, 13964, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'MainHand', 'Alliance', 'Witchblade'), +(9, 1, 15, 2, 66, 13964, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'MainHand', 'Horde', 'Witchblade'), +(9, 1, 16, 1, 66, 10796, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'OffHand', 'Alliance', 'Drakestone'), +(9, 1, 16, 2, 66, 10796, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'OffHand', 'Horde', 'Drakestone'), +(9, 1, 17, 1, 66, 13396, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(9, 1, 17, 2, 66, 13396, 'Phase 1 (Pre-Raid)', 'Warlock', 'Demonology', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 1, 76, 23310, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Head', 'Alliance', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 1, 0, 2, 76, 23310, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Head', 'Horde', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 1, 1, 1, 76, 18691, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(9, 1, 1, 2, 76, 18691, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(9, 1, 2, 1, 76, 23311, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Shoulders', 'Alliance', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 1, 2, 2, 76, 23311, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Shoulders', 'Horde', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 1, 4, 1, 76, 14153, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Chest', 'Alliance', 'Robe of the Void'), +(9, 1, 4, 2, 76, 14153, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Chest', 'Horde', 'Robe of the Void'), +(9, 1, 5, 1, 76, 11662, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Waist', 'Alliance', 'Ban''thok Sash'), +(9, 1, 5, 2, 76, 11662, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Waist', 'Horde', 'Ban''thok Sash'), +(9, 1, 6, 1, 76, 13170, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(9, 1, 6, 2, 76, 13170, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Legs', 'Horde', 'Skyshroud Leggings'), +(9, 1, 7, 1, 76, 18735, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(9, 1, 7, 2, 76, 18735, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(9, 1, 8, 1, 76, 11766, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(9, 1, 8, 2, 76, 11766, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(9, 1, 9, 1, 76, 18407, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Hands', 'Alliance', 'Felcloth Gloves'), +(9, 1, 9, 2, 76, 18407, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Hands', 'Horde', 'Felcloth Gloves'), +(9, 1, 10, 1, 76, 12543, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(9, 1, 10, 2, 76, 12545, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(9, 1, 11, 1, 76, 13001, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(9, 1, 11, 2, 76, 13001, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Finger2', 'Horde', 'Maiden''s Circle'), +(9, 1, 12, 1, 76, 12930, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(9, 1, 12, 2, 76, 12930, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket1', 'Horde', 'Briarwood Reed'), +(9, 1, 13, 1, 76, 18467, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket2', 'Alliance', 'Royal Seal of Eldre''Thalas'), +(9, 1, 13, 2, 76, 18467, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Trinket2', 'Horde', 'Royal Seal of Eldre''Thalas'), +(9, 1, 14, 1, 76, 13386, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Back', 'Alliance', 'Archivist Cape'), +(9, 1, 14, 2, 76, 13386, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Back', 'Horde', 'Archivist Cape'), +(9, 1, 15, 1, 76, 18372, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'MainHand', 'Alliance', 'Blade of the New Moon'), +(9, 1, 15, 2, 76, 18372, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'MainHand', 'Horde', 'Blade of the New Moon'), +(9, 1, 16, 1, 76, 10796, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'OffHand', 'Alliance', 'Drakestone'), +(9, 1, 16, 2, 76, 10796, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'OffHand', 'Horde', 'Drakestone'), +(9, 1, 17, 1, 76, 13396, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(9, 1, 17, 2, 76, 13396, 'Phase 2 (Pre-Raid)', 'Warlock', 'Demonology', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 78, 23310, 'Phase 2', 'Warlock', 'Demonology', 'Head', 'Both', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 1, 1, 0, 78, 18814, 'Phase 2', 'Warlock', 'Demonology', 'Neck', 'Both', 'Choker of the Fire Lord'), +(9, 1, 2, 0, 78, 23311, 'Phase 2', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 1, 4, 0, 78, 19145, 'Phase 2', 'Warlock', 'Demonology', 'Chest', 'Both', 'Robe of Volatile Power'), +(9, 1, 5, 0, 78, 18809, 'Phase 2', 'Warlock', 'Demonology', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(9, 1, 6, 0, 78, 19133, 'Phase 2', 'Warlock', 'Demonology', 'Legs', 'Both', 'Fel Infused Leggings'), +(9, 1, 7, 0, 78, 19131, 'Phase 2', 'Warlock', 'Demonology', 'Feet', 'Both', 'Snowblind Shoes'), +(9, 1, 8, 0, 78, 11766, 'Phase 2', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Flameweave Cuffs'), +(9, 1, 9, 0, 78, 18407, 'Phase 2', 'Warlock', 'Demonology', 'Hands', 'Both', 'Felcloth Gloves'), +(9, 1, 10, 0, 78, 19147, 'Phase 2', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Ring of Spell Power'), +(9, 1, 11, 0, 78, 19147, 'Phase 2', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of Spell Power'), +(9, 1, 12, 0, 78, 12930, 'Phase 2', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Briarwood Reed'), +(9, 1, 13, 0, 78, 18820, 'Phase 2', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 1, 14, 0, 78, 13386, 'Phase 2', 'Warlock', 'Demonology', 'Back', 'Both', 'Archivist Cape'), +(9, 1, 15, 0, 78, 17103, 'Phase 2', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Azuresong Mageblade'), +(9, 1, 16, 0, 78, 10796, 'Phase 2', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Drakestone'), +(9, 1, 17, 0, 78, 13396, 'Phase 2', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 83, 19375, 'Phase 3', 'Warlock', 'Demonology', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(9, 1, 1, 0, 83, 18814, 'Phase 3', 'Warlock', 'Demonology', 'Neck', 'Both', 'Choker of the Fire Lord'), +(9, 1, 2, 0, 83, 19370, 'Phase 3', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(9, 1, 4, 0, 83, 19145, 'Phase 3', 'Warlock', 'Demonology', 'Chest', 'Both', 'Robe of Volatile Power'), +(9, 1, 5, 0, 83, 18809, 'Phase 3', 'Warlock', 'Demonology', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(9, 1, 6, 0, 83, 19133, 'Phase 3', 'Warlock', 'Demonology', 'Legs', 'Both', 'Fel Infused Leggings'), +(9, 1, 7, 0, 83, 19131, 'Phase 3', 'Warlock', 'Demonology', 'Feet', 'Both', 'Snowblind Shoes'), +(9, 1, 8, 0, 83, 19374, 'Phase 3', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(9, 1, 9, 0, 83, 19407, 'Phase 3', 'Warlock', 'Demonology', 'Hands', 'Both', 'Ebony Flame Gloves'), +(9, 1, 11, 0, 83, 19434, 'Phase 3', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Band of Dark Dominion'), +(9, 1, 12, 0, 83, 19379, 'Phase 3', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 1, 13, 0, 83, 18820, 'Phase 3', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 1, 14, 0, 83, 19378, 'Phase 3', 'Warlock', 'Demonology', 'Back', 'Both', 'Cloak of the Brood Lord'), +(9, 1, 15, 0, 83, 19356, 'Phase 3', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Staff of the Shadow Flame'), +(9, 1, 17, 0, 83, 13396, 'Phase 3', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 88, 21337, 'Phase 5', 'Warlock', 'Demonology', 'Head', 'Both', 'Doomcaller''s Circlet'), +(9, 1, 1, 0, 88, 21608, 'Phase 5', 'Warlock', 'Demonology', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(9, 1, 2, 0, 88, 21335, 'Phase 5', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Doomcaller''s Mantle'), +(9, 1, 4, 0, 88, 19682, 'Phase 5', 'Warlock', 'Demonology', 'Chest', 'Both', 'Bloodvine Vest'), +(9, 1, 5, 0, 88, 22730, 'Phase 5', 'Warlock', 'Demonology', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(9, 1, 6, 0, 88, 19683, 'Phase 5', 'Warlock', 'Demonology', 'Legs', 'Both', 'Bloodvine Leggings'), +(9, 1, 7, 0, 88, 19684, 'Phase 5', 'Warlock', 'Demonology', 'Feet', 'Both', 'Bloodvine Boots'), +(9, 1, 8, 0, 88, 21186, 'Phase 5', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Rockfury Bracers'), +(9, 1, 9, 0, 88, 21585, 'Phase 5', 'Warlock', 'Demonology', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(9, 1, 10, 0, 88, 21417, 'Phase 5', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Ring of Unspoken Names'), +(9, 1, 11, 0, 88, 21709, 'Phase 5', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of the Fallen God'), +(9, 1, 12, 0, 88, 19379, 'Phase 5', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 1, 13, 0, 88, 18820, 'Phase 5', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 1, 14, 0, 88, 22731, 'Phase 5', 'Warlock', 'Demonology', 'Back', 'Both', 'Cloak of the Devoured'), +(9, 1, 15, 0, 88, 21622, 'Phase 5', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(9, 1, 16, 0, 88, 21597, 'Phase 5', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(9, 1, 17, 0, 88, 21603, 'Phase 5', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 92, 22506, 'Phase 6', 'Warlock', 'Demonology', 'Head', 'Both', 'Plagueheart Circlet'), +(9, 1, 1, 0, 92, 21608, 'Phase 6', 'Warlock', 'Demonology', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(9, 1, 2, 0, 92, 22507, 'Phase 6', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Plagueheart Shoulderpads'), +(9, 1, 4, 0, 92, 22504, 'Phase 6', 'Warlock', 'Demonology', 'Chest', 'Both', 'Plagueheart Robe'), +(9, 1, 5, 0, 92, 22730, 'Phase 6', 'Warlock', 'Demonology', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(9, 1, 6, 0, 92, 23070, 'Phase 6', 'Warlock', 'Demonology', 'Legs', 'Both', 'Leggings of Polarity'), +(9, 1, 7, 0, 92, 22508, 'Phase 6', 'Warlock', 'Demonology', 'Feet', 'Both', 'Plagueheart Sandals'), +(9, 1, 8, 0, 92, 21186, 'Phase 6', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Rockfury Bracers'), +(9, 1, 9, 0, 92, 21585, 'Phase 6', 'Warlock', 'Demonology', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(9, 1, 10, 0, 92, 23031, 'Phase 6', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Band of the Inevitable'), +(9, 1, 11, 0, 92, 21709, 'Phase 6', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of the Fallen God'), +(9, 1, 12, 0, 92, 19379, 'Phase 6', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 1, 13, 0, 92, 23046, 'Phase 6', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(9, 1, 14, 0, 92, 23050, 'Phase 6', 'Warlock', 'Demonology', 'Back', 'Both', 'Cloak of the Necropolis'), +(9, 1, 15, 0, 92, 22807, 'Phase 6', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Wraith Blade'), +(9, 1, 16, 0, 92, 23049, 'Phase 6', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(9, 1, 17, 0, 92, 22820, 'Phase 6', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Wand of Fates'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 120, 24266, 'Pre-Raid', 'Warlock', 'Demonology', 'Head', 'Both', 'Spellstrike Hood'), +(9, 1, 1, 0, 120, 28134, 'Pre-Raid', 'Warlock', 'Demonology', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(9, 1, 2, 0, 120, 21869, 'Pre-Raid', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Frozen Shadoweave Shoulders'), +(9, 1, 4, 0, 120, 21871, 'Pre-Raid', 'Warlock', 'Demonology', 'Chest', 'Both', 'Frozen Shadoweave Robe'), +(9, 1, 5, 0, 120, 24256, 'Pre-Raid', 'Warlock', 'Demonology', 'Waist', 'Both', 'Girdle of Ruination'), +(9, 1, 6, 0, 120, 24262, 'Pre-Raid', 'Warlock', 'Demonology', 'Legs', 'Both', 'Spellstrike Pants'), +(9, 1, 7, 0, 120, 21870, 'Pre-Raid', 'Warlock', 'Demonology', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(9, 1, 8, 0, 120, 24250, 'Pre-Raid', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Bracers of Havok'), +(9, 1, 9, 0, 120, 24450, 'Pre-Raid', 'Warlock', 'Demonology', 'Hands', 'Both', 'Manaspark Gloves'), +(9, 1, 10, 0, 120, 29172, 'Pre-Raid', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Ashyen''s Gift'), +(9, 1, 11, 0, 120, 28227, 'Pre-Raid', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(9, 1, 12, 0, 120, 29370, 'Pre-Raid', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 1, 13, 0, 120, 29132, 'Pre-Raid', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Scryer''s Bloodgem'), +(9, 1, 14, 0, 120, 27981, 'Pre-Raid', 'Warlock', 'Demonology', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(9, 1, 15, 0, 120, 23554, 'Pre-Raid', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Eternium Runed Blade'), +(9, 1, 16, 0, 120, 29273, 'Pre-Raid', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Khadgar''s Knapsack'), +(9, 1, 17, 0, 120, 29350, 'Pre-Raid', 'Warlock', 'Demonology', 'Ranged', 'Both', 'The Black Stalk'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 125, 28963, 'Phase 1', 'Warlock', 'Demonology', 'Head', 'Both', 'Voidheart Crown'), +(9, 1, 1, 0, 125, 28530, 'Phase 1', 'Warlock', 'Demonology', 'Neck', 'Both', 'Brooch of Unquenchable Fury'), +(9, 1, 2, 0, 125, 28967, 'Phase 1', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Voidheart Mantle'), +(9, 1, 4, 0, 125, 28964, 'Phase 1', 'Warlock', 'Demonology', 'Chest', 'Both', 'Voidheart Robe'), +(9, 1, 5, 0, 125, 24256, 'Phase 1', 'Warlock', 'Demonology', 'Waist', 'Both', 'Girdle of Ruination'), +(9, 1, 6, 0, 125, 30734, 'Phase 1', 'Warlock', 'Demonology', 'Legs', 'Both', 'Leggings of the Seventh Circle'), +(9, 1, 7, 0, 125, 21870, 'Phase 1', 'Warlock', 'Demonology', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(9, 1, 8, 0, 125, 24250, 'Phase 1', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Bracers of Havok'), +(9, 1, 9, 0, 125, 28968, 'Phase 1', 'Warlock', 'Demonology', 'Hands', 'Both', 'Voidheart Gloves'), +(9, 1, 10, 0, 125, 29172, 'Phase 1', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Ashyen''s Gift'), +(9, 1, 11, 0, 125, 28793, 'Phase 1', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Band of Crimson Fury'), +(9, 1, 12, 0, 125, 29370, 'Phase 1', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 1, 13, 0, 125, 27683, 'Phase 1', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(9, 1, 14, 0, 125, 28766, 'Phase 1', 'Warlock', 'Demonology', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(9, 1, 15, 0, 125, 30723, 'Phase 1', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Talon of the Tempest'), +(9, 1, 16, 0, 125, 29272, 'Phase 1', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Orb of the Soul-Eater'), +(9, 1, 17, 0, 125, 28783, 'Phase 1', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 141, 32494, 'Phase 2', 'Warlock', 'Demonology', 'Head', 'Both', 'Destruction Holo-gogs'), +(9, 1, 1, 0, 141, 30015, 'Phase 2', 'Warlock', 'Demonology', 'Neck', 'Both', 'The Sun King''s Talisman'), +(9, 1, 2, 0, 141, 28967, 'Phase 2', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Voidheart Mantle'), +(9, 1, 4, 0, 141, 30107, 'Phase 2', 'Warlock', 'Demonology', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 1, 5, 0, 141, 30038, 'Phase 2', 'Warlock', 'Demonology', 'Waist', 'Both', 'Belt of Blasting'), +(9, 1, 6, 0, 141, 30213, 'Phase 2', 'Warlock', 'Demonology', 'Legs', 'Both', 'Leggings of the Corruptor'), +(9, 1, 7, 0, 141, 30037, 'Phase 2', 'Warlock', 'Demonology', 'Feet', 'Both', 'Boots of Blasting'), +(9, 1, 8, 0, 141, 29918, 'Phase 2', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Mindstorm Wristbands'), +(9, 1, 9, 0, 141, 28968, 'Phase 2', 'Warlock', 'Demonology', 'Hands', 'Both', 'Voidheart Gloves'), +(9, 1, 10, 0, 141, 29302, 'Phase 2', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Band of Eternity'), +(9, 1, 11, 0, 141, 30109, 'Phase 2', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of Endless Coils'), +(9, 1, 12, 0, 141, 29370, 'Phase 2', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 1, 13, 0, 141, 27683, 'Phase 2', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(9, 1, 14, 0, 141, 28766, 'Phase 2', 'Warlock', 'Demonology', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(9, 1, 15, 0, 141, 30095, 'Phase 2', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Fang of the Leviathan'), +(9, 1, 16, 0, 141, 30049, 'Phase 2', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Fathomstone'), +(9, 1, 17, 0, 141, 29982, 'Phase 2', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 156, 31051, 'Phase 3', 'Warlock', 'Demonology', 'Head', 'Both', 'Hood of the Malefic'), +(9, 1, 1, 0, 156, 30015, 'Phase 3', 'Warlock', 'Demonology', 'Neck', 'Both', 'Sun King''s Talisman'), +(9, 1, 2, 0, 156, 31054, 'Phase 3', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Mantle of the Malefic'), +(9, 1, 4, 0, 156, 30107, 'Phase 3', 'Warlock', 'Demonology', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 1, 5, 0, 156, 30888, 'Phase 3', 'Warlock', 'Demonology', 'Waist', 'Both', 'Anetheron''s Noose'), +(9, 1, 6, 0, 156, 31053, 'Phase 3', 'Warlock', 'Demonology', 'Legs', 'Both', 'Leggings of the Malefic'), +(9, 1, 7, 0, 156, 32239, 'Phase 3', 'Warlock', 'Demonology', 'Feet', 'Both', 'Slippers of the Seacaller'), +(9, 1, 8, 0, 156, 32586, 'Phase 3', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(9, 1, 9, 0, 156, 31050, 'Phase 3', 'Warlock', 'Demonology', 'Hands', 'Both', 'Gloves of the Malefic'), +(9, 1, 10, 0, 156, 32527, 'Phase 3', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(9, 1, 11, 0, 156, 32247, 'Phase 3', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of Captured Storms'), +(9, 1, 12, 0, 156, 29370, 'Phase 3', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 1, 13, 0, 156, 32483, 'Phase 3', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(9, 1, 14, 0, 156, 32590, 'Phase 3', 'Warlock', 'Demonology', 'Back', 'Both', 'Nethervoid Cloak'), +(9, 1, 15, 0, 156, 32374, 'Phase 3', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(9, 1, 17, 0, 156, 29982, 'Phase 3', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 164, 31051, 'Phase 4', 'Warlock', 'Demonology', 'Head', 'Both', 'Hood of the Malefic'), +(9, 1, 1, 0, 164, 30015, 'Phase 4', 'Warlock', 'Demonology', 'Neck', 'Both', 'Sun King''s Talisman'), +(9, 1, 2, 0, 164, 31054, 'Phase 4', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Mantle of the Malefic'), +(9, 1, 4, 0, 164, 30107, 'Phase 4', 'Warlock', 'Demonology', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 1, 5, 0, 164, 30888, 'Phase 4', 'Warlock', 'Demonology', 'Waist', 'Both', 'Anetheron''s Noose'), +(9, 1, 6, 0, 164, 31053, 'Phase 4', 'Warlock', 'Demonology', 'Legs', 'Both', 'Leggings of the Malefic'), +(9, 1, 7, 0, 164, 32239, 'Phase 4', 'Warlock', 'Demonology', 'Feet', 'Both', 'Slippers of the Seacaller'), +(9, 1, 8, 0, 164, 32586, 'Phase 4', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(9, 1, 9, 0, 164, 31050, 'Phase 4', 'Warlock', 'Demonology', 'Hands', 'Both', 'Gloves of the Malefic'), +(9, 1, 10, 0, 164, 33497, 'Phase 4', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Mana Attuned Band'), +(9, 1, 11, 0, 164, 32247, 'Phase 4', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of Captured Storms'), +(9, 1, 12, 0, 164, 33829, 'Phase 4', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Hex Shrunken Head'), +(9, 1, 13, 0, 164, 32483, 'Phase 4', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(9, 1, 14, 0, 164, 32590, 'Phase 4', 'Warlock', 'Demonology', 'Back', 'Both', 'Nethervoid Cloak'), +(9, 1, 15, 0, 164, 32374, 'Phase 4', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(9, 1, 16, 0, 164, 34179, 'Phase 5', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Heart of the Pit'), +(9, 1, 17, 0, 164, 33192, 'Phase 4', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Carved Witch Doctor''s Stick'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 200, 37684, 'Pre-Raid', 'Warlock', 'Demonology', 'Head', 'Both', 'Forgotten Shadow Hood'), +(9, 1, 1, 0, 200, 40680, 'Pre-Raid', 'Warlock', 'Demonology', 'Neck', 'Both', 'Encircling Burnished Gold Chains'), +(9, 1, 2, 0, 200, 37196, 'Pre-Raid', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Runecaster''s Mantle'), +(9, 1, 4, 0, 200, 39497, 'Pre-Raid', 'Warlock', 'Demonology', 'Chest', 'Both', 'Heroes'' Plagueheart Robe'), +(9, 1, 5, 0, 200, 37408, 'Pre-Raid', 'Warlock', 'Demonology', 'Waist', 'Both', 'Girdle of Bane'), +(9, 1, 6, 0, 200, 34170, 'Pre-Raid', 'Warlock', 'Demonology', 'Legs', 'Both', 'Pantaloons of Calming Strife'), +(9, 1, 7, 0, 200, 44202, 'Pre-Raid', 'Warlock', 'Demonology', 'Feet', 'Both', 'Sandals of Crimson Fury'), +(9, 1, 8, 0, 200, 37361, 'Pre-Raid', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(9, 1, 9, 0, 200, 42113, 'Pre-Raid', 'Warlock', 'Demonology', 'Hands', 'Both', 'Spellweave Gloves'), +(9, 1, 10, 0, 200, 40585, 'Pre-Raid', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Signet of the Kirin Tor'), +(9, 1, 12, 0, 200, 40682, 'Pre-Raid', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Sundial of the Exiled'), +(9, 1, 14, 0, 200, 41610, 'Pre-Raid', 'Warlock', 'Demonology', 'Back', 'Both', 'Deathchill Cloak'), +(9, 1, 17, 0, 200, 34348, 'Pre-Raid', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Wand of Cleansing Light'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 224, 40421, 'Phase 1', 'Warlock', 'Demonology', 'Head', 'Both', 'Valorous Plagueheart Circlet'), +(9, 1, 1, 0, 224, 44661, 'Phase 1', 'Warlock', 'Demonology', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(9, 1, 2, 0, 224, 40424, 'Phase 1', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Valorous Plagueheart Shoulderpads'), +(9, 1, 4, 0, 224, 40423, 'Phase 1', 'Warlock', 'Demonology', 'Chest', 'Both', 'Valorous Plagueheart Robe'), +(9, 1, 5, 0, 224, 40561, 'Phase 1', 'Warlock', 'Demonology', 'Waist', 'Both', 'Leash of Heedless Magic'), +(9, 1, 6, 0, 224, 40560, 'Phase 1', 'Warlock', 'Demonology', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(9, 1, 7, 0, 224, 40558, 'Phase 1', 'Warlock', 'Demonology', 'Feet', 'Both', 'Arcanic Tramplers'), +(9, 1, 8, 0, 224, 44008, 'Phase 1', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Unsullied Cuffs'), +(9, 1, 9, 0, 224, 40420, 'Phase 1', 'Warlock', 'Demonology', 'Hands', 'Both', 'Valorous Plagueheart Gloves'), +(9, 1, 10, 0, 224, 40399, 'Phase 1', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(9, 1, 12, 0, 224, 40432, 'Phase 1', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Illustration of the Dragon Soul'), +(9, 1, 14, 0, 224, 44005, 'Phase 1', 'Warlock', 'Demonology', 'Back', 'Both', 'Pennant Cloak'), +(9, 1, 15, 0, 224, 40396, 'Phase 1', 'Warlock', 'Demonology', 'MainHand', 'Both', 'The Turning Tide'), +(9, 1, 17, 0, 224, 39426, 'Phase 1', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Wand of the Archlich'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 245, 45497, 'Phase 2', 'Warlock', 'Demonology', 'Head', 'Both', 'Crown of Luminescence'), +(9, 1, 1, 0, 245, 45133, 'Phase 2', 'Warlock', 'Demonology', 'Neck', 'Both', 'Pendant of Fiery Havoc'), +(9, 1, 2, 0, 245, 46068, 'Phase 2', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Amice of Inconceivable Horror'), +(9, 1, 4, 0, 245, 46137, 'Phase 2', 'Warlock', 'Demonology', 'Chest', 'Both', 'Conqueror''s Deathbringer Robe'), +(9, 1, 9, 0, 245, 45665, 'Phase 2', 'Warlock', 'Demonology', 'Hands', 'Both', 'Pharos Gloves'), +(9, 1, 10, 0, 245, 45495, 'Phase 2', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Conductive Seal'), +(9, 1, 14, 0, 245, 45618, 'Phase 2', 'Warlock', 'Demonology', 'Back', 'Both', 'Sunglimmer Cloak'), +(9, 1, 15, 0, 245, 45620, 'Phase 2', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Starshard Edge'), +(9, 1, 17, 0, 245, 45294, 'Phase 2', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 10, 0, 258, 45495, 'Phase 3', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Conductive Seal'), +(9, 1, 17, 0, 258, 45294, 'Phase 3', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 1, 0, 0, 290, 51231, 'Phase 5', 'Warlock', 'Demonology', 'Head', 'Both', 'Sanctified Dark Coven Hood'), +(9, 1, 1, 0, 290, 50182, 'Phase 5', 'Warlock', 'Demonology', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(9, 1, 2, 0, 290, 51234, 'Phase 5', 'Warlock', 'Demonology', 'Shoulders', 'Both', 'Sanctified Dark Coven Shoulderpads'), +(9, 1, 4, 0, 290, 51233, 'Phase 5', 'Warlock', 'Demonology', 'Chest', 'Both', 'Sanctified Dark Coven Robe'), +(9, 1, 5, 0, 290, 50613, 'Phase 5', 'Warlock', 'Demonology', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(9, 1, 6, 0, 290, 50694, 'Phase 5', 'Warlock', 'Demonology', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(9, 1, 7, 0, 290, 50699, 'Phase 5', 'Warlock', 'Demonology', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(9, 1, 8, 0, 290, 54582, 'Phase 5', 'Warlock', 'Demonology', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(9, 1, 9, 0, 290, 51230, 'Phase 5', 'Warlock', 'Demonology', 'Hands', 'Both', 'Sanctified Dark Coven Gloves'), +(9, 1, 10, 0, 290, 50614, 'Phase 5', 'Warlock', 'Demonology', 'Finger1', 'Both', 'Loop of the Endless Labyrinth'), +(9, 1, 11, 0, 290, 50664, 'Phase 5', 'Warlock', 'Demonology', 'Finger2', 'Both', 'Ring of Rapid Ascent'), +(9, 1, 12, 0, 290, 54588, 'Phase 5', 'Warlock', 'Demonology', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(9, 1, 13, 0, 290, 50365, 'Phase 5', 'Warlock', 'Demonology', 'Trinket2', 'Both', 'Phylactery of the Nameless Lich'), +(9, 1, 14, 0, 290, 54583, 'Phase 5', 'Warlock', 'Demonology', 'Back', 'Both', 'Cloak of Burning Dusk'), +(9, 1, 15, 0, 290, 50732, 'Phase 5', 'Warlock', 'Demonology', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(9, 1, 16, 0, 290, 50719, 'Phase 5', 'Warlock', 'Demonology', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(9, 1, 17, 0, 290, 50684, 'Phase 5', 'Warlock', 'Demonology', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + +-- Destruction (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 1, 66, 10504, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Head', 'Alliance', 'Green Lens'), +(9, 2, 0, 2, 66, 10504, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Head', 'Horde', 'Green Lens'), +(9, 2, 1, 1, 66, 18691, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(9, 2, 1, 2, 66, 18691, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(9, 2, 2, 1, 66, 14112, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Shoulders', 'Alliance', 'Felcloth Shoulders'), +(9, 2, 2, 2, 66, 14112, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Shoulders', 'Horde', 'Felcloth Shoulders'), +(9, 2, 4, 1, 66, 14153, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Chest', 'Alliance', 'Robe of the Void'), +(9, 2, 4, 2, 66, 14153, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Chest', 'Horde', 'Robe of the Void'), +(9, 2, 5, 1, 66, 11662, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Waist', 'Alliance', 'Ban''thok Sash'), +(9, 2, 5, 2, 66, 11662, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Waist', 'Horde', 'Ban''thok Sash'), +(9, 2, 6, 1, 66, 13170, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(9, 2, 6, 2, 66, 13170, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Legs', 'Horde', 'Skyshroud Leggings'), +(9, 2, 7, 1, 66, 18735, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(9, 2, 7, 2, 66, 18735, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(9, 2, 8, 1, 66, 11766, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(9, 2, 8, 2, 66, 11766, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(9, 2, 9, 1, 66, 13253, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Hands', 'Alliance', 'Hands of Power'), +(9, 2, 9, 2, 66, 13253, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Hands', 'Horde', 'Hands of Power'), +(9, 2, 10, 1, 66, 12543, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(9, 2, 10, 2, 66, 12545, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(9, 2, 11, 1, 66, 13001, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(9, 2, 11, 2, 66, 13001, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger2', 'Horde', 'Maiden''s Circle'), +(9, 2, 12, 1, 66, 12930, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(9, 2, 12, 2, 66, 12930, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket1', 'Horde', 'Briarwood Reed'), +(9, 2, 13, 1, 66, 13968, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket2', 'Alliance', 'Eye of the Beast'), +(9, 2, 13, 2, 66, 13968, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket2', 'Horde', 'Eye of the Beast'), +(9, 2, 14, 1, 66, 13386, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Back', 'Alliance', 'Archivist Cape'), +(9, 2, 14, 2, 66, 13386, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Back', 'Horde', 'Archivist Cape'), +(9, 2, 15, 1, 66, 13964, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'MainHand', 'Alliance', 'Witchblade'), +(9, 2, 15, 2, 66, 13964, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'MainHand', 'Horde', 'Witchblade'), +(9, 2, 16, 1, 66, 10796, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'OffHand', 'Alliance', 'Drakestone'), +(9, 2, 16, 2, 66, 10796, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'OffHand', 'Horde', 'Drakestone'), +(9, 2, 17, 1, 66, 13396, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(9, 2, 17, 2, 66, 13396, 'Phase 1 (Pre-Raid)', 'Warlock', 'Destruction', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 1, 76, 23310, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Head', 'Alliance', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 2, 0, 2, 76, 23310, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Head', 'Horde', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 2, 1, 1, 76, 18691, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Neck', 'Alliance', 'Dark Advisor''s Pendant'), +(9, 2, 1, 2, 76, 18691, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Neck', 'Horde', 'Dark Advisor''s Pendant'), +(9, 2, 2, 1, 76, 23311, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Shoulders', 'Alliance', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 2, 2, 2, 76, 23311, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Shoulders', 'Horde', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 2, 4, 1, 76, 14153, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Chest', 'Alliance', 'Robe of the Void'), +(9, 2, 4, 2, 76, 14153, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Chest', 'Horde', 'Robe of the Void'), +(9, 2, 5, 1, 76, 11662, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Waist', 'Alliance', 'Ban''thok Sash'), +(9, 2, 5, 2, 76, 11662, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Waist', 'Horde', 'Ban''thok Sash'), +(9, 2, 6, 1, 76, 13170, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(9, 2, 6, 2, 76, 13170, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Legs', 'Horde', 'Skyshroud Leggings'), +(9, 2, 7, 1, 76, 18735, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Feet', 'Alliance', 'Maleki''s Footwraps'), +(9, 2, 7, 2, 76, 18735, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Feet', 'Horde', 'Maleki''s Footwraps'), +(9, 2, 8, 1, 76, 11766, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(9, 2, 8, 2, 76, 11766, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(9, 2, 9, 1, 76, 18407, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Hands', 'Alliance', 'Felcloth Gloves'), +(9, 2, 9, 2, 76, 18407, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Hands', 'Horde', 'Felcloth Gloves'), +(9, 2, 10, 1, 76, 12543, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(9, 2, 10, 2, 76, 12545, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(9, 2, 11, 1, 76, 13001, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(9, 2, 11, 2, 76, 13001, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Finger2', 'Horde', 'Maiden''s Circle'), +(9, 2, 12, 1, 76, 12930, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(9, 2, 12, 2, 76, 12930, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket1', 'Horde', 'Briarwood Reed'), +(9, 2, 13, 1, 76, 18467, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket2', 'Alliance', 'Royal Seal of Eldre''Thalas'), +(9, 2, 13, 2, 76, 18467, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Trinket2', 'Horde', 'Royal Seal of Eldre''Thalas'), +(9, 2, 14, 1, 76, 13386, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Back', 'Alliance', 'Archivist Cape'), +(9, 2, 14, 2, 76, 13386, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Back', 'Horde', 'Archivist Cape'), +(9, 2, 15, 1, 76, 18372, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'MainHand', 'Alliance', 'Blade of the New Moon'), +(9, 2, 15, 2, 76, 18372, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'MainHand', 'Horde', 'Blade of the New Moon'), +(9, 2, 16, 1, 76, 10796, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'OffHand', 'Alliance', 'Drakestone'), +(9, 2, 16, 2, 76, 10796, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'OffHand', 'Horde', 'Drakestone'), +(9, 2, 17, 1, 76, 13396, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Ranged', 'Alliance', 'Skul''s Ghastly Touch'), +(9, 2, 17, 2, 76, 13396, 'Phase 2 (Pre-Raid)', 'Warlock', 'Destruction', 'Ranged', 'Horde', 'Skul''s Ghastly Touch'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 78, 23310, 'Phase 2', 'Warlock', 'Destruction', 'Head', 'Both', 'Lieutenant Commander''s Dreadweave Cowl'), +(9, 2, 1, 0, 78, 18814, 'Phase 2', 'Warlock', 'Destruction', 'Neck', 'Both', 'Choker of the Fire Lord'), +(9, 2, 2, 0, 78, 23311, 'Phase 2', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Lieutenant Commander''s Dreadweave Spaulders'), +(9, 2, 4, 0, 78, 19145, 'Phase 2', 'Warlock', 'Destruction', 'Chest', 'Both', 'Robe of Volatile Power'), +(9, 2, 5, 0, 78, 18809, 'Phase 2', 'Warlock', 'Destruction', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(9, 2, 6, 0, 78, 19133, 'Phase 2', 'Warlock', 'Destruction', 'Legs', 'Both', 'Fel Infused Leggings'), +(9, 2, 7, 0, 78, 19131, 'Phase 2', 'Warlock', 'Destruction', 'Feet', 'Both', 'Snowblind Shoes'), +(9, 2, 8, 0, 78, 11766, 'Phase 2', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Flameweave Cuffs'), +(9, 2, 9, 0, 78, 18407, 'Phase 2', 'Warlock', 'Destruction', 'Hands', 'Both', 'Felcloth Gloves'), +(9, 2, 10, 0, 78, 19147, 'Phase 2', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Ring of Spell Power'), +(9, 2, 11, 0, 78, 19147, 'Phase 2', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of Spell Power'), +(9, 2, 12, 0, 78, 12930, 'Phase 2', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Briarwood Reed'), +(9, 2, 13, 0, 78, 18820, 'Phase 2', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 2, 14, 0, 78, 13386, 'Phase 2', 'Warlock', 'Destruction', 'Back', 'Both', 'Archivist Cape'), +(9, 2, 15, 0, 78, 17103, 'Phase 2', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Azuresong Mageblade'), +(9, 2, 16, 0, 78, 10796, 'Phase 2', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Drakestone'), +(9, 2, 17, 0, 78, 13396, 'Phase 2', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 83, 19375, 'Phase 3', 'Warlock', 'Destruction', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(9, 2, 1, 0, 83, 18814, 'Phase 3', 'Warlock', 'Destruction', 'Neck', 'Both', 'Choker of the Fire Lord'), +(9, 2, 2, 0, 83, 19370, 'Phase 3', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(9, 2, 4, 0, 83, 19145, 'Phase 3', 'Warlock', 'Destruction', 'Chest', 'Both', 'Robe of Volatile Power'), +(9, 2, 5, 0, 83, 18809, 'Phase 3', 'Warlock', 'Destruction', 'Waist', 'Both', 'Sash of Whispered Secrets'), +(9, 2, 6, 0, 83, 19133, 'Phase 3', 'Warlock', 'Destruction', 'Legs', 'Both', 'Fel Infused Leggings'), +(9, 2, 7, 0, 83, 19131, 'Phase 3', 'Warlock', 'Destruction', 'Feet', 'Both', 'Snowblind Shoes'), +(9, 2, 8, 0, 83, 19374, 'Phase 3', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(9, 2, 9, 0, 83, 19407, 'Phase 3', 'Warlock', 'Destruction', 'Hands', 'Both', 'Ebony Flame Gloves'), +(9, 2, 11, 0, 83, 19434, 'Phase 3', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Band of Dark Dominion'), +(9, 2, 12, 0, 83, 19379, 'Phase 3', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 2, 13, 0, 83, 18820, 'Phase 3', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 2, 14, 0, 83, 19378, 'Phase 3', 'Warlock', 'Destruction', 'Back', 'Both', 'Cloak of the Brood Lord'), +(9, 2, 15, 0, 83, 19356, 'Phase 3', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Staff of the Shadow Flame'), +(9, 2, 17, 0, 83, 13396, 'Phase 3', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Skul''s Ghastly Touch'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 88, 21337, 'Phase 5', 'Warlock', 'Destruction', 'Head', 'Both', 'Doomcaller''s Circlet'), +(9, 2, 1, 0, 88, 21608, 'Phase 5', 'Warlock', 'Destruction', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(9, 2, 2, 0, 88, 21335, 'Phase 5', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Doomcaller''s Mantle'), +(9, 2, 4, 0, 88, 19682, 'Phase 5', 'Warlock', 'Destruction', 'Chest', 'Both', 'Bloodvine Vest'), +(9, 2, 5, 0, 88, 22730, 'Phase 5', 'Warlock', 'Destruction', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(9, 2, 6, 0, 88, 19683, 'Phase 5', 'Warlock', 'Destruction', 'Legs', 'Both', 'Bloodvine Leggings'), +(9, 2, 7, 0, 88, 19684, 'Phase 5', 'Warlock', 'Destruction', 'Feet', 'Both', 'Bloodvine Boots'), +(9, 2, 8, 0, 88, 21186, 'Phase 5', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Rockfury Bracers'), +(9, 2, 9, 0, 88, 21585, 'Phase 5', 'Warlock', 'Destruction', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(9, 2, 10, 0, 88, 21417, 'Phase 5', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Ring of Unspoken Names'), +(9, 2, 11, 0, 88, 21709, 'Phase 5', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of the Fallen God'), +(9, 2, 12, 0, 88, 19379, 'Phase 5', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 2, 13, 0, 88, 18820, 'Phase 5', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(9, 2, 14, 0, 88, 22731, 'Phase 5', 'Warlock', 'Destruction', 'Back', 'Both', 'Cloak of the Devoured'), +(9, 2, 15, 0, 88, 21622, 'Phase 5', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Sharpened Silithid Femur'), +(9, 2, 16, 0, 88, 21597, 'Phase 5', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(9, 2, 17, 0, 88, 21603, 'Phase 5', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Wand of Qiraji Nobility'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 92, 22506, 'Phase 6', 'Warlock', 'Destruction', 'Head', 'Both', 'Plagueheart Circlet'), +(9, 2, 1, 0, 92, 21608, 'Phase 6', 'Warlock', 'Destruction', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(9, 2, 2, 0, 92, 22507, 'Phase 6', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Plagueheart Shoulderpads'), +(9, 2, 4, 0, 92, 22504, 'Phase 6', 'Warlock', 'Destruction', 'Chest', 'Both', 'Plagueheart Robe'), +(9, 2, 5, 0, 92, 22730, 'Phase 6', 'Warlock', 'Destruction', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(9, 2, 6, 0, 92, 23070, 'Phase 6', 'Warlock', 'Destruction', 'Legs', 'Both', 'Leggings of Polarity'), +(9, 2, 7, 0, 92, 22508, 'Phase 6', 'Warlock', 'Destruction', 'Feet', 'Both', 'Plagueheart Sandals'), +(9, 2, 8, 0, 92, 21186, 'Phase 6', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Rockfury Bracers'), +(9, 2, 9, 0, 92, 21585, 'Phase 6', 'Warlock', 'Destruction', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(9, 2, 10, 0, 92, 23031, 'Phase 6', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Band of the Inevitable'), +(9, 2, 11, 0, 92, 21709, 'Phase 6', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of the Fallen God'), +(9, 2, 12, 0, 92, 19379, 'Phase 6', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(9, 2, 13, 0, 92, 23046, 'Phase 6', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(9, 2, 14, 0, 92, 23050, 'Phase 6', 'Warlock', 'Destruction', 'Back', 'Both', 'Cloak of the Necropolis'), +(9, 2, 15, 0, 92, 22807, 'Phase 6', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Wraith Blade'), +(9, 2, 16, 0, 92, 23049, 'Phase 6', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(9, 2, 17, 0, 92, 22820, 'Phase 6', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Wand of Fates'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 120, 24266, 'Pre-Raid', 'Warlock', 'Destruction', 'Head', 'Both', 'Spellstrike Hood'), +(9, 2, 1, 0, 120, 28134, 'Pre-Raid', 'Warlock', 'Destruction', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(9, 2, 2, 0, 120, 21869, 'Pre-Raid', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Frozen Shadoweave Shoulders'), +(9, 2, 4, 0, 120, 21871, 'Pre-Raid', 'Warlock', 'Destruction', 'Chest', 'Both', 'Frozen Shadoweave Robe'), +(9, 2, 5, 0, 120, 24256, 'Pre-Raid', 'Warlock', 'Destruction', 'Waist', 'Both', 'Girdle of Ruination'), +(9, 2, 6, 0, 120, 24262, 'Pre-Raid', 'Warlock', 'Destruction', 'Legs', 'Both', 'Spellstrike Pants'), +(9, 2, 7, 0, 120, 21870, 'Pre-Raid', 'Warlock', 'Destruction', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(9, 2, 8, 0, 120, 24250, 'Pre-Raid', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Bracers of Havok'), +(9, 2, 9, 0, 120, 24450, 'Pre-Raid', 'Warlock', 'Destruction', 'Hands', 'Both', 'Manaspark Gloves'), +(9, 2, 10, 0, 120, 29172, 'Pre-Raid', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Ashyen''s Gift'), +(9, 2, 11, 0, 120, 28227, 'Pre-Raid', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(9, 2, 12, 0, 120, 29370, 'Pre-Raid', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 2, 13, 0, 120, 29132, 'Pre-Raid', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Scryer''s Bloodgem'), +(9, 2, 14, 0, 120, 27981, 'Pre-Raid', 'Warlock', 'Destruction', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(9, 2, 15, 0, 120, 23554, 'Pre-Raid', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Eternium Runed Blade'), +(9, 2, 16, 0, 120, 29272, 'Pre-Raid', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Orb of the Soul-Eater'), +(9, 2, 17, 0, 120, 29350, 'Pre-Raid', 'Warlock', 'Destruction', 'Ranged', 'Both', 'The Black Stalk'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 125, 28963, 'Phase 1', 'Warlock', 'Destruction', 'Head', 'Both', 'Voidheart Crown'), +(9, 2, 1, 0, 125, 28530, 'Phase 1', 'Warlock', 'Destruction', 'Neck', 'Both', 'Brooch of Unquenchable Fury'), +(9, 2, 2, 0, 125, 28967, 'Phase 1', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Voidheart Mantle'), +(9, 2, 4, 0, 125, 28964, 'Phase 1', 'Warlock', 'Destruction', 'Chest', 'Both', 'Voidheart Robe'), +(9, 2, 5, 0, 125, 24256, 'Phase 1', 'Warlock', 'Destruction', 'Waist', 'Both', 'Girdle of Ruination'), +(9, 2, 6, 0, 125, 30734, 'Phase 1', 'Warlock', 'Destruction', 'Legs', 'Both', 'Leggings of the Seventh Circle'), +(9, 2, 7, 0, 125, 21870, 'Phase 1', 'Warlock', 'Destruction', 'Feet', 'Both', 'Frozen Shadoweave Boots'), +(9, 2, 8, 0, 125, 24250, 'Phase 1', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Bracers of Havok'), +(9, 2, 9, 0, 125, 28968, 'Phase 1', 'Warlock', 'Destruction', 'Hands', 'Both', 'Voidheart Gloves'), +(9, 2, 10, 0, 125, 29172, 'Phase 1', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Ashyen''s Gift'), +(9, 2, 11, 0, 125, 28793, 'Phase 1', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Band of Crimson Fury'), +(9, 2, 12, 0, 125, 29370, 'Phase 1', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 2, 13, 0, 125, 27683, 'Phase 1', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(9, 2, 14, 0, 125, 28766, 'Phase 1', 'Warlock', 'Destruction', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(9, 2, 15, 0, 125, 30723, 'Phase 1', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Talon of the Tempest'), +(9, 2, 16, 0, 125, 29272, 'Phase 1', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Orb of the Soul-Eater'), +(9, 2, 17, 0, 125, 28783, 'Phase 1', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Eredar Wand of Obliteration'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 141, 32494, 'Phase 2', 'Warlock', 'Destruction', 'Head', 'Both', 'Destruction Holo-gogs'), +(9, 2, 1, 0, 141, 30015, 'Phase 2', 'Warlock', 'Destruction', 'Neck', 'Both', 'The Sun King''s Talisman'), +(9, 2, 2, 0, 141, 28967, 'Phase 2', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Voidheart Mantle'), +(9, 2, 4, 0, 141, 30107, 'Phase 2', 'Warlock', 'Destruction', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 2, 5, 0, 141, 30038, 'Phase 2', 'Warlock', 'Destruction', 'Waist', 'Both', 'Belt of Blasting'), +(9, 2, 6, 0, 141, 30213, 'Phase 2', 'Warlock', 'Destruction', 'Legs', 'Both', 'Leggings of the Corruptor'), +(9, 2, 7, 0, 141, 30037, 'Phase 2', 'Warlock', 'Destruction', 'Feet', 'Both', 'Boots of Blasting'), +(9, 2, 8, 0, 141, 29918, 'Phase 2', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Mindstorm Wristbands'), +(9, 2, 9, 0, 141, 28968, 'Phase 2', 'Warlock', 'Destruction', 'Hands', 'Both', 'Voidheart Gloves'), +(9, 2, 10, 0, 141, 29302, 'Phase 2', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Band of Eternity'), +(9, 2, 11, 0, 141, 30109, 'Phase 2', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of Endless Coils'), +(9, 2, 12, 0, 141, 29370, 'Phase 2', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 2, 13, 0, 141, 27683, 'Phase 2', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(9, 2, 14, 0, 141, 28766, 'Phase 2', 'Warlock', 'Destruction', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(9, 2, 15, 0, 141, 30095, 'Phase 2', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Fang of the Leviathan'), +(9, 2, 16, 0, 141, 30049, 'Phase 2', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Fathomstone'), +(9, 2, 17, 0, 141, 29982, 'Phase 2', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 156, 31051, 'Phase 3', 'Warlock', 'Destruction', 'Head', 'Both', 'Hood of the Malefic'), +(9, 2, 1, 0, 156, 32589, 'Phase 3', 'Warlock', 'Destruction', 'Neck', 'Both', 'Hellfire-Encased Pendant'), +(9, 2, 2, 0, 156, 31054, 'Phase 3', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Mantle of the Malefic'), +(9, 2, 4, 0, 156, 30107, 'Phase 3', 'Warlock', 'Destruction', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 2, 5, 0, 156, 30888, 'Phase 3', 'Warlock', 'Destruction', 'Waist', 'Both', 'Anetheron''s Noose'), +(9, 2, 6, 0, 156, 31053, 'Phase 3', 'Warlock', 'Destruction', 'Legs', 'Both', 'Leggings of the Malefic'), +(9, 2, 7, 0, 156, 32239, 'Phase 3', 'Warlock', 'Destruction', 'Feet', 'Both', 'Slippers of the Seacaller'), +(9, 2, 8, 0, 156, 32586, 'Phase 3', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(9, 2, 9, 0, 156, 31050, 'Phase 3', 'Warlock', 'Destruction', 'Hands', 'Both', 'Gloves of the Malefic'), +(9, 2, 10, 0, 156, 32527, 'Phase 3', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Ring of Ancient Knowledge'), +(9, 2, 11, 0, 156, 32247, 'Phase 3', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of Captured Storms'), +(9, 2, 12, 0, 156, 29370, 'Phase 3', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(9, 2, 13, 0, 156, 32483, 'Phase 3', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(9, 2, 14, 0, 156, 32590, 'Phase 3', 'Warlock', 'Destruction', 'Back', 'Both', 'Nethervoid Cloak'), +(9, 2, 15, 0, 156, 32374, 'Phase 3', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(9, 2, 17, 0, 156, 29982, 'Phase 3', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Wand of the Forgotten Star'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 164, 31051, 'Phase 4', 'Warlock', 'Destruction', 'Head', 'Both', 'Hood of the Malefic'), +(9, 2, 1, 0, 164, 32589, 'Phase 4', 'Warlock', 'Destruction', 'Neck', 'Both', 'Hellfire-Encased Pendant'), +(9, 2, 2, 0, 164, 31054, 'Phase 4', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Mantle of the Malefic'), +(9, 2, 4, 0, 164, 30107, 'Phase 4', 'Warlock', 'Destruction', 'Chest', 'Both', 'Vestments of the Sea-Witch'), +(9, 2, 5, 0, 164, 30888, 'Phase 4', 'Warlock', 'Destruction', 'Waist', 'Both', 'Anetheron''s Noose'), +(9, 2, 6, 0, 164, 31053, 'Phase 4', 'Warlock', 'Destruction', 'Legs', 'Both', 'Leggings of the Malefic'), +(9, 2, 7, 0, 164, 32239, 'Phase 4', 'Warlock', 'Destruction', 'Feet', 'Both', 'Slippers of the Seacaller'), +(9, 2, 8, 0, 164, 32586, 'Phase 4', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Bracers of Nimble Thought'), +(9, 2, 9, 0, 164, 31050, 'Phase 4', 'Warlock', 'Destruction', 'Hands', 'Both', 'Gloves of the Malefic'), +(9, 2, 10, 0, 164, 33497, 'Phase 4', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Mana Attuned Band'), +(9, 2, 11, 0, 164, 32247, 'Phase 4', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of Captured Storms'), +(9, 2, 12, 0, 164, 33829, 'Phase 4', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Hex Shrunken Head'), +(9, 2, 13, 0, 164, 32483, 'Phase 4', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'The Skull of Gul''dan'), +(9, 2, 14, 0, 164, 32590, 'Phase 4', 'Warlock', 'Destruction', 'Back', 'Both', 'Nethervoid Cloak'), +(9, 2, 15, 0, 164, 32374, 'Phase 4', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Zhar''doom, Greatstaff of the Devourer'), +(9, 2, 16, 0, 164, 34179, 'Phase 5', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Heart of the Pit'), +(9, 2, 17, 0, 164, 33192, 'Phase 4', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Carved Witch Doctor''s Stick'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 200, 42553, 'Pre-Raid', 'Warlock', 'Destruction', 'Head', 'Both', 'Visage Liquification Goggles'), +(9, 2, 1, 0, 200, 39472, 'Pre-Raid', 'Warlock', 'Destruction', 'Neck', 'Both', 'Chain of Latent Energies'), +(9, 2, 2, 0, 200, 34393, 'Pre-Raid', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Shoulderpads of Knowledge''s Pursuit'), +(9, 2, 4, 0, 200, 39497, 'Pre-Raid', 'Warlock', 'Destruction', 'Chest', 'Both', 'Heroes'' Plagueheart Robe'), +(9, 2, 5, 0, 200, 40696, 'Pre-Raid', 'Warlock', 'Destruction', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(9, 2, 6, 0, 200, 34170, 'Pre-Raid', 'Warlock', 'Destruction', 'Legs', 'Both', 'Pantaloons of Calming Strife'), +(9, 2, 7, 0, 200, 40558, 'Pre-Raid', 'Warlock', 'Destruction', 'Feet', 'Both', 'Arcanic Tramplers'), +(9, 2, 8, 0, 200, 37361, 'Pre-Raid', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Cuffs of Winged Levitation'), +(9, 2, 9, 0, 200, 39500, 'Pre-Raid', 'Warlock', 'Destruction', 'Hands', 'Both', 'Heroes'' Plagueheart Gloves'), +(9, 2, 10, 0, 200, 43253, 'Pre-Raid', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Ring of Northern Tears'), +(9, 2, 12, 0, 200, 40682, 'Pre-Raid', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Sundial of the Exiled'), +(9, 2, 14, 0, 200, 41610, 'Pre-Raid', 'Warlock', 'Destruction', 'Back', 'Both', 'Deathchill Cloak'), +(9, 2, 17, 0, 200, 34348, 'Pre-Raid', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Wand of Cleansing Light'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 224, 40421, 'Phase 1', 'Warlock', 'Destruction', 'Head', 'Both', 'Valorous Plagueheart Circlet'), +(9, 2, 1, 0, 224, 44661, 'Phase 1', 'Warlock', 'Destruction', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(9, 2, 2, 0, 224, 40424, 'Phase 1', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Valorous Plagueheart Shoulderpads'), +(9, 2, 4, 0, 224, 40423, 'Phase 1', 'Warlock', 'Destruction', 'Chest', 'Both', 'Valorous Plagueheart Robe'), +(9, 2, 5, 0, 224, 40561, 'Phase 1', 'Warlock', 'Destruction', 'Waist', 'Both', 'Leash of Heedless Magic'), +(9, 2, 6, 0, 224, 40560, 'Phase 1', 'Warlock', 'Destruction', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(9, 2, 7, 0, 224, 40558, 'Phase 1', 'Warlock', 'Destruction', 'Feet', 'Both', 'Arcanic Tramplers'), +(9, 2, 8, 0, 224, 44008, 'Phase 1', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Unsullied Cuffs'), +(9, 2, 9, 0, 224, 40420, 'Phase 1', 'Warlock', 'Destruction', 'Hands', 'Both', 'Valorous Plagueheart Gloves'), +(9, 2, 10, 0, 224, 40399, 'Phase 1', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Signet of Manifested Pain'), +(9, 2, 12, 0, 224, 40432, 'Phase 1', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Illustration of the Dragon Soul'), +(9, 2, 14, 0, 224, 44005, 'Phase 1', 'Warlock', 'Destruction', 'Back', 'Both', 'Pennant Cloak'), +(9, 2, 15, 0, 224, 40396, 'Phase 1', 'Warlock', 'Destruction', 'MainHand', 'Both', 'The Turning Tide'), +(9, 2, 17, 0, 224, 39712, 'Phase 1', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Gemmed Wand of the Nerubians'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 245, 45497, 'Phase 2', 'Warlock', 'Destruction', 'Head', 'Both', 'Crown of Luminescence'), +(9, 2, 1, 0, 245, 45133, 'Phase 2', 'Warlock', 'Destruction', 'Neck', 'Both', 'Pendant of Fiery Havoc'), +(9, 2, 2, 0, 245, 46068, 'Phase 2', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Amice of Inconceivable Horror'), +(9, 2, 4, 0, 245, 46137, 'Phase 2', 'Warlock', 'Destruction', 'Chest', 'Both', 'Conqueror''s Deathbringer Robe'), +(9, 2, 9, 0, 245, 45665, 'Phase 2', 'Warlock', 'Destruction', 'Hands', 'Both', 'Pharos Gloves'), +(9, 2, 10, 0, 245, 45495, 'Phase 2', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Conductive Seal'), +(9, 2, 14, 0, 245, 45618, 'Phase 2', 'Warlock', 'Destruction', 'Back', 'Both', 'Sunglimmer Cloak'), +(9, 2, 15, 0, 245, 45620, 'Phase 2', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Starshard Edge'), +(9, 2, 17, 0, 245, 45294, 'Phase 2', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 10, 0, 258, 45495, 'Phase 3', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Conductive Seal'), +(9, 2, 17, 0, 258, 45294, 'Phase 3', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Petrified Ivy Sprig'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(9, 2, 0, 0, 290, 51231, 'Phase 5', 'Warlock', 'Destruction', 'Head', 'Both', 'Sanctified Dark Coven Hood'), +(9, 2, 1, 0, 290, 50182, 'Phase 5', 'Warlock', 'Destruction', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(9, 2, 2, 0, 290, 51234, 'Phase 5', 'Warlock', 'Destruction', 'Shoulders', 'Both', 'Sanctified Dark Coven Shoulderpads'), +(9, 2, 4, 0, 290, 51233, 'Phase 5', 'Warlock', 'Destruction', 'Chest', 'Both', 'Sanctified Dark Coven Robe'), +(9, 2, 5, 0, 290, 50613, 'Phase 5', 'Warlock', 'Destruction', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(9, 2, 6, 0, 290, 50694, 'Phase 5', 'Warlock', 'Destruction', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(9, 2, 7, 0, 290, 50699, 'Phase 5', 'Warlock', 'Destruction', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(9, 2, 8, 0, 290, 54582, 'Phase 5', 'Warlock', 'Destruction', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(9, 2, 9, 0, 290, 51230, 'Phase 5', 'Warlock', 'Destruction', 'Hands', 'Both', 'Sanctified Dark Coven Gloves'), +(9, 2, 10, 0, 290, 50614, 'Phase 5', 'Warlock', 'Destruction', 'Finger1', 'Both', 'Loop of the Endless Labyrinth'), +(9, 2, 11, 0, 290, 50664, 'Phase 5', 'Warlock', 'Destruction', 'Finger2', 'Both', 'Ring of Rapid Ascent'), +(9, 2, 12, 0, 290, 54588, 'Phase 5', 'Warlock', 'Destruction', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(9, 2, 13, 0, 290, 50365, 'Phase 5', 'Warlock', 'Destruction', 'Trinket2', 'Both', 'Phylactery of the Nameless Lich'), +(9, 2, 14, 0, 290, 54583, 'Phase 5', 'Warlock', 'Destruction', 'Back', 'Both', 'Cloak of Burning Dusk'), +(9, 2, 15, 0, 290, 50732, 'Phase 5', 'Warlock', 'Destruction', 'MainHand', 'Both', 'Bloodsurge, Kel''Thuzad''s Blade of Agony'), +(9, 2, 16, 0, 290, 50719, 'Phase 5', 'Warlock', 'Destruction', 'OffHand', 'Both', 'Shadow Silk Spindle'), +(9, 2, 17, 0, 290, 50684, 'Phase 5', 'Warlock', 'Destruction', 'Ranged', 'Both', 'Corpse-Impaling Spike'); + + +-- ============================================================ +-- Druid (11) +-- ============================================================ +-- Balance (tab 0) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 1, 66, 10504, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Head', 'Alliance', 'Green Lens'), +(11, 0, 0, 2, 66, 10504, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Head', 'Horde', 'Green Lens'), +(11, 0, 1, 1, 66, 12103, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Neck', 'Alliance', 'Star of Mystaria'), +(11, 0, 1, 2, 66, 12103, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Neck', 'Horde', 'Star of Mystaria'), +(11, 0, 2, 1, 66, 13013, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Shoulders', 'Alliance', 'Elder Wizard''s Mantle'), +(11, 0, 2, 2, 66, 13013, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Shoulders', 'Horde', 'Elder Wizard''s Mantle'), +(11, 0, 4, 1, 66, 11924, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Chest', 'Alliance', 'Robes of the Royal Crown'), +(11, 0, 4, 2, 66, 11924, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Chest', 'Horde', 'Robes of the Royal Crown'), +(11, 0, 5, 1, 66, 11662, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Waist', 'Alliance', 'Ban''thok Sash'), +(11, 0, 5, 2, 66, 11662, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Waist', 'Horde', 'Ban''thok Sash'), +(11, 0, 6, 1, 66, 13170, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(11, 0, 6, 2, 66, 13170, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Legs', 'Horde', 'Skyshroud Leggings'), +(11, 0, 7, 1, 66, 11822, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Feet', 'Alliance', 'Omnicast Boots'), +(11, 0, 7, 2, 66, 11822, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Feet', 'Horde', 'Omnicast Boots'), +(11, 0, 8, 1, 66, 11766, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(11, 0, 8, 2, 66, 11766, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(11, 0, 9, 1, 66, 13258, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Hands', 'Alliance', 'Slaghide Gauntlets'), +(11, 0, 9, 2, 66, 13258, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Hands', 'Horde', 'Slaghide Gauntlets'), +(11, 0, 10, 1, 66, 12543, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(11, 0, 10, 2, 66, 12545, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(11, 0, 11, 1, 66, 13001, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(11, 0, 11, 2, 66, 13001, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Finger2', 'Horde', 'Maiden''s Circle'), +(11, 0, 12, 1, 66, 12930, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(11, 0, 12, 2, 66, 12930, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Trinket1', 'Horde', 'Briarwood Reed'), +(11, 0, 13, 1, 66, 13968, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Trinket2', 'Alliance', 'Eye of the Beast'), +(11, 0, 13, 2, 66, 13968, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Trinket2', 'Horde', 'Eye of the Beast'), +(11, 0, 14, 1, 66, 13386, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Back', 'Alliance', 'Archivist Cape'), +(11, 0, 14, 2, 66, 13386, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'Back', 'Horde', 'Archivist Cape'), +(11, 0, 15, 1, 66, 13964, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'MainHand', 'Alliance', 'Witchblade'), +(11, 0, 15, 2, 66, 13964, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'MainHand', 'Horde', 'Witchblade'), +(11, 0, 16, 1, 66, 10796, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'OffHand', 'Alliance', 'Drakestone'), +(11, 0, 16, 2, 66, 10796, 'Phase 1 (Pre-Raid)', 'Druid', 'Balance', 'OffHand', 'Horde', 'Drakestone'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 1, 76, 10504, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Head', 'Alliance', 'Green Lens'), +(11, 0, 0, 2, 76, 10504, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Head', 'Horde', 'Green Lens'), +(11, 0, 1, 1, 76, 12103, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Neck', 'Alliance', 'Star of Mystaria'), +(11, 0, 1, 2, 76, 12103, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Neck', 'Horde', 'Star of Mystaria'), +(11, 0, 2, 1, 76, 13013, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Shoulders', 'Alliance', 'Elder Wizard''s Mantle'), +(11, 0, 2, 2, 76, 13013, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Shoulders', 'Horde', 'Elder Wizard''s Mantle'), +(11, 0, 4, 1, 76, 18385, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Chest', 'Alliance', 'Robe of Everlasting Night'), +(11, 0, 4, 2, 76, 18385, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Chest', 'Horde', 'Robe of Everlasting Night'), +(11, 0, 5, 1, 76, 11662, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Waist', 'Alliance', 'Ban''thok Sash'), +(11, 0, 5, 2, 76, 11662, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Waist', 'Horde', 'Ban''thok Sash'), +(11, 0, 6, 1, 76, 13170, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Legs', 'Alliance', 'Skyshroud Leggings'), +(11, 0, 6, 2, 76, 13170, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Legs', 'Horde', 'Skyshroud Leggings'), +(11, 0, 7, 1, 76, 11822, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Feet', 'Alliance', 'Omnicast Boots'), +(11, 0, 7, 2, 76, 11822, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Feet', 'Horde', 'Omnicast Boots'), +(11, 0, 8, 1, 76, 11766, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Wrists', 'Alliance', 'Flameweave Cuffs'), +(11, 0, 8, 2, 76, 11766, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Wrists', 'Horde', 'Flameweave Cuffs'), +(11, 0, 9, 1, 76, 13258, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Hands', 'Alliance', 'Slaghide Gauntlets'), +(11, 0, 9, 2, 76, 13258, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Hands', 'Horde', 'Slaghide Gauntlets'), +(11, 0, 10, 1, 76, 12543, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Finger1', 'Alliance', 'Songstone of Ironforge'), +(11, 0, 10, 2, 76, 12545, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Finger1', 'Horde', 'Eye of Orgrimmar'), +(11, 0, 11, 1, 76, 13001, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Finger2', 'Alliance', 'Maiden''s Circle'), +(11, 0, 11, 2, 76, 13001, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Finger2', 'Horde', 'Maiden''s Circle'), +(11, 0, 12, 1, 76, 12930, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Trinket1', 'Alliance', 'Briarwood Reed'), +(11, 0, 12, 2, 76, 12930, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Trinket1', 'Horde', 'Briarwood Reed'), +(11, 0, 13, 1, 76, 13968, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Trinket2', 'Alliance', 'Eye of the Beast'), +(11, 0, 13, 2, 76, 13968, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Trinket2', 'Horde', 'Eye of the Beast'), +(11, 0, 14, 1, 76, 13386, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Back', 'Alliance', 'Archivist Cape'), +(11, 0, 14, 2, 76, 13386, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'Back', 'Horde', 'Archivist Cape'), +(11, 0, 15, 1, 76, 18534, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'MainHand', 'Alliance', 'Rod of the Ogre Magi'), +(11, 0, 15, 2, 76, 18534, 'Phase 2 (Pre-Raid)', 'Druid', 'Balance', 'MainHand', 'Horde', 'Rod of the Ogre Magi'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 78, 10504, 'Phase 2', 'Druid', 'Balance', 'Head', 'Both', 'Green Lens'), +(11, 0, 1, 0, 78, 18814, 'Phase 2', 'Druid', 'Balance', 'Neck', 'Both', 'Choker of the Fire Lord'), +(11, 0, 2, 0, 78, 16836, 'Phase 2', 'Druid', 'Balance', 'Shoulders', 'Both', 'Cenarion Spaulders'), +(11, 0, 4, 0, 78, 19145, 'Phase 2', 'Druid', 'Balance', 'Chest', 'Both', 'Robe of Volatile Power'), +(11, 0, 5, 0, 78, 19136, 'Phase 2', 'Druid', 'Balance', 'Waist', 'Both', 'Mana Igniting Cord'), +(11, 0, 6, 0, 78, 13170, 'Phase 2', 'Druid', 'Balance', 'Legs', 'Both', 'Skyshroud Leggings'), +(11, 0, 7, 0, 78, 11822, 'Phase 2', 'Druid', 'Balance', 'Feet', 'Both', 'Omnicast Boots'), +(11, 0, 8, 0, 78, 11766, 'Phase 2', 'Druid', 'Balance', 'Wrists', 'Both', 'Flameweave Cuffs'), +(11, 0, 9, 0, 78, 13258, 'Phase 2', 'Druid', 'Balance', 'Hands', 'Both', 'Slaghide Gauntlets'), +(11, 0, 10, 0, 78, 19147, 'Phase 2', 'Druid', 'Balance', 'Finger1', 'Both', 'Ring of Spell Power'), +(11, 0, 11, 0, 78, 19147, 'Phase 2', 'Druid', 'Balance', 'Finger2', 'Both', 'Ring of Spell Power'), +(11, 0, 12, 0, 78, 12930, 'Phase 2', 'Druid', 'Balance', 'Trinket1', 'Both', 'Briarwood Reed'), +(11, 0, 13, 0, 78, 18820, 'Phase 2', 'Druid', 'Balance', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(11, 0, 14, 0, 78, 13386, 'Phase 2', 'Druid', 'Balance', 'Back', 'Both', 'Archivist Cape'), +(11, 0, 15, 0, 78, 18842, 'Phase 2', 'Druid', 'Balance', 'MainHand', 'Both', 'Staff of Dominance'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 83, 19375, 'Phase 3', 'Druid', 'Balance', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(11, 0, 1, 0, 83, 18814, 'Phase 3', 'Druid', 'Balance', 'Neck', 'Both', 'Choker of the Fire Lord'), +(11, 0, 2, 0, 83, 19370, 'Phase 3', 'Druid', 'Balance', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(11, 0, 4, 0, 83, 19145, 'Phase 3', 'Druid', 'Balance', 'Chest', 'Both', 'Robe of Volatile Power'), +(11, 0, 5, 0, 83, 19400, 'Phase 3', 'Druid', 'Balance', 'Waist', 'Both', 'Firemaw''s Clutch'), +(11, 0, 6, 0, 83, 13170, 'Phase 3', 'Druid', 'Balance', 'Legs', 'Both', 'Skyshroud Leggings'), +(11, 0, 7, 0, 83, 11822, 'Phase 3', 'Druid', 'Balance', 'Feet', 'Both', 'Omnicast Boots'), +(11, 0, 8, 0, 83, 19374, 'Phase 3', 'Druid', 'Balance', 'Wrists', 'Both', 'Bracers of Arcane Accuracy'), +(11, 0, 9, 0, 83, 13258, 'Phase 3', 'Druid', 'Balance', 'Hands', 'Both', 'Slaghide Gauntlets'), +(11, 0, 11, 0, 83, 19147, 'Phase 3', 'Druid', 'Balance', 'Finger2', 'Both', 'Ring of Spell Power'), +(11, 0, 12, 0, 83, 19379, 'Phase 3', 'Druid', 'Balance', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(11, 0, 13, 0, 83, 18820, 'Phase 3', 'Druid', 'Balance', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(11, 0, 14, 0, 83, 19378, 'Phase 3', 'Druid', 'Balance', 'Back', 'Both', 'Cloak of the Brood Lord'), +(11, 0, 15, 0, 83, 19360, 'Phase 3', 'Druid', 'Balance', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(11, 0, 16, 0, 83, 19308, 'Phase 3', 'Druid', 'Balance', 'OffHand', 'Both', 'Tome of Arcane Domination'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 88, 19375, 'Phase 5', 'Druid', 'Balance', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(11, 0, 1, 0, 88, 21608, 'Phase 5', 'Druid', 'Balance', 'Neck', 'Both', 'Amulet of Vek''nilash'), +(11, 0, 2, 0, 88, 19370, 'Phase 5', 'Druid', 'Balance', 'Shoulders', 'Both', 'Mantle of the Blackwing Cabal'), +(11, 0, 4, 0, 88, 19682, 'Phase 5', 'Druid', 'Balance', 'Chest', 'Both', 'Bloodvine Vest'), +(11, 0, 5, 0, 88, 22730, 'Phase 5', 'Druid', 'Balance', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(11, 0, 6, 0, 88, 19683, 'Phase 5', 'Druid', 'Balance', 'Legs', 'Both', 'Bloodvine Leggings'), +(11, 0, 7, 0, 88, 19684, 'Phase 5', 'Druid', 'Balance', 'Feet', 'Both', 'Bloodvine Boots'), +(11, 0, 8, 0, 88, 21186, 'Phase 5', 'Druid', 'Balance', 'Wrists', 'Both', 'Rockfury Bracers'), +(11, 0, 9, 0, 88, 21585, 'Phase 5', 'Druid', 'Balance', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(11, 0, 11, 0, 88, 21709, 'Phase 5', 'Druid', 'Balance', 'Finger2', 'Both', 'Ring of the Fallen God'), +(11, 0, 12, 0, 88, 19379, 'Phase 5', 'Druid', 'Balance', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(11, 0, 13, 0, 88, 18820, 'Phase 5', 'Druid', 'Balance', 'Trinket2', 'Both', 'Talisman of Ephemeral Power'), +(11, 0, 14, 0, 88, 22731, 'Phase 5', 'Druid', 'Balance', 'Back', 'Both', 'Cloak of the Devoured'), +(11, 0, 15, 0, 88, 19360, 'Phase 5', 'Druid', 'Balance', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(11, 0, 16, 0, 88, 21597, 'Phase 5', 'Druid', 'Balance', 'OffHand', 'Both', 'Royal Scepter of Vek''lor'), +(11, 0, 17, 0, 88, 23197, 'Phase 5', 'Druid', 'Balance', 'Ranged', 'Both', 'Idol of the Moon'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 92, 19375, 'Phase 6', 'Druid', 'Balance', 'Head', 'Both', 'Mish''undare, Circlet of the Mind Flayer'), +(11, 0, 1, 0, 92, 23057, 'Phase 6', 'Druid', 'Balance', 'Neck', 'Both', 'Gem of Trapped Innocents'), +(11, 0, 2, 0, 92, 22983, 'Phase 6', 'Druid', 'Balance', 'Shoulders', 'Both', 'Rime Covered Mantle'), +(11, 0, 4, 0, 92, 19682, 'Phase 6', 'Druid', 'Balance', 'Chest', 'Both', 'Bloodvine Vest'), +(11, 0, 5, 0, 92, 22730, 'Phase 6', 'Druid', 'Balance', 'Waist', 'Both', 'Eyestalk Waist Cord'), +(11, 0, 6, 0, 92, 19683, 'Phase 6', 'Druid', 'Balance', 'Legs', 'Both', 'Bloodvine Leggings'), +(11, 0, 7, 0, 92, 19684, 'Phase 6', 'Druid', 'Balance', 'Feet', 'Both', 'Bloodvine Boots'), +(11, 0, 8, 0, 92, 23021, 'Phase 6', 'Druid', 'Balance', 'Wrists', 'Both', 'The Soul Harvester''s Bindings'), +(11, 0, 9, 0, 92, 21585, 'Phase 6', 'Druid', 'Balance', 'Hands', 'Both', 'Dark Storm Gauntlets'), +(11, 0, 10, 0, 92, 23025, 'Phase 6', 'Druid', 'Balance', 'Finger1', 'Both', 'Seal of the Damned'), +(11, 0, 11, 0, 92, 21709, 'Phase 6', 'Druid', 'Balance', 'Finger2', 'Both', 'Ring of the Fallen God'), +(11, 0, 12, 0, 92, 19379, 'Phase 6', 'Druid', 'Balance', 'Trinket1', 'Both', 'Neltharion''s Tear'), +(11, 0, 13, 0, 92, 23046, 'Phase 6', 'Druid', 'Balance', 'Trinket2', 'Both', 'The Restrained Essence of Sapphiron'), +(11, 0, 14, 0, 92, 23050, 'Phase 6', 'Druid', 'Balance', 'Back', 'Both', 'Cloak of the Necropolis'), +(11, 0, 15, 0, 92, 22988, 'Phase 6', 'Druid', 'Balance', 'MainHand', 'Both', 'The End of Dreams'), +(11, 0, 16, 0, 92, 23049, 'Phase 6', 'Druid', 'Balance', 'OffHand', 'Both', 'Sapphiron''s Left Eye'), +(11, 0, 17, 0, 92, 23197, 'Phase 6', 'Druid', 'Balance', 'Ranged', 'Both', 'Idol of the Moon'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 120, 24266, 'Pre-Raid', 'Druid', 'Balance', 'Head', 'Both', 'Spellstrike Hood'), +(11, 0, 1, 0, 120, 28134, 'Pre-Raid', 'Druid', 'Balance', 'Neck', 'Both', 'Brooch of Heightened Potential'), +(11, 0, 2, 0, 120, 27796, 'Pre-Raid', 'Druid', 'Balance', 'Shoulders', 'Both', 'Mana-Etched Spaulders'), +(11, 0, 4, 0, 120, 21848, 'Pre-Raid', 'Druid', 'Balance', 'Chest', 'Both', 'Spellfire Robe'), +(11, 0, 5, 0, 120, 21846, 'Pre-Raid', 'Druid', 'Balance', 'Waist', 'Both', 'Spellfire Belt'), +(11, 0, 6, 0, 120, 24262, 'Pre-Raid', 'Druid', 'Balance', 'Legs', 'Both', 'Spellstrike Pants'), +(11, 0, 7, 0, 120, 28406, 'Pre-Raid', 'Druid', 'Balance', 'Feet', 'Both', 'Sigil-Laced Boots'), +(11, 0, 8, 0, 120, 24250, 'Pre-Raid', 'Druid', 'Balance', 'Wrists', 'Both', 'Bracers of Havok'), +(11, 0, 9, 0, 120, 21847, 'Pre-Raid', 'Druid', 'Balance', 'Hands', 'Both', 'Spellfire Gloves'), +(11, 0, 10, 0, 120, 29172, 'Pre-Raid', 'Druid', 'Balance', 'Finger1', 'Both', 'Ashyen''s Gift'), +(11, 0, 11, 0, 120, 28227, 'Pre-Raid', 'Druid', 'Balance', 'Finger2', 'Both', 'Sparking Arcanite Ring'), +(11, 0, 12, 0, 120, 29370, 'Pre-Raid', 'Druid', 'Balance', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(11, 0, 13, 0, 120, 27683, 'Pre-Raid', 'Druid', 'Balance', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(11, 0, 14, 0, 120, 27981, 'Pre-Raid', 'Druid', 'Balance', 'Back', 'Both', 'Sethekk Oracle Cloak'), +(11, 0, 15, 0, 120, 30832, 'Pre-Raid', 'Druid', 'Balance', 'MainHand', 'Both', 'Gavel of Unearthed Secrets'), +(11, 0, 16, 0, 120, 29271, 'Pre-Raid', 'Druid', 'Balance', 'OffHand', 'Both', 'Talisman of Kalecgos'), +(11, 0, 17, 0, 120, 27518, 'Pre-Raid', 'Druid', 'Balance', 'Ranged', 'Both', 'Ivory Idol of the Moongoddess'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 125, 29093, 'Phase 1', 'Druid', 'Balance', 'Head', 'Both', 'Antlers of Malorne'), +(11, 0, 1, 0, 125, 28530, 'Phase 1', 'Druid', 'Balance', 'Neck', 'Both', 'Brooch of Unquenchable Fury'), +(11, 0, 2, 0, 125, 29095, 'Phase 1', 'Druid', 'Balance', 'Shoulders', 'Both', 'Pauldrons of Malorne'), +(11, 0, 4, 0, 125, 21848, 'Phase 1', 'Druid', 'Balance', 'Chest', 'Both', 'Spellfire Robe'), +(11, 0, 5, 0, 125, 21846, 'Phase 1', 'Druid', 'Balance', 'Waist', 'Both', 'Spellfire Belt'), +(11, 0, 6, 0, 125, 24262, 'Phase 1', 'Druid', 'Balance', 'Legs', 'Both', 'Spellstrike Pants'), +(11, 0, 7, 0, 125, 28517, 'Phase 1', 'Druid', 'Balance', 'Feet', 'Both', 'Boots of Foretelling'), +(11, 0, 8, 0, 125, 24250, 'Phase 1', 'Druid', 'Balance', 'Wrists', 'Both', 'Bracers of Havok'), +(11, 0, 9, 0, 125, 21847, 'Phase 1', 'Druid', 'Balance', 'Hands', 'Both', 'Spellfire Gloves'), +(11, 0, 10, 0, 125, 28793, 'Phase 1', 'Druid', 'Balance', 'Finger1', 'Both', 'Band of Crimson Fury'), +(11, 0, 11, 0, 125, 28753, 'Phase 1', 'Druid', 'Balance', 'Finger2', 'Both', 'Ring of Recurrence'), +(11, 0, 12, 0, 125, 29370, 'Phase 1', 'Druid', 'Balance', 'Trinket1', 'Both', 'Icon of the Silver Crescent'), +(11, 0, 13, 0, 125, 27683, 'Phase 1', 'Druid', 'Balance', 'Trinket2', 'Both', 'Quagmirran''s Eye'), +(11, 0, 14, 0, 125, 28766, 'Phase 1', 'Druid', 'Balance', 'Back', 'Both', 'Ruby Drape of the Mysticant'), +(11, 0, 15, 0, 125, 28770, 'Phase 1', 'Druid', 'Balance', 'MainHand', 'Both', 'Nathrezim Mindblade'), +(11, 0, 16, 0, 125, 29271, 'Phase 1', 'Druid', 'Balance', 'OffHand', 'Both', 'Talisman of Kalecgos'), +(11, 0, 17, 0, 125, 27518, 'Phase 1', 'Druid', 'Balance', 'Ranged', 'Both', 'Ivory Idol of the Moongoddess'); + +-- ilvl 200 (Fresh 80) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 200, 39545, 'Fresh 80', 'Druid', 'Balance', 'Head', 'Both', 'Heroes'' Dreamwalker Cover'), +(11, 0, 1, 0, 200, 44658, 'Fresh 80', 'Druid', 'Balance', 'Neck', 'Both', 'Chain of the Ancient Wyrm'), +(11, 0, 2, 0, 200, 39548, 'Fresh 80', 'Druid', 'Balance', 'Shoulders', 'Both', 'Heroes'' Dreamwalker Mantle'), +(11, 0, 4, 0, 200, 39547, 'Fresh 80', 'Druid', 'Balance', 'Chest', 'Both', 'Heroes'' Dreamwalker Vestments'), +(11, 0, 5, 0, 200, 40696, 'Fresh 80', 'Druid', 'Balance', 'Waist', 'Both', 'Plush Sash of Guzbah'), +(11, 0, 6, 0, 200, 39546, 'Fresh 80', 'Druid', 'Balance', 'Legs', 'Both', 'Heroes'' Dreamwalker Trousers'), +(11, 0, 7, 0, 200, 40519, 'Fresh 80', 'Druid', 'Balance', 'Feet', 'Both', 'Footsteps of Malygos'), +(11, 0, 8, 0, 200, 40740, 'Fresh 80', 'Druid', 'Balance', 'Wrists', 'Both', 'Wraps of the Astral Traveler'), +(11, 0, 9, 0, 200, 39544, 'Fresh 80', 'Druid', 'Balance', 'Hands', 'Both', 'Heroes'' Dreamwalker Gloves'), +(11, 0, 10, 0, 200, 40719, 'Fresh 80', 'Druid', 'Balance', 'Finger1', 'Both', 'Band of Channeled Magic'), +(11, 0, 12, 0, 200, 39229, 'Fresh 80', 'Druid', 'Balance', 'Trinket1', 'Both', 'Embrace of the Spider'), +(11, 0, 14, 0, 200, 40723, 'Fresh 80', 'Druid', 'Balance', 'Back', 'Both', 'Disguise of the Kumiho'), +(11, 0, 15, 0, 200, 39423, 'Fresh 80', 'Druid', 'Balance', 'MainHand', 'Both', 'Hammer of the Astral Plane'), +(11, 0, 17, 0, 200, 40321, 'Fresh 80', 'Druid', 'Balance', 'Ranged', 'Both', 'Idol of the Shooting Star'); + +-- ilvl 213 (Pre-Ulduar) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 213, 40467, 'Pre-Ulduar', 'Druid', 'Balance', 'Head', 'Both', 'Valorous Dreamwalker Cover'), +(11, 0, 1, 0, 213, 44661, 'Pre-Ulduar', 'Druid', 'Balance', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(11, 0, 2, 0, 213, 40470, 'Pre-Ulduar', 'Druid', 'Balance', 'Shoulders', 'Both', 'Valorous Dreamwalker Mantle'), +(11, 0, 4, 0, 213, 40469, 'Pre-Ulduar', 'Druid', 'Balance', 'Chest', 'Both', 'Valorous Dreamwalker Vestments'), +(11, 0, 5, 0, 213, 40561, 'Pre-Ulduar', 'Druid', 'Balance', 'Waist', 'Both', 'Leash of Heedless Magic'), +(11, 0, 6, 0, 213, 40560, 'Pre-Ulduar', 'Druid', 'Balance', 'Legs', 'Both', 'Leggings of the Wanton Spellcaster'), +(11, 0, 7, 0, 213, 40519, 'Pre-Ulduar', 'Druid', 'Balance', 'Feet', 'Both', 'Footsteps of Malygos'), +(11, 0, 8, 0, 213, 40323, 'Pre-Ulduar', 'Druid', 'Balance', 'Wrists', 'Both', 'Esteemed Bindings'), +(11, 0, 9, 0, 213, 40460, 'Pre-Ulduar', 'Druid', 'Balance', 'Hands', 'Both', 'Valorous Dreamwalker Handguards'), +(11, 0, 10, 0, 213, 40080, 'Pre-Ulduar', 'Druid', 'Balance', 'Finger1', 'Both', 'Lost Jewel'), +(11, 0, 12, 0, 213, 40682, 'Pre-Ulduar', 'Druid', 'Balance', 'Trinket1', 'Both', 'Sundial of the Exiled'), +(11, 0, 14, 0, 213, 40405, 'Pre-Ulduar', 'Druid', 'Balance', 'Back', 'Both', 'Cape of the Unworthy Wizard'), +(11, 0, 17, 0, 213, 40321, 'Pre-Ulduar', 'Druid', 'Balance', 'Ranged', 'Both', 'Idol of the Shooting Star'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 245, 46191, 'Phase 2', 'Druid', 'Balance', 'Head', 'Both', 'Conqueror''s Nightsong Cover'), +(11, 0, 1, 0, 245, 45933, 'Phase 2', 'Druid', 'Balance', 'Neck', 'Both', 'Pendant of the Shallow Grave'), +(11, 0, 2, 0, 245, 45136, 'Phase 2', 'Druid', 'Balance', 'Shoulders', 'Both', 'Shoulderpads of Dormant Energies'), +(11, 0, 4, 0, 245, 45519, 'Phase 2', 'Druid', 'Balance', 'Chest', 'Both', 'Vestments of the Blind Denizen'), +(11, 0, 5, 0, 245, 45619, 'Phase 2', 'Druid', 'Balance', 'Waist', 'Both', 'Starwatcher''s Binding'), +(11, 0, 6, 0, 245, 46192, 'Phase 2', 'Druid', 'Balance', 'Legs', 'Both', 'Conqueror''s Nightsong Trousers'), +(11, 0, 7, 0, 245, 45537, 'Phase 2', 'Druid', 'Balance', 'Feet', 'Both', 'Treads of the False Oracle'), +(11, 0, 8, 0, 245, 45446, 'Phase 2', 'Druid', 'Balance', 'Wrists', 'Both', 'Grasps of Reason'), +(11, 0, 9, 0, 245, 45665, 'Phase 2', 'Druid', 'Balance', 'Hands', 'Both', 'Pharos Gloves'), +(11, 0, 10, 0, 245, 45495, 'Phase 2', 'Druid', 'Balance', 'Finger1', 'Both', 'Conductive Seal'), +(11, 0, 12, 0, 245, 45466, 'Phase 2', 'Druid', 'Balance', 'Trinket1', 'Both', 'Scale of Fates'), +(11, 0, 14, 0, 245, 45242, 'Phase 2', 'Druid', 'Balance', 'Back', 'Both', 'Drape of Mortal Downfall'), +(11, 0, 17, 0, 245, 40321, 'Phase 2', 'Druid', 'Balance', 'Ranged', 'Both', 'Idol of the Shooting Star'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 258, 48171, 'Phase 3', 'Druid', 'Balance', 'Head', 'Both', 'Cover of Triumph'), +(11, 0, 1, 1, 258, 47144, 'Phase 3', 'Druid', 'Balance', 'Neck', 'Alliance', 'Wail of the Val''kyr'), +(11, 0, 1, 2, 258, 47468, 'Phase 3', 'Druid', 'Balance', 'Neck', 'Horde', 'Cry of the Val''kyr'), +(11, 0, 2, 0, 258, 48168, 'Phase 3', 'Druid', 'Balance', 'Shoulders', 'Both', 'Mantle of Triumph'), +(11, 0, 4, 0, 258, 48169, 'Phase 3', 'Druid', 'Balance', 'Chest', 'Both', 'Vestments of Triumph'), +(11, 0, 5, 1, 258, 47084, 'Phase 3', 'Druid', 'Balance', 'Waist', 'Alliance', 'Cord of Biting Cold'), +(11, 0, 5, 2, 258, 47447, 'Phase 3', 'Druid', 'Balance', 'Waist', 'Horde', 'Belt of Biting Cold'), +(11, 0, 6, 1, 258, 47190, 'Phase 3', 'Druid', 'Balance', 'Legs', 'Alliance', 'Legwraps of the Awakening'), +(11, 0, 6, 2, 258, 47479, 'Phase 3', 'Druid', 'Balance', 'Legs', 'Horde', 'Leggings of the Awakening'), +(11, 0, 7, 1, 258, 47097, 'Phase 3', 'Druid', 'Balance', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(11, 0, 7, 2, 258, 47454, 'Phase 3', 'Druid', 'Balance', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(11, 0, 8, 1, 258, 47066, 'Phase 3', 'Druid', 'Balance', 'Wrists', 'Alliance', 'Bracers of the Autumn Willow'), +(11, 0, 8, 2, 258, 47438, 'Phase 3', 'Druid', 'Balance', 'Wrists', 'Horde', 'Bindings of the Autumn Willow'), +(11, 0, 9, 0, 258, 48172, 'Phase 3', 'Druid', 'Balance', 'Hands', 'Both', 'Gloves of Triumph'), +(11, 0, 10, 1, 258, 47237, 'Phase 3', 'Druid', 'Balance', 'Finger1', 'Alliance', 'Band of Deplorable Violence'), +(11, 0, 10, 2, 258, 47489, 'Phase 3', 'Druid', 'Balance', 'Finger1', 'Horde', 'Lurid Manifestation'), +(11, 0, 12, 2, 258, 45518, 'Phase 3', 'Druid', 'Balance', 'Trinket1', 'Horde', 'Flare of the Heavens'), +(11, 0, 14, 1, 258, 47552, 'Phase 3', 'Druid', 'Balance', 'Back', 'Alliance', 'Jaina''s Radiance'), +(11, 0, 14, 2, 258, 47551, 'Phase 3', 'Druid', 'Balance', 'Back', 'Horde', 'Aethas'' Intensity'), +(11, 0, 17, 0, 258, 47670, 'Phase 3', 'Druid', 'Balance', 'Ranged', 'Both', 'Idol of Lunar Fury'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 264, 51290, 'Phase 4', 'Druid', 'Balance', 'Head', 'Both', 'Sanctified Lasherweave Cover'), +(11, 0, 1, 0, 264, 50724, 'Phase 4', 'Druid', 'Balance', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(11, 0, 2, 0, 264, 51292, 'Phase 4', 'Druid', 'Balance', 'Shoulders', 'Both', 'Sanctified Lasherweave Mantle'), +(11, 0, 4, 0, 264, 51294, 'Phase 4', 'Druid', 'Balance', 'Chest', 'Both', 'Sanctified Lasherweave Vestment'), +(11, 0, 5, 0, 264, 50613, 'Phase 4', 'Druid', 'Balance', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(11, 0, 6, 0, 264, 50694, 'Phase 4', 'Druid', 'Balance', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(11, 0, 7, 0, 264, 50699, 'Phase 4', 'Druid', 'Balance', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(11, 0, 8, 0, 264, 50630, 'Phase 4', 'Druid', 'Balance', 'Wrists', 'Both', 'Bracers of Eternal Dreaming'), +(11, 0, 9, 0, 264, 51291, 'Phase 4', 'Druid', 'Balance', 'Hands', 'Both', 'Sanctified Lasherweave Gloves'), +(11, 0, 10, 0, 264, 50398, 'Phase 4', 'Druid', 'Balance', 'Finger1', 'Both', 'Ashen Band of Endless Destruction'), +(11, 0, 12, 0, 264, 50348, 'Phase 4', 'Druid', 'Balance', 'Trinket1', 'Both', 'Dislodged Foreign Object'), +(11, 0, 14, 0, 264, 50628, 'Phase 4', 'Druid', 'Balance', 'Back', 'Both', 'Frostbinder''s Shredded Cape'), +(11, 0, 17, 0, 264, 50719, 'Phase 4', 'Druid', 'Balance', 'Ranged', 'Both', 'Shadow Silk Spindle'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 0, 0, 0, 290, 51290, 'Phase 5', 'Druid', 'Balance', 'Head', 'Both', 'Sanctified Lasherweave Cover'), +(11, 0, 1, 0, 290, 50182, 'Phase 5', 'Druid', 'Balance', 'Neck', 'Both', 'Blood Queen''s Crimson Choker'), +(11, 0, 2, 0, 290, 51292, 'Phase 5', 'Druid', 'Balance', 'Shoulders', 'Both', 'Sanctified Lasherweave Mantle'), +(11, 0, 4, 0, 290, 51294, 'Phase 5', 'Druid', 'Balance', 'Chest', 'Both', 'Sanctified Lasherweave Vestment'), +(11, 0, 5, 0, 290, 50613, 'Phase 5', 'Druid', 'Balance', 'Waist', 'Both', 'Crushing Coldwraith Belt'), +(11, 0, 6, 0, 290, 50694, 'Phase 5', 'Druid', 'Balance', 'Legs', 'Both', 'Plaguebringer''s Stained Pants'), +(11, 0, 7, 0, 290, 50699, 'Phase 5', 'Druid', 'Balance', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(11, 0, 8, 0, 290, 54582, 'Phase 5', 'Druid', 'Balance', 'Wrists', 'Both', 'Bracers of Fiery Night'), +(11, 0, 9, 0, 290, 51291, 'Phase 5', 'Druid', 'Balance', 'Hands', 'Both', 'Sanctified Lasherweave Gloves'), +(11, 0, 10, 0, 290, 50398, 'Phase 5', 'Druid', 'Balance', 'Finger1', 'Both', 'Ashen Band of Endless Destruction'), +(11, 0, 11, 0, 290, 50714, 'Phase 5', 'Druid', 'Balance', 'Finger2', 'Both', 'Valanar''s Other Signet Ring'), +(11, 0, 12, 0, 290, 54588, 'Phase 5', 'Druid', 'Balance', 'Trinket1', 'Both', 'Charred Twilight Scale'), +(11, 0, 13, 0, 290, 50365, 'Phase 5', 'Druid', 'Balance', 'Trinket2', 'Both', 'Phylactery of the Nameless Lich'), +(11, 0, 14, 0, 290, 54583, 'Phase 5', 'Druid', 'Balance', 'Back', 'Both', 'Cloak of Burning Dusk'), +(11, 0, 15, 0, 290, 50734, 'Phase 5', 'Druid', 'Balance', 'MainHand', 'Both', 'Royal Scepter of Terenas II'), +(11, 0, 16, 0, 290, 50719, 'Phase 5', 'Druid', 'Balance', 'OffHand', 'Both', 'Shadow Silk Spindle'); + +-- Feral Cat (tab 1) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 66, 8345, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 66, 15411, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Mark of Fordring'), +(11, 1, 2, 0, 66, 12927, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(11, 1, 4, 0, 66, 14637, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Cadaverous Armor'), +(11, 1, 5, 0, 66, 13252, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Cloudrunner Girdle'), +(11, 1, 6, 0, 66, 15062, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Devilsaur Leggings'), +(11, 1, 7, 0, 66, 12553, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Swiftwalker Boots'), +(11, 1, 8, 0, 66, 16710, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Shadowcraft Bracers'), +(11, 1, 9, 0, 66, 15063, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(11, 1, 10, 0, 66, 17713, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Blackstone Ring'), +(11, 1, 11, 0, 66, 2246, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Finger2', 'Both', 'Myrmidon''s Signet'), +(11, 1, 12, 0, 66, 13965, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(11, 1, 13, 0, 66, 11815, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Hand of Justice'), +(11, 1, 14, 0, 66, 13340, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cape of the Black Baron'), +(11, 1, 15, 0, 66, 11921, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Impervious Giant'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 76, 8345, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 76, 15411, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Mark of Fordring'), +(11, 1, 2, 0, 76, 12927, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(11, 1, 4, 0, 76, 14637, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Cadaverous Armor'), +(11, 1, 5, 0, 76, 13252, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Cloudrunner Girdle'), +(11, 1, 6, 0, 76, 15062, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Devilsaur Leggings'), +(11, 1, 7, 0, 76, 12553, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Swiftwalker Boots'), +(11, 1, 8, 0, 76, 18375, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Bracers of the Eclipse'), +(11, 1, 9, 0, 76, 15063, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Devilsaur Gauntlets'), +(11, 1, 10, 0, 76, 17713, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Blackstone Ring'), +(11, 1, 11, 0, 76, 18500, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(11, 1, 12, 0, 76, 13965, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(11, 1, 13, 0, 76, 11815, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Hand of Justice'), +(11, 1, 14, 0, 76, 13340, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cape of the Black Baron'), +(11, 1, 15, 0, 76, 18420, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Bonecrusher'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 78, 8345, 'Phase 2', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 78, 18404, 'Phase 2', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Onyxia Tooth Pendant'), +(11, 1, 2, 0, 78, 12927, 'Phase 2', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(11, 1, 4, 0, 78, 14637, 'Phase 2', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Cadaverous Armor'), +(11, 1, 5, 0, 78, 13252, 'Phase 2', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Cloudrunner Girdle'), +(11, 1, 6, 0, 78, 15062, 'Phase 2', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Devilsaur Leggings'), +(11, 1, 7, 0, 78, 12553, 'Phase 2', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Swiftwalker Boots'), +(11, 1, 8, 0, 78, 19146, 'Phase 2', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Wristguards of Stability'), +(11, 1, 9, 0, 78, 18823, 'Phase 2', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Aged Core Leather Gloves'), +(11, 1, 10, 0, 78, 17063, 'Phase 2', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Band of Accuria'), +(11, 1, 11, 0, 78, 18500, 'Phase 2', 'Druid', 'Feral Cat', 'Finger2', 'Both', 'Tarnished Elven Ring'), +(11, 1, 12, 0, 78, 13965, 'Phase 2', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Blackhand''s Breadth'), +(11, 1, 13, 0, 78, 11815, 'Phase 2', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Hand of Justice'), +(11, 1, 14, 0, 78, 13340, 'Phase 2', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cape of the Black Baron'), +(11, 1, 15, 0, 78, 18420, 'Phase 2', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Bonecrusher'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 83, 8345, 'Phase 3', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 83, 19377, 'Phase 3', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(11, 1, 2, 0, 83, 12927, 'Phase 3', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Truestrike Shoulders'), +(11, 1, 4, 0, 83, 19405, 'Phase 3', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Malfurion''s Blessed Bulwark'), +(11, 1, 5, 0, 83, 19396, 'Phase 3', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Taut Dragonhide Belt'), +(11, 1, 6, 0, 83, 15062, 'Phase 3', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Devilsaur Leggings'), +(11, 1, 7, 0, 83, 19381, 'Phase 3', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Boots of the Shadow Flame'), +(11, 1, 8, 0, 83, 19146, 'Phase 3', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Wristguards of Stability'), +(11, 1, 9, 0, 83, 18823, 'Phase 3', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Aged Core Leather Gloves'), +(11, 1, 10, 0, 83, 19384, 'Phase 3', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Master Dragonslayer''s Ring'), +(11, 1, 12, 0, 83, 19406, 'Phase 3', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(11, 1, 13, 0, 83, 11815, 'Phase 3', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Hand of Justice'), +(11, 1, 14, 0, 83, 19436, 'Phase 3', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cloak of Draconic Might'), +(11, 1, 15, 0, 83, 19358, 'Phase 3', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Draconic Maul'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 88, 8345, 'Phase 5', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 88, 19377, 'Phase 5', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(11, 1, 2, 0, 88, 21665, 'Phase 5', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Mantle of Wicked Revenge'), +(11, 1, 4, 0, 88, 21680, 'Phase 5', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Vest of Swift Execution'), +(11, 1, 5, 0, 88, 21586, 'Phase 5', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Belt of Never-ending Agony'), +(11, 1, 6, 0, 88, 20665, 'Phase 5', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Abyssal Leather Leggings'), +(11, 1, 7, 0, 88, 21493, 'Phase 5', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Boots of the Vanguard'), +(11, 1, 8, 0, 88, 21602, 'Phase 5', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Qiraji Execution Bracers'), +(11, 1, 10, 0, 88, 21205, 'Phase 5', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Signet Ring of the Bronze Dragonflight'), +(11, 1, 11, 0, 88, 17063, 'Phase 5', 'Druid', 'Feral Cat', 'Finger2', 'Both', 'Band of Accuria'), +(11, 1, 12, 0, 88, 19406, 'Phase 5', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(11, 1, 13, 0, 88, 13965, 'Phase 5', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Blackhand''s Breadth'), +(11, 1, 14, 0, 88, 21701, 'Phase 5', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cloak of Concentrated Hatred'), +(11, 1, 15, 0, 88, 21268, 'Phase 5', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Blessed Qiraji War Hammer'), +(11, 1, 16, 0, 88, 13385, 'Phase 5', 'Druid', 'Feral Cat', 'OffHand', 'Both', 'Tome of Knowledge'), +(11, 1, 17, 0, 88, 22397, 'Phase 5', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of Ferocity'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 92, 8345, 'Phase 6', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 92, 19377, 'Phase 6', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Prestor''s Talisman of Connivery'), +(11, 1, 2, 0, 92, 21665, 'Phase 6', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Mantle of Wicked Revenge'), +(11, 1, 4, 0, 92, 23226, 'Phase 6', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Ghoul Skin Tunic'), +(11, 1, 5, 0, 92, 21586, 'Phase 6', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Belt of Never-ending Agony'), +(11, 1, 6, 0, 92, 23071, 'Phase 6', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Leggings of Apocalypse'), +(11, 1, 7, 0, 92, 21493, 'Phase 6', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Boots of the Vanguard'), +(11, 1, 8, 0, 92, 21602, 'Phase 6', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Qiraji Execution Bracers'), +(11, 1, 12, 0, 92, 19406, 'Phase 6', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(11, 1, 13, 0, 92, 23041, 'Phase 6', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Slayer''s Crest'), +(11, 1, 14, 0, 92, 21710, 'Phase 6', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cloak of the Fallen God'), +(11, 1, 15, 0, 92, 22988, 'Phase 6', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'The End of Dreams'), +(11, 1, 16, 0, 92, 13385, 'Phase 6', 'Druid', 'Feral Cat', 'OffHand', 'Both', 'Tome of Knowledge'), +(11, 1, 17, 0, 92, 22397, 'Phase 6', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of Ferocity'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 120, 8345, 'Pre-Raid', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 120, 29381, 'Pre-Raid', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Choker of Vile Intent'), +(11, 1, 2, 0, 120, 27797, 'Pre-Raid', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Wastewalker Shoulderpads'), +(11, 1, 4, 0, 120, 29525, 'Pre-Raid', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Primalstrike Vest'), +(11, 1, 5, 0, 120, 29247, 'Pre-Raid', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(11, 1, 6, 0, 120, 31544, 'Pre-Raid', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Clefthoof Hide Leggings'), +(11, 1, 7, 0, 120, 25686, 'Pre-Raid', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Fel Leather Boots'), +(11, 1, 8, 0, 120, 29246, 'Pre-Raid', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Nightfall Wristguards'), +(11, 1, 9, 0, 120, 29507, 'Pre-Raid', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Windslayer Wraps'), +(11, 1, 10, 0, 120, 30365, 'Pre-Raid', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Overseer''s Signet'), +(11, 1, 11, 0, 120, 31920, 'Pre-Raid', 'Druid', 'Feral Cat', 'Finger2', 'Both', 'Shaffar''s Band of Brutality'), +(11, 1, 12, 0, 120, 29383, 'Pre-Raid', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(11, 1, 13, 0, 120, 28034, 'Pre-Raid', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Hourglass of the Unraveller'), +(11, 1, 14, 0, 120, 31255, 'Pre-Raid', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cloak of the Craft'), +(11, 1, 15, 0, 120, 31334, 'Pre-Raid', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Staff of Natural Fury'), +(11, 1, 17, 0, 120, 28372, 'Pre-Raid', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of Feral Shadows'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 125, 8345, 'Phase 1', 'Druid', 'Feral Cat', 'Head', 'Both', 'Wolfshead Helm'), +(11, 1, 1, 0, 125, 29381, 'Phase 1', 'Druid', 'Feral Cat', 'Neck', 'Both', 'Choker of Vile Intent'), +(11, 1, 2, 0, 125, 29100, 'Phase 1', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Mantle of Malorne'), +(11, 1, 4, 0, 125, 29096, 'Phase 1', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Breastplate of Malorne'), +(11, 1, 5, 0, 125, 29247, 'Phase 1', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Girdle of the Deathdealer'), +(11, 1, 6, 0, 125, 28741, 'Phase 1', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Skulker''s Greaves'), +(11, 1, 7, 0, 125, 28545, 'Phase 1', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Edgewalker Longboots'), +(11, 1, 8, 0, 125, 29246, 'Phase 1', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Nightfall Wristguards'), +(11, 1, 9, 0, 125, 28506, 'Phase 1', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Gloves of Dexterous Manipulation'), +(11, 1, 10, 0, 125, 28757, 'Phase 1', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Ring of a Thousand Marks'), +(11, 1, 11, 0, 125, 30834, 'Phase 1', 'Druid', 'Feral Cat', 'Finger2', 'Both', 'Shapeshifter''s Signet'), +(11, 1, 12, 0, 125, 28830, 'Phase 1', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Dragonspine Trophy'), +(11, 1, 13, 0, 125, 29383, 'Phase 1', 'Druid', 'Feral Cat', 'Trinket2', 'Both', 'Bloodlust Brooch'), +(11, 1, 14, 0, 125, 28660, 'Phase 1', 'Druid', 'Feral Cat', 'Back', 'Both', 'Gilded Thorium Cloak'), +(11, 1, 15, 0, 125, 28658, 'Phase 1', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Terestian''s Stranglestaff'), +(11, 1, 17, 0, 125, 29390, 'Phase 1', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Everbloom Idol'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 200, 42550, 'Pre-Raid', 'Druid', 'Feral Cat', 'Head', 'Both', 'Weakness Spectralizers'), +(11, 1, 2, 0, 200, 43481, 'Pre-Raid', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Trollwoven Spaulders'), +(11, 1, 4, 0, 200, 39554, 'Pre-Raid', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Heroes'' Dreamwalker Raiments'), +(11, 1, 5, 0, 200, 40694, 'Pre-Raid', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Jorach''s Crocolisk Skin Belt'), +(11, 1, 6, 0, 200, 37644, 'Pre-Raid', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Gored Hide Legguards'), +(11, 1, 7, 0, 200, 44297, 'Pre-Raid', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Boots of the Neverending Path'), +(11, 1, 8, 0, 200, 44203, 'Pre-Raid', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Dragonfriend Bracers'), +(11, 1, 9, 0, 200, 37409, 'Pre-Raid', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Gilt-Edged Leather Gauntlets'), +(11, 1, 10, 0, 200, 40586, 'Pre-Raid', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Band of the Kirin Tor'), +(11, 1, 12, 0, 200, 44253, 'Pre-Raid', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(11, 1, 14, 0, 200, 43406, 'Pre-Raid', 'Druid', 'Feral Cat', 'Back', 'Both', 'Cloak of the Gushing Wound'), +(11, 1, 15, 0, 200, 41257, 'Pre-Raid', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Titansteel Destroyer'), +(11, 1, 17, 0, 200, 40713, 'Pre-Raid', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of the Ravenous Beast'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 224, 40473, 'Phase 1', 'Druid', 'Feral Cat', 'Head', 'Both', 'Valorous Dreamwalker Headguard'), +(11, 1, 2, 0, 224, 40494, 'Phase 1', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Valorous Dreamwalker Shoulderpads'), +(11, 1, 4, 0, 224, 40539, 'Phase 1', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Chestguard of the Recluse'), +(11, 1, 5, 0, 224, 40205, 'Phase 1', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Stalk-Skin Belt'), +(11, 1, 6, 0, 224, 44011, 'Phase 1', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Leggings of the Honored'), +(11, 1, 7, 0, 224, 40243, 'Phase 1', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Footwraps of Vile Deceit'), +(11, 1, 8, 0, 224, 40738, 'Phase 1', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Wristwraps of the Cutthroat'), +(11, 1, 9, 0, 224, 40541, 'Phase 1', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Frosted Adroit Handguards'), +(11, 1, 10, 0, 224, 40474, 'Phase 1', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Surge Needle Ring'), +(11, 1, 12, 0, 224, 42987, 'Phase 1', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(11, 1, 14, 0, 224, 40403, 'Phase 1', 'Druid', 'Feral Cat', 'Back', 'Both', 'Drape of the Deadly Foe'), +(11, 1, 17, 0, 224, 40713, 'Phase 1', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of the Ravenous Beast'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 245, 46161, 'Phase 2', 'Druid', 'Feral Cat', 'Head', 'Both', 'Conqueror''s Nightsong Headguard'), +(11, 1, 2, 0, 245, 45245, 'Phase 2', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Shoulderpads of the Intruder'), +(11, 1, 4, 0, 245, 45473, 'Phase 2', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Embrace of the Gladiator'), +(11, 1, 5, 0, 245, 46095, 'Phase 2', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Soul-Devouring Cinch'), +(11, 1, 6, 0, 245, 45536, 'Phase 2', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(11, 1, 7, 0, 245, 45564, 'Phase 2', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Footpads of Silence'), +(11, 1, 8, 0, 245, 45869, 'Phase 2', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Fluxing Energy Coils'), +(11, 1, 9, 0, 245, 46158, 'Phase 2', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Conqueror''s Nightsong Handgrips'), +(11, 1, 10, 0, 245, 45608, 'Phase 2', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Brann''s Signet Ring'), +(11, 1, 12, 0, 245, 45931, 'Phase 2', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Mjolnir Runestone'), +(11, 1, 14, 0, 245, 46032, 'Phase 2', 'Druid', 'Feral Cat', 'Back', 'Both', 'Drape of the Faceless General'), +(11, 1, 17, 0, 245, 40713, 'Phase 2', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of the Ravenous Beast'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 258, 48201, 'Phase 3', 'Druid', 'Feral Cat', 'Head', 'Both', 'Headguard of Triumph'), +(11, 1, 2, 0, 258, 48198, 'Phase 3', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Shoulderpads of Triumph'), +(11, 1, 4, 0, 258, 48139, 'Phase 3', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Robe of Triumph'), +(11, 1, 5, 1, 258, 47112, 'Phase 3', 'Druid', 'Feral Cat', 'Waist', 'Alliance', 'Belt of the Merciless Killer'), +(11, 1, 5, 2, 258, 47460, 'Phase 3', 'Druid', 'Feral Cat', 'Waist', 'Horde', 'Belt of the Pitiless Killer'), +(11, 1, 6, 0, 258, 45536, 'Phase 3', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(11, 1, 7, 1, 258, 47077, 'Phase 3', 'Druid', 'Feral Cat', 'Feet', 'Alliance', 'Treads of the Icewalker'), +(11, 1, 7, 2, 258, 47445, 'Phase 3', 'Druid', 'Feral Cat', 'Feet', 'Horde', 'Icewalker Treads'), +(11, 1, 8, 1, 258, 47155, 'Phase 3', 'Druid', 'Feral Cat', 'Wrists', 'Alliance', 'Bracers of Dark Determination'), +(11, 1, 8, 2, 258, 47474, 'Phase 3', 'Druid', 'Feral Cat', 'Wrists', 'Horde', 'Armbands of Dark Determination'), +(11, 1, 9, 0, 258, 48202, 'Phase 3', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Handgrips of Triumph'), +(11, 1, 10, 1, 258, 47075, 'Phase 3', 'Druid', 'Feral Cat', 'Finger1', 'Alliance', 'Ring of Callous Aggression'), +(11, 1, 10, 2, 258, 47443, 'Phase 3', 'Druid', 'Feral Cat', 'Finger1', 'Horde', 'Band of Callous Aggression'), +(11, 1, 12, 1, 258, 47131, 'Phase 3', 'Druid', 'Feral Cat', 'Trinket1', 'Alliance', 'Death''s Verdict'), +(11, 1, 12, 2, 258, 47464, 'Phase 3', 'Druid', 'Feral Cat', 'Trinket1', 'Horde', 'Death''s Choice'), +(11, 1, 14, 1, 258, 47545, 'Phase 3', 'Druid', 'Feral Cat', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(11, 1, 14, 2, 258, 47546, 'Phase 3', 'Druid', 'Feral Cat', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(11, 1, 15, 1, 258, 47130, 'Phase 3', 'Druid', 'Feral Cat', 'MainHand', 'Alliance', 'Lupine Longstaff'), +(11, 1, 15, 2, 258, 47463, 'Phase 3', 'Druid', 'Feral Cat', 'MainHand', 'Horde', 'Twin''s Pact'), +(11, 1, 17, 0, 258, 47668, 'Phase 3', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of Mutilation'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 264, 51296, 'Phase 4', 'Druid', 'Feral Cat', 'Head', 'Both', 'Sanctified Lasherweave Headguard'), +(11, 1, 2, 0, 264, 51299, 'Phase 4', 'Druid', 'Feral Cat', 'Shoulders', 'Both', 'Sanctified Lasherweave Shoulderpads'), +(11, 1, 4, 0, 264, 51298, 'Phase 4', 'Druid', 'Feral Cat', 'Chest', 'Both', 'Sanctified Lasherweave Raiment'), +(11, 1, 5, 0, 264, 50707, 'Phase 4', 'Druid', 'Feral Cat', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(11, 1, 6, 0, 264, 51297, 'Phase 4', 'Druid', 'Feral Cat', 'Legs', 'Both', 'Sanctified Lasherweave Legguards'), +(11, 1, 7, 0, 264, 50607, 'Phase 4', 'Druid', 'Feral Cat', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(11, 1, 8, 0, 264, 50670, 'Phase 4', 'Druid', 'Feral Cat', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(11, 1, 9, 0, 264, 50675, 'Phase 4', 'Druid', 'Feral Cat', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(11, 1, 10, 0, 264, 50402, 'Phase 4', 'Druid', 'Feral Cat', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(11, 1, 12, 0, 264, 50363, 'Phase 4', 'Druid', 'Feral Cat', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(11, 1, 14, 1, 264, 47545, 'Phase 4', 'Druid', 'Feral Cat', 'Back', 'Alliance', 'Vereesa''s Dexterity'), +(11, 1, 14, 2, 264, 47546, 'Phase 4', 'Druid', 'Feral Cat', 'Back', 'Horde', 'Sylvanas'' Cunning'), +(11, 1, 15, 0, 264, 50735, 'Phase 4', 'Druid', 'Feral Cat', 'MainHand', 'Both', 'Oathbinder, Charge of the Ranger-General'), +(11, 1, 17, 0, 264, 50456, 'Phase 4', 'Druid', 'Feral Cat', 'Ranged', 'Both', 'Idol of the Crying Moon'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 1, 0, 0, 290, 51296, 'Phase 5', 'Druid', 'FeralCat', 'Head', 'Both', 'Sanctified Lasherweave Headguard'), +(11, 1, 1, 0, 290, 50633, 'Phase 5', 'Druid', 'FeralCat', 'Neck', 'Both', 'Sindragosa''s Cruel Claw'), +(11, 1, 2, 0, 290, 51299, 'Phase 5', 'Druid', 'FeralCat', 'Shoulders', 'Both', 'Sanctified Lasherweave Shoulderpads'), +(11, 1, 4, 0, 290, 51298, 'Phase 5', 'Druid', 'FeralCat', 'Chest', 'Both', 'Sanctified Lasherweave Raiment'), +(11, 1, 5, 0, 290, 50707, 'Phase 5', 'Druid', 'FeralCat', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(11, 1, 6, 0, 290, 51297, 'Phase 5', 'Druid', 'FeralCat', 'Legs', 'Both', 'Sanctified Lasherweave Legguards'), +(11, 1, 7, 0, 290, 50607, 'Phase 5', 'Druid', 'FeralCat', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(11, 1, 8, 0, 290, 50670, 'Phase 5', 'Druid', 'FeralCat', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(11, 1, 9, 0, 290, 50675, 'Phase 5', 'Druid', 'FeralCat', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(11, 1, 10, 0, 290, 50618, 'Phase 5', 'Druid', 'FeralCat', 'Finger1', 'Both', 'Frostbrood Sapphire Ring'), +(11, 1, 11, 0, 290, 50402, 'Phase 5', 'Druid', 'FeralCat', 'Finger2', 'Both', 'Ashen Band of Endless Vengeance'), +(11, 1, 12, 0, 290, 50363, 'Phase 5', 'Druid', 'FeralCat', 'Trinket1', 'Both', 'Deathbringer''s Will'), +(11, 1, 13, 0, 290, 54590, 'Phase 5', 'Druid', 'FeralCat', 'Trinket2', 'Both', 'Sharpened Twilight Scale'), +(11, 1, 14, 0, 290, 50653, 'Phase 5', 'Druid', 'FeralCat', 'Back', 'Both', 'Shadowvault Slayer''s Cloak'), +(11, 1, 15, 0, 290, 50735, 'Phase 5', 'Druid', 'FeralCat', 'MainHand', 'Both', 'Oathbinder, Charge of the Ranger-General'), +(11, 1, 17, 0, 290, 50456, 'Phase 5', 'Druid', 'FeralCat', 'Ranged', 'Both', 'Idol of the Crying Moon'); + +-- Restoration (tab 2) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 66, 13102, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Head', 'Both', 'Cassandra''s Grace'), +(11, 2, 1, 0, 66, 18723, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(11, 2, 2, 0, 66, 15061, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Living Shoulders'), +(11, 2, 4, 0, 66, 13346, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(11, 2, 5, 0, 66, 14553, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Waist', 'Both', 'Sash of Mercy'), +(11, 2, 6, 0, 66, 11841, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Legs', 'Both', 'Senior Designer''s Pantaloons'), +(11, 2, 7, 0, 66, 13954, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Feet', 'Both', 'Verdant Footpads'), +(11, 2, 8, 0, 66, 13208, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bleak Howler Armguards'), +(11, 2, 9, 0, 66, 10787, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Hands', 'Both', 'Atal''ai Gloves'), +(11, 2, 10, 0, 66, 13178, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Finger1', 'Both', 'Rosewine Circle'), +(11, 2, 11, 0, 66, 16058, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Finger2', 'Both', 'Fordring''s Seal'), +(11, 2, 12, 0, 66, 12930, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Briarwood Reed'), +(11, 2, 13, 0, 66, 11819, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Second Wind'), +(11, 2, 14, 0, 66, 13386, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'Back', 'Both', 'Archivist Cape'), +(11, 2, 15, 0, 66, 11923, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'MainHand', 'Both', 'The Hammer of Grace'), +(11, 2, 16, 0, 66, 11928, 'Phase 1 (Pre-Raid)', 'Druid', 'Restoration', 'OffHand', 'Both', 'Thaurissan''s Royal Scepter'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 76, 13102, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Head', 'Both', 'Cassandra''s Grace'), +(11, 2, 1, 0, 76, 18723, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(11, 2, 2, 0, 76, 15061, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Living Shoulders'), +(11, 2, 4, 0, 76, 13346, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(11, 2, 5, 0, 76, 14553, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Waist', 'Both', 'Sash of Mercy'), +(11, 2, 6, 0, 76, 18386, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Legs', 'Both', 'Padre''s Trousers'), +(11, 2, 7, 0, 76, 13954, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Feet', 'Both', 'Verdant Footpads'), +(11, 2, 8, 0, 76, 18525, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bracers of Prosperity'), +(11, 2, 9, 0, 76, 10787, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Hands', 'Both', 'Atal''ai Gloves'), +(11, 2, 10, 0, 76, 13178, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Finger1', 'Both', 'Rosewine Circle'), +(11, 2, 11, 0, 76, 16058, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Finger2', 'Both', 'Fordring''s Seal'), +(11, 2, 12, 0, 76, 18470, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Royal Seal of Eldre''Thalas'), +(11, 2, 13, 0, 76, 18371, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Mindtap Talisman'), +(11, 2, 14, 0, 76, 18510, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'Back', 'Both', 'Hide of the Wild'), +(11, 2, 15, 0, 76, 11923, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'MainHand', 'Both', 'The Hammer of Grace'), +(11, 2, 16, 0, 76, 18523, 'Phase 2 (Pre-Raid)', 'Druid', 'Restoration', 'OffHand', 'Both', 'Brightly Glowing Stone'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 78, 19132, 'Phase 2', 'Druid', 'Restoration', 'Head', 'Both', 'Crystal Adorned Crown'), +(11, 2, 1, 0, 78, 18723, 'Phase 2', 'Druid', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(11, 2, 2, 0, 78, 18810, 'Phase 2', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(11, 2, 4, 0, 78, 13346, 'Phase 2', 'Druid', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(11, 2, 5, 0, 78, 19162, 'Phase 2', 'Druid', 'Restoration', 'Waist', 'Both', 'Corehound Belt'), +(11, 2, 6, 0, 78, 18875, 'Phase 2', 'Druid', 'Restoration', 'Legs', 'Both', 'Salamander Scale Pants'), +(11, 2, 7, 0, 78, 16829, 'Phase 2', 'Druid', 'Restoration', 'Feet', 'Both', 'Cenarion Boots'), +(11, 2, 8, 0, 78, 18263, 'Phase 2', 'Druid', 'Restoration', 'Wrists', 'Both', 'Flarecore Wraps'), +(11, 2, 9, 0, 78, 10787, 'Phase 2', 'Druid', 'Restoration', 'Hands', 'Both', 'Atal''ai Gloves'), +(11, 2, 10, 0, 78, 19140, 'Phase 2', 'Druid', 'Restoration', 'Finger1', 'Both', 'Cauterizing Band'), +(11, 2, 11, 0, 78, 19140, 'Phase 2', 'Druid', 'Restoration', 'Finger2', 'Both', 'Cauterizing Band'), +(11, 2, 12, 0, 78, 18470, 'Phase 2', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Royal Seal of Eldre''Thalas'), +(11, 2, 13, 0, 78, 17064, 'Phase 2', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Shard of the Scale'), +(11, 2, 14, 0, 78, 18510, 'Phase 2', 'Druid', 'Restoration', 'Back', 'Both', 'Hide of the Wild'), +(11, 2, 15, 0, 78, 17070, 'Phase 2', 'Druid', 'Restoration', 'MainHand', 'Both', 'Fang of the Mystics'), +(11, 2, 16, 0, 78, 18523, 'Phase 2', 'Druid', 'Restoration', 'OffHand', 'Both', 'Brightly Glowing Stone'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 83, 19132, 'Phase 3', 'Druid', 'Restoration', 'Head', 'Both', 'Crystal Adorned Crown'), +(11, 2, 1, 0, 83, 18723, 'Phase 3', 'Druid', 'Restoration', 'Neck', 'Both', 'Animated Chain Necklace'), +(11, 2, 2, 0, 83, 18810, 'Phase 3', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(11, 2, 4, 0, 83, 13346, 'Phase 3', 'Druid', 'Restoration', 'Chest', 'Both', 'Robes of the Exalted'), +(11, 2, 5, 0, 83, 19162, 'Phase 3', 'Druid', 'Restoration', 'Waist', 'Both', 'Corehound Belt'), +(11, 2, 6, 0, 83, 19385, 'Phase 3', 'Druid', 'Restoration', 'Legs', 'Both', 'Empowered Leggings'), +(11, 2, 7, 0, 83, 16898, 'Phase 3', 'Druid', 'Restoration', 'Feet', 'Both', 'Stormrage Boots'), +(11, 2, 8, 0, 83, 16904, 'Phase 3', 'Druid', 'Restoration', 'Wrists', 'Both', 'Stormrage Bracers'), +(11, 2, 9, 0, 83, 16899, 'Phase 3', 'Druid', 'Restoration', 'Hands', 'Both', 'Stormrage Handguards'), +(11, 2, 10, 0, 83, 19140, 'Phase 3', 'Druid', 'Restoration', 'Finger1', 'Both', 'Cauterizing Band'), +(11, 2, 11, 0, 83, 19382, 'Phase 3', 'Druid', 'Restoration', 'Finger2', 'Both', 'Pure Elementium Band'), +(11, 2, 12, 0, 83, 19395, 'Phase 3', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Rejuvenating Gem'), +(11, 2, 13, 0, 83, 17064, 'Phase 3', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Shard of the Scale'), +(11, 2, 14, 0, 83, 19430, 'Phase 3', 'Druid', 'Restoration', 'Back', 'Both', 'Shroud of Pure Thought'), +(11, 2, 15, 0, 83, 19360, 'Phase 3', 'Druid', 'Restoration', 'MainHand', 'Both', 'Lok''amir il Romathis'), +(11, 2, 16, 0, 83, 19312, 'Phase 3', 'Druid', 'Restoration', 'OffHand', 'Both', 'Lei of the Lifegiver'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 88, 20628, 'Phase 5', 'Druid', 'Restoration', 'Head', 'Both', 'Deviate Growth Cap'), +(11, 2, 1, 0, 88, 21712, 'Phase 5', 'Druid', 'Restoration', 'Neck', 'Both', 'Amulet of the Fallen God'), +(11, 2, 2, 0, 88, 18810, 'Phase 5', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Wild Growth Spaulders'), +(11, 2, 4, 0, 88, 21663, 'Phase 5', 'Druid', 'Restoration', 'Chest', 'Both', 'Robes of the Guardian Saint'), +(11, 2, 5, 0, 88, 21582, 'Phase 5', 'Druid', 'Restoration', 'Waist', 'Both', 'Grasp of the Old God'), +(11, 2, 6, 0, 88, 19385, 'Phase 5', 'Druid', 'Restoration', 'Legs', 'Both', 'Empowered Leggings'), +(11, 2, 7, 0, 88, 19437, 'Phase 5', 'Druid', 'Restoration', 'Feet', 'Both', 'Boots of Pure Thought'), +(11, 2, 8, 0, 88, 21604, 'Phase 5', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bracelets of Royal Redemption'), +(11, 2, 9, 0, 88, 21617, 'Phase 5', 'Druid', 'Restoration', 'Hands', 'Both', 'Wasphide Gauntlets'), +(11, 2, 10, 0, 88, 21620, 'Phase 5', 'Druid', 'Restoration', 'Finger1', 'Both', 'Ring of the Martyr'), +(11, 2, 11, 0, 88, 19382, 'Phase 5', 'Druid', 'Restoration', 'Finger2', 'Both', 'Pure Elementium Band'), +(11, 2, 12, 0, 88, 19395, 'Phase 5', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Rejuvenating Gem'), +(11, 2, 13, 0, 88, 17064, 'Phase 5', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Shard of the Scale'), +(11, 2, 14, 0, 88, 21583, 'Phase 5', 'Druid', 'Restoration', 'Back', 'Both', 'Cloak of Clarity'), +(11, 2, 15, 0, 88, 21839, 'Phase 5', 'Druid', 'Restoration', 'MainHand', 'Both', 'Scepter of the False Prophet'), +(11, 2, 16, 0, 88, 21666, 'Phase 5', 'Druid', 'Restoration', 'OffHand', 'Both', 'Sartura''s Might'), +(11, 2, 17, 0, 88, 22399, 'Phase 5', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of Health'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 92, 20628, 'Phase 6', 'Druid', 'Restoration', 'Head', 'Both', 'Deviate Growth Cap'), +(11, 2, 1, 0, 92, 21712, 'Phase 6', 'Druid', 'Restoration', 'Neck', 'Both', 'Amulet of the Fallen God'), +(11, 2, 2, 0, 92, 22491, 'Phase 6', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Dreamwalker Spaulders'), +(11, 2, 4, 0, 92, 22488, 'Phase 6', 'Druid', 'Restoration', 'Chest', 'Both', 'Dreamwalker Tunic'), +(11, 2, 5, 0, 92, 21582, 'Phase 6', 'Druid', 'Restoration', 'Waist', 'Both', 'Grasp of the Old God'), +(11, 2, 6, 0, 92, 22489, 'Phase 6', 'Druid', 'Restoration', 'Legs', 'Both', 'Dreamwalker Legguards'), +(11, 2, 7, 0, 92, 22492, 'Phase 6', 'Druid', 'Restoration', 'Feet', 'Both', 'Dreamwalker Boots'), +(11, 2, 8, 0, 92, 21604, 'Phase 6', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bracelets of Royal Redemption'), +(11, 2, 9, 0, 92, 22493, 'Phase 6', 'Druid', 'Restoration', 'Hands', 'Both', 'Dreamwalker Handguards'), +(11, 2, 10, 0, 92, 21620, 'Phase 6', 'Druid', 'Restoration', 'Finger1', 'Both', 'Ring of the Martyr'), +(11, 2, 11, 0, 92, 22939, 'Phase 6', 'Druid', 'Restoration', 'Finger2', 'Both', 'Band of Unanswered Prayers'), +(11, 2, 12, 0, 92, 23047, 'Phase 6', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Eye of the Dead'), +(11, 2, 13, 0, 92, 23027, 'Phase 6', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Warmth of Forgiveness'), +(11, 2, 14, 0, 92, 22960, 'Phase 6', 'Druid', 'Restoration', 'Back', 'Both', 'Cloak of Suturing'), +(11, 2, 15, 0, 92, 23056, 'Phase 6', 'Druid', 'Restoration', 'MainHand', 'Both', 'Hammer of the Twisting Nether'), +(11, 2, 16, 0, 92, 23048, 'Phase 6', 'Druid', 'Restoration', 'OffHand', 'Both', 'Sapphiron''s Right Eye'), +(11, 2, 17, 0, 92, 22399, 'Phase 6', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of Health'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 120, 24264, 'Pre-Raid', 'Druid', 'Restoration', 'Head', 'Both', 'Whitemend Hood'), +(11, 2, 1, 0, 120, 30377, 'Pre-Raid', 'Druid', 'Restoration', 'Neck', 'Both', 'Karja''s Medallion'), +(11, 2, 2, 0, 120, 21874, 'Pre-Raid', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Primal Mooncloth Shoulders'), +(11, 2, 4, 0, 120, 21875, 'Pre-Raid', 'Druid', 'Restoration', 'Chest', 'Both', 'Primal Mooncloth Robe'), +(11, 2, 5, 0, 120, 21873, 'Pre-Raid', 'Druid', 'Restoration', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(11, 2, 6, 0, 120, 24261, 'Pre-Raid', 'Druid', 'Restoration', 'Legs', 'Both', 'Whitemend Pants'), +(11, 2, 7, 0, 120, 27411, 'Pre-Raid', 'Druid', 'Restoration', 'Feet', 'Both', 'Slippers of Serenity'), +(11, 2, 8, 0, 120, 29183, 'Pre-Raid', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bindings of the Timewalker'), +(11, 2, 9, 0, 120, 29506, 'Pre-Raid', 'Druid', 'Restoration', 'Hands', 'Both', 'Gloves of the Living Touch'), +(11, 2, 10, 0, 120, 27780, 'Pre-Raid', 'Druid', 'Restoration', 'Finger1', 'Both', 'Ring of Fabled Hope'), +(11, 2, 11, 0, 120, 31383, 'Pre-Raid', 'Druid', 'Restoration', 'Finger2', 'Both', 'Spiritualist''s Mark of the Sha''tar'), +(11, 2, 12, 0, 120, 29376, 'Pre-Raid', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(11, 2, 13, 0, 120, 30841, 'Pre-Raid', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Lower City Prayerbook'), +(11, 2, 14, 0, 120, 24254, 'Pre-Raid', 'Druid', 'Restoration', 'Back', 'Both', 'White Remedy Cape'), +(11, 2, 15, 0, 120, 23556, 'Pre-Raid', 'Druid', 'Restoration', 'MainHand', 'Both', 'Hand of Eternity'), +(11, 2, 16, 0, 120, 29274, 'Pre-Raid', 'Druid', 'Restoration', 'OffHand', 'Both', 'Tears of Heaven'), +(11, 2, 17, 0, 120, 27886, 'Pre-Raid', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Emerald Queen'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 125, 24264, 'Phase 1', 'Druid', 'Restoration', 'Head', 'Both', 'Whitemend Hood'), +(11, 2, 1, 0, 125, 30726, 'Phase 1', 'Druid', 'Restoration', 'Neck', 'Both', 'Archaic Charm of Presence'), +(11, 2, 2, 0, 125, 21874, 'Phase 1', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Primal Mooncloth Shoulders'), +(11, 2, 4, 0, 125, 21875, 'Phase 1', 'Druid', 'Restoration', 'Chest', 'Both', 'Primal Mooncloth Robe'), +(11, 2, 5, 0, 125, 21873, 'Phase 1', 'Druid', 'Restoration', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(11, 2, 6, 0, 125, 30727, 'Phase 1', 'Druid', 'Restoration', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(11, 2, 7, 0, 125, 30737, 'Phase 1', 'Druid', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(11, 2, 8, 0, 125, 29183, 'Phase 1', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bindings of the Timewalker'), +(11, 2, 9, 0, 125, 28521, 'Phase 1', 'Druid', 'Restoration', 'Hands', 'Both', 'Mitts of the Treemender'), +(11, 2, 10, 0, 125, 28763, 'Phase 1', 'Druid', 'Restoration', 'Finger1', 'Both', 'Jade Ring of the Everliving'), +(11, 2, 11, 0, 125, 30736, 'Phase 1', 'Druid', 'Restoration', 'Finger2', 'Both', 'Ring of Flowing Light'), +(11, 2, 12, 0, 125, 29376, 'Phase 1', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(11, 2, 13, 0, 125, 30841, 'Phase 1', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Lower City Prayerbook'), +(11, 2, 14, 0, 125, 31329, 'Phase 1', 'Druid', 'Restoration', 'Back', 'Both', 'Lifegiving Cloak'), +(11, 2, 15, 0, 125, 28771, 'Phase 1', 'Druid', 'Restoration', 'MainHand', 'Both', 'Light''s Justice'), +(11, 2, 16, 0, 125, 29274, 'Phase 1', 'Druid', 'Restoration', 'OffHand', 'Both', 'Tears of Heaven'), +(11, 2, 17, 0, 125, 27886, 'Phase 1', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Emerald Queen'); + +-- ilvl 141 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 141, 30219, 'Phase 2', 'Druid', 'Restoration', 'Head', 'Both', 'Nordrassil Headguard'), +(11, 2, 1, 0, 141, 30018, 'Phase 2', 'Druid', 'Restoration', 'Neck', 'Both', 'Lord Sanguinar''s Claim'), +(11, 2, 2, 0, 141, 30221, 'Phase 2', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Nordrassil Life-Mantle'), +(11, 2, 4, 0, 141, 30216, 'Phase 2', 'Druid', 'Restoration', 'Chest', 'Both', 'Nordrassil Chestguard'), +(11, 2, 5, 0, 141, 21873, 'Phase 2', 'Druid', 'Restoration', 'Waist', 'Both', 'Primal Mooncloth Belt'), +(11, 2, 6, 0, 141, 30727, 'Phase 2', 'Druid', 'Restoration', 'Legs', 'Both', 'Gilded Trousers of Benediction'), +(11, 2, 7, 0, 141, 30737, 'Phase 2', 'Druid', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(11, 2, 8, 0, 141, 30062, 'Phase 2', 'Druid', 'Restoration', 'Wrists', 'Both', 'Grove-Bands of Remulos'), +(11, 2, 9, 0, 141, 28521, 'Phase 2', 'Druid', 'Restoration', 'Hands', 'Both', 'Mitts of the Treemender'), +(11, 2, 10, 0, 141, 30110, 'Phase 2', 'Druid', 'Restoration', 'Finger1', 'Both', 'Coral Band of the Revived'), +(11, 2, 11, 0, 141, 28763, 'Phase 2', 'Druid', 'Restoration', 'Finger2', 'Both', 'Jade Ring of the Everliving'), +(11, 2, 12, 0, 141, 29376, 'Phase 2', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(11, 2, 13, 0, 141, 30841, 'Phase 2', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Lower City Prayerbook'), +(11, 2, 14, 0, 141, 29989, 'Phase 2', 'Druid', 'Restoration', 'Back', 'Both', 'Sunshower Light Cloak'), +(11, 2, 15, 0, 141, 30108, 'Phase 2', 'Druid', 'Restoration', 'MainHand', 'Both', 'Lightfathom Scepter'), +(11, 2, 16, 0, 141, 29274, 'Phase 2', 'Druid', 'Restoration', 'OffHand', 'Both', 'Tears of Heaven'), +(11, 2, 17, 0, 141, 27886, 'Phase 2', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Emerald Queen'); + +-- ilvl 156 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 156, 31037, 'Phase 3', 'Druid', 'Restoration', 'Head', 'Both', 'Thunderheart Helmet'), +(11, 2, 1, 0, 156, 30018, 'Phase 3', 'Druid', 'Restoration', 'Neck', 'Both', 'Lord Sanguinar''s Claim'), +(11, 2, 2, 0, 156, 32583, 'Phase 3', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Shoulderpads of Renewed Life'), +(11, 2, 4, 0, 156, 31041, 'Phase 3', 'Druid', 'Restoration', 'Chest', 'Both', 'Thunderheart Tunic'), +(11, 2, 5, 0, 156, 30895, 'Phase 3', 'Druid', 'Restoration', 'Waist', 'Both', 'Angelista''s Sash'), +(11, 2, 6, 0, 156, 30912, 'Phase 3', 'Druid', 'Restoration', 'Legs', 'Both', 'Leggings of Eternity'), +(11, 2, 7, 0, 156, 30737, 'Phase 3', 'Druid', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(11, 2, 8, 0, 156, 32584, 'Phase 3', 'Druid', 'Restoration', 'Wrists', 'Both', 'Swiftheal Wraps'), +(11, 2, 9, 0, 156, 32328, 'Phase 3', 'Druid', 'Restoration', 'Hands', 'Both', 'Botanist''s Gloves of Growth'), +(11, 2, 10, 0, 156, 32528, 'Phase 3', 'Druid', 'Restoration', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(11, 2, 11, 0, 156, 32528, 'Phase 3', 'Druid', 'Restoration', 'Finger2', 'Both', 'Blessed Band of Karabor'), +(11, 2, 12, 0, 156, 29376, 'Phase 3', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(11, 2, 13, 0, 156, 32496, 'Phase 3', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Memento of Tyrande'), +(11, 2, 14, 0, 156, 32524, 'Phase 3', 'Druid', 'Restoration', 'Back', 'Both', 'Shroud of the Highborne'), +(11, 2, 15, 0, 156, 32500, 'Phase 3', 'Druid', 'Restoration', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(11, 2, 16, 0, 156, 30911, 'Phase 3', 'Druid', 'Restoration', 'OffHand', 'Both', 'Scepter of Purification'), +(11, 2, 17, 0, 156, 27886, 'Phase 3', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Emerald Queen'); + +-- ilvl 164 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 164, 31037, 'Phase 4', 'Druid', 'Restoration', 'Head', 'Both', 'Thunderheart Helmet'), +(11, 2, 1, 0, 164, 33281, 'Phase 4', 'Druid', 'Restoration', 'Neck', 'Both', 'Brooch of Nature''s Mercy'), +(11, 2, 2, 0, 164, 32583, 'Phase 4', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Shoulderpads of Renewed Life'), +(11, 2, 4, 0, 164, 31041, 'Phase 4', 'Druid', 'Restoration', 'Chest', 'Both', 'Thunderheart Tunic'), +(11, 2, 5, 0, 164, 30895, 'Phase 4', 'Druid', 'Restoration', 'Waist', 'Both', 'Angelista''s Sash'), +(11, 2, 6, 0, 164, 30912, 'Phase 4', 'Druid', 'Restoration', 'Legs', 'Both', 'Leggings of Eternity'), +(11, 2, 7, 0, 164, 30737, 'Phase 4', 'Druid', 'Restoration', 'Feet', 'Both', 'Gold-Leaf Wildboots'), +(11, 2, 8, 0, 164, 32584, 'Phase 4', 'Druid', 'Restoration', 'Wrists', 'Both', 'Swiftheal Wraps'), +(11, 2, 9, 0, 164, 32328, 'Phase 4', 'Druid', 'Restoration', 'Hands', 'Both', 'Botanist''s Gloves of Growth'), +(11, 2, 10, 0, 164, 32528, 'Phase 4', 'Druid', 'Restoration', 'Finger1', 'Both', 'Blessed Band of Karabor'), +(11, 2, 11, 0, 164, 32528, 'Phase 4', 'Druid', 'Restoration', 'Finger2', 'Both', 'Blessed Band of Karabor'), +(11, 2, 12, 0, 164, 29376, 'Phase 4', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Essence of the Martyr'), +(11, 2, 13, 0, 164, 32496, 'Phase 4', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Memento of Tyrande'), +(11, 2, 14, 0, 164, 32524, 'Phase 4', 'Druid', 'Restoration', 'Back', 'Both', 'Shroud of the Highborne'), +(11, 2, 15, 0, 164, 32500, 'Phase 4', 'Druid', 'Restoration', 'MainHand', 'Both', 'Crystal Spire of Karabor'), +(11, 2, 16, 0, 164, 30911, 'Phase 4', 'Druid', 'Restoration', 'OffHand', 'Both', 'Scepter of Purification'), +(11, 2, 17, 0, 164, 27886, 'Phase 4', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Emerald Queen'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 200, 42554, 'Pre-Raid', 'Druid', 'Restoration', 'Head', 'Both', 'Greensight Gogs'), +(11, 2, 1, 0, 200, 42647, 'Pre-Raid', 'Druid', 'Restoration', 'Neck', 'Both', 'Titanium Spellshock Necklace'), +(11, 2, 2, 0, 200, 37368, 'Pre-Raid', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Silent Spectator Shoulderpads'), +(11, 2, 4, 0, 200, 42102, 'Pre-Raid', 'Druid', 'Restoration', 'Chest', 'Both', 'Spellweave Robe'), +(11, 2, 5, 0, 200, 37643, 'Pre-Raid', 'Druid', 'Restoration', 'Waist', 'Both', 'Sash of Blood Removal'), +(11, 2, 6, 0, 200, 37791, 'Pre-Raid', 'Druid', 'Restoration', 'Legs', 'Both', 'Leggings of the Winged Serpent'), +(11, 2, 7, 0, 200, 43502, 'Pre-Raid', 'Druid', 'Restoration', 'Feet', 'Both', 'Earthgiving Boots'), +(11, 2, 8, 0, 200, 37696, 'Pre-Raid', 'Druid', 'Restoration', 'Wrists', 'Both', 'Plague-Infected Bracers'), +(11, 2, 9, 0, 200, 37230, 'Pre-Raid', 'Druid', 'Restoration', 'Hands', 'Both', 'Grotto Mist Gloves'), +(11, 2, 10, 0, 200, 37694, 'Pre-Raid', 'Druid', 'Restoration', 'Finger1', 'Both', 'Band of Guile'), +(11, 2, 12, 0, 200, 40685, 'Pre-Raid', 'Druid', 'Restoration', 'Trinket1', 'Both', 'The Egg of Mortal Essence'), +(11, 2, 14, 0, 200, 41609, 'Pre-Raid', 'Druid', 'Restoration', 'Back', 'Both', 'Wispcloak'), +(11, 2, 17, 0, 200, 40711, 'Pre-Raid', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of Lush Moss'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 224, 44007, 'Phase 1', 'Druid', 'Restoration', 'Head', 'Both', 'Headpiece of Reconciliation'), +(11, 2, 1, 0, 224, 44661, 'Phase 1', 'Druid', 'Restoration', 'Neck', 'Both', 'Wyrmrest Necklace of Power'), +(11, 2, 2, 0, 224, 39719, 'Phase 1', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Mantle of the Locusts'), +(11, 2, 4, 0, 224, 44002, 'Phase 1', 'Druid', 'Restoration', 'Chest', 'Both', 'The Sanctum''s Flowing Vestments'), +(11, 2, 5, 0, 224, 40561, 'Phase 1', 'Druid', 'Restoration', 'Waist', 'Both', 'Leash of Heedless Magic'), +(11, 2, 6, 0, 224, 40379, 'Phase 1', 'Druid', 'Restoration', 'Legs', 'Both', 'Legguards of the Boneyard'), +(11, 2, 7, 0, 224, 40558, 'Phase 1', 'Druid', 'Restoration', 'Feet', 'Both', 'Arcanic Tramplers'), +(11, 2, 8, 0, 224, 44008, 'Phase 1', 'Druid', 'Restoration', 'Wrists', 'Both', 'Unsullied Cuffs'), +(11, 2, 9, 0, 224, 40460, 'Phase 1', 'Druid', 'Restoration', 'Hands', 'Both', 'Valorous Dreamwalker Handguards'), +(11, 2, 10, 0, 224, 40375, 'Phase 1', 'Druid', 'Restoration', 'Finger1', 'Both', 'Ring of Decaying Beauty'), +(11, 2, 12, 0, 224, 40432, 'Phase 1', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Illustration of the Dragon Soul'), +(11, 2, 14, 0, 224, 44005, 'Phase 1', 'Druid', 'Restoration', 'Back', 'Both', 'Pennant Cloak'), +(11, 2, 17, 0, 224, 40342, 'Phase 1', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of Awakening'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 245, 46184, 'Phase 2', 'Druid', 'Restoration', 'Head', 'Both', 'Conqueror''s Nightsong Headpiece'), +(11, 2, 1, 0, 245, 45243, 'Phase 2', 'Druid', 'Restoration', 'Neck', 'Both', 'Sapphire Amulet of Renewal'), +(11, 2, 2, 0, 245, 46187, 'Phase 2', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Conqueror''s Nightsong Spaulders'), +(11, 2, 4, 0, 245, 45519, 'Phase 2', 'Druid', 'Restoration', 'Chest', 'Both', 'Vestments of the Blind Denizen'), +(11, 2, 5, 0, 245, 45616, 'Phase 2', 'Druid', 'Restoration', 'Waist', 'Both', 'Star-beaded Clutch'), +(11, 2, 6, 0, 245, 46185, 'Phase 2', 'Druid', 'Restoration', 'Legs', 'Both', 'Conqueror''s Nightsong Leggings'), +(11, 2, 7, 0, 245, 45135, 'Phase 2', 'Druid', 'Restoration', 'Feet', 'Both', 'Boots of Fiery Resolution'), +(11, 2, 8, 0, 245, 45446, 'Phase 2', 'Druid', 'Restoration', 'Wrists', 'Both', 'Grasps of Reason'), +(11, 2, 9, 0, 245, 46183, 'Phase 2', 'Druid', 'Restoration', 'Hands', 'Both', 'Conqueror''s Nightsong Handguards'), +(11, 2, 10, 0, 245, 45495, 'Phase 2', 'Druid', 'Restoration', 'Finger1', 'Both', 'Conductive Seal'), +(11, 2, 12, 0, 245, 45535, 'Phase 2', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Show of Faith'), +(11, 2, 14, 0, 245, 45618, 'Phase 2', 'Druid', 'Restoration', 'Back', 'Both', 'Sunglimmer Cloak'), +(11, 2, 17, 0, 245, 40342, 'Phase 2', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of Awakening'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 1, 258, 48141, 'Phase 3', 'Druid', 'Restoration', 'Head', 'Alliance', 'Malfurion''s Headpiece of Triumph'), +(11, 2, 1, 1, 258, 47144, 'Phase 3', 'Druid', 'Restoration', 'Neck', 'Alliance', 'Wail of the Val''kyr'), +(11, 2, 1, 2, 258, 47468, 'Phase 3', 'Druid', 'Restoration', 'Neck', 'Horde', 'Cry of the Val''kyr'), +(11, 2, 2, 1, 258, 48138, 'Phase 3', 'Druid', 'Restoration', 'Shoulders', 'Alliance', 'Malfurion''s Spaulders of Triumph'), +(11, 2, 4, 0, 258, 48169, 'Phase 3', 'Druid', 'Restoration', 'Chest', 'Both', 'Vestments of Triumph'), +(11, 2, 5, 1, 258, 47145, 'Phase 3', 'Druid', 'Restoration', 'Waist', 'Alliance', 'Cord of Pale Thorns'), +(11, 2, 5, 2, 258, 47469, 'Phase 3', 'Druid', 'Restoration', 'Waist', 'Horde', 'Belt of Pale Thorns'), +(11, 2, 6, 0, 258, 48140, 'Phase 3', 'Druid', 'Restoration', 'Legs', 'Both', 'Leggings of Triumph'), +(11, 2, 7, 1, 258, 47097, 'Phase 3', 'Druid', 'Restoration', 'Feet', 'Alliance', 'Boots of the Mourning Widow'), +(11, 2, 7, 2, 258, 47454, 'Phase 3', 'Druid', 'Restoration', 'Feet', 'Horde', 'Sandals of the Mourning Widow'), +(11, 2, 8, 1, 258, 47066, 'Phase 3', 'Druid', 'Restoration', 'Wrists', 'Alliance', 'Bracers of the Autumn Willow'), +(11, 2, 8, 2, 258, 47438, 'Phase 3', 'Druid', 'Restoration', 'Wrists', 'Horde', 'Bindings of the Autumn Willow'), +(11, 2, 9, 0, 258, 48142, 'Phase 3', 'Druid', 'Restoration', 'Hands', 'Both', 'Handguards of Triumph'), +(11, 2, 10, 1, 258, 47224, 'Phase 3', 'Druid', 'Restoration', 'Finger1', 'Alliance', 'Ring of the Darkmender'), +(11, 2, 10, 2, 258, 47439, 'Phase 3', 'Druid', 'Restoration', 'Finger1', 'Horde', 'Circle of the Darkmender'), +(11, 2, 12, 1, 258, 47059, 'Phase 3', 'Druid', 'Restoration', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(11, 2, 12, 2, 258, 47432, 'Phase 3', 'Druid', 'Restoration', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(11, 2, 14, 1, 258, 47553, 'Phase 3', 'Druid', 'Restoration', 'Back', 'Alliance', 'Bolvar''s Devotion'), +(11, 2, 14, 2, 258, 47554, 'Phase 3', 'Druid', 'Restoration', 'Back', 'Horde', 'Lady Liadrin''s Conviction'), +(11, 2, 17, 0, 258, 47671, 'Phase 3', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of Flaring Growth'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 264, 51302, 'Phase 4', 'Druid', 'Restoration', 'Head', 'Both', 'Sanctified Lasherweave Helmet'), +(11, 2, 1, 0, 264, 50609, 'Phase 4', 'Druid', 'Restoration', 'Neck', 'Both', 'Bone Sentinel''s Amulet'), +(11, 2, 2, 0, 264, 51304, 'Phase 4', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Sanctified Lasherweave Pauldrons'), +(11, 2, 4, 0, 264, 50717, 'Phase 4', 'Druid', 'Restoration', 'Chest', 'Both', 'Sanguine Silk Robes'), +(11, 2, 5, 0, 264, 50705, 'Phase 4', 'Druid', 'Restoration', 'Waist', 'Both', 'Professor''s Bloodied Smock'), +(11, 2, 6, 0, 264, 51303, 'Phase 4', 'Druid', 'Restoration', 'Legs', 'Both', 'Sanctified Lasherweave Legplates'), +(11, 2, 7, 0, 264, 50699, 'Phase 4', 'Druid', 'Restoration', 'Feet', 'Both', 'Plague Scientist''s Boots'), +(11, 2, 8, 0, 264, 50630, 'Phase 4', 'Druid', 'Restoration', 'Wrists', 'Both', 'Bracers of Eternal Dreaming'), +(11, 2, 9, 0, 264, 51301, 'Phase 4', 'Druid', 'Restoration', 'Hands', 'Both', 'Sanctified Lasherweave Gauntlets'), +(11, 2, 10, 0, 264, 50636, 'Phase 4', 'Druid', 'Restoration', 'Finger1', 'Both', 'Memory of Malygos'), +(11, 2, 12, 1, 264, 47059, 'Phase 4', 'Druid', 'Restoration', 'Trinket1', 'Alliance', 'Solace of the Defeated'), +(11, 2, 12, 2, 264, 47432, 'Phase 4', 'Druid', 'Restoration', 'Trinket1', 'Horde', 'Solace of the Fallen'), +(11, 2, 14, 0, 264, 50668, 'Phase 4', 'Druid', 'Restoration', 'Back', 'Both', 'Greatcloak of the Turned Champion'), +(11, 2, 15, 0, 264, 46017, 'Phase 4', 'Druid', 'Restoration', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(11, 2, 17, 0, 264, 50454, 'Phase 4', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Black Willow'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 2, 0, 0, 290, 51302, 'Phase 5', 'Druid', 'Restoration', 'Head', 'Both', 'Sanctified Lasherweave Helmet'), +(11, 2, 1, 0, 290, 50609, 'Phase 5', 'Druid', 'Restoration', 'Neck', 'Both', 'Bone Sentinel''s Amulet'), +(11, 2, 2, 0, 290, 51304, 'Phase 5', 'Druid', 'Restoration', 'Shoulders', 'Both', 'Sanctified Lasherweave Pauldrons'), +(11, 2, 4, 0, 290, 50717, 'Phase 5', 'Druid', 'Restoration', 'Chest', 'Both', 'Sanguine Silk Robes'), +(11, 2, 5, 0, 290, 50705, 'Phase 5', 'Druid', 'Restoration', 'Waist', 'Both', 'Professor''s Bloodied Smock'), +(11, 2, 6, 0, 290, 51303, 'Phase 5', 'Druid', 'Restoration', 'Legs', 'Both', 'Sanctified Lasherweave Legplates'), +(11, 2, 7, 0, 290, 50665, 'Phase 5', 'Druid', 'Restoration', 'Feet', 'Both', 'Boots Of Unnatural Growth'), +(11, 2, 8, 0, 290, 54584, 'Phase 5', 'Druid', 'Restoration', 'Wrists', 'Both', 'Phaseshifter Bracers'), +(11, 2, 9, 0, 290, 51301, 'Phase 5', 'Druid', 'Restoration', 'Hands', 'Both', 'Sanctified Lasherweave Gauntlets'), +(11, 2, 10, 0, 290, 50400, 'Phase 5', 'Druid', 'Restoration', 'Finger1', 'Both', 'Ashen Band of Endless Wisdom'), +(11, 2, 11, 0, 290, 50636, 'Phase 5', 'Druid', 'Restoration', 'Finger2', 'Both', 'Memory of Malygos'), +(11, 2, 12, 0, 290, 47059, 'Phase 5', 'Druid', 'Restoration', 'Trinket1', 'Both', 'Solace of the Defeated'), +(11, 2, 13, 0, 290, 54589, 'Phase 5', 'Druid', 'Restoration', 'Trinket2', 'Both', 'Glowing Twilight Scale'), +(11, 2, 14, 0, 290, 50668, 'Phase 5', 'Druid', 'Restoration', 'Back', 'Both', 'Greatcloak of the Turned Champion'), +(11, 2, 15, 0, 290, 46017, 'Phase 5', 'Druid', 'Restoration', 'MainHand', 'Both', 'Val''anyr, Hammer of Ancient Kings'), +(11, 2, 16, 0, 290, 50635, 'Phase 5', 'Druid', 'Restoration', 'OffHand', 'Both', 'Sundial of Eternal Dusk'), +(11, 2, 17, 0, 290, 50454, 'Phase 5', 'Druid', 'Restoration', 'Ranged', 'Both', 'Idol of the Black Willow'); + +-- Feral Bear (tab 10) +-- ilvl 66 (Phase 1 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 66, 14539, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Head', 'Both', 'Bone Ring Helm'), +(11, 10, 1, 0, 66, 13177, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Neck', 'Both', 'Talisman of Evasion'), +(11, 10, 2, 0, 66, 10783, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Atal''ai Spaulders'), +(11, 10, 4, 0, 66, 15064, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Warbear Harness'), +(11, 10, 5, 0, 66, 13252, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Cloudrunner Girdle'), +(11, 10, 6, 0, 66, 11821, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Warstrife Leggings'), +(11, 10, 7, 0, 66, 16711, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Shadowcraft Boots'), +(11, 10, 8, 0, 66, 12966, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Blackmist Armguards'), +(11, 10, 9, 0, 66, 13258, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Slaghide Gauntlets'), +(11, 10, 10, 0, 66, 15855, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Ring of Protection'), +(11, 10, 11, 0, 66, 11669, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Finger2', 'Both', 'Naglering'), +(11, 10, 12, 0, 66, 13966, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Mark of Tyranny'), +(11, 10, 13, 0, 66, 11811, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Trinket2', 'Both', 'Smoking Heart of the Mountain'), +(11, 10, 14, 0, 66, 12551, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'Back', 'Both', 'Stoneshield Cloak'), +(11, 10, 15, 0, 66, 943, 'Phase 1 (Pre-Raid)', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Warden Staff'); + +-- ilvl 76 (Phase 2 (Pre-Raid)) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 76, 14539, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Head', 'Both', 'Bone Ring Helm'), +(11, 10, 1, 0, 76, 13177, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Neck', 'Both', 'Talisman of Evasion'), +(11, 10, 2, 0, 76, 10783, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Atal''ai Spaulders'), +(11, 10, 4, 0, 76, 15064, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Warbear Harness'), +(11, 10, 5, 0, 76, 13252, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Cloudrunner Girdle'), +(11, 10, 6, 0, 76, 11821, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Warstrife Leggings'), +(11, 10, 7, 0, 76, 16711, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Shadowcraft Boots'), +(11, 10, 8, 0, 76, 12966, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Blackmist Armguards'), +(11, 10, 9, 0, 76, 13258, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Slaghide Gauntlets'), +(11, 10, 10, 0, 76, 15855, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Ring of Protection'), +(11, 10, 11, 0, 76, 11669, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Finger2', 'Both', 'Naglering'), +(11, 10, 12, 0, 76, 13966, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Mark of Tyranny'), +(11, 10, 13, 0, 76, 11811, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Trinket2', 'Both', 'Smoking Heart of the Mountain'), +(11, 10, 14, 0, 76, 12551, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'Back', 'Both', 'Stoneshield Cloak'), +(11, 10, 15, 0, 76, 943, 'Phase 2 (Pre-Raid)', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Warden Staff'); + +-- ilvl 78 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 78, 14539, 'Phase 2', 'Druid', 'Feral Bear', 'Head', 'Both', 'Bone Ring Helm'), +(11, 10, 1, 0, 78, 17065, 'Phase 2', 'Druid', 'Feral Bear', 'Neck', 'Both', 'Medallion of Steadfast Might'), +(11, 10, 2, 0, 78, 19139, 'Phase 2', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Fireguard Shoulders'), +(11, 10, 4, 0, 78, 15064, 'Phase 2', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Warbear Harness'), +(11, 10, 5, 0, 78, 19149, 'Phase 2', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Lava Belt'), +(11, 10, 6, 0, 78, 11821, 'Phase 2', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Warstrife Leggings'), +(11, 10, 7, 0, 78, 16711, 'Phase 2', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Shadowcraft Boots'), +(11, 10, 8, 0, 78, 12966, 'Phase 2', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Blackmist Armguards'), +(11, 10, 9, 0, 78, 13258, 'Phase 2', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Slaghide Gauntlets'), +(11, 10, 10, 0, 78, 15855, 'Phase 2', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Ring of Protection'), +(11, 10, 11, 0, 78, 18879, 'Phase 2', 'Druid', 'Feral Bear', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(11, 10, 12, 0, 78, 13966, 'Phase 2', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Mark of Tyranny'), +(11, 10, 13, 0, 78, 11811, 'Phase 2', 'Druid', 'Feral Bear', 'Trinket2', 'Both', 'Smoking Heart of the Mountain'), +(11, 10, 14, 0, 78, 17107, 'Phase 2', 'Druid', 'Feral Bear', 'Back', 'Both', 'Dragon''s Blood Cape'), +(11, 10, 15, 0, 78, 943, 'Phase 2', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Warden Staff'); + +-- ilvl 83 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 83, 14539, 'Phase 3', 'Druid', 'Feral Bear', 'Head', 'Both', 'Bone Ring Helm'), +(11, 10, 1, 0, 83, 17065, 'Phase 3', 'Druid', 'Feral Bear', 'Neck', 'Both', 'Medallion of Steadfast Might'), +(11, 10, 2, 0, 83, 19389, 'Phase 3', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Taut Dragonhide Shoulderpads'), +(11, 10, 4, 0, 83, 19405, 'Phase 3', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Malfurion''s Blessed Bulwark'), +(11, 10, 5, 0, 83, 19149, 'Phase 3', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Lava Belt'), +(11, 10, 6, 0, 83, 11821, 'Phase 3', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Warstrife Leggings'), +(11, 10, 7, 0, 83, 19381, 'Phase 3', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Boots of the Shadow Flame'), +(11, 10, 8, 0, 83, 12966, 'Phase 3', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Blackmist Armguards'), +(11, 10, 9, 0, 83, 13258, 'Phase 3', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Slaghide Gauntlets'), +(11, 10, 10, 0, 83, 19376, 'Phase 3', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Archimtiros'' Ring of Reckoning'), +(11, 10, 11, 0, 83, 18879, 'Phase 3', 'Druid', 'Feral Bear', 'Finger2', 'Both', 'Heavy Dark Iron Ring'), +(11, 10, 12, 0, 83, 13966, 'Phase 3', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Mark of Tyranny'), +(11, 10, 13, 0, 83, 11811, 'Phase 3', 'Druid', 'Feral Bear', 'Trinket2', 'Both', 'Smoking Heart of the Mountain'), +(11, 10, 14, 0, 83, 19386, 'Phase 3', 'Druid', 'Feral Bear', 'Back', 'Both', 'Elementium Threaded Cloak'), +(11, 10, 15, 0, 83, 943, 'Phase 3', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Warden Staff'); + +-- ilvl 88 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 1, 88, 21693, 'Phase 5', 'Druid', 'Feral Bear', 'Head', 'Alliance', 'Guise of the Devourer'), +(11, 10, 0, 2, 88, 21693, 'Phase 5', 'Druid', 'Feral Bear', 'Head', 'Horde', 'Guise of the Devourer'), +(11, 10, 1, 1, 88, 22732, 'Phase 5', 'Druid', 'Feral Bear', 'Neck', 'Alliance', 'Mark of C''Thun'), +(11, 10, 1, 2, 88, 22732, 'Phase 5', 'Druid', 'Feral Bear', 'Neck', 'Horde', 'Mark of C''Thun'), +(11, 10, 2, 1, 88, 20059, 'Phase 5', 'Druid', 'Feral Bear', 'Shoulders', 'Alliance', 'Highlander''s Leather Shoulders'), +(11, 10, 2, 2, 88, 20194, 'Phase 5', 'Druid', 'Feral Bear', 'Shoulders', 'Horde', 'Defiler''s Leather Shoulders'), +(11, 10, 4, 1, 88, 19405, 'Phase 5', 'Druid', 'Feral Bear', 'Chest', 'Alliance', 'Malfurion''s Blessed Bulwark'), +(11, 10, 4, 2, 88, 19405, 'Phase 5', 'Druid', 'Feral Bear', 'Chest', 'Horde', 'Malfurion''s Blessed Bulwark'), +(11, 10, 5, 1, 88, 21675, 'Phase 5', 'Druid', 'Feral Bear', 'Waist', 'Alliance', 'Thick Qirajihide Belt'), +(11, 10, 5, 2, 88, 21675, 'Phase 5', 'Druid', 'Feral Bear', 'Waist', 'Horde', 'Thick Qirajihide Belt'), +(11, 10, 6, 1, 88, 22749, 'Phase 5', 'Druid', 'Feral Bear', 'Legs', 'Alliance', 'Sentinel''s Leather Pants'), +(11, 10, 6, 2, 88, 22740, 'Phase 5', 'Druid', 'Feral Bear', 'Legs', 'Horde', 'Outrider''s Leather Pants'), +(11, 10, 7, 1, 88, 19381, 'Phase 5', 'Druid', 'Feral Bear', 'Feet', 'Alliance', 'Boots of the Shadow Flame'), +(11, 10, 7, 2, 88, 19381, 'Phase 5', 'Druid', 'Feral Bear', 'Feet', 'Horde', 'Boots of the Shadow Flame'), +(11, 10, 8, 1, 88, 21602, 'Phase 5', 'Druid', 'Feral Bear', 'Wrists', 'Alliance', 'Qiraji Execution Bracers'), +(11, 10, 8, 2, 88, 21602, 'Phase 5', 'Druid', 'Feral Bear', 'Wrists', 'Horde', 'Qiraji Execution Bracers'), +(11, 10, 9, 1, 88, 21605, 'Phase 5', 'Druid', 'Feral Bear', 'Hands', 'Alliance', 'Gloves of the Hidden Temple'), +(11, 10, 9, 2, 88, 21605, 'Phase 5', 'Druid', 'Feral Bear', 'Hands', 'Horde', 'Gloves of the Hidden Temple'), +(11, 10, 10, 1, 88, 21601, 'Phase 5', 'Druid', 'Feral Bear', 'Finger1', 'Alliance', 'Ring of Emperor Vek''lor'), +(11, 10, 10, 2, 88, 21601, 'Phase 5', 'Druid', 'Feral Bear', 'Finger1', 'Horde', 'Ring of Emperor Vek''lor'), +(11, 10, 11, 1, 88, 18879, 'Phase 5', 'Druid', 'Feral Bear', 'Finger2', 'Alliance', 'Heavy Dark Iron Ring'), +(11, 10, 11, 2, 88, 18879, 'Phase 5', 'Druid', 'Feral Bear', 'Finger2', 'Horde', 'Heavy Dark Iron Ring'), +(11, 10, 12, 1, 88, 13966, 'Phase 5', 'Druid', 'Feral Bear', 'Trinket1', 'Alliance', 'Mark of Tyranny'), +(11, 10, 12, 2, 88, 13966, 'Phase 5', 'Druid', 'Feral Bear', 'Trinket1', 'Horde', 'Mark of Tyranny'), +(11, 10, 13, 1, 88, 11811, 'Phase 5', 'Druid', 'Feral Bear', 'Trinket2', 'Alliance', 'Smoking Heart of the Mountain'), +(11, 10, 13, 2, 88, 11811, 'Phase 5', 'Druid', 'Feral Bear', 'Trinket2', 'Horde', 'Smoking Heart of the Mountain'), +(11, 10, 14, 1, 88, 19386, 'Phase 5', 'Druid', 'Feral Bear', 'Back', 'Alliance', 'Elementium Threaded Cloak'), +(11, 10, 14, 2, 88, 19386, 'Phase 5', 'Druid', 'Feral Bear', 'Back', 'Horde', 'Elementium Threaded Cloak'), +(11, 10, 15, 1, 88, 943, 'Phase 5', 'Druid', 'Feral Bear', 'MainHand', 'Alliance', 'Warden Staff'), +(11, 10, 15, 2, 88, 943, 'Phase 5', 'Druid', 'Feral Bear', 'MainHand', 'Horde', 'Warden Staff'), +(11, 10, 17, 1, 88, 23198, 'Phase 5', 'Druid', 'Feral Bear', 'Ranged', 'Alliance', 'Idol of Brutality'), +(11, 10, 17, 2, 88, 23198, 'Phase 5', 'Druid', 'Feral Bear', 'Ranged', 'Horde', 'Idol of Brutality'); + +-- ilvl 92 (Phase 6) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 1, 92, 21693, 'Phase 6', 'Druid', 'Feral Bear', 'Head', 'Alliance', 'Guise of the Devourer'), +(11, 10, 0, 2, 92, 21693, 'Phase 6', 'Druid', 'Feral Bear', 'Head', 'Horde', 'Guise of the Devourer'), +(11, 10, 1, 1, 92, 22732, 'Phase 6', 'Druid', 'Feral Bear', 'Neck', 'Alliance', 'Mark of C''Thun'), +(11, 10, 1, 2, 92, 22732, 'Phase 6', 'Druid', 'Feral Bear', 'Neck', 'Horde', 'Mark of C''Thun'), +(11, 10, 2, 1, 92, 20059, 'Phase 6', 'Druid', 'Feral Bear', 'Shoulders', 'Alliance', 'Highlander''s Leather Shoulders'), +(11, 10, 2, 2, 92, 20194, 'Phase 6', 'Druid', 'Feral Bear', 'Shoulders', 'Horde', 'Defiler''s Leather Shoulders'), +(11, 10, 4, 1, 92, 23226, 'Phase 6', 'Druid', 'Feral Bear', 'Chest', 'Alliance', 'Ghoul Skin Tunic'), +(11, 10, 4, 2, 92, 23226, 'Phase 6', 'Druid', 'Feral Bear', 'Chest', 'Horde', 'Ghoul Skin Tunic'), +(11, 10, 5, 1, 92, 21675, 'Phase 6', 'Druid', 'Feral Bear', 'Waist', 'Alliance', 'Thick Qirajihide Belt'), +(11, 10, 5, 2, 92, 21675, 'Phase 6', 'Druid', 'Feral Bear', 'Waist', 'Horde', 'Thick Qirajihide Belt'), +(11, 10, 6, 1, 92, 22749, 'Phase 6', 'Druid', 'Feral Bear', 'Legs', 'Alliance', 'Sentinel''s Leather Pants'), +(11, 10, 6, 2, 92, 22740, 'Phase 6', 'Druid', 'Feral Bear', 'Legs', 'Horde', 'Outrider''s Leather Pants'), +(11, 10, 7, 1, 92, 19381, 'Phase 6', 'Druid', 'Feral Bear', 'Feet', 'Alliance', 'Boots of the Shadow Flame'), +(11, 10, 7, 2, 92, 19381, 'Phase 6', 'Druid', 'Feral Bear', 'Feet', 'Horde', 'Boots of the Shadow Flame'), +(11, 10, 8, 1, 92, 22663, 'Phase 6', 'Druid', 'Feral Bear', 'Wrists', 'Alliance', 'Polar Bracers'), +(11, 10, 8, 2, 92, 22663, 'Phase 6', 'Druid', 'Feral Bear', 'Wrists', 'Horde', 'Polar Bracers'), +(11, 10, 9, 1, 92, 21605, 'Phase 6', 'Druid', 'Feral Bear', 'Hands', 'Alliance', 'Gloves of the Hidden Temple'), +(11, 10, 9, 2, 92, 21605, 'Phase 6', 'Druid', 'Feral Bear', 'Hands', 'Horde', 'Gloves of the Hidden Temple'), +(11, 10, 10, 1, 92, 21601, 'Phase 6', 'Druid', 'Feral Bear', 'Finger1', 'Alliance', 'Ring of Emperor Vek''lor'), +(11, 10, 10, 2, 92, 21601, 'Phase 6', 'Druid', 'Feral Bear', 'Finger1', 'Horde', 'Ring of Emperor Vek''lor'), +(11, 10, 11, 1, 92, 23018, 'Phase 6', 'Druid', 'Feral Bear', 'Finger2', 'Alliance', 'Signet of the Fallen Defender'), +(11, 10, 11, 2, 92, 23018, 'Phase 6', 'Druid', 'Feral Bear', 'Finger2', 'Horde', 'Signet of the Fallen Defender'), +(11, 10, 12, 1, 92, 13966, 'Phase 6', 'Druid', 'Feral Bear', 'Trinket1', 'Alliance', 'Mark of Tyranny'), +(11, 10, 12, 2, 92, 13966, 'Phase 6', 'Druid', 'Feral Bear', 'Trinket1', 'Horde', 'Mark of Tyranny'), +(11, 10, 13, 1, 92, 11811, 'Phase 6', 'Druid', 'Feral Bear', 'Trinket2', 'Alliance', 'Smoking Heart of the Mountain'), +(11, 10, 13, 2, 92, 11811, 'Phase 6', 'Druid', 'Feral Bear', 'Trinket2', 'Horde', 'Smoking Heart of the Mountain'), +(11, 10, 14, 1, 92, 22938, 'Phase 6', 'Druid', 'Feral Bear', 'Back', 'Alliance', 'Cryptfiend Silk Cloak'), +(11, 10, 14, 2, 92, 22938, 'Phase 6', 'Druid', 'Feral Bear', 'Back', 'Horde', 'Cryptfiend Silk Cloak'), +(11, 10, 15, 1, 92, 943, 'Phase 6', 'Druid', 'Feral Bear', 'MainHand', 'Alliance', 'Warden Staff'), +(11, 10, 15, 2, 92, 943, 'Phase 6', 'Druid', 'Feral Bear', 'MainHand', 'Horde', 'Warden Staff'), +(11, 10, 17, 1, 92, 23198, 'Phase 6', 'Druid', 'Feral Bear', 'Ranged', 'Alliance', 'Idol of Brutality'), +(11, 10, 17, 2, 92, 23198, 'Phase 6', 'Druid', 'Feral Bear', 'Ranged', 'Horde', 'Idol of Brutality'); + +-- ilvl 120 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 120, 28182, 'Pre-Raid', 'Druid', 'Feral Bear', 'Head', 'Both', 'Helm of the Claw'), +(11, 10, 1, 0, 120, 27779, 'Pre-Raid', 'Druid', 'Feral Bear', 'Neck', 'Both', 'Bone Chain Necklace'), +(11, 10, 2, 0, 120, 27434, 'Pre-Raid', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Mantle of Perenolde'), +(11, 10, 4, 0, 120, 25689, 'Pre-Raid', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Heavy Clefthoof Vest'), +(11, 10, 5, 0, 120, 30942, 'Pre-Raid', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Manimal''s Cinch'), +(11, 10, 6, 0, 120, 31544, 'Pre-Raid', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Clefthoof Hide Leggings'), +(11, 10, 7, 0, 120, 25691, 'Pre-Raid', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Heavy Clefthoof Boots'), +(11, 10, 8, 0, 120, 29263, 'Pre-Raid', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Forestheart Bracers'), +(11, 10, 9, 0, 120, 30341, 'Pre-Raid', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Flesh Handler''s Gauntlets'), +(11, 10, 10, 0, 120, 30834, 'Pre-Raid', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Shapeshifter''s Signet'), +(11, 10, 12, 0, 120, 19406, 'Pre-Raid', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Drake Fang Talisman'), +(11, 10, 13, 0, 120, 29383, 'Pre-Raid', 'Druid', 'Feral Bear', 'Trinket2', 'Both', 'Bloodlust Brooch'), +(11, 10, 14, 0, 120, 24258, 'Pre-Raid', 'Druid', 'Feral Bear', 'Back', 'Both', 'Resolute Cape'), +(11, 10, 15, 0, 120, 29171, 'Pre-Raid', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Earthwarden'), +(11, 10, 17, 0, 120, 28064, 'Pre-Raid', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of the Wild'); + +-- ilvl 125 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 125, 29098, 'Phase 1', 'Druid', 'Feral Bear', 'Head', 'Both', 'Stag-Helm of Malorne'), +(11, 10, 1, 0, 125, 28509, 'Phase 1', 'Druid', 'Feral Bear', 'Neck', 'Both', 'Worgen Claw Necklace'), +(11, 10, 2, 0, 125, 29100, 'Phase 1', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Mantle of Malorne'), +(11, 10, 4, 0, 125, 29096, 'Phase 1', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Breastplate of Malorne'), +(11, 10, 5, 0, 125, 29264, 'Phase 1', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Tree-Mender''s Belt'), +(11, 10, 6, 0, 125, 29099, 'Phase 1', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Greaves of Malorne'), +(11, 10, 7, 0, 125, 28545, 'Phase 1', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Edgewalker Longboots'), +(11, 10, 8, 0, 125, 29263, 'Phase 1', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Forestheart Bracers'), +(11, 10, 9, 0, 125, 29097, 'Phase 1', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Gauntlets of Malorne'), +(11, 10, 10, 0, 125, 30834, 'Phase 1', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Shapeshifter''s Signet'), +(11, 10, 11, 0, 125, 29279, 'Phase 1', 'Druid', 'Feral Bear', 'Finger2', 'Both', 'Violet Signet of the Great Protector'), +(11, 10, 12, 0, 125, 29383, 'Phase 1', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Bloodlust Brooch'), +(11, 10, 13, 0, 125, 28830, 'Phase 1', 'Druid', 'Feral Bear', 'Trinket2', 'Both', 'Dragonspine Trophy'), +(11, 10, 14, 0, 125, 28660, 'Phase 1', 'Druid', 'Feral Bear', 'Back', 'Both', 'Gilded Thorium Cloak'), +(11, 10, 15, 0, 125, 28658, 'Phase 1', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Terestian''s Stranglestaff'), +(11, 10, 17, 0, 125, 23198, 'Phase 1', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of Brutality'); + +-- ilvl 200 (Pre-Raid) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 200, 42550, 'Pre-Raid', 'Druid', 'Feral Bear', 'Head', 'Both', 'Weakness Spectralizers'), +(11, 10, 2, 0, 200, 43481, 'Pre-Raid', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Trollwoven Spaulders'), +(11, 10, 4, 0, 200, 39554, 'Pre-Raid', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Heroes'' Dreamwalker Raiments'), +(11, 10, 5, 0, 200, 40694, 'Pre-Raid', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Jorach''s Crocolisk Skin Belt'), +(11, 10, 6, 0, 200, 37644, 'Pre-Raid', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Gored Hide Legguards'), +(11, 10, 7, 0, 200, 44297, 'Pre-Raid', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Boots of the Neverending Path'), +(11, 10, 8, 0, 200, 44203, 'Pre-Raid', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Dragonfriend Bracers'), +(11, 10, 9, 0, 200, 37409, 'Pre-Raid', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Gilt-Edged Leather Gauntlets'), +(11, 10, 10, 0, 200, 40586, 'Pre-Raid', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Band of the Kirin Tor'), +(11, 10, 12, 0, 200, 42987, 'Pre-Raid', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(11, 10, 14, 0, 200, 43406, 'Pre-Raid', 'Druid', 'Feral Bear', 'Back', 'Both', 'Cloak of the Gushing Wound'), +(11, 10, 17, 0, 200, 40713, 'Pre-Raid', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of the Ravenous Beast'); + +-- ilvl 224 (Phase 1) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 224, 40329, 'Phase 1', 'Druid', 'Feral Bear', 'Head', 'Both', 'Hood of the Exodus'), +(11, 10, 2, 0, 224, 40494, 'Phase 1', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Valorous Dreamwalker Shoulderpads'), +(11, 10, 4, 0, 224, 40471, 'Phase 1', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Valorous Dreamwalker Raiments'), +(11, 10, 5, 0, 224, 43591, 'Phase 1', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Polar Cord'), +(11, 10, 6, 0, 224, 44011, 'Phase 1', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Leggings of the Honored'), +(11, 10, 7, 0, 224, 40243, 'Phase 1', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Footwraps of Vile Deceit'), +(11, 10, 8, 0, 224, 40186, 'Phase 1', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Thrusting Bands'), +(11, 10, 9, 0, 224, 40472, 'Phase 1', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Valorous Dreamwalker Handgrips'), +(11, 10, 10, 0, 224, 40370, 'Phase 1', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Gatekeeper'), +(11, 10, 12, 0, 224, 44253, 'Phase 1', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Darkmoon Card: Greatness'), +(11, 10, 14, 0, 224, 40252, 'Phase 1', 'Druid', 'Feral Bear', 'Back', 'Both', 'Cloak of the Shadowed Sun'), +(11, 10, 17, 0, 224, 38365, 'Phase 1', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of Perspicacious Attacks'); + +-- ilvl 245 (Phase 2) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 245, 41678, 'Phase 2', 'Druid', 'Feral Bear', 'Head', 'Both', 'Furious Gladiator''s Dragonhide Helm'), +(11, 10, 2, 0, 245, 45245, 'Phase 2', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Shoulderpads of the Intruder'), +(11, 10, 4, 0, 245, 45473, 'Phase 2', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Embrace of the Gladiator'), +(11, 10, 5, 0, 245, 46095, 'Phase 2', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Soul-Devouring Cinch'), +(11, 10, 6, 0, 245, 45536, 'Phase 2', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Legguards of Cunning Deception'), +(11, 10, 7, 0, 245, 45232, 'Phase 2', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Runed Ironhide Boots'), +(11, 10, 8, 0, 245, 45611, 'Phase 2', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Solar Bindings'), +(11, 10, 9, 0, 245, 46043, 'Phase 2', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Gloves of the Endless Dark'), +(11, 10, 10, 0, 245, 45471, 'Phase 2', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Fate''s Clutch'), +(11, 10, 12, 0, 245, 45158, 'Phase 2', 'Druid', 'Feral Bear', 'Trinket1', 'Both', 'Heart of Iron'), +(11, 10, 14, 0, 245, 45496, 'Phase 2', 'Druid', 'Feral Bear', 'Back', 'Both', 'Titanskin Cloak'), +(11, 10, 17, 0, 245, 45509, 'Phase 2', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of the Corruptor'); + +-- ilvl 258 (Phase 3) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 258, 48201, 'Phase 3', 'Druid', 'Feral Bear', 'Head', 'Both', 'Headguard of Triumph'), +(11, 10, 2, 0, 258, 45245, 'Phase 3', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Shoulderpads of the Intruder'), +(11, 10, 4, 1, 258, 47004, 'Phase 3', 'Druid', 'Feral Bear', 'Chest', 'Alliance', 'Cuirass of Calamitous Fate'), +(11, 10, 4, 2, 258, 47431, 'Phase 3', 'Druid', 'Feral Bear', 'Chest', 'Horde', 'Vest of Calamitous Fate'), +(11, 10, 5, 1, 258, 47112, 'Phase 3', 'Druid', 'Feral Bear', 'Waist', 'Alliance', 'Belt of the Merciless Killer'), +(11, 10, 5, 2, 258, 47460, 'Phase 3', 'Druid', 'Feral Bear', 'Waist', 'Horde', 'Belt of the Pitiless Killer'), +(11, 10, 6, 1, 258, 46975, 'Phase 3', 'Druid', 'Feral Bear', 'Legs', 'Alliance', 'Leggings of the Broken Beast'), +(11, 10, 6, 2, 258, 47420, 'Phase 3', 'Druid', 'Feral Bear', 'Legs', 'Horde', 'Legwraps of the Broken Beast'), +(11, 10, 7, 1, 258, 47077, 'Phase 3', 'Druid', 'Feral Bear', 'Feet', 'Alliance', 'Treads of the Icewalker'), +(11, 10, 7, 2, 258, 47445, 'Phase 3', 'Druid', 'Feral Bear', 'Feet', 'Horde', 'Icewalker Treads'), +(11, 10, 8, 1, 258, 47155, 'Phase 3', 'Druid', 'Feral Bear', 'Wrists', 'Alliance', 'Bracers of Dark Determination'), +(11, 10, 8, 2, 258, 47474, 'Phase 3', 'Druid', 'Feral Bear', 'Wrists', 'Horde', 'Armbands of Dark Determination'), +(11, 10, 9, 0, 258, 48202, 'Phase 3', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Handgrips of Triumph'), +(11, 10, 10, 1, 258, 47955, 'Phase 3', 'Druid', 'Feral Bear', 'Finger1', 'Alliance', 'Loop of the Twin Val''kyr'), +(11, 10, 10, 2, 258, 48027, 'Phase 3', 'Druid', 'Feral Bear', 'Finger1', 'Horde', 'Band of the Twin Val''kyr'), +(11, 10, 12, 1, 258, 47088, 'Phase 3', 'Druid', 'Feral Bear', 'Trinket1', 'Alliance', 'Satrina''s Impeding Scarab'), +(11, 10, 12, 2, 258, 47451, 'Phase 3', 'Druid', 'Feral Bear', 'Trinket1', 'Horde', 'Juggernaut''s Vitality'), +(11, 10, 14, 1, 258, 47549, 'Phase 3', 'Druid', 'Feral Bear', 'Back', 'Alliance', 'Magni''s Resolution'), +(11, 10, 14, 2, 258, 47550, 'Phase 3', 'Druid', 'Feral Bear', 'Back', 'Horde', 'Cairne''s Endurance'), +(11, 10, 15, 0, 258, 48523, 'Phase 3', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Relentless Gladiator''s Greatstaff'), +(11, 10, 17, 0, 258, 45509, 'Phase 3', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of the Corruptor'); + +-- ilvl 264 (Phase 4) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 264, 51296, 'Phase 4', 'Druid', 'Feral Bear', 'Head', 'Both', 'Sanctified Lasherweave Headguard'), +(11, 10, 2, 0, 264, 51299, 'Phase 4', 'Druid', 'Feral Bear', 'Shoulders', 'Both', 'Sanctified Lasherweave Shoulderpads'), +(11, 10, 4, 0, 264, 51298, 'Phase 4', 'Druid', 'Feral Bear', 'Chest', 'Both', 'Sanctified Lasherweave Raiment'), +(11, 10, 5, 0, 264, 50707, 'Phase 4', 'Druid', 'Feral Bear', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(11, 10, 6, 0, 264, 51297, 'Phase 4', 'Druid', 'Feral Bear', 'Legs', 'Both', 'Sanctified Lasherweave Legguards'), +(11, 10, 7, 0, 264, 50607, 'Phase 4', 'Druid', 'Feral Bear', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(11, 10, 8, 0, 264, 50670, 'Phase 4', 'Druid', 'Feral Bear', 'Wrists', 'Both', 'Toskk''s Maximized Wristguards'), +(11, 10, 9, 0, 264, 50675, 'Phase 4', 'Druid', 'Feral Bear', 'Hands', 'Both', 'Aldriana''s Gloves of Secrecy'), +(11, 10, 10, 0, 264, 50402, 'Phase 4', 'Druid', 'Feral Bear', 'Finger1', 'Both', 'Ashen Band of Endless Vengeance'), +(11, 10, 12, 1, 264, 47088, 'Phase 4', 'Druid', 'Feral Bear', 'Trinket1', 'Alliance', 'Satrina''s Impeding Scarab'), +(11, 10, 12, 2, 264, 47451, 'Phase 4', 'Druid', 'Feral Bear', 'Trinket1', 'Horde', 'Juggernaut''s Vitality'), +(11, 10, 14, 0, 264, 50466, 'Phase 4', 'Druid', 'Feral Bear', 'Back', 'Both', 'Sentinel''s Winter Cloak'), +(11, 10, 15, 0, 264, 50735, 'Phase 4', 'Druid', 'Feral Bear', 'MainHand', 'Both', 'Oathbinder, Charge of the Ranger-General'), +(11, 10, 17, 0, 264, 50456, 'Phase 4', 'Druid', 'Feral Bear', 'Ranged', 'Both', 'Idol of the Crying Moon'); + +-- ilvl 290 (Phase 5) +INSERT INTO `playerbots_bis_gear` VALUES +(11, 10, 0, 0, 290, 51296, 'Phase 5', 'Druid', 'FeralBear', 'Head', 'Both', 'Sanctified Lasherweave Headguard'), +(11, 10, 1, 0, 290, 50682, 'Phase 5', 'Druid', 'FeralBear', 'Neck', 'Both', 'Bile-Encrusted Medallion'), +(11, 10, 2, 0, 290, 51299, 'Phase 5', 'Druid', 'FeralBear', 'Shoulders', 'Both', 'Sanctified Lasherweave Shoulderpads'), +(11, 10, 4, 0, 290, 50656, 'Phase 5', 'Druid', 'FeralBear', 'Chest', 'Both', 'Ikfirus''s Sack of Wonder'), +(11, 10, 5, 0, 290, 50707, 'Phase 5', 'Druid', 'FeralBear', 'Waist', 'Both', 'Astrylian''s Sutured Cinch'), +(11, 10, 6, 0, 290, 51297, 'Phase 5', 'Druid', 'FeralBear', 'Legs', 'Both', 'Sanctified Lasherweave Legguards'), +(11, 10, 7, 0, 290, 50607, 'Phase 5', 'Druid', 'FeralBear', 'Feet', 'Both', 'Frostbitten Fur Boots'), +(11, 10, 8, 0, 290, 54580, 'Phase 5', 'Druid', 'FeralBear', 'Wrists', 'Both', 'Umbrage Armbands'), +(11, 10, 9, 0, 290, 51295, 'Phase 5', 'Druid', 'FeralBear', 'Hands', 'Both', 'Sanctified Lasherweave Handgrips'), +(11, 10, 10, 0, 290, 50622, 'Phase 5', 'Druid', 'FeralBear', 'Finger1', 'Both', 'Devium''s Eternally Cold Ring'), +(11, 10, 11, 0, 290, 50404, 'Phase 5', 'Druid', 'FeralBear', 'Finger2', 'Both', 'Ashen Band of Endless Courage'), +(11, 10, 12, 0, 290, 50364, 'Phase 5', 'Druid', 'FeralBear', 'Trinket1', 'Both', 'Sindragosa''s Flawless Fang'), +(11, 10, 13, 0, 290, 50356, 'Phase 5', 'Druid', 'FeralBear', 'Trinket2', 'Both', 'Corroded Skeleton Key'), +(11, 10, 14, 0, 290, 50466, 'Phase 5', 'Druid', 'FeralBear', 'Back', 'Both', 'Sentinel''s Winter Cloak'), +(11, 10, 15, 0, 290, 50735, 'Phase 5', 'Druid', 'FeralBear', 'MainHand', 'Both', 'Oathbinder, Charge of the Ranger-General'), +(11, 10, 17, 0, 290, 50456, 'Phase 5', 'Druid', 'FeralBear', 'Ranged', 'Both', 'Idol of the Crying Moon'); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_01_ai_playerbot_bis_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_01_ai_playerbot_bis_texts.sql new file mode 100644 index 000000000..42fece3d6 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_04_28_01_ai_playerbot_bis_texts.sql @@ -0,0 +1,183 @@ +-- ######################################################### +-- Playerbots - Add /p autogear bis command texts +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'bis_autogear_unavailable_error', + 'bis_no_rows_fallback', + 'bis_command_unavailable_error', + 'bis_altbot_refused_error', + 'bis_quality_floor_error', + 'bis_pvp_refused_error', + 'bis_invalid_arg_error', + 'bis_arg_above_limit_error', + 'bis_no_rows_autogear_msg', + 'bis_closest_match_msg', + 'bis_applying_msg', + 'bis_applied_msg' +); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'bis_autogear_unavailable_error', + 'bis_no_rows_fallback', + 'bis_command_unavailable_error', + 'bis_altbot_refused_error', + 'bis_quality_floor_error', + 'bis_pvp_refused_error', + 'bis_invalid_arg_error', + 'bis_arg_above_limit_error', + 'bis_no_rows_autogear_msg', + 'bis_closest_match_msg', + 'bis_applying_msg', + 'bis_applied_msg' +); + +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 + (1767, 'bis_autogear_unavailable_error', + 'autogear command is not allowed, please check the configuration.', 0, 0, + '자동 장비 명령이 허용되지 않습니다. 설정을 확인하세요.', + 'La commande autogear n''est pas autorisée, veuillez vérifier la configuration.', + 'Der autogear-Befehl ist nicht erlaubt, bitte überprüfe die Konfiguration.', + '自动装备命令未启用,请检查配置。', + '自動裝備指令未啟用,請檢查設定。', + 'El comando autogear no está permitido, por favor revisa la configuración.', + 'El comando autogear no está permitido, por favor revisa la configuración.', + 'Команда autogear не разрешена, проверьте конфигурацию.'), + + (1768, 'bis_no_rows_fallback', + 'No BiS for your tier/spec/level, check cfg, running autogear instead', 0, 0, + '해당 등급/전문화/레벨에 BiS 목록이 없습니다. 설정을 확인하세요. 대신 자동 장비를 실행합니다.', + 'Pas de BiS pour votre tier/spé/niveau, vérifiez la config, exécution d''autogear à la place.', + 'Kein BiS für deinen Tier/Spec/Level, prüfe die Config, führe stattdessen autogear aus.', + '您的等级/天赋/级别没有BiS数据,请检查配置,改为运行autogear。', + '你的等級/天賦/級別沒有BiS資料,請檢查設定,改為執行autogear。', + 'No hay BiS para tu tier/spec/nivel, revisa la configuración, ejecutando autogear en su lugar.', + 'No hay BiS para tu tier/spec/nivel, revisa la configuración, ejecutando autogear en su lugar.', + 'Нет BiS для вашего тира/спека/уровня, проверьте конфиг, запускаю autogear.'), + + (1769, 'bis_command_unavailable_error', + 'bis command is not allowed, please check the configuration.', 0, 0, + 'bis 명령이 허용되지 않습니다. 설정을 확인하세요.', + 'La commande bis n''est pas autorisée, veuillez vérifier la configuration.', + 'Der bis-Befehl ist nicht erlaubt, bitte überprüfe die Konfiguration.', + 'bis命令未启用,请检查配置。', + 'bis指令未啟用,請檢查設定。', + 'El comando bis no está permitido, por favor revisa la configuración.', + 'El comando bis no está permitido, por favor revisa la configuración.', + 'Команда bis не разрешена, проверьте конфигурацию.'), + + (1770, 'bis_altbot_refused_error', + 'You cannot use bis on alt bots.', 0, 0, + '부캐 봇에는 bis를 사용할 수 없습니다.', + 'Vous ne pouvez pas utiliser bis sur des bots alternatifs.', + 'Du kannst bis nicht auf Zweitbots verwenden.', + '你不能在副号机器人上使用bis。', + '你不能在副號機器人上使用bis。', + 'No puedes usar bis en bots alternativos.', + 'No puedes usar bis en bots alternativos.', + 'Вы не можете использовать bis на дополнительных ботах.'), + + (1771, 'bis_quality_floor_error', + 'AutoGearQualityLimit must be 4 for BiS.', 0, 0, + 'BiS를 사용하려면 AutoGearQualityLimit이 4여야 합니다.', + 'AutoGearQualityLimit doit être à 4 pour utiliser BiS.', + 'AutoGearQualityLimit muss für BiS auf 4 stehen.', + 'BiS要求AutoGearQualityLimit设置为4。', + 'BiS要求AutoGearQualityLimit設定為4。', + 'AutoGearQualityLimit debe ser 4 para BiS.', + 'AutoGearQualityLimit debe ser 4 para BiS.', + 'AutoGearQualityLimit должен быть 4 для BiS.'), + + (1772, 'bis_pvp_refused_error', + 'bis is PvE only, bot is configured as PvP.', 0, 0, + 'bis는 PvE 전용이며, 이 봇은 PvP로 설정되어 있습니다.', + 'bis est uniquement pour le JcE, ce bot est configuré en JcJ.', + 'bis ist nur für PvE, dieser Bot ist als PvP konfiguriert.', + 'bis仅适用于PvE,此机器人配置为PvP。', + 'bis僅適用於PvE,此機器人設定為PvP。', + 'bis es solo para PvE, el bot está configurado como PvP.', + 'bis es solo para PvE, el bot está configurado como PvP.', + 'bis только для PvE, бот настроен на PvP.'), + + (1773, 'bis_invalid_arg_error', + 'Invalid BiS ilvl argument ''%param''. Use a positive integer.', 0, 0, + '잘못된 BiS 아이템 레벨 인수 ''%param''. 양의 정수를 사용하세요.', + 'Argument iLvl BiS invalide ''%param''. Utilisez un entier positif.', + 'Ungültiges BiS-iLvl-Argument ''%param''. Verwende eine positive ganze Zahl.', + '无效的BiS物品等级参数“%param”。请使用正整数。', + '無效的BiS物品等級參數「%param」。請使用正整數。', + 'Argumento iLvl BiS inválido ''%param''. Usa un entero positivo.', + 'Argumento iLvl BiS inválido ''%param''. Usa un entero positivo.', + 'Неверный аргумент iLvl BiS ''%param''. Используйте положительное целое число.'), + + (1774, 'bis_arg_above_limit_error', + 'BiS ilvl %requested exceeds AutoGearScoreLimit %limit, refusing', 0, 0, + 'BiS 아이템 레벨 %requested이(가) AutoGearScoreLimit %limit을(를) 초과합니다. 거부합니다.', + 'iLvl BiS %requested dépasse AutoGearScoreLimit %limit, refusé.', + 'BiS-iLvl %requested überschreitet AutoGearScoreLimit %limit, abgelehnt.', + 'BiS物品等级%requested超过AutoGearScoreLimit %limit,已拒绝。', + 'BiS物品等級%requested超過AutoGearScoreLimit %limit,已拒絕。', + 'iLvl BiS %requested supera AutoGearScoreLimit %limit, rechazado.', + 'iLvl BiS %requested supera AutoGearScoreLimit %limit, rechazado.', + 'BiS iLvl %requested превышает AutoGearScoreLimit %limit, отказано.'), + + (1775, 'bis_no_rows_autogear_msg', + 'No BiS at ilvl %ilvl, using Autogear %ilvl instead', 0, 0, + '아이템 레벨 %ilvl의 BiS가 없어 대신 Autogear %ilvl을(를) 사용합니다.', + 'Pas de BiS à l''iLvl %ilvl, utilisation d''Autogear %ilvl à la place.', + 'Kein BiS auf iLvl %ilvl, verwende stattdessen Autogear %ilvl.', + '物品等级%ilvl没有BiS,改用Autogear %ilvl。', + '物品等級%ilvl沒有BiS,改用Autogear %ilvl。', + 'No hay BiS en iLvl %ilvl, usando Autogear %ilvl en su lugar.', + 'No hay BiS en iLvl %ilvl, usando Autogear %ilvl en su lugar.', + 'Нет BiS на iLvl %ilvl, использую Autogear %ilvl.'), + + (1776, 'bis_closest_match_msg', + 'No BiS at ilvl %requested, using closest match at ilvl %resolved', 0, 0, + '아이템 레벨 %requested의 BiS가 없어 가장 가까운 아이템 레벨 %resolved을(를) 사용합니다.', + 'Pas de BiS à l''iLvl %requested, utilisation de la correspondance la plus proche à l''iLvl %resolved.', + 'Kein BiS auf iLvl %requested, verwende nächstliegende Übereinstimmung auf iLvl %resolved.', + '物品等级%requested没有BiS,使用最接近的物品等级%resolved。', + '物品等級%requested沒有BiS,使用最接近的物品等級%resolved。', + 'No hay BiS en iLvl %requested, usando coincidencia más cercana en iLvl %resolved.', + 'No hay BiS en iLvl %requested, usando coincidencia más cercana en iLvl %resolved.', + 'Нет BiS на iLvl %requested, использую ближайшее совпадение на iLvl %resolved.'), + + (1777, 'bis_applying_msg', 'Applying BiS gear', 0, 0, + 'BiS 장비를 적용합니다.', + 'Application de l''équipement BiS.', + 'Wende BiS-Ausrüstung an.', + '正在装备BiS装备。', + '正在裝備BiS裝備。', + 'Aplicando equipo BiS.', + 'Aplicando equipo BiS.', + 'Применяю BiS-снаряжение.'), + + (1778, 'bis_applied_msg', 'BiS applied', 0, 0, + 'BiS 장비가 적용되었습니다.', + 'Équipement BiS appliqué.', + 'BiS-Ausrüstung angewendet.', + 'BiS装备已应用。', + 'BiS裝備已套用。', + 'Equipo BiS aplicado.', + 'Equipo BiS aplicado.', + 'BiS-снаряжение применено.'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES + ('bis_autogear_unavailable_error', 100), + ('bis_no_rows_fallback', 100), + ('bis_command_unavailable_error', 100), + ('bis_altbot_refused_error', 100), + ('bis_quality_floor_error', 100), + ('bis_pvp_refused_error', 100), + ('bis_invalid_arg_error', 100), + ('bis_arg_above_limit_error', 100), + ('bis_no_rows_autogear_msg', 100), + ('bis_closest_match_msg', 100), + ('bis_applying_msg', 100), + ('bis_applied_msg', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_05_19_00_ai_playerbot_audit_texts.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_05_19_00_ai_playerbot_audit_texts.sql new file mode 100644 index 000000000..39639b968 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_05_19_00_ai_playerbot_audit_texts.sql @@ -0,0 +1,489 @@ +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'quest_accept_debug', + 'quest_already_have_error', + 'quest_cant_take_error', + 'arena_team_already_in_team', + 'arena_team_thanks_for_invite', + 'area_trigger_follow_too_far_error', + 'area_trigger_wait_for_me', + 'attack_no_target_error', + 'attack_target_not_in_world_error', + 'attack_in_flight_error', + 'attack_pvp_prohibited_error', + 'attack_target_friendly_error', + 'attack_target_dead_error', + 'attack_target_not_in_sight_error', + 'attack_already_attacking_error', + 'attack_invalid_target_error', + 'bank_no_banker_nearby_error', + 'move_from_group', + 'running_away', + 'clean_quest_log_started', + 'quest_trivial_will_remove', + 'quest_has_been_removed', + 'quest_not_trivial_kept', + 'quest_removed_debug', + 'quest_removed_with_name', + 'guild_accept_inviter_not_in_guild', + 'guild_accept_already_in_guild', + 'guild_accept_declined', + 'outfit_usage_add', + 'outfit_usage_remove', + 'outfit_usage_equip', + 'outfit_set_as', + 'outfit_equipping', + 'outfit_replace_current', + 'outfit_resetting', + 'outfit_updating_current', + 'outfit_item_removed_from', + 'outfit_item_added_to', + 'release_spirit_not_dead_wait', + 'release_spirit_already_spirit', + 'release_spirit_releasing', + 'release_spirit_meet_graveyard', + 'send_mail_no_mailbox_nearby', + 'send_mail_one_item_only', + 'send_mail_cannot_send_money', + 'send_mail_not_enough_money', + 'send_mail_sending_to', + 'send_mail_cannot_send_item', + 'send_mail_item_not_for_sale', + 'send_mail_sent_to', + 'craft_reset', + 'craft_usage', + 'craft_cannot_craft', + 'craft_summary', + 'set_home_success', + 'set_home_no_innkeeper_error', + 'quest_shared', + 'tame_invalid_id_error', + 'tame_usage_error', + 'tame_pet_changed', + 'tame_pet_changed_initialized', + 'tame_exotic_requires_beast_mastery', + 'tame_no_pet_by_name', + 'tame_no_pet_by_id', + 'tame_no_pet_by_family', + 'tame_no_pet_to_rename', + 'tame_pet_name_length_error', + 'tame_pet_name_alpha_error', + 'tame_pet_name_forbidden_error', + 'tame_pet_renamed', + 'tame_pet_rename_refresh_hint', + 'tame_only_hunters_level_10', + 'tame_creature_template_not_found', + 'tame_create_pet_failed', + 'tame_pet_abandoned', + 'tame_no_hunter_pet_to_abandon', + 'taxi_ready_next_flight', + 'taxi_cant_fly_with_you', + 'taxi_no_flightmaster_nearby', + 'trade_busy_now', + 'trade_disabled', + 'trade_thank_you_player', + 'trade_selling_disabled', + 'trade_buying_disabled', + 'trade_item_not_for_sale', + 'trade_item_not_needed', + 'trade_no_items_error', + 'trade_discount_buy_only', + 'trade_success_pleasure', + 'trade_success_fair_trade', + 'trade_success_thanks', + 'trade_success_off_with_you', + 'trade_want_money_for_this', + 'use_item_none_available', + 'use_gameobject', + 'socket_does_not_fit', + 'use_item_on_target', + 'use_item', + 'socketing_item_with_gem', + 'meeting_stone_in_combat', + 'meeting_stone_welcome', + 'meeting_stone_none_nearby', + 'meeting_stone_none_near_you', + 'meeting_stone_no_hearthstone_self', + 'meeting_stone_no_hearthstone_you', + 'meeting_stone_hearthstone_not_ready_self', + 'meeting_stone_hearthstone_not_ready_you', + 'meeting_stone_no_innkeepers_nearby', + 'meeting_stone_no_innkeepers_near_you', + 'meeting_stone_cannot_summon_vehicle', + 'meeting_stone_cannot_summon_master_in_combat', + 'meeting_stone_cannot_summon_master_dead', + 'meeting_stone_cannot_summon_bot_dead', + 'meeting_stone_revived', + 'meeting_stone_not_enough_space', + 'new_rpg_quest_accepted', + 'new_rpg_quest_rewarded', + 'new_rpg_quest_dropped', + 'rpg_item_better_for_player', + 'rpg_start_trade_with_player' +); + +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'quest_accept_debug', + 'quest_already_have_error', + 'quest_cant_take_error', + 'arena_team_already_in_team', + 'arena_team_thanks_for_invite', + 'area_trigger_follow_too_far_error', + 'area_trigger_wait_for_me', + 'attack_no_target_error', + 'attack_target_not_in_world_error', + 'attack_in_flight_error', + 'attack_pvp_prohibited_error', + 'attack_target_friendly_error', + 'attack_target_dead_error', + 'attack_target_not_in_sight_error', + 'attack_already_attacking_error', + 'attack_invalid_target_error', + 'bank_no_banker_nearby_error', + 'move_from_group', + 'running_away', + 'clean_quest_log_started', + 'quest_trivial_will_remove', + 'quest_has_been_removed', + 'quest_not_trivial_kept', + 'quest_removed_debug', + 'quest_removed_with_name', + 'guild_accept_inviter_not_in_guild', + 'guild_accept_already_in_guild', + 'guild_accept_declined', + 'outfit_usage_add', + 'outfit_usage_remove', + 'outfit_usage_equip', + 'outfit_set_as', + 'outfit_equipping', + 'outfit_replace_current', + 'outfit_resetting', + 'outfit_updating_current', + 'outfit_item_removed_from', + 'outfit_item_added_to', + 'release_spirit_not_dead_wait', + 'release_spirit_already_spirit', + 'release_spirit_releasing', + 'release_spirit_meet_graveyard', + 'send_mail_no_mailbox_nearby', + 'send_mail_one_item_only', + 'send_mail_cannot_send_money', + 'send_mail_not_enough_money', + 'send_mail_sending_to', + 'send_mail_cannot_send_item', + 'send_mail_item_not_for_sale', + 'send_mail_sent_to', + 'craft_reset', + 'craft_usage', + 'craft_cannot_craft', + 'craft_summary', + 'set_home_success', + 'set_home_no_innkeeper_error', + 'quest_shared', + 'tame_invalid_id_error', + 'tame_usage_error', + 'tame_pet_changed', + 'tame_pet_changed_initialized', + 'tame_exotic_requires_beast_mastery', + 'tame_no_pet_by_name', + 'tame_no_pet_by_id', + 'tame_no_pet_by_family', + 'tame_no_pet_to_rename', + 'tame_pet_name_length_error', + 'tame_pet_name_alpha_error', + 'tame_pet_name_forbidden_error', + 'tame_pet_renamed', + 'tame_pet_rename_refresh_hint', + 'tame_only_hunters_level_10', + 'tame_creature_template_not_found', + 'tame_create_pet_failed', + 'tame_pet_abandoned', + 'tame_no_hunter_pet_to_abandon', + 'taxi_ready_next_flight', + 'taxi_cant_fly_with_you', + 'taxi_no_flightmaster_nearby', + 'trade_busy_now', + 'trade_disabled', + 'trade_thank_you_player', + 'trade_selling_disabled', + 'trade_buying_disabled', + 'trade_item_not_for_sale', + 'trade_item_not_needed', + 'trade_no_items_error', + 'trade_discount_buy_only', + 'trade_success_pleasure', + 'trade_success_fair_trade', + 'trade_success_thanks', + 'trade_success_off_with_you', + 'trade_want_money_for_this', + 'use_item_none_available', + 'use_gameobject', + 'socket_does_not_fit', + 'use_item_on_target', + 'use_item', + 'socketing_item_with_gem', + 'meeting_stone_in_combat', + 'meeting_stone_welcome', + 'meeting_stone_none_nearby', + 'meeting_stone_none_near_you', + 'meeting_stone_no_hearthstone_self', + 'meeting_stone_no_hearthstone_you', + 'meeting_stone_hearthstone_not_ready_self', + 'meeting_stone_hearthstone_not_ready_you', + 'meeting_stone_no_innkeepers_nearby', + 'meeting_stone_no_innkeepers_near_you', + 'meeting_stone_cannot_summon_vehicle', + 'meeting_stone_cannot_summon_master_in_combat', + 'meeting_stone_cannot_summon_master_dead', + 'meeting_stone_cannot_summon_bot_dead', + 'meeting_stone_revived', + 'meeting_stone_not_enough_space', + 'new_rpg_quest_accepted', + 'new_rpg_quest_rewarded', + 'new_rpg_quest_dropped', + 'rpg_item_better_for_player', + 'rpg_start_trade_with_player' +); + +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 +(1779, 'quest_accept_debug', 'Quest [%quest] accepted', 0, 0, '퀘스트 [%quest] 수락', 'Quête [%quest] acceptée', 'Quest [%quest] angenommen', '任务 [%quest] 已接受', '任務 [%quest] 已接受', 'Misión [%quest] aceptada', 'Misión [%quest] aceptada', 'Задание [%quest] принято'), +(1780, 'quest_already_have_error', 'I have this quest', 0, 0, '이미 이 퀘스트를 가지고 있습니다', 'J''ai déjà cette quête', 'Ich habe diese Quest bereits', '我已经有这个任务了', '我已經有這個任務了', 'Ya tengo esta misión', 'Ya tengo esta misión', 'У меня уже есть это задание'), +(1781, 'quest_cant_take_error', 'I can''t take this quest', 0, 0, '이 퀘스트를 받을 수 없습니다', 'Je ne peux pas prendre cette quête', 'Ich kann diese Quest nicht annehmen', '我无法接受这个任务', '我無法接受這個任務', 'No puedo aceptar esta misión', 'No puedo aceptar esta misión', 'Я не могу взять это задание'), +(1782, 'arena_team_already_in_team', 'Sorry, I am already in such team', 0, 0, '죄송하지만 이미 그런 팀에 속해 있습니다', 'Désolé, je suis déjà dans une telle équipe', 'Entschuldigung, ich bin bereits in so einem Team', '抱歉,我已经在这样的队伍里了', '抱歉,我已經在這樣的隊伍裡了', 'Lo siento, ya estoy en un equipo así', 'Lo siento, ya estoy en un equipo así', 'Извините, я уже состою в такой команде'), +(1783, 'arena_team_thanks_for_invite', 'Thanks for the invite!', 0, 0, '초대해 주셔서 감사합니다!', 'Merci pour l''invitation !', 'Danke für die Einladung!', '谢谢你的邀请!', '謝謝你的邀請!', 'Gracias por la invitación!', 'Gracias por la invitación!', 'Спасибо за приглашение!'), +(1784, 'area_trigger_follow_too_far_error', 'I won''t follow: too far away', 0, 0, '너무 멀어서 따라가지 않겠습니다', 'Je ne suivrai pas : c''est trop loin', 'Ich werde nicht folgen: zu weit entfernt', '我不会跟随:距离太远了', '我不會跟隨:距離太遠了', 'No te seguiré: estás demasiado lejos', 'No te seguiré: estás demasiado lejos', 'Я не пойду следом: слишком далеко'), +(1785, 'area_trigger_wait_for_me', 'Wait for me', 0, 0, '잠깐만 기다려 주세요', 'Attendez-moi', 'Warte auf mich', '等等我', '等等我', 'Espérame', 'Espérame', 'Подожди меня'), +(1786, 'attack_no_target_error', 'I have no target', 0, 0, '대상이 없습니다', 'Je n''ai pas de cible', 'Ich habe kein Ziel', '我没有目标', '我沒有目標', 'No tengo objetivo', 'No tengo objetivo', 'У меня нет цели'), +(1787, 'attack_target_not_in_world_error', '%target is no longer in the world.', 0, 0, '%target은(는) 더 이상 월드에 없습니다.', '%target n''est plus dans le monde.', '%target ist nicht mehr in der Welt.', '%target 已不在世界中。', '%target 已不在世界中。', '%target ya no está en el mundo.', '%target ya no está en el mundo.', '%target больше не находится в мире.'), +(1788, 'attack_in_flight_error', 'I cannot attack in flight', 0, 0, '비행 중에는 공격할 수 없습니다', 'Je ne peux pas attaquer en vol', 'Ich kann im Flug nicht angreifen', '飞行中无法攻击', '飛行中無法攻擊', 'No puedo atacar en vuelo', 'No puedo atacar en vuelo', 'Я не могу атаковать в полёте'), +(1789, 'attack_pvp_prohibited_error', 'I cannot attack other players in PvP prohibited areas.', 0, 0, 'PvP 금지 지역에서는 다른 플레이어를 공격할 수 없습니다.', 'Je ne peux pas attaquer d''autres joueurs dans les zones où le PvP est interdit.', 'Ich kann andere Spieler in PvP-verbotenen Gebieten nicht angreifen.', '我不能在禁止 PvP 的区域攻击其他玩家。', '我不能在禁止 PvP 的區域攻擊其他玩家。', 'No puedo atacar a otros jugadores en zonas donde el JcJ está prohibido.', 'No puedo atacar a otros jugadores en zonas donde el JcJ está prohibido.', 'Я не могу атаковать других игроков в зонах, где PvP запрещено.'), +(1790, 'attack_target_friendly_error', '%target is friendly to me.', 0, 0, '%target은(는) 우호적인 대상입니다.', '%target m''est amical.', '%target ist mir gegenüber freundlich.', '%target 对我是友方目标。', '%target 對我是友方目標。', '%target es amistoso conmigo.', '%target es amistoso conmigo.', '%target дружелюбен ко мне.'), +(1791, 'attack_target_dead_error', '%target is dead.', 0, 0, '%target은(는) 죽었습니다.', '%target est mort.', '%target ist tot.', '%target 已经死了。', '%target 已經死了。', '%target está muerto.', '%target está muerto.', '%target мёртв.'), +(1792, 'attack_target_not_in_sight_error', '%target is not in my sight.', 0, 0, '%target이(가) 시야에 없습니다.', '%target n''est pas dans mon champ de vision.', '%target ist nicht in Sichtweite.', '%target 不在我的视野中。', '%target 不在我的視野中。', '%target no está a mi vista.', '%target no está a mi vista.', '%target вне поля моего зрения.'), +(1793, 'attack_already_attacking_error', 'I am already attacking %target.', 0, 0, '이미 %target을(를) 공격하고 있습니다.', 'J''attaque déjà %target.', 'Ich greife %target bereits an.', '我已经在攻击 %target。', '我已經在攻擊 %target。', 'Ya estoy atacando a %target.', 'Ya estoy atacando a %target.', 'Я уже атакую %target.'), +(1794, 'attack_invalid_target_error', 'I cannot attack an invalid target.', 0, 0, '유효하지 않은 대상은 공격할 수 없습니다.', 'Je ne peux pas attaquer une cible invalide.', 'Ich kann kein ungültiges Ziel angreifen.', '我不能攻击无效目标。', '我不能攻擊無效目標。', 'No puedo atacar un objetivo no válido.', 'No puedo atacar un objetivo no válido.', 'Я не могу атаковать недопустимую цель.'), +(1795, 'bank_no_banker_nearby_error', 'Cannot find banker nearby', 0, 0, '근처에 은행원이 없습니다', 'Impossible de trouver un banquier à proximité', 'Kein Bankier in der Nähe gefunden', '附近找不到银行职员', '附近找不到銀行職員', 'No encuentro un banquero cerca', 'No encuentro un banquero cerca', 'Поблизости нет банкира'), +(1796, 'move_from_group', 'Moving away from group', 0, 0, '파티에서 멀어지는 중입니다', 'Je m''éloigne du groupe', 'Ich entferne mich von der Gruppe', '正在远离队伍', '正在遠離隊伍', 'Me estoy alejando del grupo', 'Me estoy alejando del grupo', 'Отхожу от группы'), +(1797, 'running_away', 'Running away', 0, 0, '도망치는 중입니다', 'Je m''enfuis', 'Ich laufe weg', '正在逃跑', '正在逃跑', 'Estoy huyendo', 'Estoy huyendo', 'Убегаю'), +(1798, 'clean_quest_log_started', 'Clean Quest Log command received, removing grey/trivial quests...', 0, 0, '퀘스트 로그 정리 명령을 받았습니다. 회색/사소한 퀘스트를 제거하는 중입니다...', 'Commande de nettoyage du journal des quêtes reçue, suppression des quêtes grises/triviales...', 'Befehl zum Bereinigen des Questlogs erhalten, graue/triviale Quests werden entfernt...', '已收到清理任务日志命令,正在移除灰色/琐碎任务...', '已收到清理任務日誌命令,正在移除灰色/瑣碎任務...', 'Comando para limpiar el registro de misiones recibido, eliminando misiones grises/triviales...', 'Comando para limpiar el registro de misiones recibido, eliminando misiones grises/triviales...', 'Получена команда очистки журнала заданий, удаляю серые/простые задания...'), +(1799, 'quest_trivial_will_remove', 'Quest [%title] will be removed because it is trivial (grey).', 0, 0, '퀘스트 [%title]은(는) 사소한(회색) 퀘스트이므로 제거됩니다.', 'La quête [%title] sera supprimée car elle est triviale (grise).', 'Quest [%title] wird entfernt, weil sie trivial (grau) ist.', '任务 [%title] 将被移除,因为它是琐碎的(灰色)任务。', '任務 [%title] 將被移除,因為它是瑣碎的(灰色)任務。', 'La misión [%title] se eliminará porque es trivial (gris).', 'La misión [%title] se eliminará porque es trivial (gris).', 'Задание [%title] будет удалено, так как оно простое (серое).'), +(1800, 'quest_has_been_removed', 'Quest [%title] has been removed.', 0, 0, '퀘스트 [%title]이(가) 제거되었습니다.', 'La quête [%title] a été supprimée.', 'Quest [%title] wurde entfernt.', '任务 [%title] 已被移除。', '任務 [%title] 已被移除。', 'La misión [%title] ha sido eliminada.', 'La misión [%title] ha sido eliminada.', 'Задание [%title] было удалено.'), +(1801, 'quest_not_trivial_kept', 'Quest [%title] is not trivial and will be kept.', 0, 0, '퀘스트 [%title]은(는) 사소하지 않으므로 유지됩니다.', 'La quête [%title] n''est pas triviale et sera conservée.', 'Quest [%title] ist nicht trivial und wird behalten.', '任务 [%title] 并不琐碎,将被保留。', '任務 [%title] 並不瑣碎,將被保留。', 'La misión [%title] no es trivial y se conservará.', 'La misión [%title] no es trivial y se conservará.', 'Задание [%title] не является простым и будет сохранено.'), +(1802, 'quest_removed_debug', 'Quest [%quest] removed', 0, 0, '퀘스트 [%quest] 제거됨', 'Quête [%quest] supprimée', 'Quest [%quest] entfernt', '任务 [%quest] 已移除', '任務 [%quest] 已移除', 'Misión [%quest] eliminada', 'Misión [%quest] eliminada', 'Задание [%quest] удалено'), +(1803, 'quest_removed_with_name', 'Quest removed %quest', 0, 0, '퀘스트 제거됨 %quest', 'Quête supprimée %quest', 'Quest entfernt %quest', '任务已移除 %quest', '任務已移除 %quest', 'Misión eliminada %quest', 'Misión eliminada %quest', 'Задание удалено %quest'), +(1804, 'guild_accept_inviter_not_in_guild', 'You are not in a guild!', 0, 0, '당신은 길드에 속해 있지 않습니다!', 'Vous n''êtes pas dans une guilde !', 'Du bist in keiner Gilde!', '你不在公会中!', '你不在公會中!', 'No estás en un gremio!', 'No estás en un gremio!', 'Вы не состоите в гильдии!'), +(1805, 'guild_accept_already_in_guild', 'Sorry, I am in a guild already', 0, 0, '죄송하지만 이미 길드에 속해 있습니다', 'Désolé, je suis déjà dans une guilde', 'Entschuldigung, ich bin bereits in einer Gilde', '抱歉,我已经在公会里了', '抱歉,我已經在公會裡了', 'Lo siento, ya estoy en un gremio', 'Lo siento, ya estoy en un gremio', 'Извините, я уже в гильдии'), +(1806, 'guild_accept_declined', 'Sorry, I don''t want to join your guild :(', 0, 0, '죄송하지만 당신의 길드에 가입하고 싶지 않습니다 :(', 'Désolé, je ne veux pas rejoindre votre guilde :(', 'Entschuldigung, ich möchte deiner Gilde nicht beitreten :(', '抱歉,我不想加入你的公会 :(', '抱歉,我不想加入你的公會 :(', 'Lo siento, no quiero unirme a tu gremio :(', 'Lo siento, no quiero unirme a tu gremio :(', 'Извините, я не хочу вступать в вашу гильдию :('), +(1807, 'outfit_usage_add', 'outfit +[item] to add items', 0, 0, '아이템을 추가하려면 outfit +[item]', 'outfit +[item] pour ajouter des objets', 'outfit +[item], um Gegenstände hinzuzufügen', '使用 outfit +[item] 添加物品', '使用 outfit +[item] 添加物品', 'outfit +[item] para agregar objetos', 'outfit +[item] para agregar objetos', 'outfit +[item], чтобы добавить предметы'), +(1808, 'outfit_usage_remove', 'outfit -[item] to remove items', 0, 0, '아이템을 제거하려면 outfit -[item]', 'outfit -[item] pour retirer des objets', 'outfit -[item], um Gegenstände zu entfernen', '使用 outfit -[item] 移除物品', '使用 outfit -[item] 移除物品', 'outfit -[item] para eliminar objetos', 'outfit -[item] para eliminar objetos', 'outfit -[item], чтобы убрать предметы'), +(1809, 'outfit_usage_equip', 'outfit equip/replace to equip items', 0, 0, '아이템을 장착하려면 outfit equip/replace', 'outfit equip/replace pour équiper des objets', 'outfit equip/replace, um Gegenstände anzulegen', '使用 outfit equip/replace 装备物品', '使用 outfit equip/replace 裝備物品', 'outfit equip/replace para equipar objetos', 'outfit equip/replace para equipar objetos', 'outfit equip/replace, чтобы экипировать предметы'), +(1810, 'outfit_set_as', 'Setting outfit %name as %param', 0, 0, '복장 %name을(를) %param(으)로 설정하는 중입니다', 'Définition de la tenue %name comme %param', 'Setze Outfit %name als %param', '正在将装备方案 %name 设置为 %param', '正在將裝備方案 %name 設定為 %param', 'Estableciendo el atuendo %name como %param', 'Estableciendo el atuendo %name como %param', 'Устанавливаю комплект %name как %param'), +(1811, 'outfit_equipping', 'Equipping outfit %name', 0, 0, '복장 %name을(를) 장착하는 중입니다', 'Équipement de la tenue %name', 'Outfit %name wird angelegt', '正在装备方案 %name', '正在裝備方案 %name', 'Equipando el atuendo %name', 'Equipando el atuendo %name', 'Экипирую комплект %name'), +(1812, 'outfit_replace_current', 'Replacing current equip with outfit %name', 0, 0, '현재 장비를 복장 %name으로 교체하는 중입니다', 'Remplacement de l''équipement actuel par la tenue %name', 'Aktuelle Ausrüstung wird durch Outfit %name ersetzt', '正在用装备方案 %name 替换当前装备', '正在用裝備方案 %name 替換目前裝備', 'Reemplazando el equipo actual con el atuendo %name', 'Reemplazando el equipo actual con el atuendo %name', 'Заменяю текущую экипировку комплектом %name'), +(1813, 'outfit_resetting', 'Resetting outfit %name', 0, 0, '복장 %name을(를) 초기화하는 중입니다', 'Réinitialisation de la tenue %name', 'Outfit %name wird zurückgesetzt', '正在重置装备方案 %name', '正在重置裝備方案 %name', 'Restableciendo el atuendo %name', 'Restableciendo el atuendo %name', 'Сбрасываю комплект %name'), +(1814, 'outfit_updating_current', 'Updating with current items outfit %name', 0, 0, '현재 아이템으로 복장 %name을(를) 업데이트하는 중입니다', 'Mise à jour de la tenue %name avec les objets actuels', 'Outfit %name wird mit der aktuellen Ausrüstung aktualisiert', '正在用当前物品更新装备方案 %name', '正在用目前物品更新裝備方案 %name', 'Actualizando el atuendo %name con los objetos actuales', 'Actualizando el atuendo %name con los objetos actuales', 'Обновляю комплект %name текущими предметами'), +(1815, 'outfit_item_removed_from', '%item removed from %name', 0, 0, '%name에서 %item이(가) 제거되었습니다', '%item retiré de %name', '%item aus %name entfernt', '已从 %name 中移除 %item', '已從 %name 中移除 %item', '%item eliminado de %name', '%item eliminado de %name', '%item удалён из %name'), +(1816, 'outfit_item_added_to', '%item added to %name', 0, 0, '%item이(가) %name에 추가되었습니다', '%item ajouté à %name', '%item zu %name hinzugefügt', '已将 %item 添加到 %name', '已將 %item 添加到 %name', '%item agregado a %name', '%item agregado a %name', '%item добавлен в %name'), +(1817, 'release_spirit_not_dead_wait', 'I am not dead, will wait here', 0, 0, '저는 죽지 않았습니다. 여기서 기다리겠습니다', 'Je ne suis pas mort, j''attendrai ici', 'Ich bin nicht tot und werde hier warten', '我还没死,会在这里等', '我還沒死,會在這裡等', 'No estoy muerto, esperaré aquí', 'No estoy muerto, esperaré aquí', 'Я не мёртв, буду ждать здесь'), +(1818, 'release_spirit_already_spirit', 'I am already a spirit', 0, 0, '저는 이미 유령입니다', 'Je suis déjà un esprit', 'Ich bin bereits ein Geist', '我已经是灵魂状态了', '我已經是靈魂狀態了', 'Ya soy un espíritu', 'Ya soy un espíritu', 'Я уже дух'), +(1819, 'release_spirit_releasing', 'Releasing...', 0, 0, '해방 중...', 'Je libère mon esprit...', 'Geist wird freigesetzt...', '正在释放灵魂...', '正在釋放靈魂...', 'Liberando espíritu...', 'Liberando espíritu...', 'Освобождаю дух...'), +(1820, 'release_spirit_meet_graveyard', 'Meet me at the graveyard', 0, 0, '묘지에서 만나요', 'Retrouvez-moi au cimetière', 'Triff mich auf dem Friedhof', '墓地见', '墓地見', 'Encuéntrame en el cementerio', 'Encuéntrame en el cementerio', 'Встречаемся на кладбище'), +(1821, 'send_mail_no_mailbox_nearby', 'There is no mailbox nearby', 0, 0, '근처에 우체통이 없습니다', 'Il n''y a pas de boîte aux lettres à proximité', 'Kein Briefkasten in der Nähe', '附近没有邮箱', '附近沒有郵箱', 'No hay un buzón cerca', 'No hay un buzón cerca', 'Поблизости нет почтового ящика'), +(1822, 'send_mail_one_item_only', 'You can not request more than one item', 0, 0, '둘 이상의 아이템은 요청할 수 없습니다', 'Vous ne pouvez pas demander plus d''un objet', 'Du kannst nicht mehr als einen Gegenstand anfordern', '你不能请求多于一件物品', '你不能要求超過一件物品', 'No puedes solicitar más de un objeto', 'No puedes solicitar más de un objeto', 'Нельзя запрашивать больше одного предмета'), +(1823, 'send_mail_cannot_send_money', 'I cannot send money', 0, 0, '돈은 보낼 수 없습니다', 'Je ne peux pas envoyer d''argent', 'Ich kann kein Geld senden', '我不能寄送钱', '我不能寄送金錢', 'No puedo enviar dinero', 'No puedo enviar dinero', 'Я не могу отправить деньги'), +(1824, 'send_mail_not_enough_money', 'I don''t have enough money', 0, 0, '돈이 충분하지 않습니다', 'Je n''ai pas assez d''argent', 'Ich habe nicht genug Geld', '我没有足够的钱', '我沒有足夠的錢', 'No tengo suficiente dinero', 'No tengo suficiente dinero', 'У меня недостаточно денег'), +(1825, 'send_mail_sending_to', 'Sending mail to %receiver', 0, 0, '%receiver에게 우편을 보내는 중입니다', 'Envoi du courrier à %receiver', 'Sende Post an %receiver', '正在向 %receiver 发送邮件', '正在向 %receiver 發送郵件', 'Enviando correo a %receiver', 'Enviando correo a %receiver', 'Отправляю почту %receiver'), +(1826, 'send_mail_cannot_send_item', 'Cannot send %item', 0, 0, '%item을(를) 보낼 수 없습니다', 'Impossible d''envoyer %item', '%item kann nicht gesendet werden', '无法发送 %item', '無法發送 %item', 'No se puede enviar %item', 'No se puede enviar %item', 'Невозможно отправить %item'), +(1827, 'send_mail_item_not_for_sale', '%item: it is not for sale', 0, 0, '%item: 판매 대상이 아닙니다', '%item : ce n''est pas à vendre', '%item: Das steht nicht zum Verkauf', '%item:这不是出售物品', '%item:這不是出售物品', '%item: esto no está en venta', '%item: esto no está en venta', '%item: это не продаётся'), +(1828, 'send_mail_sent_to', 'Sent mail to %receiver', 0, 0, '%receiver에게 우편을 보냈습니다', 'Courrier envoyé à %receiver', 'Post an %receiver gesendet', '已向 %receiver 发送邮件', '已向 %receiver 發送郵件', 'Correo enviado a %receiver', 'Correo enviado a %receiver', 'Почта отправлена %receiver'), +(1829, 'craft_reset', 'I will not craft anything', 0, 0, '아무것도 제작하지 않겠습니다', 'Je ne fabriquerai rien', 'Ich werde nichts herstellen', '我不会制作任何东西', '我不會製作任何東西', 'No fabricaré nada', 'No fabricaré nada', 'Я ничего не буду создавать'), +(1830, 'craft_usage', 'Usage: ''craft [itemId]'' or ''craft reset''', 0, 0, '사용법: ''craft [itemId]'' 또는 ''craft reset''', 'Utilisation : ''craft [itemId]'' ou ''craft reset''', 'Verwendung: ''craft [itemId]'' oder ''craft reset''', '用法:''craft [itemId]'' 或 ''craft reset''', '用法:''craft [itemId]'' 或 ''craft reset''', 'Uso: ''craft [itemId]'' o ''craft reset''', 'Uso: ''craft [itemId]'' o ''craft reset''', 'Использование: ''craft [itemId]'' или ''craft reset'''), +(1831, 'craft_cannot_craft', 'I cannot craft this', 0, 0, '이것은 제작할 수 없습니다', 'Je ne peux pas fabriquer ceci', 'Ich kann das nicht herstellen', '我无法制作这个', '我無法製作這個', 'No puedo fabricar esto', 'No puedo fabricar esto', 'Я не могу это создать'), +(1832, 'craft_summary', 'I will craft %item using reagents: %reagents (craft fee: %money)', 0, 0, '재료 %reagents을(를) 사용해 %item을(를) 제작하겠습니다 (제작 수수료: %money)', 'Je fabriquerai %item en utilisant les composants : %reagents (frais de fabrication : %money)', 'Ich werde %item mit folgenden Reagenzien herstellen: %reagents (Herstellungsgebühr: %money)', '我将使用材料 %reagents 制作 %item(制作费用:%money)', '我將使用材料 %reagents 製作 %item(製作費用:%money)', 'Fabricaré %item usando los materiales: %reagents (tarifa de fabricación: %money)', 'Fabricaré %item usando los materiales: %reagents (tarifa de fabricación: %money)', 'Я изготовлю %item, используя реагенты: %reagents (плата за изготовление: %money)'), +(1833, 'set_home_success', 'This inn is my new home', 0, 0, '이 여관을 새로운 집으로 설정했습니다', 'Cette auberge est ma nouvelle maison', 'Dieses Gasthaus ist mein neues Zuhause', '这家旅店是我的新家', '這間旅店是我的新家', 'Esta posada es mi nuevo hogar', 'Esta posada es mi nuevo hogar', 'Этот трактир теперь мой новый дом'), +(1834, 'set_home_no_innkeeper_error', 'Can''t find any innkeeper around', 0, 0, '주변에서 여관주인을 찾을 수 없습니다', 'Impossible de trouver un aubergiste aux alentours', 'Kein Gastwirt in der Nähe gefunden', '附近找不到旅店老板', '附近找不到旅店老闆', 'No encuentro ningún posadero cerca', 'No encuentro ningún posadero cerca', 'Не удалось найти поблизости трактирщика'), +(1835, 'quest_shared', 'Quest shared', 0, 0, '퀘스트 공유됨', 'Quête partagée', 'Quest geteilt', '任务已共享', '任務已共享', 'Misión compartida', 'Misión compartida', 'Задание поделено'), +(1836, 'tame_invalid_id_error', 'Invalid tame id.', 0, 0, '잘못된 길들이기 ID입니다.', 'Identifiant de dressage invalide.', 'Ungültige Zähm-ID.', '无效的驯服 ID。', '無效的馴服 ID。', 'ID de domesticación no válido.', 'ID de domesticación no válido.', 'Недопустимый ID приручения.'), +(1837, 'tame_usage_error', 'Usage: tame name | tame id | tame family | tame rename | tame abandon', 0, 0, '사용법: tame name | tame id | tame family | tame rename | tame abandon', 'Utilisation : tame name | tame id | tame family | tame rename | tame abandon', 'Verwendung: tame name | tame id | tame family | tame rename | tame abandon', '用法:tame name | tame id | tame family | tame rename | tame abandon', '用法:tame name | tame id | tame family | tame rename | tame abandon', 'Uso: tame name | tame id | tame family | tame rename | tame abandon', 'Uso: tame name | tame id | tame family | tame rename | tame abandon', 'Использование: tame name | tame id | tame family | tame rename | tame abandon'), +(1838, 'tame_pet_changed', 'Pet changed to %name, ID: %id.', 0, 0, '소환수가 %name, ID: %id(으)로 변경되었습니다.', 'Familier changé en %name, ID : %id.', 'Begleiter geändert zu %name, ID: %id.', '宠物已更改为 %name,ID:%id。', '寵物已更改為 %name,ID:%id。', 'Mascota cambiada a %name, ID: %id.', 'Mascota cambiada a %name, ID: %id.', 'Питомец изменён на %name, ID: %id.'), +(1839, 'tame_pet_changed_initialized', 'Pet changed and initialized!', 0, 0, '소환수가 변경되고 초기화되었습니다!', 'Familier changé et initialisé !', 'Begleiter geändert und initialisiert!', '宠物已更改并初始化!', '寵物已更改並初始化!', 'La mascota ha sido cambiada e inicializada!', 'La mascota ha sido cambiada e inicializada!', 'Питомец изменён и инициализирован!'), +(1840, 'tame_exotic_requires_beast_mastery', 'I cannot use exotic pets unless I have the Beast Mastery talent.', 0, 0, '야수 지배 특성이 없으면 특수 야수 소환수를 사용할 수 없습니다.', 'Je ne peux pas utiliser de familiers exotiques sans le talent Maîtrise des bêtes.', 'Ich kann ohne das Talent Tierherrschaft keine exotischen Begleiter benutzen.', '如果没有野兽掌握天赋,我无法使用异种宠物。', '如果沒有野獸控制天賦,我無法使用異種寵物。', 'No puedo usar mascotas exóticas a menos que tenga el talento Dominio de bestias.', 'No puedo usar mascotas exóticas a menos que tenga el talento Dominio de bestias.', 'Я не могу использовать экзотических питомцев без таланта Повелитель зверей.'), +(1841, 'tame_no_pet_by_name', 'No tameable pet found with name: %name', 0, 0, '이름이 %name인 길들일 수 있는 소환수를 찾을 수 없습니다', 'Aucun familier apprivoisable trouvé avec le nom : %name', 'Kein zähmbares Tier mit dem Namen %name gefunden', '未找到名为 %name 的可驯服宠物', '未找到名為 %name 的可馴服寵物', 'No se encontró ninguna mascota domesticable con el nombre: %name', 'No se encontró ninguna mascota domesticable con el nombre: %name', 'Не найден приручаемый питомец с именем: %name'), +(1842, 'tame_no_pet_by_id', 'No tameable pet found with id: %id', 0, 0, 'ID가 %id인 길들일 수 있는 소환수를 찾을 수 없습니다', 'Aucun familier apprivoisable trouvé avec l''id : %id', 'Kein zähmbares Tier mit der ID %id gefunden', '未找到 ID 为 %id 的可驯服宠物', '未找到 ID 為 %id 的可馴服寵物', 'No se encontró ninguna mascota domesticable con el id: %id', 'No se encontró ninguna mascota domesticable con el id: %id', 'Не найден приручаемый питомец с id: %id'), +(1843, 'tame_no_pet_by_family', 'No tameable pet found with family: %family', 0, 0, '계열이 %family인 길들일 수 있는 소환수를 찾을 수 없습니다', 'Aucun familier apprivoisable trouvé avec la famille : %family', 'Kein zähmbares Tier mit der Familie %family gefunden', '未找到家族为 %family 的可驯服宠物', '未找到家族為 %family 的可馴服寵物', 'No se encontró ninguna mascota domesticable con la familia: %family', 'No se encontró ninguna mascota domesticable con la familia: %family', 'Не найден приручаемый питомец семейства: %family'), +(1844, 'tame_no_pet_to_rename', 'You have no pet to rename.', 0, 0, '이름을 바꿀 소환수가 없습니다.', 'Vous n''avez pas de familier à renommer.', 'Du hast kein Tier zum Umbenennen.', '你没有可重命名的宠物。', '你沒有可重新命名的寵物。', 'No tienes ninguna mascota para renombrar.', 'No tienes ninguna mascota para renombrar.', 'У вас нет питомца для переименования.'), +(1845, 'tame_pet_name_length_error', 'Pet name must be between 1 and 12 alphabetic characters.', 0, 0, '소환수 이름은 1자에서 12자의 알파벳 문자여야 합니다.', 'Le nom du familier doit contenir entre 1 et 12 caractères alphabétiques.', 'Der Name des Begleiters muss zwischen 1 und 12 alphabetischen Zeichen lang sein.', '宠物名字必须为 1 到 12 个字母字符。', '寵物名字必須為 1 到 12 個字母字元。', 'El nombre de la mascota debe tener entre 1 y 12 caracteres alfabéticos.', 'El nombre de la mascota debe tener entre 1 y 12 caracteres alfabéticos.', 'Имя питомца должно содержать от 1 до 12 буквенных символов.'), +(1846, 'tame_pet_name_alpha_error', 'Pet name must only contain alphabetic characters (A-Z, a-z).', 0, 0, '소환수 이름에는 알파벳 문자(A-Z, a-z)만 포함될 수 있습니다.', 'Le nom du familier ne doit contenir que des caractères alphabétiques (A-Z, a-z).', 'Der Name des Begleiters darf nur alphabetische Zeichen (A-Z, a-z) enthalten.', '宠物名字只能包含字母字符(A-Z, a-z)。', '寵物名字只能包含字母字元(A-Z, a-z)。', 'El nombre de la mascota solo puede contener caracteres alfabéticos (A-Z, a-z).', 'El nombre de la mascota solo puede contener caracteres alfabéticos (A-Z, a-z).', 'Имя питомца должно содержать только буквенные символы (A-Z, a-z).'), +(1847, 'tame_pet_name_forbidden_error', 'That pet name is forbidden. Please choose another name.', 0, 0, '그 소환수 이름은 사용할 수 없습니다. 다른 이름을 선택해 주세요.', 'Ce nom de familier est interdit. Veuillez en choisir un autre.', 'Dieser Name für den Begleiter ist verboten. Bitte wähle einen anderen Namen.', '该宠物名称被禁止使用。请选择其他名字。', '該寵物名稱被禁止使用。請選擇其他名字。', 'Ese nombre de mascota está prohibido. Por favor, elige otro nombre.', 'Ese nombre de mascota está prohibido. Por favor, elige otro nombre.', 'Это имя для питомца запрещено. Пожалуйста, выберите другое имя.'), +(1848, 'tame_pet_renamed', 'Your pet has been renamed to %name!', 0, 0, '당신의 소환수 이름이 %name(으)로 변경되었습니다!', 'Votre familier a été renommé en %name !', 'Dein Begleiter wurde in %name umbenannt!', '你的宠物已重命名为 %name!', '你的寵物已重新命名為 %name!', 'Tu mascota ha sido renombrada a %name!', 'Tu mascota ha sido renombrada a %name!', 'Ваш питомец был переименован в %name!'), +(1849, 'tame_pet_rename_refresh_hint', 'If you do not see the new name, please dismiss and recall your pet.', 0, 0, '새 이름이 보이지 않으면 소환수를 해제했다가 다시 소환해 주세요.', 'Si vous ne voyez pas le nouveau nom, veuillez renvoyer puis rappeler votre familier.', 'Wenn du den neuen Namen nicht siehst, schicke deinen Begleiter weg und rufe ihn erneut.', '如果你看不到新名字,请先解散再召回你的宠物。', '如果你看不到新名字,請先解散再召回你的寵物。', 'Si no ves el nuevo nombre, por favor retira y vuelve a invocar a tu mascota.', 'Si no ves el nuevo nombre, por favor retira y vuelve a invocar a tu mascota.', 'Если вы не видите новое имя, отпустите и снова призовите питомца.'), +(1850, 'tame_only_hunters_level_10', 'Only level 10+ hunters can have pets.', 0, 0, '10레벨 이상의 사냥꾼만 소환수를 가질 수 있습니다.', 'Seuls les chasseurs de niveau 10+ peuvent avoir des familiers.', 'Nur Jäger ab Stufe 10 können Begleiter haben.', '只有 10 级以上的猎人才能拥有宠物。', '只有 10 級以上的獵人才能擁有寵物。', 'Solo los cazadores de nivel 10 o superior pueden tener mascotas.', 'Solo los cazadores de nivel 10 o superior pueden tener mascotas.', 'Только охотники 10 уровня и выше могут иметь питомцев.'), +(1851, 'tame_creature_template_not_found', 'Creature template not found.', 0, 0, '생물 템플릿을 찾을 수 없습니다.', 'Modèle de créature introuvable.', 'Kreaturvorlage nicht gefunden.', '未找到生物模板。', '未找到生物範本。', 'No se encontró la plantilla de criatura.', 'No se encontró la plantilla de criatura.', 'Шаблон существа не найден.'), +(1852, 'tame_create_pet_failed', 'Failed to create pet.', 0, 0, '소환수 생성에 실패했습니다.', 'Échec de la création du familier.', 'Erstellen des Begleiters fehlgeschlagen.', '创建宠物失败。', '建立寵物失敗。', 'No se pudo crear la mascota.', 'No se pudo crear la mascota.', 'Не удалось создать питомца.'), +(1853, 'tame_pet_abandoned', 'Your pet has been abandoned.', 0, 0, '당신의 소환수를 버렸습니다.', 'Votre familier a été abandonné.', 'Dein Begleiter wurde freigelassen.', '你的宠物已被放弃。', '你的寵物已被放棄。', 'Tu mascota ha sido abandonada.', 'Tu mascota ha sido abandonada.', 'Ваш питомец был брошен.'), +(1854, 'tame_no_hunter_pet_to_abandon', 'You have no hunter pet to abandon.', 0, 0, '버릴 사냥꾼 소환수가 없습니다.', 'Vous n''avez pas de familier de chasseur à abandonner.', 'Du hast kein Jägertier zum Freilassen.', '你没有可放弃的猎人宠物。', '你沒有可放棄的獵人寵物。', 'No tienes ninguna mascota de cazador que abandonar.', 'No tienes ninguna mascota de cazador que abandonar.', 'У вас нет питомца охотника, которого можно бросить.'), +(1855, 'taxi_ready_next_flight', 'I am ready for the next flight', 0, 0, '다음 비행을 탈 준비가 되었습니다', 'Je suis prêt pour le prochain vol', 'Ich bin bereit für den nächsten Flug', '我已准备好进行下一次飞行', '我已準備好進行下一次飛行', 'Estoy listo para el siguiente vuelo', 'Estoy listo para el siguiente vuelo', 'Я готов к следующему перелёту'), +(1856, 'taxi_cant_fly_with_you', 'I can''t fly with you', 0, 0, '당신과 함께 날 수 없습니다', 'Je ne peux pas voler avec vous', 'Ich kann nicht mit dir fliegen', '我不能和你一起飞', '我不能和你一起飛', 'No puedo volar contigo', 'No puedo volar contigo', 'Я не могу лететь с вами'), +(1857, 'taxi_no_flightmaster_nearby', 'Cannot find any flightmaster to talk', 0, 0, '대화할 비행 조련사를 찾을 수 없습니다', 'Impossible de trouver un maître de vol à qui parler', 'Keinen Flugmeister zum Ansprechen gefunden', '找不到可以交谈的飞行管理员', '找不到可以交談的飛行管理員', 'No encuentro ningún maestro de vuelo con quien hablar', 'No encuentro ningún maestro de vuelo con quien hablar', 'Не могу найти распорядителя полётов, с которым можно поговорить'), +(1858, 'trade_busy_now', 'I''m kind of busy now', 0, 0, '지금은 좀 바쁩니다', 'Je suis un peu occupé en ce moment', 'Ich bin gerade etwas beschäftigt', '我现在有点忙', '我現在有點忙', 'Ahora estoy un poco ocupado', 'Ahora estoy un poco ocupado', 'Сейчас я немного занят'), +(1859, 'trade_disabled', 'Trading is disabled', 0, 0, '거래가 비활성화되어 있습니다', 'L''échange est désactivé', 'Handel ist deaktiviert', '交易已禁用', '交易已停用', 'El comercio está deshabilitado', 'El comercio está deshabilitado', 'Торговля отключена'), +(1860, 'trade_thank_you_player', 'Thank you %player', 0, 0, '고마워요 %player', 'Merci %player', 'Danke %player', '谢谢你 %player', '謝謝你 %player', 'Gracias %player', 'Gracias %player', 'Спасибо, %player'), +(1861, 'trade_selling_disabled', 'Selling is disabled.', 0, 0, '판매가 비활성화되어 있습니다.', 'La vente est désactivée.', 'Verkaufen ist deaktiviert.', '出售已禁用。', '出售已停用。', 'La venta está deshabilitada.', 'La venta está deshabilitada.', 'Продажа отключена.'), +(1862, 'trade_buying_disabled', 'Buying is disabled.', 0, 0, '구매가 비활성화되어 있습니다.', 'L''achat est désactivé.', 'Kaufen ist deaktiviert.', '购买已禁用。', '購買已停用。', 'La compra está deshabilitada.', 'La compra está deshabilitada.', 'Покупка отключена.'), +(1863, 'trade_item_not_for_sale', '%item - This is not for sale', 0, 0, '%item - 이것은 판매용이 아닙니다', '%item - Ceci n''est pas à vendre', '%item - Das steht nicht zum Verkauf', '%item - 这不是出售物品', '%item - 這不是出售物品', '%item - Esto no está en venta', '%item - Esto no está en venta', '%item - это не продаётся'), +(1864, 'trade_item_not_needed', '%item - I don''t need this', 0, 0, '%item - 저는 이것이 필요 없습니다', '%item - Je n''en ai pas besoin', '%item - Das brauche ich nicht', '%item - 我不需要这个', '%item - 我不需要這個', '%item - No necesito esto', '%item - No necesito esto', '%item - мне это не нужно'), +(1865, 'trade_no_items_error', 'There are no items to trade', 0, 0, '거래할 아이템이 없습니다', 'Il n''y a aucun objet à échanger', 'Es gibt keine Gegenstände zum Handeln', '没有可交易的物品', '沒有可交易的物品', 'No hay objetos para comerciar', 'No hay objetos para comerciar', 'Нет предметов для обмена'), +(1866, 'trade_discount_buy_only', 'You can use discount to buy items only', 0, 0, '할인은 아이템 구매에만 사용할 수 있습니다', 'Vous ne pouvez utiliser la réduction que pour acheter des objets', 'Rabatte können nur zum Kauf von Gegenständen verwendet werden', '折扣只能用于购买物品', '折扣只能用於購買物品', 'Solo puedes usar el descuento para comprar objetos', 'Solo puedes usar el descuento para comprar objetos', 'Скидку можно использовать только для покупки предметов'), +(1867, 'trade_success_pleasure', 'A pleasure doing business with you', 0, 0, '당신과 거래해서 즐거웠습니다', 'Un plaisir de faire affaire avec vous', 'Es war mir ein Vergnügen, mit dir Geschäfte zu machen', '很高兴和你做生意', '很高興和你做生意', 'Un placer hacer negocios contigo', 'Un placer hacer negocios contigo', 'Приятно было иметь с вами дело'), +(1868, 'trade_success_fair_trade', 'Fair trade', 0, 0, '공정한 거래입니다', 'Échange équitable', 'Fairer Handel', '公平交易', '公平交易', 'Intercambio justo', 'Intercambio justo', 'Честная сделка'), +(1869, 'trade_success_thanks', 'Thanks', 0, 0, '감사합니다', 'Merci', 'Danke', '谢谢', '謝謝', 'Gracias', 'Gracias', 'Спасибо'), +(1870, 'trade_success_off_with_you', 'Off with you', 0, 0, '이제 가보세요', 'Allez-vous-en maintenant', 'Nun geh deiner Wege', '走吧你', '走吧你', 'Ahora vete', 'Ahora vete', 'Иди уже'), +(1871, 'trade_want_money_for_this', 'I want %money for this', 0, 0, '이것에 대해 %money을(를) 원합니다', 'Je veux %money pour ceci', 'Ich möchte %money dafür', '这个我想要 %money', '這個我想要 %money', 'Quiero %money por esto', 'Quiero %money por esto', 'Я хочу %money за это'), +(1872, 'use_item_none_available', 'No items (or game objects) available', 0, 0, '사용할 수 있는 아이템(또는 게임 오브젝트)이 없습니다', 'Aucun objet (ou objet interactif) disponible', 'Keine Gegenstände (oder Spielobjekte) verfügbar', '没有可用的物品(或游戏物体)', '沒有可用的物品(或遊戲物件)', 'No hay objetos (ni objetos del juego) disponibles', 'No hay objetos (ni objetos del juego) disponibles', 'Нет доступных предметов (или игровых объектов)'), +(1873, 'use_gameobject', 'Using %gameobject', 0, 0, '%gameobject 사용 중', 'Utilisation de %gameobject', 'Benutze %gameobject', '正在使用 %gameobject', '正在使用 %gameobject', 'Usando %gameobject', 'Usando %gameobject', 'Использую %gameobject'), +(1874, 'socket_does_not_fit', 'Socket does not fit', 0, 0, '소켓이 맞지 않습니다', 'La châsse ne correspond pas', 'Sockel passt nicht', '插槽不匹配', '插槽不匹配', 'La ranura no encaja', 'La ranura no encaja', 'Гнездо не подходит'), +(1875, 'use_item_on_target', 'Using %item on %target', 0, 0, '%target에게 %item을(를) 사용하는 중', 'Utilisation de %item sur %target', 'Benutze %item auf %target', '正在对 %target 使用 %item', '正在對 %target 使用 %item', 'Usando %item en %target', 'Usando %item en %target', 'Использую %item на %target'), +(1876, 'use_item', 'Using %item', 0, 0, '%item 사용 중', 'Utilisation de %item', 'Benutze %item', '正在使用 %item', '正在使用 %item', 'Usando %item', 'Usando %item', 'Использую %item'), +(1877, 'socketing_item_with_gem', 'Socketing %item with %gem', 0, 0, '%item에 %gem 보석을 장착하는 중', 'Sertissage de %item avec %gem', 'Sockele %item mit %gem', '正在将 %gem 镶嵌到 %item 上', '正在將 %gem 鑲嵌到 %item 上', 'Engarzando %item con %gem', 'Engarzando %item con %gem', 'Вставляю %gem в %item'), +(1878, 'meeting_stone_in_combat', 'I am in combat', 0, 0, '전투 중입니다', 'Je suis en combat', 'Ich bin im Kampf', '我在战斗中', '我在戰鬥中', 'Estoy en combate', 'Estoy en combate', 'Я в бою'), +(1879, 'meeting_stone_welcome', 'Welcome!', 0, 0, '환영합니다!', 'Bienvenue !', 'Willkommen!', '欢迎!', '歡迎!', 'Bienvenido!', 'Bienvenido!', 'Добро пожаловать!'), +(1880, 'meeting_stone_none_nearby', 'There is no meeting stone nearby', 0, 0, '근처에 집결의 돌이 없습니다', 'Il n''y a pas de pierre de rencontre à proximité', 'Es gibt keinen Beschwörungsstein in der Nähe', '附近没有集合石', '附近沒有集合石', 'No hay ninguna piedra de encuentro cerca', 'No hay ninguna piedra de encuentro cerca', 'Поблизости нет камня встреч'), +(1881, 'meeting_stone_none_near_you', 'There is no meeting stone near you', 0, 0, '당신 근처에 집결의 돌이 없습니다', 'Il n''y a pas de pierre de rencontre près de vous', 'Es gibt keinen Beschwörungsstein in deiner Nähe', '你附近没有集合石', '你附近沒有集合石', 'No hay ninguna piedra de encuentro cerca de ti', 'No hay ninguna piedra de encuentro cerca de ti', 'Рядом с вами нет камня встреч'), +(1882, 'meeting_stone_no_hearthstone_self', 'I have no hearthstone', 0, 0, '귀환석이 없습니다', 'Je n''ai pas de pierre de foyer', 'Ich habe keinen Ruhestein', '我没有炉石', '我沒有爐石', 'No tengo piedra de hogar', 'No tengo piedra de hogar', 'У меня нет камня возвращения'), +(1883, 'meeting_stone_no_hearthstone_you', 'You have no hearthstone', 0, 0, '당신에게 귀환석이 없습니다', 'Vous n''avez pas de pierre de foyer', 'Du hast keinen Ruhestein', '你没有炉石', '你沒有爐石', 'No tienes piedra de hogar', 'No tienes piedra de hogar', 'У вас нет камня возвращения'), +(1884, 'meeting_stone_hearthstone_not_ready_self', 'My hearthstone is not ready', 0, 0, '제 귀환석은 아직 준비되지 않았습니다', 'Ma pierre de foyer n''est pas prête', 'Mein Ruhestein ist nicht bereit', '我的炉石还没准备好', '我的爐石還沒準備好', 'Mi piedra de hogar no está lista', 'Mi piedra de hogar no está lista', 'Мой камень возвращения ещё не готов'), +(1885, 'meeting_stone_hearthstone_not_ready_you', 'Your hearthstone is not ready', 0, 0, '당신의 귀환석은 아직 준비되지 않았습니다', 'Votre pierre de foyer n''est pas prête', 'Dein Ruhestein ist nicht bereit', '你的炉石还没准备好', '你的爐石還沒準備好', 'Tu piedra de hogar no está lista', 'Tu piedra de hogar no está lista', 'Ваш камень возвращения ещё не готов'), +(1886, 'meeting_stone_no_innkeepers_nearby', 'There are no innkeepers nearby', 0, 0, '근처에 여관주인이 없습니다', 'Il n''y a pas d''aubergistes à proximité', 'Es gibt keine Gastwirte in der Nähe', '附近没有旅店老板', '附近沒有旅店老闆', 'No hay posaderos cerca', 'No hay posaderos cerca', 'Поблизости нет трактирщиков'), +(1887, 'meeting_stone_no_innkeepers_near_you', 'There are no innkeepers near you', 0, 0, '당신 근처에 여관주인이 없습니다', 'Il n''y a pas d''aubergistes près de vous', 'Es gibt keine Gastwirte in deiner Nähe', '你附近没有旅店老板', '你附近沒有旅店老闆', 'No hay posaderos cerca de ti', 'No hay posaderos cerca de ti', 'Рядом с вами нет трактирщиков'), +(1888, 'meeting_stone_cannot_summon_vehicle', 'You cannot summon me while I''m on a vehicle', 0, 0, '제가 탈것/차량에 타고 있는 동안에는 소환할 수 없습니다', 'Vous ne pouvez pas m''invoquer tant que je suis sur un véhicule', 'Du kannst mich nicht beschwören, solange ich auf einem Fahrzeug bin', '当我在载具上时,你不能召唤我', '當我在載具上時,你不能召喚我', 'No puedes invocarme mientras esté en un vehículo', 'No puedes invocarme mientras esté en un vehículo', 'Вы не можете призвать меня, пока я на транспорте'), +(1889, 'meeting_stone_cannot_summon_master_in_combat', 'You cannot summon me while you''re in combat', 0, 0, '당신이 전투 중일 때는 저를 소환할 수 없습니다', 'Vous ne pouvez pas m''invoquer pendant que vous êtes en combat', 'Du kannst mich nicht beschwören, solange du im Kampf bist', '当你在战斗中时,不能召唤我', '當你在戰鬥中時,不能召喚我', 'No puedes invocarme mientras estés en combate', 'No puedes invocarme mientras estés en combate', 'Вы не можете призвать меня, пока вы в бою'), +(1890, 'meeting_stone_cannot_summon_master_dead', 'You cannot summon me while you''re dead', 0, 0, '당신이 죽어 있는 동안에는 저를 소환할 수 없습니다', 'Vous ne pouvez pas m''invoquer pendant que vous êtes mort', 'Du kannst mich nicht beschwören, solange du tot bist', '当你死亡时,不能召唤我', '當你死亡時,不能召喚我', 'No puedes invocarme mientras estés muerto', 'No puedes invocarme mientras estés muerto', 'Вы не можете призвать меня, пока вы мертвы'), +(1891, 'meeting_stone_cannot_summon_bot_dead', 'You cannot summon me while I''m dead, you need to release my spirit first', 0, 0, '제가 죽어 있는 동안에는 저를 소환할 수 없습니다. 먼저 제 영혼을 해방해야 합니다', 'Vous ne pouvez pas m''invoquer tant que je suis mort, vous devez d''abord libérer mon esprit', 'Du kannst mich nicht beschwören, solange ich tot bin; du musst zuerst meinen Geist freisetzen', '当我死亡时,你不能召唤我,你需要先释放我的灵魂', '當我死亡時,你不能召喚我,你需要先釋放我的靈魂', 'No puedes invocarme mientras esté muerto, primero debes liberar mi espíritu', 'No puedes invocarme mientras esté muerto, primero debes liberar mi espíritu', 'Вы не можете призвать меня, пока я мёртв, сначала нужно освободить мой дух'), +(1892, 'meeting_stone_revived', 'I live, again!', 0, 0, '다시 살아났습니다!', 'Je vis à nouveau !', 'Ich lebe wieder!', '我又活了!', '我又活了!', 'Vivo de nuevo!', 'Vivo de nuevo!', 'Я снова жив!'), +(1893, 'meeting_stone_not_enough_space', 'Not enough place to summon', 0, 0, '소환할 공간이 부족합니다', 'Pas assez de place pour invoquer', 'Nicht genug Platz zum Beschwören', '没有足够的空间进行召唤', '沒有足夠的空間進行召喚', 'No hay suficiente espacio para invocar', 'No hay suficiente espacio para invocar', 'Недостаточно места для призыва'), +(1894, 'new_rpg_quest_accepted', 'Quest accepted %quest', 0, 0, '퀘스트 수락 %quest', 'Quête acceptée %quest', 'Quest angenommen %quest', '任务已接受 %quest', '任務已接受 %quest', 'Misión aceptada %quest', 'Misión aceptada %quest', 'Задание принято %quest'), +(1895, 'new_rpg_quest_rewarded', 'Quest rewarded %quest', 0, 0, '퀘스트 보상 받음 %quest', 'Quête récompensée %quest', 'Quest abgeschlossen %quest', '任务已奖励 %quest', '任務已獎勵 %quest', 'Misión completada %quest', 'Misión completada %quest', 'Награда за задание получена %quest'), +(1896, 'new_rpg_quest_dropped', 'Quest dropped %quest', 0, 0, '퀘스트 포기 %quest', 'Quête abandonnée %quest', 'Quest abgebrochen %quest', '任务已放弃 %quest', '任務已放棄 %quest', 'Misión abandonada %quest', 'Misión abandonada %quest', 'Задание отменено %quest'), +(1897, 'rpg_item_better_for_player', 'You can use this %item better than me, %player.', 0, 0, '%player님, 이 %item은(는) 저보다 당신에게 더 잘 맞습니다.', 'Vous pouvez mieux utiliser cet objet %item que moi, %player.', 'Du kannst diesen %item besser gebrauchen als ich, %player.', '%player,这个 %item 比我更适合你使用。', '%player,這個 %item 比我更適合你使用。', 'Tú puedes usar este %item mejor que yo, %player.', 'Tú puedes usar este %item mejor que yo, %player.', '%player, ты можешь использовать %item лучше, чем я.'), +(1898, 'rpg_start_trade_with_player', 'Start trade with %player', 0, 0, '%player와(과) 거래를 시작합니다', 'Début de l''échange avec %player', 'Beginne Handel mit %player', '开始与 %player 交易', '開始與 %player 交易', 'Iniciando intercambio con %player', 'Iniciando intercambio con %player', 'Начинаю обмен с %player'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES +('quest_accept_debug', 100), +('quest_already_have_error', 100), +('quest_cant_take_error', 100), +('arena_team_already_in_team', 100), +('arena_team_thanks_for_invite', 100), +('area_trigger_follow_too_far_error', 100), +('area_trigger_wait_for_me', 100), +('attack_no_target_error', 100), +('attack_target_not_in_world_error', 100), +('attack_in_flight_error', 100), +('attack_pvp_prohibited_error', 100), +('attack_target_friendly_error', 100), +('attack_target_dead_error', 100), +('attack_target_not_in_sight_error', 100), +('attack_already_attacking_error', 100), +('attack_invalid_target_error', 100), +('bank_no_banker_nearby_error', 100), +('move_from_group', 100), +('running_away', 100), +('clean_quest_log_started', 100), +('quest_trivial_will_remove', 100), +('quest_has_been_removed', 100), +('quest_not_trivial_kept', 100), +('quest_removed_debug', 100), +('quest_removed_with_name', 100), +('guild_accept_inviter_not_in_guild', 100), +('guild_accept_already_in_guild', 100), +('guild_accept_declined', 100), +('outfit_usage_add', 100), +('outfit_usage_remove', 100), +('outfit_usage_equip', 100), +('outfit_set_as', 100), +('outfit_equipping', 100), +('outfit_replace_current', 100), +('outfit_resetting', 100), +('outfit_updating_current', 100), +('outfit_item_removed_from', 100), +('outfit_item_added_to', 100), +('release_spirit_not_dead_wait', 100), +('release_spirit_already_spirit', 100), +('release_spirit_releasing', 100), +('release_spirit_meet_graveyard', 100), +('send_mail_no_mailbox_nearby', 100), +('send_mail_one_item_only', 100), +('send_mail_cannot_send_money', 100), +('send_mail_not_enough_money', 100), +('send_mail_sending_to', 100), +('send_mail_cannot_send_item', 100), +('send_mail_item_not_for_sale', 100), +('send_mail_sent_to', 100), +('craft_reset', 100), +('craft_usage', 100), +('craft_cannot_craft', 100), +('craft_summary', 100), +('set_home_success', 100), +('set_home_no_innkeeper_error', 100), +('quest_shared', 100), +('tame_invalid_id_error', 100), +('tame_usage_error', 100), +('tame_pet_changed', 100), +('tame_pet_changed_initialized', 100), +('tame_exotic_requires_beast_mastery', 100), +('tame_no_pet_by_name', 100), +('tame_no_pet_by_id', 100), +('tame_no_pet_by_family', 100), +('tame_no_pet_to_rename', 100), +('tame_pet_name_length_error', 100), +('tame_pet_name_alpha_error', 100), +('tame_pet_name_forbidden_error', 100), +('tame_pet_renamed', 100), +('tame_pet_rename_refresh_hint', 100), +('tame_only_hunters_level_10', 100), +('tame_creature_template_not_found', 100), +('tame_create_pet_failed', 100), +('tame_pet_abandoned', 100), +('tame_no_hunter_pet_to_abandon', 100), +('taxi_ready_next_flight', 100), +('taxi_cant_fly_with_you', 100), +('taxi_no_flightmaster_nearby', 100), +('trade_busy_now', 100), +('trade_disabled', 100), +('trade_thank_you_player', 100), +('trade_selling_disabled', 100), +('trade_buying_disabled', 100), +('trade_item_not_for_sale', 100), +('trade_item_not_needed', 100), +('trade_no_items_error', 100), +('trade_discount_buy_only', 100), +('trade_success_pleasure', 100), +('trade_success_fair_trade', 100), +('trade_success_thanks', 100), +('trade_success_off_with_you', 100), +('trade_want_money_for_this', 100), +('use_item_none_available', 100), +('use_gameobject', 100), +('socket_does_not_fit', 100), +('use_item_on_target', 100), +('use_item', 100), +('socketing_item_with_gem', 100), +('meeting_stone_in_combat', 100), +('meeting_stone_welcome', 100), +('meeting_stone_none_nearby', 100), +('meeting_stone_none_near_you', 100), +('meeting_stone_no_hearthstone_self', 100), +('meeting_stone_no_hearthstone_you', 100), +('meeting_stone_hearthstone_not_ready_self', 100), +('meeting_stone_hearthstone_not_ready_you', 100), +('meeting_stone_no_innkeepers_nearby', 100), +('meeting_stone_no_innkeepers_near_you', 100), +('meeting_stone_cannot_summon_vehicle', 100), +('meeting_stone_cannot_summon_master_in_combat', 100), +('meeting_stone_cannot_summon_master_dead', 100), +('meeting_stone_cannot_summon_bot_dead', 100), +('meeting_stone_revived', 100), +('meeting_stone_not_enough_space', 100), +('new_rpg_quest_accepted', 100), +('new_rpg_quest_rewarded', 100), +('new_rpg_quest_dropped', 100), +('rpg_item_better_for_player', 100), +('rpg_start_trade_with_player', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_05_22_00_ai_playerbot_send_mail_disabled_text.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_05_22_00_ai_playerbot_send_mail_disabled_text.sql new file mode 100644 index 000000000..9ed05e8b5 --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_05_22_00_ai_playerbot_send_mail_disabled_text.sql @@ -0,0 +1,13 @@ +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'send_mail_disabled' +); + +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'send_mail_disabled' +); + +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 +(1899, 'send_mail_disabled', 'I cannot send mail', 0, 0, '우편을 보낼 수 없습니다', 'Je ne peux pas envoyer de courrier', 'Ich kann keine Post senden', '我不能寄送邮件', '我不能寄送郵件', 'No puedo enviar correo', 'No puedo enviar correo', 'Я не могу отправить почту'); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES +('send_mail_disabled', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_06_13_00_ai_playerbot_missing_group_buff_reagent.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_06_13_00_ai_playerbot_missing_group_buff_reagent.sql new file mode 100644 index 000000000..54e5ec8bb --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_06_13_00_ai_playerbot_missing_group_buff_reagent.sql @@ -0,0 +1,29 @@ +DELETE FROM ai_playerbot_texts_chance +WHERE name IN ( + 'rp_missing_reagent_greater_blessing', + 'rp_missing_reagent_gift_of_the_wild', + 'rp_missing_reagent_arcane_brilliance', + 'rp_missing_reagent_generic', + 'missing_group_buff_reagent' +); + +DELETE FROM ai_playerbot_texts +WHERE name IN ( + 'rp_missing_reagent_greater_blessing', + 'rp_missing_reagent_gift_of_the_wild', + 'rp_missing_reagent_arcane_brilliance', + 'rp_missing_reagent_generic', + 'missing_group_buff_reagent' +); + +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 + (1900, 'missing_group_buff_reagent', + 'I am out of reagents for %group_spell and am casting %base_spell instead.', + 0, 0, '', '', '', '', '', '', '', ''); + +INSERT INTO ai_playerbot_texts_chance + (name, probability) +VALUES + ('missing_group_buff_reagent', 100); diff --git a/modules/mod-playerbots/data/sql/playerbots/updates/2026_06_19_00_ai_playerbot_cache_cleanup.sql b/modules/mod-playerbots/data/sql/playerbots/updates/2026_06_19_00_ai_playerbot_cache_cleanup.sql new file mode 100644 index 000000000..4de93ab5a --- /dev/null +++ b/modules/mod-playerbots/data/sql/playerbots/updates/2026_06_19_00_ai_playerbot_cache_cleanup.sql @@ -0,0 +1,5 @@ +-- Flush all caches after RandomItemMgr changes. Tables will be repopulated with +-- the correct entries on the next start. +TRUNCATE TABLE `playerbots_equip_cache`; +TRUNCATE TABLE `playerbots_rnditem_cache`; +TRUNCATE TABLE `playerbots_rarity_cache`; diff --git a/modules/mod-playerbots/icon.png b/modules/mod-playerbots/icon.png index addcb75509502903628cf7e15e544364dd786b01..0f0b2b888b0a079592afd377b1c53af018cb6778 100644 GIT binary patch literal 127630 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 MvZ^xW?~DTe7l7ABSO5S3 literal 208820 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

fLPTwvW;KlGk$B&=k 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 E9~eu7zyJUM diff --git a/modules/mod-playerbots/src/Ai/Base/ActionContext.h b/modules/mod-playerbots/src/Ai/Base/ActionContext.h index 93bba4afe..229f1b2ae 100644 --- a/modules/mod-playerbots/src/Ai/Base/ActionContext.h +++ b/modules/mod-playerbots/src/Ai/Base/ActionContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACTIONCONTEXT_H -#define _PLAYERBOT_ACTIONCONTEXT_H +#ifndef PLAYERBOTS_ACTIONCONTEXT_H +#define PLAYERBOTS_ACTIONCONTEXT_H #include "AddLootAction.h" #include "AttackAction.h" @@ -45,6 +46,7 @@ #include "NonCombatActions.h" #include "OutfitAction.h" #include "PositionAction.h" +#include "PullActions.h" #include "DropQuestAction.h" #include "RandomBotUpdateAction.h" #include "ReachTargetActions.h" @@ -63,8 +65,10 @@ #include "WorldBuffAction.h" #include "XpGainAction.h" #include "NewRpgAction.h" +#include "NewRpgOutdoorPvP.h" #include "FishingAction.h" #include "CancelChannelAction.h" +#include "WaitForAttackAction.h" class PlayerbotAI; @@ -103,6 +107,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; @@ -125,6 +136,7 @@ public: creators["runaway"] = &ActionContext::runaway; creators["stay"] = &ActionContext::stay; creators["sit"] = &ActionContext::sit; + creators["aggressive target"] = &ActionContext::aggressive_target; creators["attack anything"] = &ActionContext::attack_anything; creators["attack least hp target"] = &ActionContext::attack_least_hp_target; creators["attack enemy player"] = &ActionContext::attack_enemy_player; @@ -162,6 +174,10 @@ public: creators["war stomp"] = &ActionContext::war_stomp; creators["blood fury"] = &ActionContext::blood_fury; creators["berserking"] = &ActionContext::berserking; + creators["every man for himself"] = &ActionContext::every_man_for_himself; + creators["will of the forsaken"] = &ActionContext::will_of_the_forsaken; + creators["stoneform"] = &ActionContext::stoneform; + creators["escape artist"] = &ActionContext::escape_artist; creators["use trinket"] = &ActionContext::use_trinket; creators["auto talents"] = &ActionContext::auto_talents; creators["auto share quest"] = &ActionContext::auto_share_quest; @@ -261,6 +277,8 @@ public: creators["new rpg wander npc"] = &ActionContext::new_rpg_wander_npc; creators["new rpg do quest"] = &ActionContext::new_rpg_do_quest; creators["new rpg travel flight"] = &ActionContext::new_rpg_travel_flight; + creators["new rpg outdoor pvp"] = &ActionContext::new_rpg_outdoor_pvp; + creators["wait for attack keep safe distance"] = &ActionContext::wait_for_attack_keep_safe_distance; } private: @@ -306,6 +324,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); } @@ -315,6 +340,7 @@ private: static Action* suggest_what_to_do(PlayerbotAI* botAI) { return new SuggestWhatToDoAction(botAI); } static Action* suggest_trade(PlayerbotAI* botAI) { return new SuggestTradeAction(botAI); } static Action* suggest_dungeon(PlayerbotAI* botAI) { return new SuggestDungeonAction(botAI); } + static Action* aggressive_target(PlayerbotAI* botAI) { return new AggressiveTargetAction(botAI); } static Action* attack_anything(PlayerbotAI* botAI) { return new AttackAnythingAction(botAI); } static Action* attack_least_hp_target(PlayerbotAI* botAI) { return new AttackLeastHpTargetAction(botAI); } static Action* attack_enemy_player(PlayerbotAI* botAI) { return new AttackEnemyPlayerAction(botAI); } @@ -355,6 +381,10 @@ private: static Action* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); } static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); } static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); } + static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); } + static Action* will_of_the_forsaken(PlayerbotAI* botAI) { return new CastWillOfTheForsakenAction(botAI); } + static Action* stoneform(PlayerbotAI* botAI) { return new CastStoneformAction(botAI); } + static Action* escape_artist(PlayerbotAI* botAI) { return new CastEscapeArtistAction(botAI); } static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } @@ -454,6 +484,8 @@ private: static Action* new_rpg_wander_npc(PlayerbotAI* ai) { return new NewRpgWanderNpcAction(ai); } static Action* new_rpg_do_quest(PlayerbotAI* ai) { return new NewRpgDoQuestAction(ai); } static Action* new_rpg_travel_flight(PlayerbotAI* ai) { return new NewRpgTravelFlightAction(ai); } + static Action* new_rpg_outdoor_pvp(PlayerbotAI* ai) { return new NewRpgOutdoorPvpAction(ai); } + static Action* wait_for_attack_keep_safe_distance(PlayerbotAI* ai) { return new WaitForAttackKeepSafeDistanceAction(ai); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.cpp index 91be09df9..991a48dae 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AcceptBattlegroundInvitationAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.h index 5ff4f439e..d3ee2b603 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACCEPTBATTLEGROUNDINVITATIONACTION_H -#define _PLAYERBOT_ACCEPTBATTLEGROUNDINVITATIONACTION_H +#ifndef PLAYERBOTS_ACCEPTBATTLEGROUNDINVITATIONACTION_H +#define PLAYERBOTS_ACCEPTBATTLEGROUNDINVITATIONACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.cpp index bfa46e6bb..cfb353455 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AcceptDuelAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.h index 4606e938f..1b6fac70e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptDuelAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACCEPTDUELACTION_H -#define _PLAYERBOT_ACCEPTDUELACTION_H +#ifndef PLAYERBOTS_ACCEPTDUELACTION_H +#define PLAYERBOTS_ACCEPTDUELACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp index 2df2b2053..87c05429e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AcceptInvitationAction.h" @@ -9,6 +10,7 @@ #include "ObjectAccessor.h" #include "PlayerbotAIConfig.h" #include "PlayerbotSecurity.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "WorldPacket.h" @@ -55,7 +57,7 @@ bool AcceptInvitationAction::Execute(Event event) botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT); botAI->Reset(); - botAI->TellMaster("Здравствуйте."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault("hello", "Hello", {})); if (sPlayerbotAIConfig.summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig.sightDistance) { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.h index 396ebd7ca..8f61133ea 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACCEPTINVITATIONACTION_H -#define _PLAYERBOT_ACCEPTINVITATIONACTION_H +#ifndef PLAYERBOTS_ACCEPTINVITATIONACTION_H +#define PLAYERBOTS_ACCEPTINVITATIONACTION_H #include "Action.h" #include "UseMeetingStoneAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp index 1d6c8f564..31cb53228 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp @@ -1,11 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AcceptQuestAction.h" #include "Event.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool AcceptAllQuestsAction::ProcessQuest(Quest const* quest, Object* questGiver) @@ -18,7 +20,11 @@ bool AcceptAllQuestsAction::ProcessQuest(Quest const* quest, Object* questGiver) if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { LOG_INFO("playerbots", "{} => Quest [{}] accepted", bot->GetName(), quest->GetTitle()); - bot->Say("Задание [" + text_quest + "] accepted", LANG_UNIVERSAL); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_accept_debug", + "Quest [%quest] accepted", + {{"%quest", text_quest}}); + bot->Say(text, LANG_UNIVERSAL); } return true; @@ -113,7 +119,8 @@ bool AcceptQuestShareAction::Execute(Event event) if (bot->HasQuest(quest)) { bot->SetDivider(ObjectGuid::Empty); - botAI->TellError("У меня уже есть это задание"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_already_have_error", "I have this quest", {})); return false; } @@ -121,7 +128,8 @@ bool AcceptQuestShareAction::Execute(Event event) { // can't take quest bot->SetDivider(ObjectGuid::Empty); - botAI->TellError("Я не могу взять это задание"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_cant_take_error", "I can't take this quest", {})); return false; } @@ -149,7 +157,8 @@ bool AcceptQuestShareAction::Execute(Event event) bot->CastSpell(bot, qInfo->GetSrcSpell(), true); } - botAI->TellMaster("Задание принято."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_accept", "Quest accepted", {})); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.h index 1160e66a8..350e9e153 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACCEPTQUESTACTION_H -#define _PLAYERBOT_ACCEPTQUESTACTION_H +#ifndef PLAYERBOTS_ACCEPTQUESTACTION_H +#define PLAYERBOTS_ACCEPTQUESTACTION_H #include "QuestAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.cpp index a1cfc1de0..95f3d4fe7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AcceptResurrectAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.h index c4495cfc0..06321457e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptResurrectAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACCEPTRESURRECTACTION_H -#define _PLAYERBOT_ACCEPTRESURRECTACTION_H +#ifndef PLAYERBOTS_ACCEPTRESURRECTACTION_H +#define PLAYERBOTS_ACCEPTRESURRECTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.cpp index 40cda0e6e..ae7592e09 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AddLootAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.h index dcd388175..4d6e45df2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AddLootAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ADDLOOTACTION_H -#define _PLAYERBOT_ADDLOOTACTION_H +#ifndef PLAYERBOTS_ADDLOOTACTION_H +#define PLAYERBOTS_ADDLOOTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp index 297583e20..d652045d5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AreaTriggerAction.h" #include "Event.h" #include "LastMovementValue.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "Transport.h" @@ -36,7 +38,8 @@ bool ReachAreaTriggerAction::Execute(Event event) if (bot->GetMapId() != at->map) { - botAI->TellError("Я не последую за вами: вы слишком далеко"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "area_trigger_follow_too_far_error", "I won't follow: too far away", {})); return true; } @@ -51,7 +54,8 @@ bool ReachAreaTriggerAction::Execute(Event event) float distance = bot->GetDistance(at->x, at->y, at->z); float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay; - botAI->TellError("Подождите меня"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "area_trigger_wait_for_me", "Wait for me", {})); botAI->SetNextCheckDelay(delay); context->GetValue("last area trigger")->Get().lastAreaTrigger = triggerId; @@ -76,6 +80,6 @@ bool AreaTriggerAction::Execute(Event /*event*/) p.rpos(0); bot->GetSession()->HandleAreaTriggerOpcode(p); - botAI->TellMaster("Здравствуйте."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault("hello", "Hello", {})); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.h index 93a90f16d..ec50cb398 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AREATRIGGERACTION_H -#define _PLAYERBOT_AREATRIGGERACTION_H +#ifndef PLAYERBOTS_AREATRIGGERACTION_H +#define PLAYERBOTS_AREATRIGGERACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp index 858c76a2d..b014e5f03 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp @@ -1,11 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ArenaTeamActions.h" #include "ArenaTeamMgr.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool ArenaTeamAcceptAction::Execute(Event event) @@ -31,7 +33,9 @@ bool ArenaTeamAcceptAction::Execute(Event event) if (bot->GetArenaTeamId(at->GetSlot())) { // bot is already in an arena team - bot->Say("У меня уже есть такая команда", LANG_UNIVERSAL); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "arena_team_already_in_team", "Sorry, I am already in such team", {}); + bot->Say(text, LANG_UNIVERSAL); accept = false; } @@ -39,7 +43,9 @@ bool ArenaTeamAcceptAction::Execute(Event event) { WorldPacket data(CMSG_ARENA_TEAM_ACCEPT); bot->GetSession()->HandleArenaTeamAcceptOpcode(data); - bot->Say("Спасибо за приглашение!", LANG_UNIVERSAL); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "arena_team_thanks_for_invite", "Thanks for the invite!", {}); + bot->Say(text, LANG_UNIVERSAL); LOG_INFO("playerbots", "Bot {} <{}> accepts Arena Team invite", bot->GetGUID().ToString().c_str(), bot->GetName().c_str()); return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.h index f9fb9ebe7..3ea47aa53 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ARENATEAMACTION_H -#define _PLAYERBOT_ARENATEAMACTION_H +#ifndef PLAYERBOTS_ARENATEAMACTIONS_H +#define PLAYERBOTS_ARENATEAMACTIONS_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp index 1a4322faf..de9825f8f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AttackAction.h" @@ -10,10 +11,12 @@ #include "LastMovementValue.h" #include "LootObjectStack.h" #include "PlayerbotAI.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "ServerFacade.h" #include "SharedDefines.h" #include "Unit.h" +#include "WaitForAttackStrategy.h" bool AttackAction::Execute(Event /*event*/) { @@ -37,7 +40,8 @@ bool AttackMyTargetAction::Execute(Event /*event*/) if (!guid) { if (verbose) - botAI->TellError("У вас нет цели"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pull_no_target_error", "You have no target", {})); return false; } @@ -52,26 +56,11 @@ 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("Я не могу атаковать в полёте"); - - return false; - } - if (!target) { if (verbose) - botAI->TellError("У меня нет цели"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_no_target_error", "I have no target", {})); return false; } @@ -79,7 +68,20 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (!target->IsInWorld()) { if (verbose) - botAI->TellError(std::string(target->GetName()) + " is no longer in the world."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_target_not_in_world_error", + "%target is no longer in the world.", + {{"%target", target->GetName()}})); + + return false; + } + + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FLIGHT_MOTION_TYPE || + bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) + { + if (verbose) + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_in_flight_error", "I cannot attack in flight", {})); return false; } @@ -91,7 +93,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) sPlayerbotAIConfig.IsPvpProhibited(target->GetZoneId(), target->GetAreaId()))) { if (verbose) - botAI->TellError("Я не могу атаковать игроков в зонах, где запрещено PvP."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_pvp_prohibited_error", + "I cannot attack other players in PvP prohibited areas.", + {})); return false; } @@ -99,7 +104,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (bot->IsFriendlyTo(target)) { if (verbose) - botAI->TellError(std::string(target->GetName()) + " is friendly to me."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_target_friendly_error", + "%target is friendly to me.", + {{"%target", target->GetName()}})); return false; } @@ -107,7 +115,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (target->isDead()) { if (verbose) - botAI->TellError(std::string(target->GetName()) + " is dead."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_target_dead_error", + "%target is dead.", + {{"%target", target->GetName()}})); return false; } @@ -115,15 +126,33 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (!bot->IsWithinLOSInMap(target)) { if (verbose) - botAI->TellError(std::string(target->GetName()) + " is not in my sight."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_target_not_in_sight_error", + "%target is not in my sight.", + {{"%target", target->GetName()}})); return false; } + // Infantry attacks are not allowed from vehicles drivers. + // Check is needed to stop some auto-attack situations. + if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, 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) - botAI->TellError("Я уже атакую: " + std::string(target->GetName()) + "."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_already_attacking_error", + "I am already attacking %target.", + {{"%target", target->GetName()}})); return false; } @@ -131,7 +160,8 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (!bot->IsValidAttackTarget(target)) { if (verbose) - botAI->TellError("Я не могу атаковать недопустимую цель."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attack_invalid_target_error", "I cannot attack an invalid target.", {})); return false; } @@ -145,8 +175,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); @@ -164,7 +193,8 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) botAI->ChangeEngine(BOT_STATE_COMBAT); - bot->Attack(target, shouldMelee); + if (!WaitForAttackStrategy::ShouldWait(botAI)) + bot->Attack(target, shouldMelee); /* prevent pet dead immediately in group */ // if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat()) // { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.h index 27081350f..33db7e6b9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ATTACKACTION_H -#define _PLAYERBOT_ATTACKACTION_H +#ifndef PLAYERBOTS_ATTACKACTION_H +#define PLAYERBOTS_ATTACKACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp index 313fd7fbe..cfa7c8850 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "AutoMaintenanceOnLevelupAction.h" #include "SpellMgr.h" @@ -12,8 +18,8 @@ bool AutoMaintenanceOnLevelupAction::Execute(Event /*event*/) { AutoPickTalents(); AutoLearnSpell(); - AutoUpgradeEquip(); AutoTeleportForLevel(); + AutoUpgradeEquip(); return true; } @@ -21,13 +27,11 @@ bool AutoMaintenanceOnLevelupAction::Execute(Event /*event*/) void AutoMaintenanceOnLevelupAction::AutoTeleportForLevel() { if (!sPlayerbotAIConfig.autoTeleportForLevel || !sRandomPlayerbotMgr.IsRandomBot(bot)) - { return; - } + if (botAI->HasRealPlayerMaster()) - { return; - } + sRandomPlayerbotMgr.RandomTeleportForLevel(bot); return; } @@ -73,7 +77,7 @@ void AutoMaintenanceOnLevelupAction::LearnSpells(std::ostringstream* out) LearnQuestSpells(out); } -void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* out) +void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* /*out*/) { PlayerbotFactory factory(bot, bot->GetLevel()); factory.InitSkills(); @@ -89,21 +93,17 @@ void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out) { Quest const* quest = i->second; - // only process class-specific quests to learn class-related spells, cuz - // we don't want all these bunch of entries to be handled! - if (!quest->GetRequiredClasses()) + if (!quest->GetRequiredClasses() || quest->IsRepeatable() || quest->GetMinLevel() < 10 || + quest->GetMinLevel() > bot->GetLevel()) + { continue; + } - // skip quests that are repeatable, too low level, or above bots' level - if (quest->IsRepeatable() || quest->GetMinLevel() < 10 || quest->GetMinLevel() > bot->GetLevel()) - continue; - - // skip if bot doesnt satisfy class, race, or skill requirements if (!bot->SatisfyQuestClass(quest, false) || !bot->SatisfyQuestRace(quest, false) || !bot->SatisfyQuestSkill(quest, false)) + { continue; - - // use the same logic and impl from Player::learnQuestRewardedSpells + } int32 spellId = quest->GetRewSpellCast(); if (!spellId) @@ -113,31 +113,26 @@ void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out) if (!spellInfo) continue; - // xinef: find effect with learn spell and check if we have this spell bool found = false; for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) { if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && spellInfo->Effects[i].TriggerSpell && !bot->HasSpell(spellInfo->Effects[i].TriggerSpell)) { - // pusywizard: don't re-add profession specialties! if (SpellInfo const* triggeredInfo = sSpellMgr->GetSpellInfo(spellInfo->Effects[i].TriggerSpell)) if (triggeredInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL) - break; // pussywizard: break and not cast the spell (found is false) + break; found = true; break; } } - // xinef: we know the spell, continue if (!found) continue; bot->CastSpell(bot, spellId, true); - // Check if RewardDisplaySpell is set to output the proper spell learned - // after processing quests. Output the original RewardSpell otherwise. uint32 rewSpellId = quest->GetRewSpell(); if (rewSpellId) { @@ -167,12 +162,11 @@ std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* s void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip() { - if (!sPlayerbotAIConfig.autoUpgradeEquip || !sRandomPlayerbotMgr.IsRandomBot(bot)) + if (!sRandomPlayerbotMgr.IsRandomBot(bot)) return; PlayerbotFactory factory(bot, bot->GetLevel()); - // Clean up old consumables before adding new ones factory.CleanupConsumables(); factory.InitAmmo(); @@ -181,9 +175,6 @@ void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip() factory.InitConsumables(); factory.InitPotions(); - if (!sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) - { - if (sPlayerbotAIConfig.incrementalGearInit) - factory.InitEquipment(true); - } + if (sPlayerbotAIConfig.autoUpgradeEquip) + factory.InitEquipment(true); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.h index 5399973cf..b6a4331b9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AUTOTELEPORTFORLEVELACTION_H -#define _PLAYERBOT_AUTOTELEPORTFORLEVELACTION_H +#ifndef PLAYERBOTS_AUTOMAINTENANCEONLEVELUPACTION_H +#define PLAYERBOTS_AUTOMAINTENANCEONLEVELUPACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp index eda06e02d..77530d88c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BankAction.h" #include "Event.h" #include "ItemCountValue.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool BankAction::Execute(Event event) @@ -23,11 +25,12 @@ bool BankAction::Execute(Event event) return ExecuteBank(text, npc); } - botAI->TellError("Поблизости нет банкира"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bank_no_banker_nearby_error", "Cannot find banker nearby", {})); return false; } -bool BankAction::ExecuteBank(std::string const text, Unit* bank) +bool BankAction::ExecuteBank(std::string const text, Unit* /*bank*/) { if (text.empty() || text == "?") { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.h index 5f3ffc783..109706f0e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BANKACTION_H -#define _PLAYERBOT_BANKACTION_H +#ifndef PLAYERBOTS_BANKACTION_H +#define PLAYERBOTS_BANKACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.cpp index 24b23cb9d..bb0f0100f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BattleGroundJoinAction.h" @@ -24,8 +25,6 @@ bool BGJoinAction::Execute(Event /*event*/) BattlegroundQueueTypeId queueTypeId = (BattlegroundQueueTypeId)bgList[urand(0, bgList.size() - 1)]; BattlegroundTypeId bgTypeId = BattlegroundMgr::BGTemplateId(queueTypeId); - BattlegroundBracketId bracketId; - bool isArena = false; bool isRated = false; Battleground* bg = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); @@ -37,12 +36,8 @@ bool BGJoinAction::Execute(Event /*event*/) if (!pvpDiff) return false; - bracketId = pvpDiff->GetBracketId(); - if (ArenaType type = ArenaType(BattlegroundMgr::BGArenaType(queueTypeId))) { - isArena = true; - std::vector::iterator i = find(ratedList.begin(), ratedList.end(), queueTypeId); if (i != ratedList.end()) isRated = true; @@ -349,7 +344,7 @@ bool BGJoinAction::isUseful() return false; // check Deserter debuff - if (!bot->CanJoinToBattleground()) + if (bot->IsDeserter()) return false; // check if has free queue slots (pointless as already making sure not in queue) @@ -408,8 +403,6 @@ bool BGJoinAction::JoinQueue(uint32 type) bracketId = pvpDiff->GetBracketId(); - uint32 BracketSize = bg->GetMaxPlayersPerTeam() * 2; - uint32 TeamSize = bg->GetMaxPlayersPerTeam(); TeamId teamId = bot->GetTeamId(); // check if already in queue @@ -486,8 +479,6 @@ bool BGJoinAction::JoinQueue(uint32 type) if (isArena) { isArena = true; - BracketSize = type * 2; - TeamSize = type; isRated = botAI->GetAiObjectContext()->GetValue("arena type")->Get(); if (joinAsGroup) @@ -544,21 +535,18 @@ bool BGJoinAction::JoinQueue(uint32 type) botAI->GetAiObjectContext()->GetValue("bg type")->Set(0); + WorldPacket* packet = nullptr; if (!isArena) { - WorldPacket* packet = new WorldPacket(CMSG_BATTLEMASTER_JOIN, 20); + packet = new WorldPacket(CMSG_BATTLEMASTER_JOIN, 20); *packet << bot->GetGUID() << bgTypeId_ << instanceId << joinAsGroup; - /// FIX race condition - // bot->GetSession()->HandleBattlemasterJoinOpcode(packet); - bot->GetSession()->QueuePacket(packet); } else { - WorldPacket arena_packet(CMSG_BATTLEMASTER_JOIN_ARENA, 20); - arena_packet << unit->GetGUID() << arenaslot << asGroup << uint8(isRated); - bot->GetSession()->HandleBattlemasterJoinArena(arena_packet); + packet = new WorldPacket(CMSG_BATTLEMASTER_JOIN_ARENA, 20); + *packet << unit->GetGUID() << arenaslot << asGroup << uint8(isRated); } - + bot->GetSession()->QueuePacket(packet); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.h index d3aaa8994..79c840015 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundJoinAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BATTLEGROUNDJOINACTION_H -#define _PLAYERBOT_BATTLEGROUNDJOINACTION_H +#ifndef PLAYERBOTS_BATTLEGROUNDJOINACTION_H +#define PLAYERBOTS_BATTLEGROUNDJOINACTION_H #include "Action.h" #include "DBCEnums.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp index 3f05a2f76..af7a3ad4b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BattleGroundTactics.h" @@ -1299,7 +1300,7 @@ std::string const BGTactics::HandleConsoleCommandPrivate(WorldSession* session, Player* player = session->GetPlayer(); if (!player) return "Error - session player not found"; - if (player->GetSession()->GetSecurity() < SEC_GAMEMASTER) + if (!player->CanBeGameMaster()) return "Command can only be used by a GM"; Battleground* bg = player->GetBattleground(); if (!bg) @@ -1368,7 +1369,7 @@ std::string const BGTactics::HandleConsoleCommandPrivate(WorldSession* session, uint32 max = vPaths->size() - 1; if (num >= 0) // num specified or found { - if (num > max) + if (uint32(num) > max) return fmt::format("Path {} of range of 0 - {}", num, max); min = num; max = num; @@ -2177,7 +2178,7 @@ bool BGTactics::selectObjective(bool reset) uint8 defendersProhab = 3; // Default balanced - switch (strategy) + switch (static_cast(strategy)) { case 0: case 1: @@ -2497,7 +2498,6 @@ bool BGTactics::selectObjective(bool reset) EYBotStrategy strategyHorde = static_cast(GetBotStrategyForTeam(bg, TEAM_HORDE)); EYBotStrategy strategyAlliance = static_cast(GetBotStrategyForTeam(bg, TEAM_ALLIANCE)); EYBotStrategy strategy = (team == TEAM_ALLIANCE) ? strategyAlliance : strategyHorde; - EYBotStrategy enemyStrategy = (team == TEAM_ALLIANCE) ? strategyHorde : strategyAlliance; auto IsOwned = [&](uint32 nodeId) -> bool { return eyeOfTheStormBG->GetCapturePointInfo(nodeId)._ownerTeamId == team; }; @@ -3231,7 +3231,6 @@ bool BGTactics::selectObjectiveWp(std::vector const& vPaths) if (bgType == BATTLEGROUND_RB) bgType = bg->GetBgTypeID(true); - PositionMap& posMap = context->GetValue("position")->Get(); PositionInfo pos = context->GetValue("position")->Get()["bg objective"]; if (!pos.isSet()) return false; @@ -3326,7 +3325,7 @@ bool BGTactics::selectObjectiveWp(std::vector const& vPaths) // don't pick path where bot is already closest to the paths closest point to target (it means path cant lead it // anywhere) don't pick path where closest point is too far away - if (closestPointIndex == (reverse ? 0 : path->size() - 1) || closestPointDistToBot > botDistanceLimit) + if (closestPointIndex == int(reverse ? 0 : path->size() - 1) || closestPointDistToBot > botDistanceLimit) continue; // creates a score based on dist-to-bot and dist-to-destination, where lower is better, and dist-to-bot is more diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.h b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.h index b6d065aeb..1e7638b0f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BATTLEGROUNDTACTICSACTION_H -#define _PLAYERBOT_BATTLEGROUNDTACTICSACTION_H +#ifndef PLAYERBOTS_BATTLEGROUNDTACTICS_H +#define PLAYERBOTS_BATTLEGROUNDTACTICS_H #include "BattlegroundAV.h" #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.cpp index 90284db10..c63d53dda 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.h index e31be8c4e..dfbdd4b27 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BossAuraActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BOSSAURAACTION_H -#define _PLAYERBOT_BOSSAURAACTION_H +#ifndef PLAYERBOTS_BOSSAURAACTIONS_H +#define PLAYERBOTS_BOSSAURAACTIONS_H #include diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp index 732b33cec..a1fe6d9cb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BuffAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.h index 525badd12..6a8571c9f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BUFFACTION_H -#define _PLAYERBOT_BUFFACTION_H +#ifndef PLAYERBOTS_BUFFACTION_H +#define PLAYERBOTS_BUFFACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp index 214dd1989..7293233eb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BuyAction.h" @@ -213,13 +214,7 @@ bool BuyAction::Execute(Event event) } } - if (!vendored) - { - botAI->TellError("Поблизости нет торговцев"); - return false; - } - - return true; + return vendored; } bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.h index 0d7e7e157..7c9a4b9b8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BUYACTION_H -#define _PLAYERBOT_BUYACTION_H +#ifndef PLAYERBOTS_BUYACTION_H +#define PLAYERBOTS_BUYACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.cpp index 103aec4d8..89f930360 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CancelChannelAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.h index 220beb71d..10fdb3a57 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CancelChannelAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CANCELCHANNELACTION_H -#define _PLAYERBOT_CANCELCHANNELACTION_H +#ifndef PLAYERBOTS_CANCELCHANNELACTION_H +#define PLAYERBOTS_CANCELCHANNELACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp index 2e809c793..e64cbfaab 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CastCustomSpellAction.h" @@ -143,14 +144,17 @@ bool CastCustomSpellAction::Execute(Event event) std::ostringstream spellName; spellName << ChatHelper::FormatSpell(spellInfo) << " on "; + bool const hasItemTarget = itemTarget && + (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM); + if (bot->GetTrader()) spellName << "trade item"; - else if (itemTarget) + else if (hasItemTarget) spellName << chat->FormatItem(itemTarget->GetTemplate()); - else if (target == bot) - spellName << "self"; - else + else if (target != bot) spellName << target->GetName(); + else + spellName << "self"; if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget)) { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.h index ed53b18a5..124fae57d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CASTCUSTOMSPELLACTION_H -#define _PLAYERBOT_CASTCUSTOMSPELLACTION_H +#ifndef PLAYERBOTS_CASTCUSTOMSPELLACTION_H +#define PLAYERBOTS_CASTCUSTOMSPELLACTION_H #include "ListSpellsAction.h" @@ -21,7 +22,7 @@ public: } bool Execute(Event event) override; - virtual std::string const castString(WorldObject* target) { return "cast"; } + virtual std::string const castString(WorldObject* /*target*/) { return "cast"; } protected: bool ncCast = false; @@ -49,7 +50,7 @@ public: bool isUseful() override { return false; } virtual bool AcceptSpell(SpellInfo const* spellInfo); - virtual uint32 GetSpellPriority(SpellInfo const* spellInfo) { return 1; } + virtual uint32 GetSpellPriority(SpellInfo const* /*spellInfo*/) { return 1; } virtual bool castSpell(uint32 spellId, WorldObject* wo); bool Execute(Event event) override; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp index cbef636e6..847687093 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChangeChatAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.h index f4f476a61..a8c589d65 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHANGECHATACTION_H -#define _PLAYERBOT_CHANGECHATACTION_H +#ifndef PLAYERBOTS_CHANGECHATACTION_H +#define PLAYERBOTS_CHANGECHATACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp index 48ea3653f..0b674063c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChangeStrategyAction.h" @@ -9,28 +10,36 @@ #include "PlayerbotRepository.h" #include "Playerbots.h" +// Helper function for prefixes used by combat and non-combat strategy commands. +static void HandleStrategyCommon(PlayerbotAI* botAI, std::string const& text, BotState state) +{ + std::vector splitted = split(text, ','); + for (std::vector::iterator i = splitted.begin(); i != splitted.end(); i++) + { + const char* name = i->c_str(); + switch (name[0]) + { + case '+': + case '-': + case '~': + PlayerbotRepository::instance().Save(botAI); + break; + case '!': + botAI->SelectiveResetStrategies(state); + PlayerbotRepository::instance().Save(botAI); + break; + case '?': + break; + } + } +} + bool ChangeCombatStrategyAction::Execute(Event event) { std::string const text = event.getParam(); botAI->ChangeStrategy(text.empty() ? getName() : text, BOT_STATE_COMBAT); if (event.GetSource() == "co") - { - std::vector splitted = split(text, ','); - for (std::vector::iterator i = splitted.begin(); i != splitted.end(); i++) - { - const char* name = i->c_str(); - switch (name[0]) - { - case '+': - case '-': - case '~': - PlayerbotRepository::instance().Save(botAI); - break; - case '?': - break; - } - } - } + HandleStrategyCommon(botAI, text, BOT_STATE_COMBAT); return true; } @@ -41,34 +50,18 @@ bool ChangeNonCombatStrategyAction::Execute(Event event) uint32 account = bot->GetSession()->GetAccountId(); if (sPlayerbotAIConfig.IsInRandomAccountList(account) && botAI->GetMaster() && - botAI->GetMaster()->GetSession()->GetSecurity() < SEC_GAMEMASTER) + !botAI->GetMaster()->CanBeGameMaster()) { - if (text.find("loot") != std::string::npos || text.find("gather") != std::string::npos) + if (text.find("loot") != std::string::npos) { - botAI->TellError("Можно изменять любую стратегию, кроме loot и gather"); + botAI->TellError("You can change any strategy except loot"); return false; } } botAI->ChangeStrategy(text, BOT_STATE_NON_COMBAT); if (event.GetSource() == "nc") - { - std::vector splitted = split(text, ','); - for (std::vector::iterator i = splitted.begin(); i != splitted.end(); i++) - { - const char* name = i->c_str(); - switch (name[0]) - { - case '+': - case '-': - case '~': - PlayerbotRepository::instance().Save(botAI); - break; - case '?': - break; - } - } - } + HandleStrategyCommon(botAI, text, BOT_STATE_NON_COMBAT); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.h index b9d0ee866..d992fe704 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHANGESTRATEGYACTION_H -#define _PLAYERBOT_CHANGESTRATEGYACTION_H +#ifndef PLAYERBOTS_CHANGESTRATEGYACTION_H +#define PLAYERBOTS_CHANGESTRATEGYACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp index 6ffb6edca..9b867de71 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChangeTalentsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.h index 0b06a674e..8c7d136f9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHANGETALENTSACTION_H -#define _PLAYERBOT_CHANGETALENTSACTION_H +#ifndef PLAYERBOTS_CHANGETALENTSACTION_H +#define PLAYERBOTS_CHANGETALENTSACTION_H #include "Action.h" #include "Talentspec.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp index 598489734..c0986bfa6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChatShortcutActions.h" #include "Event.h" #include "Formations.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "PositionValue.h" @@ -68,13 +70,11 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) std::string const target = formation->GetTargetName(); bool moved = false; if (!target.empty()) - { moved = Follow(AI_VALUE(Unit*, target)); - } else { WorldLocation loc = formation->GetLocation(); - if (Formation::IsNullLocation(loc) || loc.GetMapId() == -1) + if (Formation::IsNullLocation(loc) || loc.GetMapId() == MAPID_INVALID) return false; MovementPriority priority = botAI->GetState() == BOT_STATE_COMBAT ? MovementPriority::MOVEMENT_COMBAT : MovementPriority::MOVEMENT_NORMAL; @@ -82,14 +82,13 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) true, priority); } - if (Pet* pet = bot->GetPet()) - { + if (bot->GetPet()) botAI->PetFollow(); - } if (moved) { - botAI->TellMaster("Следую за целью."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "following", "Following", {})); return true; } } @@ -112,7 +111,8 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) } */ - botAI->TellMaster("Следую за целью."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "following", "Following", {})); return true; } @@ -129,7 +129,8 @@ bool StayChatShortcutAction::Execute(Event /*event*/) SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); - botAI->TellMaster("Остаюсь на месте."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "staying", "Staying", {})); return true; } @@ -144,7 +145,8 @@ bool MoveFromGroupChatShortcutAction::Execute(Event /*event*/) botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT); botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT); - botAI->TellMaster("Отхожу от группы"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "move_from_group", "Moving away from group", {})); return true; } @@ -163,11 +165,13 @@ bool FleeChatShortcutAction::Execute(Event /*event*/) if (bot->GetMapId() != master->GetMapId() || bot->GetDistance(master) > sPlayerbotAIConfig.sightDistance) { - botAI->TellError("Я не могу отступать вместе с вами — вы слишком далеко"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "fleeing_far", "I will not flee with you - too far away", {})); return true; } - botAI->TellMaster("Отступаю."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "fleeing", "Fleeing", {})); return true; } @@ -184,7 +188,8 @@ bool GoawayChatShortcutAction::Execute(Event /*event*/) ResetReturnPosition(); ResetStayPosition(); - botAI->TellMaster("Отступаю"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "running_away", "Running away", {})); return true; } @@ -200,7 +205,8 @@ bool GrindChatShortcutAction::Execute(Event /*event*/) ResetReturnPosition(); ResetStayPosition(); - botAI->TellMaster("Фармлю."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "grinding", "Grinding", {})); return true; } @@ -220,7 +226,8 @@ bool TankAttackChatShortcutAction::Execute(Event /*event*/) ResetReturnPosition(); ResetStayPosition(); - botAI->TellMaster("Атакую."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "attacking", "Attacking", {})); return true; } @@ -241,6 +248,20 @@ bool MaxDpsChatShortcutAction::Execute(Event /*event*/) return true; } +bool NaxxChatShortcutAction::Execute(Event /*event*/) +{ + Player* master = GetMaster(); + if (!master) + return false; + + botAI->Reset(); + botAI->ChangeStrategy("+naxx", BOT_STATE_NON_COMBAT); + botAI->ChangeStrategy("+naxx", BOT_STATE_COMBAT); + botAI->TellMasterNoFacing("Add Naxx Strategies!"); + // bot->Say("Add Naxx Strategies!", LANG_UNIVERSAL); + return true; +} + bool BwlChatShortcutAction::Execute(Event /*event*/) { Player* master = GetMaster(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.h index b7e4a9e96..eb3f87dd9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATSHORTCUTACTION_H -#define _PLAYERBOT_CHATSHORTCUTACTION_H +#ifndef PLAYERBOTS_CHATSHORTCUTACTIONS_H +#define PLAYERBOTS_CHATSHORTCUTACTIONS_H #include "MovementActions.h" @@ -85,6 +86,13 @@ public: bool Execute(Event event) override; }; +class NaxxChatShortcutAction : public Action +{ +public: + NaxxChatShortcutAction(PlayerbotAI* ai) : Action(ai, "naxx chat shortcut") {} + virtual bool Execute(Event event); +}; + class BwlChatShortcutAction : public Action { public: diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.cpp index 790134e95..1f52a3275 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CheatAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.h index 512c9d390..09d0e936a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheatAction.h @@ -1,8 +1,12 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ +#ifndef PLAYERBOTS_CHEATACTION_H +#define PLAYERBOTS_CHEATACTION_H + #include "Action.h" class PlayerbotAI; @@ -21,3 +25,5 @@ private: static std::string const GetCheatName(BotCheatMask cheatMask); void ListCheats(); }; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.cpp index 4a39909cd..94c85b6fa 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CheckMailAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.h index cd0add81c..f2bb6179e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMailAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHECKMAILACTION_H -#define _PLAYERBOT_CHECKMAILACTION_H +#ifndef PLAYERBOTS_CHECKMAILACTION_H +#define PLAYERBOTS_CHECKMAILACTION_H #include "Action.h" #include "DatabaseEnvFwd.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.cpp index 0d7fe4321..74faf8cd4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.cpp @@ -1,12 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CheckMountStateAction.h" +#include "AreaDefines.h" #include "BattleGroundTactics.h" #include "BattlegroundEY.h" #include "BattlegroundWS.h" +#include "DBCStores.h" #include "Event.h" #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" @@ -14,6 +17,9 @@ #include "ServerFacade.h" #include "SpellAuraEffects.h" +static constexpr uint32 SPELL_COLD_WEATHER_FLYING = 54197; +static constexpr float PARACHUTE_LAND_THRESHOLD = 15.0f; + // Define the static map / init bool for caching bot preferred mount data globally std::unordered_map CheckMountStateAction::mountCache; bool CheckMountStateAction::preferredMountTableChecked = false; @@ -57,6 +63,21 @@ MountData CollectMountData(const Player* bot) bool CheckMountStateAction::Execute(Event /*event*/) { + // Forced flight dismount: + // Bots get stale flight movement flags after a forced dismount (e.g: Dalaran) because the post landing dismount cleanup + // needs MSG_MOVE_FALL_LAND (a client opcode) and client movement packets. The stale flags cause the bot to be stuck with + // the parachute, or even keep the bot hovering indefinitely and block MMAP routing. + // Note: Without MSG_MOVE_FALL_LAND, HandleFall doesn't trigger, meaning bots don't get fall damage in forced dismounts anyway, + // so the parachute usage here is more of an immersion feature. + if (bot->HasFeatherFallAura()) + { + float floorZ = bot->GetMapHeight(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + if (floorZ != INVALID_HEIGHT && floorZ != VMAP_INVALID_HEIGHT_VALUE && + bot->GetPositionZ() - floorZ <= PARACHUTE_LAND_THRESHOLD) + bot->RemoveAurasByType(SPELL_AURA_FEATHER_FALL); + } + ClearStaleFlightFlags(); + // Determine if there are no attackers bool noAttackers = !AI_VALUE2(bool, "combat", "self target") || !AI_VALUE(uint8, "attacker count"); bool enemy = AI_VALUE(Unit*, "enemy player target"); @@ -94,9 +115,10 @@ bool CheckMountStateAction::Execute(Event /*event*/) } bool inBattleground = bot->InBattleground(); + bool const noRealMaster = (!master || master == bot); // If there is a master and bot not in BG, follow master's mount state regardless of group leader - if (master && !inBattleground) + if (!noRealMaster && !inBattleground) { if (ShouldFollowMasterMountState(master, noAttackers, shouldMount)) return Mount(); @@ -110,8 +132,8 @@ bool CheckMountStateAction::Execute(Event /*event*/) return false; } - // If there is no master or bot in BG - if ((!master || inBattleground) && !bot->IsMounted() && + // No real master (random bot or self-bot) OR bot in BG + if ((noRealMaster || inBattleground) && !bot->IsMounted() && noAttackers && shouldMount && !bot->IsInCombat()) return Mount(); @@ -199,7 +221,7 @@ bool CheckMountStateAction::Mount() // Get bot mount data MountData mountData = CollectMountData(bot); int32 masterMountType = GetMountType(master); - int32 masterSpeed = CalculateMasterMountSpeed(master, mountData); + int32 masterSpeed = CalculateMasterMountSpeed(master); // Try shapeshift if (TryForms(master, masterMountType, masterSpeed)) @@ -228,6 +250,42 @@ void CheckMountStateAction::Dismount() WorldPacket emptyPacket; bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); + + ClearStaleFlightFlags(); +} + +void CheckMountStateAction::ClearStaleFlightFlags() +{ + if (bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura()) + return; + + if (bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING | MOVEMENTFLAG_DISABLE_GRAVITY)) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING | MOVEMENTFLAG_DISABLE_GRAVITY | MOVEMENTFLAG_CAN_FLY); + if (!bot->IsRooted()) + bot->SendMovementFlagUpdate(); + } +} + +void CheckMountStateAction::CompleteDismount(Player* bot) +{ + if (!bot || !bot->IsInWorld()) + return; + + float const x = bot->GetPositionX(); + float const y = bot->GetPositionY(); + float const startZ = bot->GetPositionZ(); + + float groundZ = startZ; + bot->UpdateAllowedPositionZ(x, y, groundZ); + + bot->GetMotionMaster()->MoveFall(); + MovementInfo fallInfo = bot->m_movementInfo; + // Need to set the start of the fall, otherwise the fall may start from too high of a Z and kill the bot. + bot->SetFallInformation(0, startZ); + fallInfo.pos.Relocate(x, y, groundZ); + bot->HandleFall(fallInfo); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FALLING | MOVEMENTFLAG_FALLING_FAR); } bool CheckMountStateAction::TryForms(Player* master, int32 masterMountType, int32 masterSpeed) const @@ -239,7 +297,7 @@ bool CheckMountStateAction::TryForms(Player* master, int32 masterMountType, int3 else if ((masterInShapeshiftForm == FORM_TRAVEL && botInShapeshiftForm == FORM_TRAVEL) || ((masterInShapeshiftForm == FORM_FLIGHT || (masterMountType == 1 && masterSpeed == 149)) && botInShapeshiftForm == FORM_FLIGHT) || - ((masterInShapeshiftForm == FORM_FLIGHT_EPIC || (masterMountType == 1 && masterSpeed == 279)) && botInShapeshiftForm == FORM_FLIGHT_EPIC)) + ((masterInShapeshiftForm == FORM_FLIGHT_EPIC || (masterMountType == 1 && masterSpeed >= 279)) && botInShapeshiftForm == FORM_FLIGHT_EPIC)) return true; // Check if master is in Travel Form and bot can do the same @@ -265,7 +323,7 @@ bool CheckMountStateAction::TryForms(Player* master, int32 masterMountType, int3 // Check if master is in Swift Flight Form or has an epic flying mount and bot can swift flight form if (botAI->CanCastSpell(SPELL_SWIFT_FLIGHT_FORM, bot, true) && ((masterInShapeshiftForm == FORM_FLIGHT_EPIC && botInShapeshiftForm != FORM_FLIGHT_EPIC) || - (masterMountType == 1 && masterSpeed == 279))) + (masterMountType == 1 && masterSpeed >= 279))) { botAI->CastSpell(SPELL_SWIFT_FLIGHT_FORM, bot); @@ -370,18 +428,18 @@ bool CheckMountStateAction::TryPreferredMount(Player* master) const bool CheckMountStateAction::TryRandomMountFiltered(const std::map>& spells, int32 masterSpeed) const { - for (auto const& pair : spells) + for (auto it = spells.rbegin(); it != spells.rend(); ++it) { - int32 currentSpeed = pair.first; + int32 currentSpeed = it->first; if ((masterSpeed > 59 && currentSpeed < 99) || (masterSpeed > 149 && currentSpeed < 279)) continue; // Pick a random mount from the candidate group. - auto const& ids = pair.second; + auto const& ids = it->second; if (!ids.empty()) { - // Required here as otherwise bots won't mount in BG's due to them constant moving + // Required here as otherwise bots won't mount in BGs due to them constant moving if (bot->isMoving()) bot->StopMoving(); @@ -422,19 +480,39 @@ bool CheckMountStateAction::ShouldFollowMasterMountState(Player* master, bool no bool isMasterMounted = master->IsMounted() || (masterInShapeshiftForm == FORM_FLIGHT || masterInShapeshiftForm == FORM_FLIGHT_EPIC || masterInShapeshiftForm == FORM_TRAVEL); - return isMasterMounted && !bot->IsMounted() && noAttackers && + bool isBotMountedOrForm = bot->IsMounted() || botInShapeshiftForm == FORM_FLIGHT || + botInShapeshiftForm == FORM_FLIGHT_EPIC || botInShapeshiftForm == FORM_TRAVEL; + return isMasterMounted && !isBotMountedOrForm && noAttackers && shouldMount && !bot->IsInCombat() && botAI->GetState() != BOT_STATE_COMBAT; } bool CheckMountStateAction::ShouldDismountForMaster(Player* master) const { bool isMasterMounted = master->IsMounted() || (masterInShapeshiftForm == FORM_FLIGHT || - masterInShapeshiftForm == FORM_FLIGHT_EPIC || - masterInShapeshiftForm == FORM_TRAVEL); + masterInShapeshiftForm == FORM_FLIGHT_EPIC || + masterInShapeshiftForm == FORM_TRAVEL); return !isMasterMounted && bot->IsMounted(); } -int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const MountData& mountData) const +static bool BotCanUseFlyingMount(Player const* bot) +{ + if (bot->GetPureSkillValue(SKILL_RIDING) < 225) + return false; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetAreaId()); + if (!area || !area->IsFlyable()) + return false; + if (area->flags & AREA_FLAG_NO_FLY_ZONE) + return false; + + uint32 const vmap = GetVirtualMapForMapAndZone(bot->GetMapId(), bot->GetZoneId()); + if (vmap == MAP_NORTHREND && !bot->HasSpell(SPELL_COLD_WEATHER_FLYING)) + return false; + + return true; +} + +int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master) const { // Check riding skill and level requirements int32 ridingSkill = bot->GetPureSkillValue(SKILL_RIDING); @@ -443,8 +521,10 @@ int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const Mou if (ridingSkill <= 75 && botLevel < static_cast(sPlayerbotAIConfig.useFastGroundMountAtMinLevel)) return 59; - // If there is a master and bot not in BG, use master's aura effects. - if (master && !bot->InBattleground()) + // check if bot has master and if master is self + bool const noRealMaster = (!master || master == bot); + + if (!noRealMaster && !bot->InBattleground()) { auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED); if (!auraEffects.empty()) @@ -458,27 +538,27 @@ int32 CheckMountStateAction::CalculateMasterMountSpeed(Player* master, const Mou return 279; else if (masterInShapeshiftForm == FORM_FLIGHT) return 149; - } - else - { - // Bots on their own. - int32 speed = mountData.maxSpeed; - if (bot->InBattleground() && speed > 99) - return 99; - - return speed; + return 59; // walk pace } - return 59; + // No real master OR battleground: pick speed by skill tier. + if (!bot->InBattleground() && BotCanUseFlyingMount(bot)) + return (ridingSkill >= 300) ? 279 : 149; + + int32 maxGround = (ridingSkill >= 150) ? 99 : 59; + if (bot->InBattleground() && maxGround > 99) + maxGround = 99; + return maxGround; } uint32 CheckMountStateAction::GetMountType(Player* master) const { - if (!master) - return 0; + bool const noRealMaster = (!master || master == bot); + + if (noRealMaster) + return (!bot->InBattleground() && BotCanUseFlyingMount(bot)) ? 1 : 0; auto auraEffects = master->GetAuraEffectsByType(SPELL_AURA_MOUNTED); - if (!auraEffects.empty()) { SpellInfo const* masterSpell = auraEffects.front()->GetSpellInfo(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.h index 9a21838e1..cbbe66668 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheckMountStateAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHECKMOUNTSTATEACTION_H -#define _PLAYERBOT_CHECKMOUNTSTATEACTION_H +#ifndef PLAYERBOTS_CHECKMOUNTSTATEACTION_H +#define PLAYERBOTS_CHECKMOUNTSTATEACTION_H #include #include @@ -42,6 +43,8 @@ public: bool isPossible() override { return true; } bool Mount(); + static void CompleteDismount(Player* bot); + private: Player* master; ShapeshiftForm masterInShapeshiftForm; @@ -51,9 +54,10 @@ private: float CalculateDismountDistance() const; float CalculateMountDistance() const; void Dismount(); + void ClearStaleFlightFlags(); bool ShouldFollowMasterMountState(Player* master, bool noAttackers, bool shouldMount) const; bool ShouldDismountForMaster(Player* master) const; - int32 CalculateMasterMountSpeed(Player* master, const MountData& mountData) const; + int32 CalculateMasterMountSpeed(Player* master) const; bool CheckForSwiftMount() const; std::map>> GetAllMountSpells() const; bool TryForms(Player* master, int32 masterMountType, int32 masterSpeed) const; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.cpp index dce66bd47..74bf40de4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CheckValuesAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.h index f36657560..20759057f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CheckValuesAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHECKVALUESACTION_H -#define _PLAYERBOT_CHECKVALUESACTION_H +#ifndef PLAYERBOTS_CHECKVALUESACTION_H +#define PLAYERBOTS_CHECKVALUESACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp index efbcbb2db..b1f25d136 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include @@ -112,19 +113,17 @@ float ChooseRpgTargetAction::getMaxRelevance(GuidPosition guidP) bool ChooseRpgTargetAction::Execute(Event /*event*/) { - //TravelTarget* travelTarget = AI_VALUE(TravelTarget*, "travel target"); //not used, line marked for removal. Player* master = botAI->GetMaster(); GuidPosition masterRpgTarget; if (master && master != bot && GET_PLAYERBOT_AI(master) && master->GetMapId() == bot->GetMapId() && !master->IsBeingTeleported()) { - Player* player = botAI->GetMaster(); - //GuidPosition masterRpgTarget = PAI_VALUE(GuidPosition, "rpg target"); //not used, line marked for removal. + Player* player = master; + masterRpgTarget = PAI_VALUE(GuidPosition, "rpg target"); } else master = nullptr; std::unordered_map targets; - // uint32 num = 0; //not used, line marked for removal. GuidVector possibleTargets = AI_VALUE(GuidVector, "possible rpg targets"); GuidVector possibleObjects = AI_VALUE(GuidVector, "nearest game objects no los"); GuidVector possiblePlayers = AI_VALUE(GuidVector, "nearest friendly players"); @@ -205,6 +204,7 @@ bool ChooseRpgTargetAction::Execute(Event /*event*/) SET_AI_VALUE(std::string, "next rpg action", ""); + // Only fires when following a playerbot master. for (auto it = begin(targets); it != end(targets);) { //Remove empty targets. @@ -337,7 +337,7 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) if (!botAI->HasActivePlayerMaster() && distance < 50.0f) { Player* player = groupLeader; - if (groupLeader && !groupLeader->isMoving() || + if ((groupLeader && !groupLeader->isMoving()) || PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig.reactDistance) return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.h index 2aeb89e20..5d1ffcb01 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHOOSERPGTARGETACTION_H -#define _PLAYERBOT_CHOOSERPGTARGETACTION_H +#ifndef PLAYERBOTS_CHOOSERPGTARGETACTION_H +#define PLAYERBOTS_CHOOSERPGTARGETACTION_H #include "ObjectGuid.h" #include "RpgAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp index 7ad7a4033..4d5931df4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChooseTargetActions.h" @@ -30,6 +31,14 @@ bool AttackEnemyFlagCarrierAction::isUseful() PlayerHasFlag::IsCapturingFlag(bot); } +bool AggressiveTargetAction::isUseful() +{ + if (bot->IsInCombat()) + return false; + + return true; +} + bool DropTargetAction::Execute(Event /*event*/) { Unit* target = context->GetValue("current target")->Get(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.h index 5f9050194..80c9d6acb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHOOSETARGETACTIONS_H -#define _PLAYERBOT_CHOOSETARGETACTIONS_H +#ifndef PLAYERBOTS_CHOOSETARGETACTIONS_H +#define PLAYERBOTS_CHOOSETARGETACTIONS_H #include "AttackAction.h" @@ -35,6 +36,15 @@ public: std::string const GetTargetName() override { return "tank target"; } }; +class AggressiveTargetAction : public AttackAction +{ +public: + AggressiveTargetAction(PlayerbotAI* botAI) : AttackAction(botAI, "aggressive target") {} + + std::string const GetTargetName() override { return "aggressive target"; } + bool isUseful() override; +}; + class AttackAnythingAction : public AttackAction { public: diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp index 52f96b0f8..a9e3fb289 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChooseTravelTargetAction.h" @@ -232,15 +233,6 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave QuestTravelDestination* QuestDestination = (QuestTravelDestination*)destination; Quest const* quest = QuestDestination->GetQuestTemplate(); WorldPosition botLocation(bot); - - CreatureTemplate const* cInfo = nullptr; - GameObjectTemplate const* gInfo = nullptr; - - if (destination->getEntry() > 0) - cInfo = sObjectMgr->GetCreatureTemplate(destination->getEntry()); - else - gInfo = sObjectMgr->GetGameObjectTemplate(destination->getEntry() * -1); - std::string Sub; if (newTarget->isGroupCopy()) @@ -478,7 +470,7 @@ bool ChooseTravelTargetAction::SetCurrentTarget(TravelTarget* target, TravelTarg return target->isActive(); } -bool ChooseTravelTargetAction::SetQuestTarget(TravelTarget* target, bool onlyCompleted, bool newQuests, bool activeQuests, bool completedQuests) +bool ChooseTravelTargetAction::SetQuestTarget(TravelTarget* target, bool /*onlyCompleted*/, bool newQuests, bool activeQuests, bool completedQuests) { std::vector activeDestinations; std::vector activePoints; @@ -823,10 +815,6 @@ char* strstri(char const* haystack, char const* needle); TravelDestination* ChooseTravelTargetAction::FindDestination(Player* bot, std::string const name, bool zones, bool npcs, bool quests, bool mobs, bool bosses) { - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - - // AiObjectContext* context = botAI->GetAiObjectContext(); //not used, line marked for removal. - std::vector dests; //Quests @@ -941,7 +929,7 @@ bool ChooseTravelTargetAction::needForQuest(Unit* target) int required = questTemplate->RequiredNpcOrGoCount[j]; int available = questStatus.CreatureOrGOCount[j]; - if (required && available < required && (target->GetEntry() == entry || justCheck)) + if (required && available < required && (target->GetEntry() == uint32(entry) || justCheck)) return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.h index 2ec026dc2..b486b5ea5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHOOSETRAVELTARGETACTION_H -#define _PLAYERBOT_CHOOSETRAVELTARGETACTION_H +#ifndef PLAYERBOTS_CHOOSETRAVELTARGETACTION_H +#define PLAYERBOTS_CHOOSETRAVELTARGETACTION_H #include "MovementActions.h" #include "TravelMgr.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.cpp index 2f687d5e0..e63a12e41 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CombatActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.h index 82d74527e..f9a268348 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CombatActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_COMBATACTIONS_H -#define _PLAYERBOT_COMBATACTIONS_H +#ifndef PLAYERBOTS_COMBATACTIONS_H +#define PLAYERBOTS_COMBATACTIONS_H #include "ChangeStrategyAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp index 12db7c791..98ae1b71d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CustomStrategyEditAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.h index 363307659..9be3f1edd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CUSTOMSTRATEGYEDITACTION_H -#define _PLAYERBOT_CUSTOMSTRATEGYEDITACTION_H +#ifndef PLAYERBOTS_CUSTOMSTRATEGYEDITACTION_H +#define PLAYERBOTS_CUSTOMSTRATEGYEDITACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp index eeafe2cf0..246d0013b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DebugAction.h" @@ -520,8 +521,8 @@ bool DebugAction::Execute(Event event) botPos.setY(botPos.GetPositionY() + (dy - 5) * 5); botPos.setZ(botPos.getHeight()); - Creature* wpCreature = bot->SummonCreature(effect, botPos.GetPositionX(), botPos.GetPositionY(), botPos.GetPositionZ(), 0, - TEMPSUMMON_TIMED_DESPAWN, 10000.0f); + bot->SummonCreature(effect, botPos.GetPositionX(), botPos.GetPositionY(), botPos.GetPositionZ(), 0, + TEMPSUMMON_TIMED_DESPAWN, 10000.0f); } } return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.h index 5c81c313e..8daefd529 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DEBUGACTION_H -#define _PLAYERBOT_DEBUGACTION_H +#ifndef PLAYERBOTS_DEBUGACTION_H +#define PLAYERBOTS_DEBUGACTION_H #include "Action.h" #include "ObjectGuid.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.cpp index 3daf9ea25..e4629a002 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DelayAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.h index 626c577fd..b7335d7dc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DelayAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DELAYACTION_H -#define _PLAYERBOT_DELAYACTION_H +#ifndef PLAYERBOTS_DELAYACTION_H +#define PLAYERBOTS_DELAYACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.cpp index ffcf0fdcf..e347f6297 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DestroyItemAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.h index fce18e602..fdbf28074 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DestroyItemAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DESTROYITEMACTION_H -#define _PLAYERBOT_DESTROYITEMACTION_H +#ifndef PLAYERBOTS_DESTROYITEMACTION_H +#define PLAYERBOTS_DESTROYITEMACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp index 5dece3336..64249024f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DropQuestAction.h" #include "ChatHelper.h" #include "Event.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool DropQuestAction::Execute(Event event) @@ -51,10 +53,15 @@ bool DropQuestAction::Execute(Event event) const Quest* pQuest = sObjectMgr->GetQuestTemplate(entry); const std::string text_quest = ChatHelper::FormatQuest(pQuest); LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), pQuest->GetTitle()); - bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_removed_debug", + "Quest [%quest] removed", + {{"%quest", text_quest}}); + bot->Say(text, LANG_UNIVERSAL); } - botAI->TellMaster("Задание удалено."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_remove", "Quest removed", {})); return true; } @@ -62,31 +69,19 @@ bool CleanQuestLogAction::Execute(Event event) { Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); if (!requester) - { - botAI->TellMaster("Не удалось определить владельца события"); return false; - } if (!sPlayerbotAIConfig.dropObsoleteQuests) - { return false; - } // Only output this message if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { - botAI->TellMaster("Получена команда очистки журнала: удаляю серые и незначительные задания..."); - } + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "clean_quest_log_started", + "Clean Quest Log command received, removing grey/trivial quests...", + {})); uint8 botLevel = bot->GetLevel(); // Get bot's level - uint8 numQuest = 0; - for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) - { - if (bot->GetQuestSlotQuestId(slot)) - { - numQuest++; - } - } for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) { @@ -101,34 +96,27 @@ bool CleanQuestLogAction::Execute(Event event) // Determine if quest is trivial by comparing levels int32 questLevel = quest->GetQuestLevel(); if (questLevel == -1) // For scaling quests, default to bot level - { questLevel = botLevel; - } // Set the level difference for when a quest becomes trivial // This was determined by using the Lua code the client uses int32 trivialLevel = 5; if (botLevel >= 40) - { trivialLevel = 8; - } else if (botLevel >= 30) - { trivialLevel = 7; - } else if (botLevel >= 20) - { trivialLevel = 6; - } // Check if the quest is trivial (grey) for the bot if ((botLevel - questLevel) > trivialLevel) { // Output only if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { - botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey)."); - } + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_trivial_will_remove", + "Quest [%title] will be removed because it is trivial (grey).", + {{"%title", quest->GetTitle()}})); // Remove quest botAI->rpgStatistic.questDropped++; @@ -137,27 +125,31 @@ bool CleanQuestLogAction::Execute(Event event) bot->SetQuestStatus(questId, QUEST_STATUS_NONE); bot->RemoveRewardedQuest(questId); - numQuest--; - if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { const std::string text_quest = ChatHelper::FormatQuest(quest); LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle()); - bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_removed_debug", + "Quest [%quest] removed", + {{"%quest", text_quest}}); + bot->Say(text, LANG_UNIVERSAL); } if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { - botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] has been removed."); - } + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_has_been_removed", + "Quest [%title] has been removed.", + {{"%title", quest->GetTitle()}})); } else { // Only output if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) - { - botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] is not trivial and will be kept."); - } + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_not_trivial_kept", + "Quest [%title] is not trivial and will be kept.", + {{"%title", quest->GetTitle()}})); } } @@ -174,7 +166,6 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG { std::random_device rd; std::mt19937 g(rd()); - std::shuffle(slots.begin(), slots.end(), g); } @@ -200,8 +191,10 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG bot->GetLevel() <= bot->GetQuestLevel(quest) + uint32(lowLevelDiff)) // Quest is not gray { if (bot->GetLevel() + 5 > bot->GetQuestLevel(quest)) // Quest is not red + { if (!isGreen) continue; + } } else // Quest is gray { @@ -234,9 +227,16 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG { const std::string text_quest = ChatHelper::FormatQuest(quest); LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle()); - bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_removed_debug", + "Quest [%quest] removed", + {{"%quest", text_quest}}); + bot->Say(text, LANG_UNIVERSAL); } - botAI->TellMaster("Задание удалено." + chat->FormatQuest(quest)); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_removed_with_name", + "Quest removed %quest", + {{"%quest", chat->FormatQuest(quest)}})); } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.h index a6622eec1..7c98a3394 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DROPQUESTACTION_H -#define _PLAYERBOT_DROPQUESTACTION_H +#ifndef PLAYERBOTS_DROPQUESTACTION_H +#define PLAYERBOTS_DROPQUESTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp index e7e573545..3e7b286bc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EmoteAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.h index 4819c6476..df0045aea 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EMOTEACTION_H -#define _PLAYERBOT_EMOTEACTION_H +#ifndef PLAYERBOTS_EMOTEACTION_H +#define PLAYERBOTS_EMOTEACTION_H #include diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp index 343377c4d..74ec8806c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EquipAction.h" @@ -85,8 +86,8 @@ void EquipAction::EquipItem(Item* item) if (itemProto->Class == ITEM_CLASS_CONTAINER) { // Attempt to equip as a bag - Bag* pBag = reinterpret_cast(item); uint8 newBagSlot = GetSmallestBagSlot(); + if (newBagSlot > 0) { uint16 src = ((bagIndex << 8) | slot); @@ -154,9 +155,11 @@ void EquipAction::EquipItem(Item* item) calculator.SetOverflowPenalty(false); // Calculate item scores once and store them - float newItemScore = calculator.CalculateItem(itemId); - float mainHandScore = mainHandItem ? calculator.CalculateItem(mainHandItem->GetTemplate()->ItemId) : 0.0f; - float offHandScore = offHandItem ? calculator.CalculateItem(offHandItem->GetTemplate()->ItemId) : 0.0f; + float newItemScore = calculator.CalculateItem(itemId, item->GetItemRandomPropertyId()); + float mainHandScore = mainHandItem + ? calculator.CalculateItem(mainHandItem->GetTemplate()->ItemId, mainHandItem->GetItemRandomPropertyId()) : 0.0f; + float offHandScore = offHandItem + ? calculator.CalculateItem(offHandItem->GetTemplate()->ItemId, offHandItem->GetItemRandomPropertyId()) : 0.0f; // Determine where this weapon can go bool canGoMain = (invType == INVTYPE_WEAPON || diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.h index bf609d861..551a635ad 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EQUIPACTION_H -#define _PLAYERBOT_EQUIPACTION_H +#ifndef PLAYERBOTS_EQUIPACTION_H +#define PLAYERBOTS_EQUIPACTION_H #include "ChatHelper.h" #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp index cd2d7ca73..ab267f6ac 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EquipGlyphsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.h index d2812d8e3..68aeab13e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EQUIPGLYPHSACTION_H -#define _PLAYERBOT_EQUIPGLYPHSACTION_H +#ifndef PLAYERBOTS_EQUIPGLYPHSACTION_H +#define PLAYERBOTS_EQUIPGLYPHSACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.cpp index a6057cabf..5e9fd7fc7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FishingAction.h" @@ -262,7 +263,7 @@ bool MoveNearWaterAction::isUseful() FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue("fishing spot"); WorldPosition pos = fishingSpotValueObject->Get(); return !pos.IsValid() || fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) || - bot->GetExactDist(&pos) < 0.1f; + bot->GetExactDist(&pos) > 0.1f; } @@ -292,7 +293,6 @@ bool MoveNearWaterAction::isPossible() // Water spot is out of range, lets look for a spot to move to for the fishing hole. if (distance > MAX_DISTANCE_TO_WATER || distance < MIN_DISTANCE_TO_WATER) { - float angle = bot->GetAngle(fishingHole.GetPositionX(), fishingHole.GetPositionY()); WorldPosition landSpot = FindLandRadialFromPosition(botAI, fishingHole, MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, fishingSearchWindow, 32); if (landSpot.IsValid()) { @@ -323,7 +323,6 @@ bool MoveNearWaterAction::isPossible() if (!water.IsValid()) return false; - bool hasLOS = bot->IsWithinLOS(water.GetPositionX(), water.GetPositionY(), water.GetPositionZ()); float angle = bot->GetAngle(water.GetPositionX(), water.GetPositionY()); WorldPosition landSpot = FindLandFromPosition(botAI, 0.0f, MAX_DISTANCE_TO_WATER, 1.0f, angle, water, fishingSearchWindow, false); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.h index 35a7ab6fe..0b702382e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FishingAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FISHINGACTION_H -#define _PLAYERBOT_FISHINGACTION_H +#ifndef PLAYERBOTS_FISHINGACTION_H +#define PLAYERBOTS_FISHINGACTION_H #include "Action.h" #include "Event.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp index 78e5529b4..e21e0899f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FlagAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.h index 3d50d7ad2..2386a91f6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FLAGACTION_H -#define _PLAYERBOT_FLAGACTION_H +#ifndef PLAYERBOTS_FLAGACTION_H +#define PLAYERBOTS_FLAGACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp index 6a74d3ca3..f051773de 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp @@ -5,18 +5,211 @@ #include "FollowActions.h" +#include +#include +#include + #include "Event.h" #include "Formations.h" #include "LastMovementValue.h" +#include "MotionMaster.h" #include "PlayerbotAI.h" #include "Playerbots.h" #include "ServerFacade.h" +#include "Transport.h" +#include "Map.h" + +namespace +{ + Transport* GetTransportForPosTolerant(Map* map, WorldObject* ref, uint32 phaseMask, float x, float y, float z) + { + if (!map || !ref) + return nullptr; + + std::array const probes = { z, z + 0.5f, z + 1.5f, z - 0.5f }; + for (float const pz : probes) + { + if (Transport* t = map->GetTransportForPos(phaseMask, x, y, pz, ref)) + return t; + } + + return nullptr; + } + + // Attempts to find a point on the leader's transport that is closer to the bot, + // by probing along the segment from master -> bot and returning the last point + // that is still detected as being on the expected transport. + bool FindBoardingPointOnTransport(Map* map, Transport* expectedTransport, WorldObject* ref, + float masterX, float masterY, float masterZ, + float botX, float botY, float botZ, + float& outX, float& outY, float& outZ) + { + if (!map || !expectedTransport || !ref) + return false; + + uint32 const phaseMask = ref->GetPhaseMask(); + + // Ensure master is actually detected on that transport (tolerant). + if (GetTransportForPosTolerant(map, ref, phaseMask, masterX, masterY, masterZ) != expectedTransport) + return false; + + // The raycast in GetTransportForPos starts at (z + 2). Probe with a safe Z. + float const probeZ = std::max(masterZ, botZ); + + // Adaptive step count: small platforms need tighter sampling. + float const dx2 = botX - masterX; + float const dy2 = botY - masterY; + float const dist2d = std::sqrt(dx2 * dx2 + dy2 * dy2); + int32 const steps = std::clamp(static_cast(dist2d / 0.75f), 10, 28); + + float const dx = (botX - masterX) / static_cast(steps); + float const dy = (botY - masterY) / static_cast(steps); + + // Master must actually be on the expected transport for this to work. + if (map->GetTransportForPos(ref->GetPhaseMask(), masterX, masterY, probeZ, ref) != expectedTransport) + return false; + + float lastX = masterX; + float lastY = masterY; + bool found = false; + + for (int32 i = 1; i <= steps; ++i) + { + float const px = masterX + dx * i; + float const py = masterY + dy * i; + + Transport* const t = GetTransportForPosTolerant(map, ref, phaseMask, px, py, probeZ); + if (t != expectedTransport) + break; + + lastX = px; + lastY = py; + found = true; + } + + if (!found) + return false; + + outX = lastX; + outY = lastY; + outZ = masterZ; // keep deck-level Z to encourage stepping onto the platform/boat + return true; + } +} bool FollowAction::Execute(Event /*event*/) { Formation* formation = AI_VALUE(Formation*, "formation"); std::string const target = formation->GetTargetName(); + // Transport handling for moving transports only (boats/zeppelins). + Player* master = botAI->GetMaster(); + if (master && master->IsInWorld() && bot->IsInWorld() && bot->GetMapId() == master->GetMapId()) + { + Map* map = master->GetMap(); + uint32 const mapId = bot->GetMapId(); + Transport* transport = nullptr; + bool masterOnTransport = false; + + if (master->GetTransport()) + { + transport = master->GetTransport(); + masterOnTransport = true; + } + else if (map) + { + transport = GetTransportForPosTolerant(map, master, master->GetPhaseMask(), + master->GetPositionX(), master->GetPositionY(), master->GetPositionZ()); + masterOnTransport = (transport != nullptr); + } + + // Ignore static transports (elevators/trams): only keep boats/zeppelins here. + if (transport && transport->IsStaticTransport()) + transport = nullptr; + + if (transport && map && bot->GetTransport() != transport) + { + float const botProbeZ = std::max(bot->GetPositionZ(), transport->GetPositionZ()); + Transport* botSurfaceTransport = GetTransportForPosTolerant(map, bot, bot->GetPhaseMask(), + bot->GetPositionX(), bot->GetPositionY(), botProbeZ); + + if (botSurfaceTransport == transport) + { + transport->AddPassenger(bot, true); + bot->StopMovingOnCurrentPos(); + return true; + } + + float const boardingAssistDistance = 60.0f; + float const dist2d = ServerFacade::instance().GetDistance2d(bot, master); + bool const inAssist = ServerFacade::instance().IsDistanceLessOrEqualThan(dist2d, boardingAssistDistance); + + if (inAssist) + { + float destX = masterOnTransport ? master->GetPositionX() : transport->GetPositionX(); + float destY = masterOnTransport ? master->GetPositionY() : transport->GetPositionY(); + float destZ = masterOnTransport ? master->GetPositionZ() : transport->GetPositionZ(); + float edgeX = 0.0f; + float edgeY = 0.0f; + float edgeZ = 0.0f; + + if (masterOnTransport && + FindBoardingPointOnTransport(map, transport, master, + master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(), + bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + edgeX, edgeY, edgeZ)) + { + destX = edgeX; + destY = edgeY; + destZ = edgeZ; + } + + MovementPriority const priority = botAI->GetState() == BOT_STATE_COMBAT + ? MovementPriority::MOVEMENT_COMBAT + : MovementPriority::MOVEMENT_NORMAL; + + bool const movingAllowed = IsMovingAllowed(); + bool const dupMove = IsDuplicateMove(destX, destY, destZ); + bool const waiting = IsWaitingForLastMove(priority); + + if (movingAllowed && !dupMove && !waiting) + { + if (bot->IsSitState()) + bot->SetStandState(UNIT_STAND_STATE_STAND); + + if (bot->IsNonMeleeSpellCast(true)) + { + bot->CastStop(); + botAI->InterruptSpell(); + } + + if (MotionMaster* mm = bot->GetMotionMaster()) + { + mm->MovePoint( + /*id*/ 0, + /*coords*/ destX, destY, destZ, + /*forcedMovement*/ FORCED_MOVEMENT_NONE, + /*speed*/ 0.0f, + /*orientation*/ 0.0f, + /*generatePath*/ false, + /*forceDestination*/ false); + } + else + return false; + + float delay = 1000.0f * MoveDelay(bot->GetExactDist(destX, destY, destZ)); + delay = std::clamp(delay, 0.0f, static_cast(sPlayerbotAIConfig.maxWaitForMove)); + + AI_VALUE(LastMovement&, "last movement") + .Set(mapId, destX, destY, destZ, bot->GetOrientation(), delay, priority); + ClearIdleState(); + return true; + } + } + } + } + // end unified transport handling + bool moved = false; if (!target.empty()) { @@ -25,7 +218,7 @@ bool FollowAction::Execute(Event /*event*/) else { WorldLocation loc = formation->GetLocation(); - if (Formation::IsNullLocation(loc) || loc.GetMapId() == -1) + if (Formation::IsNullLocation(loc) || loc.GetMapId() == MAPID_INVALID) return false; MovementPriority priority = botAI->GetState() == BOT_STATE_COMBAT ? MovementPriority::MOVEMENT_COMBAT : MovementPriority::MOVEMENT_NORMAL; @@ -66,6 +259,8 @@ bool FollowAction::isUseful() Unit* fTarget = nullptr; if (!target.empty()) fTarget = AI_VALUE(Unit*, target); + else if (botAI->GetMaster()) + fTarget = botAI->GetMaster(); else fTarget = AI_VALUE(Unit*, "group leader"); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.h index 9331bbf55..6c8e9b7e1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FOLLOWACTIONS_H -#define _PLAYERBOT_FOLLOWACTIONS_H +#ifndef PLAYERBOTS_FOLLOWACTIONS_H +#define PLAYERBOTS_FOLLOWACTIONS_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp index 40a753ff5..39480c824 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericActions.h" @@ -8,6 +9,7 @@ #include "Player.h" #include "Pet.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotTextMgr.h" #include "CreatureAI.h" #include "Playerbots.h" #include "CharmInfo.h" @@ -49,7 +51,10 @@ bool MeleeAction::isUseful() if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true)) return false; - return true; + // Do not start autoattack while prowled — let opener spells break stealth intentionally. + // Future rogue stealth implementation should use this instead: + // return !(botAI->HasAura("stealth", bot) || botAI->HasAura("prowl", bot)); + return !botAI->HasAura("prowl", bot); } bool TogglePetSpellAutoCastAction::Execute(Event /*event*/) @@ -82,7 +87,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event /*event*/) uint32 spellId = itr->first; const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); - if (!spellInfo->IsAutocastable()) + if (!spellInfo || !spellInfo->IsAutocastable()) continue; bool shouldApply = true; @@ -178,7 +183,8 @@ bool SetPetStanceAction::Execute(Event /*event*/) // If there are no controlled pets or guardians, notify the player and exit if (targets.empty()) { - botAI->TellError("У вас нет питомца или стража."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pet_no_pet_error", "You have no pet or guardian pet.", {})); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.h index b089b6764..51b457805 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICACTIONS_H -#define _PLAYERBOT_GENERICACTIONS_H +#ifndef PLAYERBOTS_GENERICACTIONS_H +#define PLAYERBOTS_GENERICACTIONS_H #include "AttackAction.h" #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.cpp deleted file mode 100644 index e33c12f18..000000000 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 "GenericBuffUtils.h" -#include "PlayerbotAIConfig.h" - -#include - -#include "Player.h" -#include "Group.h" -#include "SpellMgr.h" -#include "Chat.h" -#include "PlayerbotAI.h" -#include "ServerFacade.h" -#include "AiObjectContext.h" -#include "Value.h" -#include "Config.h" -#include "PlayerbotTextMgr.h" - -namespace ai::buff -{ - std::string MakeAuraQualifierForBuff(std::string const& name) - { - // Paladin - if (name == "blessing of kings") return "blessing of kings,greater blessing of kings"; - if (name == "blessing of might") return "blessing of might,greater blessing of might"; - if (name == "blessing of wisdom") return "blessing of wisdom,greater blessing of wisdom"; - if (name == "blessing of sanctuary") return "blessing of sanctuary,greater blessing of sanctuary"; - // Druid - if (name == "mark of the wild") return "mark of the wild,gift of the wild"; - // Mage - if (name == "arcane intellect") return "arcane intellect,arcane brilliance"; - // Priest - if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude"; - - return name; - } - - std::string GroupVariantFor(std::string const& name) - { - // Paladin - if (name == "blessing of kings") return "greater blessing of kings"; - if (name == "blessing of might") return "greater blessing of might"; - if (name == "blessing of wisdom") return "greater blessing of wisdom"; - if (name == "blessing of sanctuary") return "greater blessing of sanctuary"; - // Druid - if (name == "mark of the wild") return "gift of the wild"; - // Mage - if (name == "arcane intellect") return "arcane brilliance"; - // Priest - if (name == "power word: fortitude") return "prayer of fortitude"; - - return std::string(); - } - - bool HasRequiredReagents(Player* bot, uint32 spellId) - { - if (!spellId) - return false; - - if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId)) - { - for (int i = 0; i < MAX_SPELL_REAGENTS; ++i) - { - if (info->Reagent[i] > 0) - { - uint32 const itemId = info->Reagent[i]; - int32 const need = info->ReagentCount[i]; - if ((int32)bot->GetItemCount(itemId, false) < need) - return false; - } - } - // No reagent required - return true; - } - return false; - } - - std::string UpgradeToGroupIfAppropriate( - Player* bot, - PlayerbotAI* botAI, - std::string const& baseName, - bool announceOnMissing, - std::function announce) - { - std::string castName = baseName; - Group* g = bot->GetGroup(); - if (!g || g->GetMembersCount() < static_cast(sPlayerbotAIConfig.minBotsForGreaterBuff)) - return castName; // Group too small: stay in solo mode - - if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty()) - { - uint32 const groupVariantSpellId = botAI->GetAiObjectContext() - ->GetValue("spell id", groupName)->Get(); - - // We check usefulness on the **basic** buff (not the greater version), - // because "spell cast useful" may return false for the greater variant. - bool const usefulBase = botAI->GetAiObjectContext() - ->GetValue("spell cast useful", baseName)->Get(); - - if (groupVariantSpellId && HasRequiredReagents(bot, groupVariantSpellId)) - { - // Learned + reagents OK -> switch to greater - return groupName; - } - - // Missing reagents -> announce if (a) greater is known, (b) base buff is useful, - // (c) announce was requested, (d) a callback is provided. - if (announceOnMissing && groupVariantSpellId && usefulBase && announce) - { - static std::map, time_t> s_lastWarn; // par bot & par buff - time_t now = std::time(nullptr); - uint32 botLow = static_cast(bot->GetGUID().GetCounter()); - time_t& last = s_lastWarn[ std::make_pair(botLow, groupName) ]; - if (!last || now - last >= sPlayerbotAIConfig.rpWarningCooldown) // Configurable anti-spam - { - // DB Key choice in regard of the buff - std::string key; - if (groupName.find("greater blessing") != std::string::npos) - key = "rp_missing_reagent_greater_blessing"; - else if (groupName == "gift of the wild") - key = "rp_missing_reagent_gift_of_the_wild"; - else if (groupName == "arcane brilliance") - key = "rp_missing_reagent_arcane_brilliance"; - else - key = "rp_missing_reagent_generic"; - - // Placeholders - std::map placeholders; - placeholders["%group_spell"] = groupName; - placeholders["%base_spell"] = baseName; - - std::string announceText = PlayerbotTextMgr::instance().GetBotTextOrDefault(key, - "Out of components for %group_spell. Using %base_spell!", placeholders); - - announce(announceText); - last = now; - } - } - } - return castName; - } -} diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.h b/modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.h deleted file mode 100644 index c893de597..000000000 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericBuffUtils.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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. - */ - -#pragma once - -#include -#include -#include "Common.h" -#include "Group.h" -#include "Chat.h" -#include "Language.h" - -class Player; -class PlayerbotAI; - -namespace ai::buff -{ - - // Build an aura qualifier "single + greater" to avoid double-buffing - std::string MakeAuraQualifierForBuff(std::string const& name); - - // Returns the group spell name for a given single-target buff. - // If no group equivalent exists, returns "". - std::string GroupVariantFor(std::string const& name); - - // Checks if the bot has the required reagents to cast a spell (by its spellId). - // Returns false if the spellId is invalid. - bool HasRequiredReagents(Player* bot, uint32 spellId); - - // Applies the "switch to group buff" policy if: the bot is in a group of size x+, - // the group variant is known/useful, and reagents are available. Otherwise, returns baseName. - // If announceOnMissing == true and reagents are missing, calls the 'announce' callback - // (if provided) to notify the party/raid. - std::string UpgradeToGroupIfAppropriate( - Player* bot, - PlayerbotAI* botAI, - std::string const& baseName, - bool announceOnMissing = false, - std::function announce = {} - ); -} - -namespace ai::chat { - inline std::function MakeGroupAnnouncer(Player* me) - { - return [me](std::string const& msg) - { - if (Group* g = me->GetGroup()) - { - WorldPacket data; - ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY; - ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str()); - g->BroadcastPacket(&data, true, -1, me->GetGUID()); - } - else - { - me->Say(msg, LANG_UNIVERSAL); - } - }; - } -} diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.cpp index 148bc6d3c..0e385b152 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -1,11 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericSpellActions.h" #include +#include #include "Event.h" #include "ItemTemplate.h" @@ -21,11 +23,120 @@ #include "PlayerbotAI.h" using ai::buff::MakeAuraQualifierForBuff; +using ai::spell::HasSpellOrCategoryCooldown; + +namespace +{ + std::unordered_set const& GetMixedTriggerTrinketSpellIds() + { + static std::unordered_set const mixedTriggerSpellIds = []() + { + std::unordered_set onUseSpellIds; + std::unordered_set onEquipSpellIds; + std::unordered_set mixedSpellIds; + + auto const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + if (!itemTemplates) + return mixedSpellIds; + + auto const markSpellId = [&](int32 spellId, uint8 spellTrigger) + { + if (spellId <= 0) + return; + + if (spellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + if (onEquipSpellIds.find(spellId) != onEquipSpellIds.end()) + mixedSpellIds.insert(spellId); + + onUseSpellIds.insert(spellId); + } + else if (spellTrigger == ITEM_SPELLTRIGGER_ON_EQUIP) + { + if (onUseSpellIds.find(spellId) != onUseSpellIds.end()) + mixedSpellIds.insert(spellId); + + onEquipSpellIds.insert(spellId); + } + }; + + for (auto const& itr : *itemTemplates) + { + ItemTemplate const& proto = itr.second; + if (proto.InventoryType != INVTYPE_TRINKET) + continue; + + for (uint8 spellIndex = 0; spellIndex < MAX_ITEM_PROTO_SPELLS; ++spellIndex) + { + auto const& spellData = proto.Spells[spellIndex]; + markSpellId(spellData.SpellId, spellData.SpellTrigger); + } + } + + return mixedSpellIds; + }(); + + return mixedTriggerSpellIds; + } + + bool IsManaRestoreEffect(SpellEffectInfo const& effectInfo) + { + return (effectInfo.Effect == SPELL_EFFECT_ENERGIZE && + effectInfo.MiscValue == POWER_MANA) || + (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA && + effectInfo.ApplyAuraName == SPELL_AURA_PERIODIC_ENERGIZE && + effectInfo.MiscValue == POWER_MANA); + } + + bool IsManaEfficiencyEffect(SpellEffectInfo const& effectInfo) + { + return effectInfo.Effect == SPELL_EFFECT_APPLY_AURA && + (((effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN_PERCENT) && + effectInfo.MiscValue == POWER_MANA) || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_COST_SCHOOL || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT || + effectInfo.ApplyAuraName == SPELL_AURA_MOD_MANA_REGEN_INTERRUPT); + } + + bool IsDefensiveTankEffect(SpellEffectInfo const& effectInfo) + { + if (effectInfo.Effect != SPELL_EFFECT_APPLY_AURA) + return false; + + uint32 const tankRatingsMask = + (1u << CR_DEFENSE_SKILL) | + (1u << CR_DODGE) | + (1u << CR_PARRY) | + (1u << CR_BLOCK) | + (1u << CR_HIT_TAKEN_MELEE) | + (1u << CR_HIT_TAKEN_RANGED) | + (1u << CR_HIT_TAKEN_SPELL) | + (1u << CR_CRIT_TAKEN_MELEE) | + (1u << CR_CRIT_TAKEN_RANGED) | + (1u << CR_CRIT_TAKEN_SPELL); + + switch (effectInfo.ApplyAuraName) + { + case SPELL_AURA_MOD_RESISTANCE: + return (effectInfo.MiscValue & SPELL_SCHOOL_MASK_NORMAL) != 0; + case SPELL_AURA_MOD_RATING: + return (effectInfo.MiscValue & tankRatingsMask) != 0; + case SPELL_AURA_MOD_INCREASE_HEALTH: + case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT: + case SPELL_AURA_MOD_PARRY_PERCENT: + case SPELL_AURA_MOD_DODGE_PERCENT: + case SPELL_AURA_MOD_BLOCK_PERCENT: + case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN: + return true; + default: + return false; + } + } +} CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) - : Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) -{ -} + : Action(botAI, spell), spell(spell), range(botAI->GetRange("spell")) {} bool CastSpellAction::Execute(Event /*event*/) { @@ -52,18 +163,12 @@ bool CastSpellAction::Execute(Event /*event*/) wstrToLower(wnamepart); - if (!Utf8FitTo(spell, wnamepart)) - continue; - - if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM) + if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM) continue; uint32 itemId = spellInfo->Effects[0].ItemType; ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (!proto) - continue; - - if (bot->CanUseItem(proto) != EQUIP_ERR_OK) + if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK) continue; if (spellInfo->Id > castId) @@ -91,10 +196,7 @@ bool CastSpellAction::isUseful() } Unit* spellTarget = GetTarget(); - if (!spellTarget) - return false; - - if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId()) + if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId()) return false; // float combatReach = bot->GetCombatReach() + target->GetCombatReach(); @@ -133,7 +235,8 @@ bool CastSpellAction::isPossible() return botAI->CanCastSpell(spell, GetTarget()); } -CastMeleeSpellAction::CastMeleeSpellAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) +CastMeleeSpellAction::CastMeleeSpellAction( + PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) { range = ATTACK_DISTANCE; } @@ -141,16 +244,15 @@ CastMeleeSpellAction::CastMeleeSpellAction(PlayerbotAI* botAI, std::string const bool CastMeleeSpellAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - - if (!bot->IsWithinMeleeRange(target)) + if (!target || !bot->IsWithinMeleeRange(target)) return false; return CastSpellAction::isUseful(); } -CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) +CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction( + PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : + CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) { range = ATTACK_DISTANCE; } @@ -158,10 +260,7 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(PlayerbotAI* botAI, std:: bool CastMeleeDebuffSpellAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - - if (!bot->IsWithinMeleeRange(target)) + if (!target || !bot->IsWithinMeleeRange(target)) return false; return CastDebuffSpellAction::isUseful(); @@ -171,40 +270,110 @@ bool CastAuraSpellAction::isUseful() { if (!GetTarget() || !CastSpellAction::isUseful()) return false; + Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration); - if (!aura) - return true; - if (beforeDuration && aura->GetDuration() < beforeDuration) + if (!aura || (beforeDuration && uint32(aura->GetDuration()) < beforeDuration)) return true; + return false; } -CastEnchantItemAction::CastEnchantItemAction(PlayerbotAI* botAI, std::string const spell) - : CastSpellAction(botAI, spell) +bool CastBuffSpellAction::isUseful() { - range = botAI->GetRange("spell"); + Unit* target = GetTarget(); + if (!target || !CastSpellAction::isUseful()) + return false; + + Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration); + return !aura || (beforeDuration && uint32(aura->GetDuration()) < beforeDuration); } -bool CastEnchantItemAction::isPossible() +bool CastBuffSpellAction::Execute(Event /*event*/) { - // if (!CastSpellAction::isPossible()) - // { - // botAI->TellMasterNoFacing("Impossible: " + spell); - // return false; - // } + return botAI->CastSpell(spell, GetTarget()); +} - uint32 spellId = AI_VALUE2(uint32, "spell id", spell); +bool GroupBuffSpellAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target || !CastSpellAction::isUseful()) + return false; - // bool ok = AI_VALUE2(Item*, "item for spell", spellId); - // Item* item = AI_VALUE2(Item*, "item for spell", spellId); - // botAI->TellMasterNoFacing("spell: " + spell + ", spell id: " + std::to_string(spellId) + " item for spell: " + - // std::to_string(ok)); - return spellId && AI_VALUE2(Item*, "item for spell", spellId); + if (ai::buff::IsGroupVariantEnabled(bot, spell)) + { + std::string const groupVariant = ai::buff::GroupVariantFor(spell); + if (!groupVariant.empty() && botAI->HasAura(groupVariant, target, false, isOwner, -1, checkDuration)) + return false; + } + + Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration); + if (!aura || (beforeDuration && uint32(aura->GetDuration()) < beforeDuration)) + return true; + + return false; +} + +bool GroupBuffSpellAction::Execute(Event /*event*/) +{ + std::string missingReagentGroupName; + std::string const castName = ai::buff::UpgradeToGroupIfAppropriate( + bot, botAI, spell, &missingReagentGroupName); + + if (!botAI->CastSpell(castName, GetTarget())) + return false; + + if (!missingReagentGroupName.empty()) + ai::buff::TryAnnounceMissingBuffReagents(botAI, spell, missingReagentGroupName); + + return true; +} + +CastEnchantItemMainHandAction::CastEnchantItemMainHandAction( + PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} + +bool CastEnchantItemMainHandAction::Execute(Event /*event*/) +{ + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + return item && botAI->CastSpell(spell, bot, item); +} + +bool CastEnchantItemMainHandAction::isPossible() +{ + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!item || item->GetTemplate()->SubClass == ITEM_SUBCLASS_WEAPON_MISC || + item->GetTemplate()->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE || + item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + { + return false; + } + + return botAI->CanCastSpell(spell, bot, item); +} + +CastEnchantItemOffHandAction::CastEnchantItemOffHandAction( + PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} + +bool CastEnchantItemOffHandAction::Execute(Event /*event*/) +{ + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + return item && botAI->CastSpell(spell, bot, item); +} + +bool CastEnchantItemOffHandAction::isPossible() +{ + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!item || item->GetTemplate()->SubClass == ITEM_SUBCLASS_WEAPON_MISC || + item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + { + return false; + } + + return botAI->CanCastSpell(spell, bot, item); } CastHealingSpellAction::CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount, HealingManaEfficiency manaEfficiency, bool isOwner) - : CastAuraSpellAction(botAI, spell, isOwner), estAmount(estAmount), manaEfficiency(manaEfficiency) + : CastAuraSpellAction(botAI, spell, isOwner), manaEfficiency(manaEfficiency), estAmount(estAmount) { range = botAI->GetRange("heal"); } @@ -213,7 +382,8 @@ bool CastHealingSpellAction::isUseful() { return CastAuraSpellAction::isUseful() bool CastAoeHealSpellAction::isUseful() { return CastSpellAction::isUseful(); } -CastCureSpellAction::CastCureSpellAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) +CastCureSpellAction::CastCureSpellAction( + PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) { range = botAI->GetRange("heal"); } @@ -223,25 +393,18 @@ Value* CurePartyMemberAction::GetTargetValue() return context->GetValue("party member to dispel", dispelType); } -// Make Bots Paladin, druid, mage use the greater buff rank spell -// TODO Priest doen't verify il he have components Value* BuffOnPartyAction::GetTargetValue() +{ + return context->GetValue("party member without aura", spell); +} + +Value* GroupBuffOnPartyAction::GetTargetValue() { return context->GetValue("party member without aura", MakeAuraQualifierForBuff(spell)); } -bool BuffOnPartyAction::Execute(Event /*event*/) -{ - std::string castName = spell; // default = mono - - auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot); - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP); - - return botAI->CastSpell(castName, GetTarget()); -} -// 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)) { @@ -251,17 +414,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); @@ -272,7 +458,8 @@ Value* CastDebuffSpellOnMeleeAttackerAction::GetTargetValue() return context->GetValue("melee attacker without aura", spell); } -CastBuffSpellAction::CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner, uint32 beforeDuration) +CastBuffSpellAction::CastBuffSpellAction( + PlayerbotAI* botAI, std::string const spell, bool checkIsOwner, uint32 beforeDuration) : CastAuraSpellAction(botAI, spell, checkIsOwner, false, beforeDuration) { range = botAI->GetRange("spell"); @@ -311,6 +498,18 @@ bool CastVehicleSpellAction::Execute(Event /*event*/) return botAI->CastVehicleSpell(spellId, GetTarget()); } +bool CastEveryManForHimselfAction::isPossible() +{ + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + return spellId && !HasSpellOrCategoryCooldown(bot, spellId); +} + +bool CastWillOfTheForsakenAction::isPossible() +{ + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + return spellId && !HasSpellOrCategoryCooldown(bot, spellId); +} + bool UseTrinketAction::Execute(Event /*event*/) { Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1); @@ -327,69 +526,141 @@ bool UseTrinketAction::Execute(Event /*event*/) bool UseTrinketAction::UseTrinket(Item* item) { - if (bot->CanUseItem(item) != EQUIP_ERR_OK) - return false; - - if (bot->IsNonMeleeSpellCast(true)) + if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true)) return false; uint8 bagIndex = item->GetBagSlot(); uint8 slot = item->GetSlot(); - // uint8 spell_index = 0; //not used, line marked for removal. uint8 cast_count = 1; ObjectGuid item_guid = item->GetGUID(); uint32 glyphIndex = 0; uint8 castFlags = 0; uint32 targetFlag = TARGET_FLAG_NONE; uint32 spellId = 0; + int32 itemSpellCooldown = 0; + uint32 itemSpellCategory = 0; + int32 itemSpellCategoryCooldown = 0; + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) { - if (item->GetTemplate()->Spells[i].SpellId > 0 && item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) + if (item->GetTemplate()->Spells[i].SpellId > 0 && + item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) { spellId = item->GetTemplate()->Spells[i].SpellId; - const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); + itemSpellCooldown = item->GetTemplate()->Spells[i].SpellCooldown; + itemSpellCategory = item->GetTemplate()->Spells[i].SpellCategory; + itemSpellCategoryCooldown = item->GetTemplate()->Spells[i].SpellCategoryCooldown; + uint64 const itemCooldownKey = (static_cast(item->GetEntry()) << 32) | spellId; + uint32 const now = getMSTime(); + if (itemSpellCooldown > 0) + { + auto const itemCooldownItr = trinketItemCooldownExpiries.find(itemCooldownKey); + if (itemCooldownItr != trinketItemCooldownExpiries.end()) + { + if (itemCooldownItr->second > now) + return false; + + trinketItemCooldownExpiries.erase(itemCooldownItr); + } + } + + if (itemSpellCategory && itemSpellCategoryCooldown > 0) + { + auto const categoryCooldownItr = trinketCategoryCooldownExpiries.find(itemSpellCategory); + if (categoryCooldownItr != trinketCategoryCooldownExpiries.end()) + { + if (categoryCooldownItr->second > now) + return false; + + trinketCategoryCooldownExpiries.erase(categoryCooldownItr); + } + } + + const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); if (!spellInfo || !spellInfo->IsPositive()) return false; bool applyAura = false; + bool restoresMana = false; + bool improvesManaEfficiency = false; + bool defensiveTankEffect = false; for (int i = 0; i < MAX_SPELL_EFFECTS; i++) { const SpellEffectInfo& effectInfo = spellInfo->Effects[i]; if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA) - { applyAura = true; - break; + + restoresMana = restoresMana || IsManaRestoreEffect(effectInfo); + improvesManaEfficiency = improvesManaEfficiency || IsManaEfficiencyEffect(effectInfo); + defensiveTankEffect = defensiveTankEffect || IsDefensiveTankEffect(effectInfo); + } + + if (!applyAura && !restoresMana) + return false; + + if (restoresMana || improvesManaEfficiency) + { + if (!AI_VALUE2(bool, "has mana", "self target")) + return false; + + uint8 const manaPct = AI_VALUE2(uint8, "mana", "self target"); + if ((restoresMana && manaPct >= sPlayerbotAIConfig.mediumMana) || + manaPct >= sPlayerbotAIConfig.highMana) + { + return false; } } - if (!applyAura) - return false; - - uint32 spellProcFlag = spellInfo->ProcFlags; - - // Handle items with procflag "if you kill a target that grants honor or experience" - // Bots will "learn" the trinket proc, so CanCastSpell() will be true - // e.g. on Item https://www.wowhead.com/wotlk/item=44074/oracle-talisman-of-ablution leading to - // constant casting of the proc spell onto themselfes https://www.wowhead.com/wotlk/spell=59787/oracle-ablutions - // This will lead to multiple hundreds of entries in m_appliedAuras -> Once killing an enemy -> Big diff time spikes - if (spellProcFlag != 0) return false; - - if (!botAI->CanCastSpell(spellId, bot, false)) + if (defensiveTankEffect) { - return false; + uint8 const healthPct = AI_VALUE2(uint8, "health", "self target"); + if (healthPct > sPlayerbotAIConfig.lowHealth) + return false; } + + auto const& mixedTriggerTrinketSpellIds = GetMixedTriggerTrinketSpellIds(); + // Exclude trinkets that expose the same spell as both ON_EQUIP and ON_USE across + // item templates. Those are equip/proc effects leaking into the active-use path, + // as seen with the error versions of Oracle Talisman of Ablution (44870) and + // Frenzyheart Insignia of Fury (44869). + if (mixedTriggerTrinketSpellIds.find(spellId) != mixedTriggerTrinketSpellIds.end()) + return false; + + if (!botAI->CanCastSpell(spellId, bot, false, nullptr, item)) + return false; + break; } } + if (!spellId) return false; + WorldPacket packet(CMSG_USE_ITEM); packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; targetFlag = TARGET_FLAG_NONE; packet << targetFlag << bot->GetPackGUID(); + bot->GetSession()->HandleUseItemOpcode(packet); + + uint32 const now = getMSTime(); + uint32 const cooldownDelay = bot->GetSpellCooldownDelay(spellId); + if (cooldownDelay > 0) + { + if (itemSpellCooldown > 0) + { + uint64 const itemCooldownKey = (static_cast(item->GetEntry()) << 32) | spellId; + trinketItemCooldownExpiries[itemCooldownKey] = now + static_cast(itemSpellCooldown); + } + + if (itemSpellCategory && itemSpellCategoryCooldown > 0) + { + trinketCategoryCooldownExpiries[itemSpellCategory] = now + static_cast(itemSpellCategoryCooldown); + } + } + return true; } @@ -399,9 +670,8 @@ bool CastDebuffSpellAction::isUseful() { Unit* target = GetTarget(); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } + return CastAuraSpellAction::isUseful() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.h index 9aa83f62d..1c810a1b4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GenericSpellActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICSPELLACTIONS_H -#define _PLAYERBOT_GENERICSPELLACTIONS_H +#ifndef PLAYERBOTS_GENERICSPELLACTIONS_H +#define PLAYERBOTS_GENERICSPELLACTIONS_H #include "Action.h" #include "PlayerbotAI.h" @@ -69,9 +70,7 @@ class CastDebuffSpellAction : public CastAuraSpellAction { public: CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f) - : CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) - { - } + : CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) {} bool isUseful() override; private: @@ -90,9 +89,7 @@ class CastDebuffSpellOnAttackerAction : public CastDebuffSpellAction public: CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true, float needLifeTime = 8.0f) - : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) - { - } + : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -104,9 +101,7 @@ class CastDebuffSpellOnMeleeAttackerAction : public CastDebuffSpellAction public: CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true, float needLifeTime = 8.0f) - : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) - { - } + : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -119,15 +114,37 @@ public: CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0); std::string const GetTargetName() override { return "self target"; } + bool isUseful() override; + bool Execute(Event event) override; }; -class CastEnchantItemAction : public CastSpellAction +class GroupBuffSpellAction : public CastBuffSpellAction { public: - CastEnchantItemAction(PlayerbotAI* botAI, std::string const spell); + GroupBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, + uint32 beforeDuration = 0) + : CastBuffSpellAction(botAI, spell, checkIsOwner, beforeDuration) {} - bool isPossible() override; + bool isUseful() override; + bool Execute(Event event) override; +}; + +class CastEnchantItemMainHandAction : public CastSpellAction +{ +public: + CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell); std::string const GetTargetName() override { return "self target"; } + bool Execute(Event event) override; + bool isPossible() override; +}; + +class CastEnchantItemOffHandAction : public CastSpellAction +{ +public: + CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell); + std::string const GetTargetName() override { return "self target"; } + bool Execute(Event event) override; + bool isPossible() override; }; class CastHealingSpellAction : public CastAuraSpellAction @@ -142,8 +159,6 @@ public: // Yunfan: Mana efficiency tell the bot how to save mana. The higher the better. HealingManaEfficiency manaEfficiency; uint8 estAmount; - - // protected: }; class CastAoeHealSpellAction : public CastHealingSpellAction @@ -183,9 +198,7 @@ class HealPartyMemberAction : public CastHealingSpellAction, public PartyMemberA public: HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f, HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true) - : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) - { - } + : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) {} std::string const GetTargetName() override { return "party member to heal"; } std::string const getName() override { return PartyMemberActionNameSupport::getName(); } @@ -210,9 +223,7 @@ class CurePartyMemberAction : public CastSpellAction, public PartyMemberActionNa { public: CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType) - : CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) - { - } + : CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) {} Value* GetTargetValue() override; std::string const getName() override { return PartyMemberActionNameSupport::getName(); } @@ -221,25 +232,37 @@ protected: uint32 dispelType; }; -// Make Bots Paladin, druid, mage use the greater buff rank spell class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport { public: BuffOnPartyAction(PlayerbotAI* botAI, std::string const spell) - : CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) { } + : CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {} + + Value* GetTargetValue() override; + std::string const getName() override { return PartyMemberActionNameSupport::getName(); } +}; + +class GroupBuffOnPartyAction : public GroupBuffSpellAction, public PartyMemberActionNameSupport +{ +public: + GroupBuffOnPartyAction(PlayerbotAI* botAI, std::string const spell) + : GroupBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {} Value* GetTargetValue() override; - bool Execute(Event event) override; std::string const getName() override { return PartyMemberActionNameSupport::getName(); } }; -// End Fix 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 @@ -284,13 +307,52 @@ public: CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {} }; +class CastEveryManForHimselfAction : public CastSpellAction +{ +public: + CastEveryManForHimselfAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "every man for himself") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; +}; + +class CastWillOfTheForsakenAction : public CastSpellAction +{ +public: + CastWillOfTheForsakenAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "will of the forsaken") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; +}; + +class CastStoneformAction : public CastSpellAction +{ +public: + CastStoneformAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "stoneform") {} + + std::string const GetTargetName() override { return "self target"; } +}; + +class CastEscapeArtistAction : public CastSpellAction +{ +public: + CastEscapeArtistAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "escape artist") {} + + std::string const GetTargetName() override { return "self target"; } +}; + class UseTrinketAction : public Action { public: UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {} bool Execute(Event event) override; + protected: bool UseTrinket(Item* trinket); + +private: + std::unordered_map trinketItemCooldownExpiries; + std::unordered_map trinketCategoryCooldownExpiries; }; class CastSpellOnEnemyHealerAction : public CastSpellAction @@ -427,12 +489,11 @@ class BuffOnMainTankAction : public CastBuffSpellAction, public MainTankActionNa { public: BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false) - : CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) - { - } + : CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) {} public: virtual Value* GetTargetValue(); virtual std::string const getName() { return MainTankActionNameSupport::getName(); } }; + #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp index 363aca7a8..bd0512513 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GiveItemAction.h" @@ -28,7 +29,6 @@ bool GiveItemAction::Execute(Event /*event*/) if (receiverAi->GetAiObjectContext()->GetValue("item count", item)->Get()) return true; - bool moved = false; std::vector items = InventoryAction::parseItems(item, ITERATE_ITEMS_IN_BAGS); for (Item* item : items) { @@ -42,7 +42,6 @@ bool GiveItemAction::Execute(Event /*event*/) bot->MoveItemFromInventory(item->GetBagSlot(), item->GetSlot(), true); item->SetOwnerGUID(target->GetGUID()); receiver->MoveItemToInventory(dest, item, true); - moved = true; std::ostringstream out; out << "Получено: " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " от " << bot->GetName(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.h index bd3f3b087..3f797e770 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GIVEITEMACTION_H -#define _PLAYERBOT_GIVEITEMACTION_H +#ifndef PLAYERBOTS_GIVEITEMACTION_H +#define PLAYERBOTS_GIVEITEMACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp index df2043b9f..c57e200d3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GoAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.h index f94181567..91030416c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GOACTION_H -#define _PLAYERBOT_GOACTION_H +#ifndef PLAYERBOTS_GOACTION_H +#define PLAYERBOTS_GOACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp index 8e19dd01b..1f9ce2d80 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GossipHelloAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.h index 71ef373fc..3f01ab4b7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GOSSIPHELLOACTION_H -#define _PLAYERBOT_GOSSIPHELLOACTION_H +#ifndef PLAYERBOTS_GOSSIPHELLOACTION_H +#define PLAYERBOTS_GOSSIPHELLOACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.cpp index bf6d1e455..28eebcc9b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GreetAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.h index 8b1e910ea..bd00e2f21 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GreetAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GREETACTION_H -#define _PLAYERBOT_GREETACTION_H +#ifndef PLAYERBOTS_GREETACTION_H +#define PLAYERBOTS_GREETACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp index 18cc0139a..c6c7e95a1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildAcceptAction.h" @@ -8,6 +9,7 @@ #include "Event.h" #include "GuildPackets.h" #include "PlayerbotSecurity.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool GuildAcceptAction::Execute(Event event) @@ -28,17 +30,20 @@ bool GuildAcceptAction::Execute(Event event) uint32 guildId = inviter->GetGuildId(); if (!guildId) { - botAI->TellError("Вы не состоите в гильдии!"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "guild_accept_inviter_not_in_guild", "You are not in a guild!", {})); accept = false; } else if (bot->GetGuildId()) { - botAI->TellError("Я уже состою в гильдии"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "guild_accept_already_in_guild", "Sorry, I am in a guild already", {})); accept = false; } else if (!botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, inviter, true)) { - botAI->TellError("Я не хочу вступать в вашу гильдию :("); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "guild_accept_declined", "Sorry, I don't want to join your guild :(", {})); accept = false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.h index 8d60d07e6..c72952289 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDACCEPTACTION_H -#define _PLAYERBOT_GUILDACCEPTACTION_H +#ifndef PLAYERBOTS_GUILDACCEPTACTION_H +#define PLAYERBOTS_GUILDACCEPTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp index eeea973ec..edd704117 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildBankAction.h" @@ -53,7 +54,7 @@ bool GuildBankAction::Execute(std::string const text, GameObject* bank) return result; } -bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* bank) +bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* /*bank*/) { uint32 playerSlot = item->GetSlot(); uint32 playerBag = item->GetBagSlot(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.h index 5454a3105..b3bb89c35 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDBANKACTION_H -#define _PLAYERBOT_GUILDBANKACTION_H +#ifndef PLAYERBOTS_GUILDBANKACTION_H +#define PLAYERBOTS_GUILDBANKACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.cpp index 0e7098f1e..984d1db5b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildCreateActions.h" @@ -17,7 +18,6 @@ bool BuyPetitionAction::Execute(Event /*event*/) { GuidVector vendors = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - bool vendored = false, result = false; for (GuidVector::iterator i = vendors.begin(); i != vendors.end(); ++i) { ObjectGuid vendorguid = *i; @@ -97,7 +97,6 @@ bool BuyPetitionAction::canBuyPetition(Player* bot) bool PetitionOfferAction::Execute(Event event) { - uint32 petitionEntry = 5863; // GUILD_CHARTER std::vector petitions = AI_VALUE2(std::vector, "inventory items", chat->FormatQItem(5863)); if (petitions.empty()) @@ -212,7 +211,6 @@ bool PetitionOfferNearbyAction::isUseful() bool PetitionTurnInAction::Execute(Event /*event*/) { GuidVector vendors = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - bool vendored = false, result = false; std::vector petitions = AI_VALUE2(std::vector, "inventory items", chat->FormatQItem(5863)); if (petitions.empty()) @@ -299,7 +297,7 @@ bool PetitionTurnInAction::isUseful() bool BuyTabardAction::Execute(Event /*event*/) { - bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:")); + bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"), true); if (canBuy && AI_VALUE2(uint32, "item count", chat->FormatQItem(5976))) return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.h index 07de5824f..05d3a177d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildCreateActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDCREATEACTION_H -#define _PLAYERBOT_GUILDCREATEACTION_H +#ifndef PLAYERBOTS_GUILDCREATEACTIONS_H +#define PLAYERBOTS_GUILDCREATEACTIONS_H #include "ChooseTravelTargetAction.h" #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp index 2f021e5b7..fe0fa7e66 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildManagementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.h index b1d363e89..e9f62e2d2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDMAMANEGEMENTACTION_H -#define _PLAYERBOT_GUILDMAMANEGEMENTACTION_H +#ifndef PLAYERBOTS_GUILDMANAGEMENTACTIONS_H +#define PLAYERBOTS_GUILDMANAGEMENTACTIONS_H #include "Action.h" #include "Opcodes.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp index 0fbfa3411..33e8176df 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HelpAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.h index e1d374b0c..895685cbf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HELPACTION_H -#define _PLAYERBOT_HELPACTION_H +#ifndef PLAYERBOTS_HELPACTION_H +#define PLAYERBOTS_HELPACTION_H #include "Action.h" #include "NamedObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp index 2cd47d8e5..e22ba85e1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HireAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.h index 6738f02c1..c71bc2018 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HIREACTION_H -#define _PLAYERBOT_HIREACTION_H +#ifndef PLAYERBOTS_HIREACTION_H +#define PLAYERBOTS_HIREACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.cpp index a6d56d2e5..0f5718b38 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ImbueAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.h index 398a8fa18..ffff3e2ff 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ImbueAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_IMBUEACTION_H -#define _PLAYERBOT_IMBUEACTION_H +#ifndef PLAYERBOTS_IMBUEACTION_H +#define PLAYERBOTS_IMBUEACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp index 247dc5117..bc8101fb1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "InventoryAction.h" @@ -10,6 +11,31 @@ #include "ItemVisitors.h" #include "Playerbots.h" +namespace +{ +bool isReservedQualifier(std::string const& text) +{ + static std::array const exactQualifiers = { + "ammo", + "conjured drink", + "conjured food", + "conjured water", + "drink", + "food", + "healing potion", + "mount", + "mana potion", + "pet", + "quest", + "recipe", + "water" + }; + + return std::find(exactQualifiers.begin(), exactQualifiers.end(), text) != exactQualifiers.end() || + text.rfind("usage ", 0) == 0; +} +} + void InventoryAction::IterateItems(IterateItemsVisitor* visitor, IterateItemsMask mask) { if (mask & ITERATE_ITEMS_IN_BAGS) @@ -292,9 +318,12 @@ std::vector InventoryAction::parseItems(std::string const text, IterateIt found.insert(visitor.GetResult().begin(), visitor.GetResult().end()); } - FindNamedItemVisitor visitor(bot, text); - IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); - found.insert(visitor.GetResult().begin(), visitor.GetResult().end()); + if (!isReservedQualifier(text)) + { + FindNamedItemVisitor visitor(bot, text); + IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); + found.insert(visitor.GetResult().begin(), visitor.GetResult().end()); + } uint32 quality = chat->parseItemQuality(text); if (quality != MAX_ITEM_QUALITY) @@ -372,7 +401,7 @@ ItemIds InventoryAction::FindOutfitItems(std::string const name) std::string const InventoryAction::parseOutfitName(std::string const outfit) { uint32 pos = outfit.find("="); - if (pos == -1) + if (pos == uint32(-1)) return ""; return outfit.substr(0, pos); @@ -386,7 +415,7 @@ ItemIds InventoryAction::parseOutfitItems(std::string const text) while (pos < text.size()) { uint32 endPos = text.find(',', pos); - if (endPos == -1) + if (endPos == uint32(-1)) endPos = text.size(); std::string const idC = text.substr(pos, endPos - pos); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.h index 7ab0f59de..5f6b60d44 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_INVENTORYACTION_H -#define _PLAYERBOT_INVENTORYACTION_H +#ifndef PLAYERBOTS_INVENTORYACTION_H +#define PLAYERBOTS_INVENTORYACTION_H #include "Action.h" #include "ItemVisitors.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.cpp index ef7cd9239..9e502e83a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "InventoryChangeFailureAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.h index 6877d0986..469e44340 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryChangeFailureAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_INVENTORYCHANGEFAILUREACTION_H -#define _PLAYERBOT_INVENTORYCHANGEFAILUREACTION_H +#ifndef PLAYERBOTS_INVENTORYCHANGEFAILUREACTION_H +#define PLAYERBOTS_INVENTORYCHANGEFAILUREACTION_H #include "Action.h" #include "Item.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp index 027243752..e36544791 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp @@ -1,6 +1,16 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * Sebastiaan Keek (mostlikely4r) - original author + * David Parra Ausina (Flekz) + * celguar */ #include "InviteToGroupAction.h" @@ -167,8 +177,6 @@ std::vector InviteGuildToGroupAction::getGuildMembers() bool InviteGuildToGroupAction::Execute(Event /*event*/) { - Guild* guild = sGuildMgr->GetGuildById(bot->GetGuildId()); - for (auto& member : getGuildMembers()) { Player* player = member; @@ -310,9 +318,8 @@ bool LfgAction::Execute(Event event) allowedRoles[BOT_ROLE_HEALER] = 1; allowedRoles[BOT_ROLE_DPS] = 3; - BotRoles role = botAI->IsTank(requester, false) - ? BOT_ROLE_TANK - : (botAI->IsHeal(requester, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS); + BotRoles role = botAI->IsTank(requester, true) ? BOT_ROLE_TANK + : (botAI->IsHeal(requester, true) ? BOT_ROLE_HEALER : BOT_ROLE_DPS); Classes cls = (Classes)requester->getClass(); if (group) @@ -385,8 +392,8 @@ bool LfgAction::Execute(Event event) if (!botAI->IsSafe(player)) return false; - role = botAI->IsTank(player, false) ? BOT_ROLE_TANK - : (botAI->IsHeal(player, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS); + role = botAI->IsTank(player, true) ? BOT_ROLE_TANK + : (botAI->IsHeal(player, true) ? BOT_ROLE_HEALER : BOT_ROLE_DPS); cls = (Classes)player->getClass(); if (allowedRoles[role] > 0) @@ -405,7 +412,7 @@ bool LfgAction::Execute(Event event) allowedClassNr[cls][role]--; } - role = botAI->IsTank(bot, false) ? BOT_ROLE_TANK : (botAI->IsHeal(bot, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS); + role = botAI->IsTank(bot, true) ? BOT_ROLE_TANK : (botAI->IsHeal(bot, true) ? BOT_ROLE_HEALER : BOT_ROLE_DPS); cls = (Classes)bot->getClass(); if (allowedRoles[role] == 0) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.h index 4b9f2c7f6..7f7a746b7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.h @@ -1,10 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_INVITETOGROUPACTION_H -#define _PLAYERBOT_INVITETOGROUPACTION_H +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * Sebastiaan Keek (mostlikely4r) - original author + * David Parra Ausina (Flekz) + * celguar + */ + +#ifndef PLAYERBOTS_INVITETOGROUPACTION_H +#define PLAYERBOTS_INVITETOGROUPACTION_H #include "Action.h" #include "Player.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp index 38193b310..1ef5a85bf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LeaveGroupAction.h" #include "Event.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool LeaveGroupAction::Execute(Event event) @@ -86,7 +88,9 @@ bool LeaveGroupAction::Leave() Player* master = botAI -> GetMaster(); if (master) - botAI->TellMaster("До свидания!", PLAYERBOT_SECURITY_TALK); + botAI->TellMaster( + PlayerbotTextMgr::instance().GetBotTextOrDefault("goodbye", "Goodbye!", {}), + PLAYERBOT_SECURITY_TALK); botAI->LeaveOrDisbandGroup(); return true; @@ -123,7 +127,7 @@ bool LeaveFarAwayAction::isUseful() if (trueMaster && !GET_PLAYERBOT_AI(trueMaster)) return false; - if (botAI->IsAlt() && + if (botAI->IsAltBot() && (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player groupLeader. return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.h index 83baea3c1..912b59dcc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LEAVEGROUPACTION_H -#define _PLAYERBOT_LEAVEGROUPACTION_H +#ifndef PLAYERBOTS_LEAVEGROUPACTION_H +#define PLAYERBOTS_LEAVEGROUPACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.cpp index 93e6ff4e3..fd2fe10d1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LfgActions.h" @@ -16,7 +17,7 @@ using namespace lfg; -bool LfgJoinAction::Execute(Event event) { return JoinLFG(); } +bool LfgJoinAction::Execute(Event /*event*/) { return JoinLFG(); } uint32 LfgJoinAction::GetRoles() { @@ -115,7 +116,7 @@ bool LfgJoinAction::JoinLFG() auto const& botLevel = bot->GetLevel(); /*LFG_TYPE_RANDOM on classic is 15-58 so bot over level 25 will never queue*/ - if (dungeon->MinLevel && (botLevel < dungeon->MinLevel || botLevel > dungeon->MaxLevel) || + if ((dungeon->MinLevel && (botLevel < dungeon->MinLevel || botLevel > dungeon->MaxLevel)) || (botLevel > dungeon->MinLevel + 10 && dungeon->TypeID == LFG_TYPE_DUNGEON)) continue; @@ -174,7 +175,6 @@ bool LfgRoleCheckAction::Execute(Event /*event*/) { if (Group* group = bot->GetGroup()) { - uint32 currentRoles = sLFGMgr->GetRoles(bot->GetGUID()); uint32 newRoles = GetRoles(); // if (currentRoles == newRoles) // return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.h index 8622a42d7..055d19e60 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LfgActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LFGACTIONS_H -#define _PLAYERBOT_LFGACTIONS_H +#ifndef PLAYERBOTS_LFGACTIONS_H +#define PLAYERBOTS_LFGACTIONS_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp index 341603c19..6a1428670 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ListQuestsActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.h index 2cf2d592a..15ccc8fdd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LISTQUESTSACTIONS_H -#define _PLAYERBOT_LISTQUESTSACTIONS_H +#ifndef PLAYERBOTS_LISTQUESTSACTIONS_H +#define PLAYERBOTS_LISTQUESTSACTIONS_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp index afa9ed0cb..36939a050 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ListSpellsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.h index 9e674bf33..bdb1874cb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LISTSPELLSACTION_H -#define _PLAYERBOT_LISTSPELLSACTION_H +#ifndef PLAYERBOTS_LISTSPELLSACTION_H +#define PLAYERBOTS_LISTSPELLSACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp index bd529379b..c80d037ce 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LogLevelAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.h index 3aefaf970..35f61c0d5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOGLEVELACTION_H -#define _PLAYERBOT_LOGLEVELACTION_H +#ifndef PLAYERBOTS_LOGLEVELACTION_H +#define PLAYERBOTS_LOGLEVELACTION_H #include "Action.h" #include "LogCommon.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.cpp index 93368f524..d7cf6a04b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.cpp @@ -50,7 +50,8 @@ bool LootAction::Execute(Event /*event*/) bool LootAction::isUseful() { - return sPlayerbotAIConfig.freeMethodLoot || !bot->GetGroup() || bot->GetGroup()->GetLootMethod() != FREE_FOR_ALL; + return sPlayerbotAIConfig.freeMethodLoot || !bot->GetGroup() || + bot->GetGroup()->GetLootMethod() != FREE_FOR_ALL || botAI->IsRealPlayer(); } enum ProfessionSpells diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.h index b657ee8cb..411335d6c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTACTION_H -#define _PLAYERBOT_LOOTACTION_H +#ifndef PLAYERBOTS_LOOTACTION_H +#define PLAYERBOTS_LOOTACTION_H #include "InventoryAction.h" #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.cpp index 7bbdc95c1..b17558aa3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootRollAction.h" @@ -22,12 +23,11 @@ bool LootRollAction::Execute(Event /*event*/) std::vector rolls = group->GetRolls(); for (Roll*& roll : rolls) { - if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET) - { + auto voteItr = roll->playerVote.find(bot->GetGUID()); + if (voteItr == roll->playerVote.end() || voteItr->second != NOT_EMITED_YET) continue; - } + ObjectGuid guid = roll->itemGUID; - uint32 slot = roll->itemSlot; uint32 itemId = roll->itemid; int32 randomProperty = 0; if (roll->itemRandomPropId) @@ -42,27 +42,22 @@ bool LootRollAction::Execute(Event /*event*/) std::string itemUsageParam; if (randomProperty != 0) - { itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } else - { itemUsageParam = std::to_string(itemId); - } + ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); // Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality. if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC) { if (CanBotUseToken(proto, bot)) - { vote = NEED; // Eligible for "Need" - } else - { vote = GREED; // Not eligible, so "Greed" - } } + else if (usage == ITEM_USAGE_DISENCHANT) + vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED; else { switch (proto->Class) @@ -70,40 +65,34 @@ bool LootRollAction::Execute(Event /*event*/) case ITEM_CLASS_WEAPON: case ITEM_CLASS_ARMOR: if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { vote = NEED; - } else if (usage != ITEM_USAGE_NONE) - { vote = GREED; - } + break; + case ITEM_CLASS_RECIPE: + if (!sPlayerbotAIConfig.lootRollRecipe) + vote = PASS; + else if (usage == ITEM_USAGE_SKILL) + vote = NEED; // Bot can learn this recipe + else if (proto->Bonding != BIND_WHEN_PICKED_UP) + vote = GREED; // BoE recipe bot can't learn - GREED for AH/trade break; default: if (StoreLootAction::IsLootAllowed(itemId, botAI)) - vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior + vote = CalculateRollVote(proto, usage); break; } } - if (sPlayerbotAIConfig.lootRollLevel == 0) + if (vote == NEED) { + if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot)) + vote = PASS; + else if (sPlayerbotAIConfig.lootNeedRollLevel == 1) + vote = GREED; + } + else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel) vote = PASS; - } - else if (sPlayerbotAIConfig.lootRollLevel == 1) - { - // Level 1 = "greed" mode: bots greed on useful items but never need - // Only downgrade NEED to GREED, preserve GREED votes as-is - if (vote == NEED) - { - if (RollUniqueCheck(proto, bot)) - { - vote = PASS; - } - else - { - vote = GREED; - } - } - } + switch (group->GetLootMethod()) { case MASTER_LOOT: @@ -121,11 +110,14 @@ bool LootRollAction::Execute(Event /*event*/) return false; } -RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) +RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage) { - std::ostringstream out; - out << proto->ItemId; - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str()); + if (usage == ITEM_USAGE_NONE) + { + std::ostringstream out; + out << proto->ItemId; + usage = AI_VALUE2(ItemUsage, "item usage", out.str()); + } RollVote needVote = PASS; switch (usage) @@ -138,11 +130,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) break; case ITEM_USAGE_SKILL: case ITEM_USAGE_USE: - case ITEM_USAGE_DISENCHANT: case ITEM_USAGE_AH: case ITEM_USAGE_VENDOR: needVote = GREED; break; + case ITEM_USAGE_DISENCHANT: + needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED; + break; default: break; } @@ -184,7 +178,6 @@ bool MasterLootRollAction::Execute(Event event) if (!group) return false; - RollVote vote = CalculateRollVote(proto); group->CountRollVote(bot->GetGUID(), creatureGuid, CalculateRollVote(proto)); return true; @@ -197,9 +190,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot) // Check if the bot's class is allowed to use the token if (proto->AllowableClass & botClassMask) - { return true; // Bot's class is eligible to use this token - } return false; // Bot's class cannot use this token } @@ -215,13 +206,9 @@ bool RollUniqueCheck(ItemTemplate const* proto, Player* bot) // Determine if the unique item is already equipped bool isEquipped = (totalItemCount > bagItemCount); if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE)) - { return true; // Unique Item is already equipped - } else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1)) - { return true; // Unique item already in bag, don't roll for it - } return false; // Item is not equipped or in bags, roll for it } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.h index 13d895860..586d8c29d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootRollAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTROLLACTION_H -#define _PLAYERBOT_LOOTROLLACTION_H +#ifndef PLAYERBOTS_LOOTROLLACTION_H +#define PLAYERBOTS_LOOTROLLACTION_H #include "QueryItemUsageAction.h" @@ -22,7 +23,7 @@ public: bool Execute(Event event) override; protected: - RollVote CalculateRollVote(ItemTemplate const* proto); + RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE); }; bool CanBotUseToken(ItemTemplate const* proto, Player* bot); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp index 6239f15a2..83073cee8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootStrategyAction.h" @@ -16,7 +17,6 @@ bool LootStrategyAction::Execute(Event event) { std::string const strategy = event.getParam(); - LootObjectStack* lootItems = AI_VALUE(LootObjectStack*, "available loot"); std::set& alwaysLootItems = AI_VALUE(std::set&, "always loot list"); Value* lootStrategy = context->GetValue("loot strategy"); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.h index a972066ad..f62640722 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTSTRATEGYACTION_H -#define _PLAYERBOT_LOOTSTRATEGYACTION_H +#ifndef PLAYERBOTS_LOOTSTRATEGYACTION_H +#define PLAYERBOTS_LOOTSTRATEGYACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp index 09cec55af..ac67a0725 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MailAction.h" @@ -78,7 +79,7 @@ private: class TakeMailProcessor : public MailProcessor { public: - bool Process(uint32 index, Mail* mail, PlayerbotAI* botAI) override + bool Process(uint32 /*index*/, Mail* mail, PlayerbotAI* botAI) override { Player* bot = botAI->GetBot(); if (!CheckBagSpace(bot)) @@ -104,7 +105,7 @@ public: { std::vector guids; for (MailItemInfoVec::iterator i = mail->items.begin(); i != mail->items.end(); ++i) - if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(i->item_template)) + if (sObjectMgr->GetItemTemplate(i->item_template)) guids.push_back(i->item_guid); for (std::vector::iterator i = guids.begin(); i != guids.end(); ++i) @@ -134,7 +135,7 @@ public: private: bool CheckBagSpace(Player* bot) { - uint32 totalused = 0, total = 16; + uint32 totalused = 0; for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; slot++) if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) ++totalused; @@ -157,7 +158,7 @@ private: class DeleteMailProcessor : public MailProcessor { public: - bool Process(uint32 index, Mail* mail, PlayerbotAI* botAI) override + bool Process(uint32 /*index*/, Mail* mail, PlayerbotAI* botAI) override { std::ostringstream out; out << "|cffffffff" << mail->subject << "|cffff0000 deleted"; @@ -172,7 +173,7 @@ public: class ReadMailProcessor : public MailProcessor { public: - bool Process(uint32 index, Mail* mail, PlayerbotAI* botAI) override + bool Process(uint32 /*index*/, Mail* mail, PlayerbotAI* botAI) override { std::ostringstream out, body; out << "|cffffffff" << mail->subject; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.h index e66d4147f..c1f2ba047 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MAILACTION_H -#define _PLAYERBOT_MAILACTION_H +#ifndef PLAYERBOTS_MAILACTION_H +#define PLAYERBOTS_MAILACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp index 11175d059..d5b60f125 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MoveToRpgTargetAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.h index 6dd6558bb..b924b301f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MOVETORPGTARGETACTION_H -#define _PLAYERBOT_MOVETORPGTARGETACTION_H +#ifndef PLAYERBOTS_MOVETORPGTARGETACTION_H +#define PLAYERBOTS_MOVETORPGTARGETACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp index 6bca4931d..1896b728a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MoveToTravelTargetAction.h" @@ -74,8 +75,18 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) float maxDistance = target->getDestination()->getRadiusMin(); - // Evenly distribute around the target. - float angle = 2 * M_PI * urand(0, 100) / 100.0; + // Spread bots around the target but keep the offset stable per + // (bot, destination) pair. Previously the angle and radius were + // re-rolled every time the action re-entered (i.e. every tick the + // bot wasn't already moving), which made bots oscillate between + // two random points around the same quest POI instead of + // committing to one approach. + uint32 botLow = bot->GetGUID().GetCounter(); + int32 destSeed = static_cast(location.GetPositionX()) * 73856093 ^ + static_cast(location.GetPositionY()) * 19349663; + uint32 seed = botLow ^ static_cast(destSeed); + float angle = 2.0f * static_cast(M_PI) * static_cast(seed % 1000) / 1000.0f; + float mod = 0.5f + static_cast((seed / 1000) % 1000) / 2000.0f; // [0.5, 1.0] if (target->getMaxTravelTime() > target->getTimeLeft()) // The bot is late. Speed it up. { @@ -89,9 +100,6 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) float z = location.GetPositionZ(); float mapId = location.GetMapId(); - // Move between 0.5 and 1.0 times the maxDistance. - float mod = frand(50.f, 100.f) / 100.0f; - x += cos(angle) * maxDistance * mod; y += sin(angle) * maxDistance * mod; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.h index 3d8609cbb..30489150c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MOVETOTRAVELTARGETACTION_H -#define _PLAYERBOT_MOVETOTRAVELTARGETACTION_H +#ifndef PLAYERBOTS_MOVETOTRAVELTARGETACTION_H +#define PLAYERBOTS_MOVETOTRAVELTARGETACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp index 73707228c..881d60b11 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MovementActions.h" @@ -63,10 +64,10 @@ void MovementAction::CreateWp(Player* wpOwner, float x, float y, float z, float bool MovementAction::JumpTo(uint32 mapId, float x, float y, float z, MovementPriority priority) { UpdateMovementState(); - if (!IsMovingAllowed(mapId, x, y, z)) + if (!IsMovingAllowed()) return false; - if (IsDuplicateMove(mapId, x, y, z)) + if (IsDuplicateMove(x, y, z)) return false; if (IsWaitingForLastMove(priority)) @@ -93,9 +94,6 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior distance += target->GetCombatReach(); - float x = target->GetPositionX(); - float y = target->GetPositionY(); - float z = target->GetPositionZ(); float followAngle = GetFollowAngle(); for (float angle = followAngle; angle <= followAngle + static_cast(2 * M_PI); @@ -104,6 +102,11 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior float x = target->GetPositionX() + cos(angle) * distance; float y = target->GetPositionY() + sin(angle) * distance; float z = target->GetPositionZ(); + // Clamp Z to the terrain under the offset point so we don't + // hand PointMovementGenerator a Z that matches the target's + // floor but not the sampled (x,y) — avoids straight-line + // fallbacks through geometry. + bot->UpdateAllowedPositionZ(x, y, z); if (!bot->IsWithinLOS(x, y, z)) continue; @@ -113,7 +116,6 @@ bool MovementAction::MoveNear(WorldObject* target, float distance, MovementPrior return true; } - // botAI->TellError("All paths not in LOS"); return false; } @@ -122,9 +124,6 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged) if (!target) return false; - // std::ostringstream out; out << "Moving to LOS!"; - // bot->Say(out.str(), LANG_UNIVERSAL); - float x = target->GetPositionX(); float y = target->GetPositionY(); float z = target->GetPositionZ(); @@ -169,15 +168,15 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged) return false; } -bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, bool react, bool normal_only, +bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool /*idle*/, bool /*react*/, bool normal_only, bool exact_waypoint, MovementPriority priority, bool lessDelay, bool backwards) { UpdateMovementState(); - if (!IsMovingAllowed(mapId, x, y, z)) + if (!IsMovingAllowed()) { return false; } - if (IsDuplicateMove(mapId, x, y, z)) + if (IsDuplicateMove(x, y, z)) { return false; } @@ -257,8 +256,7 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, // bot->CastStop(); // botAI->InterruptSpell(); // } - G3D::Vector3 endP = path.back(); - DoMovePoint(bot, x, y, z, generatePath, backwards); + DoMovePoint(bot, x, y, modifiedZ, generatePath, backwards); float delay = 1000.0f * MoveDelay(distance, backwards); if (lessDelay) { @@ -266,7 +264,8 @@ bool MovementAction::MoveTo(uint32 mapId, float x, float y, float z, bool idle, } delay = std::max(.0f, delay); delay = std::min((float)sPlayerbotAIConfig.maxWaitForMove, delay); - AI_VALUE(LastMovement&, "last movement").Set(mapId, x, y, z, bot->GetOrientation(), delay, priority); + AI_VALUE(LastMovement&, "last movement") + .Set(mapId, x, y, modifiedZ, bot->GetOrientation(), delay, priority); return true; } } @@ -772,8 +771,6 @@ bool MovementAction::MoveTo(WorldObject* target, float distance, MovementPriorit float by = bot->GetPositionY(); float bz = bot->GetPositionZ(); - float tx = target->GetPositionX(); - float ty = target->GetPositionY(); float tz = target->GetPositionZ(); float distanceToTarget = bot->GetDistance(target); @@ -788,15 +785,17 @@ bool MovementAction::MoveTo(WorldObject* target, float distance, MovementPriorit float dx = cos(angle) * needToGo + bx; float dy = sin(angle) * needToGo + by; - float dz; // = std::max(bz, tz); // calc accurate z position to avoid stuck + // Start from a seed Z between bot and target, then clamp to the + // terrain under (dx,dy). Linear interpolation alone ignores hills + // between the two units and fed PointMovementGenerator a Z that + // could be well above/below ground, triggering straight-line + // fallbacks through walls. + float dz; if (distanceToTarget > CONTACT_DISTANCE) - { dz = bz + (tz - bz) * (needToGo / distanceToTarget); - } else - { dz = tz; - } + bot->UpdateAllowedPositionZ(dx, dy, dz); return MoveTo(target->GetMapId(), dx, dy, dz, false, false, false, false, priority); } @@ -805,10 +804,6 @@ bool MovementAction::ReachCombatTo(Unit* target, float distance) if (!IsMovingAllowed(target)) return false; - float bx = bot->GetPositionX(); - float by = bot->GetPositionY(); - float bz = bot->GetPositionZ(); - float tx = target->GetPositionX(); float ty = target->GetPositionY(); float tz = target->GetPositionZ(); @@ -868,6 +863,11 @@ float MovementAction::GetFollowAngle() if (!group) return 0.0f; + // Prevent bots with orphaned raid groups from dividing by 0, which freezes the server. + uint32 memberCount = group->GetMembersCount(); + if (memberCount <= 1) + return 0.0f; + uint32 index = 1; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -875,7 +875,7 @@ float MovementAction::GetFollowAngle() continue; if (ref->GetSource() == bot) - return 2 * M_PI / (group->GetMembersCount() - 1) * index; + return 2 * M_PI / (memberCount - 1) * index; ++index; } @@ -898,20 +898,7 @@ bool MovementAction::IsMovingAllowed(WorldObject* target) return IsMovingAllowed(); } -bool MovementAction::IsMovingAllowed(uint32 mapId, float x, float y, float z) -{ - // removed sqrt as means distance limit was effectively 22500 (ReactDistance�) - // leaving it commented incase we find ReactDistance limit causes problems - // float distance = sqrt(bot->GetDistance(x, y, z)); - - // Remove react distance limit - // if (!bot->InBattleground()) - // return false; - - return IsMovingAllowed(); -} - -bool MovementAction::IsDuplicateMove(uint32 mapId, float x, float y, float z) +bool MovementAction::IsDuplicateMove(float x, float y, float z) { LastMovement& lastMove = *context->GetValue("last movement"); @@ -957,14 +944,15 @@ void MovementAction::UpdateMovementState() const auto liquidState = bot->GetLiquidData().Status; const float gZ = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); const bool onGroundZ = bot->GetPositionZ() < gZ + 1.f; - const bool canSwim = liquidState == LIQUID_MAP_IN_WATER || liquidState == LIQUID_MAP_UNDER_WATER; - const bool canFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); + const bool wantsSwim = liquidState == LIQUID_MAP_IN_WATER || liquidState == LIQUID_MAP_UNDER_WATER; + const bool wantsFly = bot->HasIncreaseMountedFlightSpeedAura() || bot->HasFlyAura(); const bool canWaterWalk = bot->HasWaterWalkAura(); const bool isMasterFlying = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_FLYING) : true; const bool isMasterSwimming = master ? master->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING) : true; const bool isFlying = bot->HasUnitMovementFlag(MOVEMENTFLAG_FLYING); const bool isSwimming = bot->HasUnitMovementFlag(MOVEMENTFLAG_SWIMMING); const bool isWaterWalking = bot->HasUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); + const bool hasGravityDisabled = bot->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); bool movementFlagsUpdated = false; // handle water (fragile logic do not alter without testing every detail, animation and transition) @@ -979,11 +967,11 @@ void MovementAction::UpdateMovementState() else if ((!canWaterWalk || isMasterSwimming) && isWaterWalking) { bot->RemoveUnitMovementFlag(MOVEMENTFLAG_WATERWALKING); - if (canSwim) + if (wantsSwim) bot->SetSwim(true); movementFlagsUpdated = true; } - else if (!canSwim && isSwimming) + else if (!wantsSwim && isSwimming) { bot->SetSwim(false); movementFlagsUpdated = true; @@ -999,17 +987,21 @@ void MovementAction::UpdateMovementState() } // handle flying - if ((canFly && !isFlying) && isMasterFlying) + if (wantsFly && !isFlying && isMasterFlying) { bot->AddUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); bot->AddUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); bot->AddUnitMovementFlag(MOVEMENTFLAG_FLYING); - - // required for transition and state monitoring. - if (MotionMaster* mm = bot->GetMotionMaster()) - mm->MoveTakeoff(0, {bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ() + 1.F}, 0.F, true); + movementFlagsUpdated = true; } - else if ((!canFly && !isWaterWalking && isFlying) || (!isMasterFlying && isFlying && onGroundZ)) + else if (!wantsFly && !isWaterWalking && (isFlying || hasGravityDisabled)) + { + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); + bot->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING); + movementFlagsUpdated = true; + } + else if (!isMasterFlying && isFlying && onGroundZ) { bot->RemoveUnitMovementFlag(MOVEMENTFLAG_CAN_FLY); bot->RemoveUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY); @@ -1282,7 +1274,7 @@ bool MovementAction::Follow(Unit* target, float distance, float angle) return true; } -bool MovementAction::ChaseTo(WorldObject* obj, float distance, float angle) +bool MovementAction::ChaseTo(WorldObject* obj, float distance) { if (!IsMovingAllowed()) { @@ -1396,8 +1388,8 @@ bool MovementAction::Flee(Unit* target) } } - HostileReference* ref = target->GetThreatMgr().getCurrentVictim(); - if (ref && ref->getTarget() == bot) // bot is target - try to flee to tank or master + Unit* currentVictim = target->GetThreatMgr().GetCurrentVictim(); + if (currentVictim && currentVictim == bot) // bot is target - try to flee to tank or master { if (Group* group = bot->GetGroup()) { @@ -1413,7 +1405,6 @@ bool MovementAction::Flee(Unit* target) if (botAI->IsTank(player)) { float distanceToTank = ServerFacade::instance().GetDistance2d(bot, player); - float distanceToTarget = ServerFacade::instance().GetDistance2d(bot, target); if (distanceToTank < fleeDistance) { fleeTarget = player; @@ -1434,8 +1425,6 @@ bool MovementAction::Flee(Unit* target) else // bot is not targeted, try to flee dps/healers { bool isHealer = botAI->IsHeal(bot); - bool isDps = !isHealer && !botAI->IsTank(bot); - bool isTank = botAI->IsTank(bot); bool needHealer = !isHealer && AI_VALUE2(uint8, "health", "self target") < 50; bool isRanged = botAI->IsRanged(bot); @@ -1858,7 +1847,7 @@ bool FleeAction::isUseful() bool FleeWithPetAction::Execute(Event /*event*/) { - if (Pet* pet = bot->GetPet()) + if (bot->GetPet()) botAI->PetFollow(); return Flee(AI_VALUE(Unit*, "current target")); @@ -1866,7 +1855,7 @@ bool FleeWithPetAction::Execute(Event /*event*/) bool AvoidAoeAction::isUseful() { - if (getMSTime() - moveInterval < lastMoveTimer) + if (getMSTime() - moveInterval < uint32(lastMoveTimer)) return false; GuidVector traps = AI_VALUE(GuidVector, "nearest trap with damage"); @@ -2297,7 +2286,7 @@ bool MovementAction::CheckLastFlee(float curAngle, std::list& infoList bool CombatFormationMoveAction::isUseful() { - if (getMSTime() - moveInterval < lastMoveTimer) + if (getMSTime() - moveInterval < uint32(lastMoveTimer)) return false; if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr) @@ -2771,9 +2760,7 @@ bool MoveRandomAction::Execute(Event /*event*/) float angle = (float)rand_norm() * static_cast(M_PI); x += urand(0, distance) * cos(angle); y += urand(0, distance) * sin(angle); - float ox = x; - float oy = y; - float oz = z; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), x, y, z)) continue; @@ -2894,10 +2881,8 @@ bool MoveAwayFromCreatureAction::isPossible() { return bot->CanFreeMove(); } bool MoveAwayFromPlayerWithDebuffAction::Execute(Event /*event*/) { - Player* closestPlayer = nullptr; - float minDistance = 0.0f; + Group* const group = bot->GetGroup(); - Group* group = bot->GetGroup(); if (!group) return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.h index 7db911baa..14bae524d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MOVEMENTACTIONS_H -#define _PLAYERBOT_MOVEMENTACTIONS_H +#ifndef PLAYERBOTS_MOVEMENTACTIONS_H +#define PLAYERBOTS_MOVEMENTACTIONS_H #include @@ -43,14 +44,13 @@ protected: float GetFollowAngle(); bool Follow(Unit* target, float distance = sPlayerbotAIConfig.followDistance); bool Follow(Unit* target, float distance, float angle); - bool ChaseTo(WorldObject* obj, float distance = 0.0f, float angle = 0.0f); + bool ChaseTo(WorldObject* obj, float distance = 0.0f); bool ReachCombatTo(Unit* target, float distance = 0.0f); float MoveDelay(float distance, bool backwards = false); void WaitForReach(float distance); void SetNextMovementDelay(float delayMillis); bool IsMovingAllowed(WorldObject* target); - bool IsMovingAllowed(uint32 mapId, float x, float y, float z); - bool IsDuplicateMove(uint32 mapId, float x, float y, float z); + bool IsDuplicateMove(float x, float y, float z); bool IsWaitingForLastMove(MovementPriority priority); bool IsMovingAllowed(); bool Flee(Unit* target); @@ -276,7 +276,7 @@ public: this->intervals = intervals; this->clockwise = clockwise; this->call_counters = 0; - for (int i = 0; i < intervals; i++) + for (uint32 i = 0; i < intervals; i++) { float angle = start_angle + 2 * M_PI * i / intervals; waypoints.push_back(std::make_pair(center_x + cos(angle) * radius, center_y + sin(angle) * radius)); @@ -332,7 +332,6 @@ public: private: uint32 spellId; float range; - bool alive; }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.cpp index 9de1e0bae..e2a6e3003 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NonCombatActions.h" @@ -8,6 +9,31 @@ #include "Event.h" #include "Playerbots.h" +namespace +{ +constexpr uint32 BG_WS_SPELL_WARSONG_FLAG = 23333; +constexpr uint32 BG_WS_SPELL_SILVERWING_FLAG = 23335; +constexpr uint32 BG_EY_NETHERSTORM_FLAG_SPELL = 34976; + +bool IsDisallowedShapeshiftForm(Player* bot) +{ + if (bot->getClass() == CLASS_DRUID) + { + ShapeshiftForm form = bot->GetShapeshiftForm(); + return form == FORM_TRAVEL || form == FORM_AQUA || + form == FORM_FLIGHT || form == FORM_FLIGHT_EPIC || + form == FORM_BEAR || form == FORM_DIREBEAR || + form == FORM_CAT; + } + else if (bot->getClass() == CLASS_PRIEST) + { + return bot->GetShapeshiftForm() == FORM_SPIRITOFREDEMPTION; + } + + return false; +} +} + bool DrinkAction::Execute(Event event) { if (botAI->HasCheat(BotCheatMask::food)) @@ -55,10 +81,16 @@ bool DrinkAction::isUseful() bool DrinkAction::isPossible() { - return !bot->IsInCombat() && !bot->IsMounted() && - !botAI->HasAnyAuraOf(GetTarget(), "dire bear form", "bear form", "cat form", "travel form", "aquatic form", - "flight form", "swift flight form", nullptr) && - (botAI->HasCheat(BotCheatMask::food) || UseItemAction::isPossible()); + if (bot->IsInCombat() || bot->IsMounted() || IsDisallowedShapeshiftForm(bot)) + return false; + + if (bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) || bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG) || + bot->HasAura(BG_EY_NETHERSTORM_FLAG_SPELL)) + { + return false; + } + + return botAI->HasCheat(BotCheatMask::food) || UseItemAction::isPossible(); } bool EatAction::Execute(Event event) @@ -104,8 +136,14 @@ bool EatAction::isUseful() { return UseItemAction::isUseful() && AI_VALUE2(uint8 bool EatAction::isPossible() { - return !bot->IsInCombat() && !bot->IsMounted() && - !botAI->HasAnyAuraOf(GetTarget(), "dire bear form", "bear form", "cat form", "travel form", "aquatic form", - "flight form", "swift flight form", nullptr) && - (botAI->HasCheat(BotCheatMask::food) || UseItemAction::isPossible()); + if (bot->IsInCombat() || bot->IsMounted() || IsDisallowedShapeshiftForm(bot)) + return false; + + if (bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) || bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG) || + bot->HasAura(BG_EY_NETHERSTORM_FLAG_SPELL)) + { + return false; + } + + return botAI->HasCheat(BotCheatMask::food) || UseItemAction::isPossible(); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.h index 3156d4992..311974085 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/NonCombatActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NONCOMBATACTIONS_H -#define _PLAYERBOT_NONCOMBATACTIONS_H +#ifndef PLAYERBOTS_NONCOMBATACTIONS_H +#define PLAYERBOTS_NONCOMBATACTIONS_H #include "UseItemAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp index 3d1f02986..a5dc2893c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "OpenItemAction.h" #include "PlayerbotAI.h" #include "ItemTemplate.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.h index c221741b3..321d7e111 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_OPENITEMACTION_H -#define _PLAYERBOT_OPENITEMACTION_H +#ifndef PLAYERBOTS_OPENITEMACTION_H +#define PLAYERBOTS_OPENITEMACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp index dcb05e94d..3266dd01c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp @@ -1,12 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "OutfitAction.h" #include "Event.h" #include "ItemVisitors.h" +#include "PlayerbotTextMgr.h" +#include "PlayerbotRepository.h" #include "Playerbots.h" #include "ItemPackets.h" @@ -17,9 +20,12 @@ bool OutfitAction::Execute(Event event) if (param == "?") { List(); - botAI->TellMaster("outfit <имя> +[предмет] — добавить предметы"); - botAI->TellMaster("outfit <имя> -[предмет] — удалить предметы"); - botAI->TellMaster("outfit <имя> equip/replace — надеть предметы"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_usage_add", "outfit +[item] to add items", {})); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_usage_remove", "outfit -[item] to remove items", {})); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_usage_equip", "outfit equip/replace to equip items", {})); } else { @@ -28,10 +34,13 @@ bool OutfitAction::Execute(Event event) if (!name.empty()) { Save(name, items); + PlayerbotRepository::instance().Save(botAI); std::ostringstream out; - out << "Настраиваю комплект: " << name << " как " << param; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_set_as", + "Setting outfit %name as %param", + {{"%name", name}, {"%param", param}})); return true; } @@ -47,18 +56,20 @@ bool OutfitAction::Execute(Event event) std::string const command = param.substr(space + 1); if (command == "equip") { - std::ostringstream out; - out << "Надеваю комплект: " << name; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_equipping", + "Equipping outfit %name", + {{"%name", name}})); EquipItems(outfit); return true; } else if (command == "replace") { - std::ostringstream out; - out << "Заменяю текущую экипировку комплектом: " << name; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_replace_current", + "Replacing current equip with outfit %name", + {{"%name", name}})); for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++) { @@ -81,20 +92,24 @@ bool OutfitAction::Execute(Event event) } else if (command == "reset") { - std::ostringstream out; - out << "Сбрасываю комплект: " << name; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_resetting", + "Resetting outfit %name", + {{"%name", name}})); Save(name, ItemIds()); + PlayerbotRepository::instance().Save(botAI); return true; } else if (command == "update") { - std::ostringstream out; - out << "Обновляю комплект текущими предметами: " << name; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_updating_current", + "Updating with current items outfit %name", + {{"%name", name}})); Update(name); + PlayerbotRepository::instance().Save(botAI); return true; } @@ -103,27 +118,29 @@ bool OutfitAction::Execute(Event event) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemid); - std::ostringstream out; - out << chat->FormatItem(proto); if (remove) { std::set::iterator j = outfit.find(itemid); if (j != outfit.end()) outfit.erase(j); - out << " removed from "; + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_item_removed_from", + "%item removed from %name", + {{"%item", chat->FormatItem(proto)}, {"%name", name}})); } else { outfit.insert(itemid); - out << " added to "; + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "outfit_item_added_to", + "%item added to %name", + {{"%item", chat->FormatItem(proto)}, {"%name", name}})); } - - out << name; - botAI->TellMaster(out.str()); } Save(name, outfit); + PlayerbotRepository::instance().Save(botAI); } return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.h index b530504c8..d25f6bc4d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_OUTFITACTION_H -#define _PLAYERBOT_OUTFITACTION_H +#ifndef PLAYERBOTS_OUTFITACTION_H +#define PLAYERBOTS_OUTFITACTION_H #include "ChatHelper.h" #include "EquipAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.cpp index e2e9f0fe6..d634b0dc1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PassLeadershipToMasterAction.h" @@ -34,7 +35,7 @@ bool PassLeadershipToMasterAction::Execute(Event /*event*/) bool PassLeadershipToMasterAction::isUseful() { - return botAI->IsAlt() && bot->GetGroup() && bot->GetGroup()->IsLeader(bot->GetGUID()); + return botAI->IsAltBot() && bot->GetGroup() && bot->GetGroup()->IsLeader(bot->GetGUID()); } bool GiveLeaderAction::isUseful() diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.h index afa5663bc..04372a8e1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PassLeadershipToMasterAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PASSLEADERSHIPTOMASTERACTION_H -#define _PLAYERBOT_PASSLEADERSHIPTOMASTERACTION_H +#ifndef PLAYERBOTS_PASSLEADERSHIPTOMASTERACTION_H +#define PLAYERBOTS_PASSLEADERSHIPTOMASTERACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp index 5b456d9bd..e74b7d3eb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PetitionSignAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.h index 9eb8b3131..20525c368 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PETITIONSIGNACTION_H -#define _PLAYERBOT_PETITIONSIGNACTION_H +#ifndef PLAYERBOTS_PETITIONSIGNACTION_H +#define PLAYERBOTS_PETITIONSIGNACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.cpp index d98740ee0..419bdfae7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PetsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.h index 78f9c0b50..6dd11d7a6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PetsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PETSACTION_H -#define _PLAYERBOT_PETSACTION_H +#ifndef PLAYERBOTS_PETSACTION_H +#define PLAYERBOTS_PETSACTION_H #include @@ -20,7 +21,6 @@ public: bool Execute(Event event) override; private: - bool warningEnabled = true; std::string defaultCmd; }; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp index 23dcd4415..b7df78487 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PositionAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.h index 7e8173924..64662a7f1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_POSITIONACTION_H -#define _PLAYERBOT_POSITIONACTION_H +#ifndef PLAYERBOTS_POSITIONACTION_H +#define PLAYERBOTS_POSITIONACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PullActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PullActions.cpp new file mode 100644 index 000000000..ce50d835d --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PullActions.cpp @@ -0,0 +1,331 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + * Sebastiaan Keek (mostlikely4r) + * Cyberium + */ + +#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/modules/mod-playerbots/src/Ai/Base/Actions/PullActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/PullActions.h new file mode 100644 index 000000000..b9acea93c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PullActions.h @@ -0,0 +1,100 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + * Sebastiaan Keek (mostlikely4r) + * Cyberium + */ + +#ifndef PLAYERBOTS_PULLACTIONS_H +#define PLAYERBOTS_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/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp index 28344d877..1685a3f5f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "QueryItemUsageAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.h index f2f8bc2ce..5828b9fc9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_QUERYITEMUSAGEACTION_H -#define _PLAYERBOT_QUERYITEMUSAGEACTION_H +#ifndef PLAYERBOTS_QUERYITEMUSAGEACTION_H +#define PLAYERBOTS_QUERYITEMUSAGEACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp index a89ec2a93..a91c198a3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "QueryQuestAction.h" @@ -16,7 +17,6 @@ void QueryQuestAction::TellObjective(std::string const name, uint32 available, u bool QueryQuestAction::Execute(Event event) { - Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); Player* bot = botAI->GetBot(); WorldPosition botPos(bot); WorldPosition* ptr_botpos = &botPos; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.h index 308850901..c5f6f4baa 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_QUERYQUESTACTION_H -#define _PLAYERBOT_QUERYQUESTACTION_H +#ifndef PLAYERBOTS_QUERYQUESTACTION_H +#define PLAYERBOTS_QUERYQUESTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp index ec4021c17..932fa8da8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "QuestAction.h" @@ -352,7 +353,6 @@ bool QuestUpdateAddItemAction::Execute(Event event) uint32 itemId, count; p >> itemId >> count; - Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); auto const* itemPrototype = sObjectMgr->GetItemTemplate(itemId); if (itemPrototype) { @@ -407,8 +407,6 @@ bool QuestItemPushResultAction::Execute(Event event) if (!quest) return false; - const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId); - for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++) { uint32 itemId = quest->RequiredItemId[i]; @@ -416,7 +414,7 @@ bool QuestItemPushResultAction::Execute(Event event) continue; int32 previousCount = itemCount - count; - if (itemId == itemEntry && previousCount < quest->RequiredItemCount[i]) + if (itemId == itemEntry && uint32(previousCount) < quest->RequiredItemCount[i]) { if (botAI->GetMaster()) { @@ -448,8 +446,6 @@ bool QuestUpdateFailedTimerAction::Execute(Event event) uint32 questId; p >> questId; - Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); - Quest const* qInfo = sObjectMgr->GetQuestTemplate(questId); if (qInfo) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.h index c19faf9ec..3663afdb9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_QUESTACTION_H -#define _PLAYERBOT_QUESTACTION_H +#ifndef PLAYERBOTS_QUESTACTION_H +#define PLAYERBOTS_QUESTACTION_H #include "Action.h" #include "Object.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp index dbea74686..ca92e6b9d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp @@ -1,5 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "QuestConfirmAcceptAction.h" +#include "QuestPackets.h" #include "WorldPacket.h" bool QuestConfirmAcceptAction::Execute(Event event) @@ -18,6 +25,8 @@ bool QuestConfirmAcceptAction::Execute(Event event) std::ostringstream out; out << "Задание: " << chat->FormatQuest(quest) << " — подтвердите принятие"; botAI->TellMaster(out); - bot->GetSession()->HandleQuestConfirmAccept(sendPacket); + WorldPackets::Quest::QuestConfirmAcceptClient confirmAccept(std::move(sendPacket)); + confirmAccept.Read(); + bot->GetSession()->HandleQuestConfirmAccept(confirmAccept); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.h index 1f13a0abf..e15e1f2ec 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_QUESTCONFIRMACCEPTACTION_H -#define _PLAYERBOT_QUESTCONFIRMACCEPTACTION_H +#ifndef PLAYERBOTS_QUESTCONFIRMACCEPTACTION_H +#define PLAYERBOTS_QUESTCONFIRMACCEPTACTION_H #include "AiObjectContext.h" #include "Player.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.cpp index 7d7cba605..9654ca7db 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RandomBotUpdateAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.h index b5d940ad8..42cb3ebc7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RandomBotUpdateAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANDOMBOTUPDATEACTION_H -#define _PLAYERBOT_RANDOMBOTUPDATEACTION_H +#ifndef PLAYERBOTS_RANDOMBOTUPDATEACTION_H +#define PLAYERBOTS_RANDOMBOTUPDATEACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.cpp index 3cf9e6930..5cfcd8a62 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RangeAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.h index 828fcc978..6f1592ddb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RangeAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANGEACTION_H -#define _PLAYERBOT_RANGEACTION_H +#ifndef PLAYERBOTS_RANGEACTION_H +#define PLAYERBOTS_RANGEACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.cpp index 03eecda65..f8ee1744a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ReachTargetActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.h index 1a8ce0c9c..26b96d365 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReachTargetActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REACHTARGETACTIONS_H -#define _PLAYERBOT_REACHTARGETACTIONS_H +#ifndef PLAYERBOTS_REACHTARGETACTIONS_H +#define PLAYERBOTS_REACHTARGETACTIONS_H #include "GenericSpellActions.h" #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp index 835a60ffe..c11627a41 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include @@ -45,7 +46,7 @@ std::once_flag ReadyChecker::initFlag; class HealthChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* /*botAI*/, AiObjectContext* context) override { return AI_VALUE2(uint8, "health", "self target") > sPlayerbotAIConfig.almostFullHealth; } @@ -56,7 +57,7 @@ public: class ManaChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* /*botAI*/, AiObjectContext* context) override { return !AI_VALUE2(bool, "has mana", "self target") || AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; @@ -68,7 +69,7 @@ public: class DistanceChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* botAI, AiObjectContext* /*context*/) override { Player* bot = botAI->GetBot(); if (Player* master = botAI->GetMaster()) @@ -90,7 +91,7 @@ public: class HunterChecker : public ReadyChecker { public: - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* botAI, AiObjectContext* /*context*/) override { Player* bot = botAI->GetBot(); if (bot->getClass() == CLASS_HUNTER) @@ -126,7 +127,7 @@ class ItemCountChecker : public ReadyChecker public: ItemCountChecker(std::string const item, std::string const name) : item(item), name(name) {} - bool Check(PlayerbotAI* botAI, AiObjectContext* context) override + bool Check(PlayerbotAI* /*botAI*/, AiObjectContext* context) override { return AI_VALUE2(uint32, "item count", item) > 0; } @@ -225,4 +226,4 @@ bool ReadyCheckAction::ReadyCheck() return true; } -bool FinishReadyCheckAction::Execute(Event event) { return ReadyCheck(); } +bool FinishReadyCheckAction::Execute(Event /*event*/) { return ReadyCheck(); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.h index 0c16c489b..75de03311 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_READYCHECKACTION_H -#define _PLAYERBOT_READYCHECKACTION_H +#ifndef PLAYERBOTS_READYCHECKACTION_H +#define PLAYERBOTS_READYCHECKACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp index bff6eaa72..78897c082 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp @@ -10,6 +10,7 @@ #include "NearestNpcsValue.h" #include "ObjectDefines.h" #include "ObjectGuid.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "ServerFacade.h" #include "Corpse.h" @@ -22,7 +23,8 @@ bool ReleaseSpiritAction::Execute(Event event) { if (!bot->InBattleground()) { - botAI->TellMasterNoFacing("Мне не требуется воскрешение, подожду здесь"); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "release_spirit_not_dead_wait", "I am not dead, will wait here", {})); // -follow in bg is overwriten each tick with +follow // +stay in bg causes stuttering effect as bot is cycled between +stay and +follow each tick botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT); @@ -33,14 +35,15 @@ bool ReleaseSpiritAction::Execute(Event event) if (bot->GetCorpse() && bot->HasPlayerFlag(PLAYER_FLAGS_GHOST)) { - botAI->TellMasterNoFacing("Я уже дух"); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "release_spirit_already_spirit", "I am already a spirit", {})); return false; } const WorldPacket& packet = event.getPacket(); const std::string message = !packet.empty() && packet.GetOpcode() == CMSG_REPOP_REQUEST - ? "Releasing..." - : "Meet me at the graveyard"; + ? PlayerbotTextMgr::instance().GetBotTextOrDefault("release_spirit_releasing", "Releasing...", {}) + : PlayerbotTextMgr::instance().GetBotTextOrDefault("release_spirit_meet_graveyard", "Meet me at the graveyard", {}); botAI->TellMasterNoFacing(message); IncrementDeathCount(); @@ -65,7 +68,7 @@ void ReleaseSpiritAction::IncrementDeathCount() const } } -void ReleaseSpiritAction::LogRelease(const std::string& releaseMsg, bool isAutoRelease) const +void ReleaseSpiritAction::LogRelease(const std::string& releaseMsg) const { const std::string teamPrefix = bot->GetTeamId() == TEAM_ALLIANCE ? "A" : "H"; @@ -82,13 +85,13 @@ bool AutoReleaseSpiritAction::Execute(Event /*event*/) { IncrementDeathCount(); bot->DurabilityRepairAll(false, 1.0f, false); - LogRelease("auto released", true); + LogRelease("auto released"); WorldPacket packet(CMSG_REPOP_REQUEST); packet << uint8(0); bot->GetSession()->HandleRepopRequestOpcode(packet); - LogRelease("releases spirit", true); + LogRelease("releases spirit"); if (bot->InBattleground()) { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.h index 57851214a..ac3fd99e2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef PLAYERBOT_RELEASESPIRITACTION_H -#define PLAYERBOT_RELEASESPIRITACTION_H +#ifndef PLAYERBOTS_RELEASESPIRITACTION_H +#define PLAYERBOTS_RELEASESPIRITACTION_H #include "Action.h" #include "ReviveFromCorpseAction.h" @@ -18,7 +19,7 @@ public: : Action(botAI, name) {} bool Execute(Event event) override; - void LogRelease(const std::string& releaseType, bool isAutoRelease = false) const; + void LogRelease(const std::string& releaseType) const; protected: void IncrementDeathCount() const; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.cpp index 16132d7cb..e76b54237 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RememberTaxiAction.h" @@ -28,7 +29,7 @@ bool RememberTaxiAction::Execute(Event event) case CMSG_ACTIVATETAXIEXPRESS: { ObjectGuid guid; - uint32 node_count, totalcost; + uint32 node_count; p >> guid >> node_count; LastMovement& movement = context->GetValue("last taxi")->Get(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.h index e2de4b440..c62a7d7d3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RememberTaxiAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REMEMBERTAXIACTION_H -#define _PLAYERBOT_REMEMBERTAXIACTION_H +#ifndef PLAYERBOTS_REMEMBERTAXIACTION_H +#define PLAYERBOTS_REMEMBERTAXIACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.cpp index a863a5973..4de21cddc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RemoveAuraAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.h index 14f4073cd..33f5efdcf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RemoveAuraAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REMOVEAURAACTION_H -#define _PLAYERBOT_REMOVEAURAACTION_H +#ifndef PLAYERBOTS_REMOVEAURAACTION_H +#define PLAYERBOTS_REMOVEAURAACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp index c9f1a5ec6..3aa528a81 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RepairAllAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.h index aec1af961..ade353343 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REPAIRALLACTION_H -#define _PLAYERBOT_REPAIRALLACTION_H +#ifndef PLAYERBOTS_REPAIRALLACTION_H +#define PLAYERBOTS_REPAIRALLACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp index d8015ab65..d9d1e3b7e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ResetAiAction.h" @@ -44,6 +45,21 @@ bool ResetAiAction::Execute(Event event) } } } + if (Player* master = botAI->GetMaster()) + { + Group* botGroup = bot->GetGroup(); + Group* masterGroup = master->GetGroup(); + if (botGroup && (!masterGroup || masterGroup != botGroup)) + botAI->SetMaster(nullptr); + } + if (sRandomPlayerbotMgr.IsRandomBot(bot) && !bot->InBattleground()) + { + if (bot->GetGroup() && (!botAI->GetMaster() || GET_PLAYERBOT_AI(botAI->GetMaster()))) + { + if (Player* newMaster = botAI->FindNewMaster()) + botAI->SetMaster(newMaster); + } + } PlayerbotRepository::instance().Reset(botAI); botAI->ResetStrategies(false); botAI->TellMaster("Настройки ИИ сброшены"); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.h index 092a03137..ecdbf7865 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RESETAIACTION_H -#define _PLAYERBOT_RESETAIACTION_H +#ifndef PLAYERBOTS_RESETAIACTION_H +#define PLAYERBOTS_RESETAIACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.cpp index 841c97fd5..555e5594e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ResetInstancesAction.h" @@ -8,10 +9,13 @@ #include "PlayerbotAI.h" #include "InstancePackets.h" +#include "InstancePackets.h" + bool ResetInstancesAction::Execute(Event /*event*/) { - WorldPackets::Instance::ResetInstances packet(WorldPacket(CMSG_RESET_INSTANCES, 0)); - bot->GetSession()->HandleResetInstancesOpcode(packet); + WorldPacket packet(CMSG_RESET_INSTANCES, 0); + WorldPackets::Instance::ResetInstances resetInstance(std::move(packet)); + bot->GetSession()->HandleResetInstancesOpcode(resetInstance); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.h index b69256bc1..5588313a9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ResetInstancesAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RESETINSTANCESACTION_H -#define _PLAYERBOT_RESETINSTANCESACTION_H +#ifndef PLAYERBOTS_RESETINSTANCESACTION_H +#define PLAYERBOTS_RESETINSTANCESACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp index 6396f709a..637987879 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RevealGatheringItemAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.h index cd0422476..f16cff774 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REVEALGATHERINGITEMACTION_H -#define _PLAYERBOT_REVEALGATHERINGITEMACTION_H +#ifndef PLAYERBOTS_REVEALGATHERINGITEMACTION_H +#define PLAYERBOTS_REVEALGATHERINGITEMACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp index 9b26e19c9..3a0da4b05 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ReviveFromCorpseAction.h" @@ -9,6 +10,7 @@ #include "FleeManager.h" #include "GameGraveyard.h" #include "MapMgr.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "RandomPlayerbotMgr.h" #include "ServerFacade.h" @@ -251,9 +253,9 @@ GraveyardStruct const* SpiritHealerAction::GetGrave(bool startZone) std::vector races; if (bot->GetTeamId() == TEAM_ALLIANCE) - races = {RACE_HUMAN, RACE_DWARF, RACE_GNOME, RACE_NIGHTELF}; + races = {RACE_HUMAN, RACE_DWARF, RACE_GNOME, RACE_NIGHTELF, RACE_DRAENEI}; else - races = {RACE_ORC, RACE_TROLL, RACE_TAUREN, RACE_UNDEAD_PLAYER}; + races = {RACE_ORC, RACE_TROLL, RACE_TAUREN, RACE_UNDEAD_PLAYER, RACE_BLOODELF}; float graveDistance = -1; @@ -322,7 +324,7 @@ bool SpiritHealerAction::Execute(Event /*event*/) bot->SpawnCorpseBones(); context->GetValue("current target")->Set(nullptr); bot->SetTarget(); - botAI->TellMaster("Здравствуйте."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault("hello", "Hello", {})); if (dCount > 20) context->GetValue("death count")->Set(0); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.h index a20dd3c07..e158a8b4b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REVIVEFROMCORPSEACTION_H -#define _PLAYERBOT_REVIVEFROMCORPSEACTION_H +#ifndef PLAYERBOTS_REVIVEFROMCORPSEACTION_H +#define PLAYERBOTS_REVIVEFROMCORPSEACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp index 01255c5bf..6a2299fe9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RewardAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.h index 328777a94..68f2dfcaf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_REWARDACTION_H -#define _PLAYERBOT_REWARDACTION_H +#ifndef PLAYERBOTS_REWARDACTION_H +#define PLAYERBOTS_REWARDACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.cpp index 807a59724..7e964804d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RtiAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.h index 18b0aa44d..2954f4d8d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RtiAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTIACTION_H -#define _PLAYERBOT_RTIACTION_H +#ifndef PLAYERBOTS_RTIACTION_H +#define PLAYERBOTS_RTIACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp index ee8657968..fae9583d5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RtscAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.h index 81ec27c03..34df1d0fc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTSCACTION_H -#define _PLAYERBOT_RTSCACTION_H +#ifndef PLAYERBOTS_RTSCACTION_H +#define PLAYERBOTS_RTSCACTION_H #include "SeeSpellAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp index ff0dbd341..3d7f48288 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SaveManaAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.h index 09edec989..919ba68a7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SAVEMANAACTION_H -#define _PLAYERBOT_SAVEMANAACTION_H +#ifndef PLAYERBOTS_SAVEMANAACTION_H +#define PLAYERBOTS_SAVEMANAACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.cpp index 69d8b5330..f87881ef8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AiFactory.h" @@ -167,9 +168,8 @@ bool SayAction::isUseful() return (time(nullptr) - lastSaid) > 30; } -void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint32& guid2, std::string& msg, std::string& chanName, std::string& name) +void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, std::string& msg, std::string& chanName, std::string& name) { - ChatReplyType replyType = REPLY_NOT_UNDERSTAND; // default not understand std::string respondsText = ""; // if we're just commanding bots around, don't respond... @@ -219,14 +219,14 @@ void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint if (msg.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && (GET_PLAYERBOT_AI(bot)->GetChatHelper()->ExtractAllItemIds(msg).size() > 0 || GET_PLAYERBOT_AI(bot)->GetChatHelper()->ExtractAllQuestIds(msg).size() > 0)) { - HandleToxicLinksReply(bot, chatChannelSource, msg, name); + HandleToxicLinksReply(bot, chatChannelSource); return; } //thunderfury if (GET_PLAYERBOT_AI(bot)->GetChatHelper()->ExtractAllItemIds(msg).count(19019)) { - HandleThunderfuryReply(bot, chatChannelSource, msg, name); + HandleThunderfuryReply(bot, chatChannelSource); return; } @@ -234,7 +234,7 @@ void ChatReplyAction::ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint SendGeneralResponse(bot, chatChannelSource, messageRepy, name); } -bool ChatReplyAction::HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name) +bool ChatReplyAction::HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource) { std::map placeholders; const auto thunderfury = sObjectMgr->GetItemTemplate(19019); @@ -262,7 +262,7 @@ bool ChatReplyAction::HandleThunderfuryReply(Player* bot, ChatChannelSource chat return true; } -bool ChatReplyAction::HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name) +bool ChatReplyAction::HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource) { //quests std::vector incompleteQuests; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.h index 5bf9a8f04..98ccc71e8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SayAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SAYACTION_H -#define _PLAYERBOT_SAYACTION_H +#ifndef PLAYERBOTS_SAYACTION_H +#define PLAYERBOTS_SAYACTION_H #include "Action.h" #include "PlayerbotAI.h" @@ -29,12 +30,12 @@ class ChatReplyAction : public Action { public: ChatReplyAction(PlayerbotAI* ai) : Action(ai, "chat message") {} - virtual bool Execute(Event event) { return true; } + virtual bool Execute(Event /*event*/) { return true; } bool isUseful() { return true; } - static void ChatReplyDo(Player* bot, uint32& type, uint32& guid1, uint32& guid2, std::string& msg, std::string& chanName, std::string& name); - static bool HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); - static bool HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); + static void ChatReplyDo(Player* bot, uint32& type, uint32& guid1, std::string& msg, std::string& chanName, std::string& name); + static bool HandleThunderfuryReply(Player* bot, ChatChannelSource chatChannelSource); + static bool HandleToxicLinksReply(Player* bot, ChatChannelSource chatChannelSource); static bool HandleWTBItemsReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); static bool HandleLFGQuestsReply(Player* bot, ChatChannelSource chatChannelSource, std::string& msg, std::string& name); static bool SendGeneralResponse(Player* bot, ChatChannelSource chatChannelSource, std::string& responseMessage, std::string& name); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp index 6260ee734..4313a74bd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SecurityCheckAction.h" @@ -10,8 +11,10 @@ bool SecurityCheckAction::isUseful() { - return sRandomPlayerbotMgr.IsRandomBot(bot) && botAI->GetMaster() && - botAI->GetMaster()->GetSession()->GetSecurity() < SEC_GAMEMASTER && !GET_PLAYERBOT_AI(botAI->GetMaster()); + return RandomPlayerbotMgr::instance().IsRandomBot(bot) + && botAI->GetMaster() + && !botAI->GetMaster()->CanBeGameMaster() + && !GET_PLAYERBOT_AI(botAI->GetMaster()); } bool SecurityCheckAction::Execute(Event /*event*/) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.h index 21f343ccb..6b8636ee7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SECURITYCHECKACTION_H -#define _PLAYERBOT_SECURITYCHECKACTION_H +#ifndef PLAYERBOTS_SECURITYCHECKACTION_H +#define PLAYERBOTS_SECURITYCHECKACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.cpp index f7d7eab23..7413698c5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SeeSpellAction.h" @@ -15,7 +16,7 @@ std::set const FISHING_SPELLS = {7620, 7731, 7732, 18248, 33095, 51294}; -Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp, +Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* /*lastWp*/, bool important) { float dist = wpOwner->GetDistance(x, y, z); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.h index 91556d635..970fac39b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SeeSpellAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SEESPELLACTION_H -#define _PLAYERBOT_SEESPELLACTION_H +#ifndef PLAYERBOTS_SEESPELLACTION_H +#define PLAYERBOTS_SEESPELLACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp index a89941dd8..c8a286f16 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SellAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.h index 7c985de9d..93a28dfcf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SELLACTION_H -#define _PLAYERBOT_SELLACTION_H +#ifndef PLAYERBOTS_SELLACTION_H +#define PLAYERBOTS_SELLACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp index 39e30bad2..4cae8302f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SendMailAction.h" @@ -9,6 +10,7 @@ #include "Event.h" #include "ItemVisitors.h" #include "Mail.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool SendMailAction::Execute(Event event) @@ -33,34 +35,37 @@ bool SendMailAction::Execute(Event event) Player* receiver = GetMaster(); Player* tellTo = receiver; - std::vector ss = split(text, ' '); - if (ss.size() > 1) - { - if (Player* p = ObjectAccessor::FindPlayer(ObjectGuid(uint64(ss[ss.size() - 1].c_str())))) - receiver = p; - } - if (!receiver) receiver = event.getOwner(); if (!receiver || receiver == bot) - { return false; - } if (!tellTo) tellTo = receiver; + if (!sPlayerbotAIConfig.botSendMailEnabled) + { + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_disabled", "I cannot send mail", {}), + LANG_UNIVERSAL, tellTo); + return false; + } + if (!mailboxFound && !randomBot) { - bot->Whisper("Поблизости нет почтового ящика", LANG_UNIVERSAL, tellTo); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_no_mailbox_nearby", "There is no mailbox nearby", {}), + LANG_UNIVERSAL, tellTo); return false; } ItemIds ids = chat->parseItems(text); if (ids.size() > 1) { - bot->Whisper("Можно запросить только один предмет", LANG_UNIVERSAL, tellTo); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_one_item_only", "You can not request more than one item", {}), + LANG_UNIVERSAL, tellTo); return false; } @@ -72,13 +77,16 @@ bool SendMailAction::Execute(Event event) if (randomBot) { - bot->Whisper("Я не могу отправить деньги", LANG_UNIVERSAL, tellTo); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_cannot_send_money", "I cannot send money", {}), + LANG_UNIVERSAL, tellTo); return false; } if (bot->GetMoney() < money) { - botAI->TellError("У меня недостаточно денег"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_not_enough_money", "I don't have enough money", {})); return false; } @@ -100,8 +108,10 @@ bool SendMailAction::Execute(Event event) CharacterDatabase.CommitTransaction(trans); std::ostringstream out; - out << "Отправляю письмо: " << receiver->GetName(); - botAI->TellMaster(out.str()); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_sending_to", + "Sending mail to %receiver", + {{"%receiver", receiver->GetName()}})); return true; } @@ -125,7 +135,10 @@ bool SendMailAction::Execute(Event event) if (item->IsSoulBound() || item->IsConjuredConsumable()) { std::ostringstream out; - out << "Не удалось отправить: " << ChatHelper::FormatItem(item->GetTemplate()); + out << PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_cannot_send_item", + "Cannot send %item", + {{"%item", ChatHelper::FormatItem(item->GetTemplate())}}); bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo); continue; } @@ -140,7 +153,10 @@ bool SendMailAction::Execute(Event event) if (!price) { std::ostringstream out; - out << ChatHelper::FormatItem(item->GetTemplate()) << ": it is not for sale"; + out << PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_item_not_for_sale", + "%item: it is not for sale", + {{"%item", ChatHelper::FormatItem(item->GetTemplate())}}); bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo); return false; } @@ -160,7 +176,10 @@ bool SendMailAction::Execute(Event event) CharacterDatabase.CommitTransaction(trans); std::ostringstream out; - out << "Письмо отправлено: " << receiver->GetName(); + out << PlayerbotTextMgr::instance().GetBotTextOrDefault( + "send_mail_sent_to", + "Sent mail to %receiver", + {{"%receiver", receiver->GetName()}}); bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.h index 06affa6bd..82503c9d6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SENDMAILACTION_H -#define _PLAYERBOT_SENDMAILACTION_H +#ifndef PLAYERBOTS_SENDMAILACTION_H +#define PLAYERBOTS_SENDMAILACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp index c789ef740..6fe3480cd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SetCraftAction.h" @@ -8,6 +9,7 @@ #include "ChatHelper.h" #include "CraftValue.h" #include "Event.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" std::map SetCraftAction::skillSpells; @@ -24,7 +26,8 @@ bool SetCraftAction::Execute(Event event) if (link == "reset") { data.Reset(); - botAI->TellMaster("Я ничего не буду создавать"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "craft_reset", "I will not craft anything", {})); return true; } @@ -37,7 +40,8 @@ bool SetCraftAction::Execute(Event event) ItemIds itemIds = chat->parseItems(link); if (itemIds.empty()) { - botAI->TellMaster("Использование: «craft [ID предмета]» или «craft reset»"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "craft_usage", "Usage: 'craft [itemId]' or 'craft reset'", {})); return false; } @@ -66,7 +70,8 @@ bool SetCraftAction::Execute(Event event) if (!spellInfo) continue; - if (SkillLineAbilityEntry const* skillLine = skillSpells[spellId]) + SkillLineAbilityEntry const* skillLine = skillSpells[spellId]; + if (skillLine != nullptr) { for (uint8 i = 0; i < 3; ++i) { @@ -93,7 +98,8 @@ bool SetCraftAction::Execute(Event event) if (data.required.empty()) { - botAI->TellMaster("Я не могу это создать"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "craft_cannot_craft", "I cannot craft this", {})); return false; } @@ -108,7 +114,8 @@ void SetCraftAction::TellCraft() CraftData& data = AI_VALUE(CraftData&, "craft"); if (data.IsEmpty()) { - botAI->TellMaster("Я ничего не буду создавать"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "craft_reset", "I will not craft anything", {})); return; } @@ -116,8 +123,7 @@ void SetCraftAction::TellCraft() if (!proto) return; - std::ostringstream out; - out << "Я создам: " << chat->FormatItem(proto) << "; реагенты: "; + std::ostringstream reagentsOut; bool first = true; for (std::map::iterator i = data.required.begin(); i != data.required.end(); ++i) @@ -129,20 +135,23 @@ void SetCraftAction::TellCraft() { if (first) first = false; - else - out << ", "; + reagentsOut << ", "; - out << chat->FormatItem(reagent, required); + reagentsOut << chat->FormatItem(reagent, required); uint32 given = data.obtained[item]; if (given) - out << "|cffffff00(x" << given << " given)|r "; + reagentsOut << "|cffffff00(x" << given << " given)|r "; } } - out << " (craft fee: " << chat->formatMoney(GetCraftFee(data)) << ")"; - botAI->TellMaster(out.str()); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "craft_summary", + "I will craft %item using reagents: %reagents (craft fee: %money)", + {{"%item", chat->FormatItem(proto)}, + {"%reagents", reagentsOut.str()}, + {"%money", chat->formatMoney(GetCraftFee(data))}})); } uint32 SetCraftAction::GetCraftFee(CraftData& data) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.h index d10507da9..611566149 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SETCRAFTACTION_H -#define _PLAYERBOT_SETCRAFTACTION_H +#ifndef PLAYERBOTS_SETCRAFTACTION_H +#define PLAYERBOTS_SETCRAFTACTION_H #include "Action.h" #include "CraftValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp new file mode 100644 index 000000000..3928de902 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.cpp @@ -0,0 +1,220 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "SetFocusHealTargetsAction.h" + +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "PlayerbotTextMgr.h" + +#include +#include + +static std::string LowercaseString(std::string const& str) +{ + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +static Player* FindGroupPlayerByName(Player* player, std::string const& playerName) +{ + if (!player) + return nullptr; + + Group* group = player->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (member) + { + std::string memberName = member->GetName(); + if (LowercaseString(memberName) == playerName) + return member; + } + } + + return nullptr; +} + +bool SetFocusHealTargetsAction::Execute(Event event) +{ + if (!botAI->IsHeal(bot) && !botAI->HasStrategy("offheal", BOT_STATE_COMBAT)) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_healer", + "I'm not a healer or offhealer (please change my strats to heal or offheal)", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + std::string const param = LowercaseString(event.getParam()); + if (param.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_provide_names", + "Please provide one or more player names", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + std::list focusHealTargets = + AI_VALUE(std::list, "focus heal targets"); + + // Query current focus targets + if (param.find('?') != std::string::npos) + { + if (focusHealTargets.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_no_targets", + "I don't have any focus heal targets", + {}); + botAI->TellMasterNoFacing(text); + } + else + { + std::stringstream targetNames; + for (auto it = focusHealTargets.begin(); it != focusHealTargets.end(); ++it) + { + Unit* target = botAI->GetUnit(*it); + if (target) + { + if (it != focusHealTargets.begin()) + targetNames << ", "; + targetNames << target->GetName(); + } + } + + std::map placeholders; + placeholders["%targets"] = targetNames.str(); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_current_targets", + "My focus heal targets are %targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + + return true; + } + + // Clear all targets + if (param == "none" || param == "unset" || param == "clear") + { + focusHealTargets.clear(); + SET_AI_VALUE(std::list, "focus heal targets", focusHealTargets); + botAI->ChangeStrategy("-focus heal targets", BOT_STATE_COMBAT); + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_cleared", + "Removed focus heal targets", + {}); + botAI->TellMasterNoFacing(text); + return true; + } + + // Parse multiple targets separated by commas + std::vector targetNames; + if (param.find(',') != std::string::npos) + { + std::string targetName; + std::stringstream ss(param); + while (std::getline(ss, targetName, ',')) + targetNames.push_back(targetName); + } + else + targetNames.push_back(param); + + if (targetNames.empty()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_provide_names", + "Please provide one or more player names", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + if (!bot->GetGroup()) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_in_group", + "I'm not in a group", + {}); + botAI->TellMasterNoFacing(text); + return false; + } + + for (std::string const& targetName : targetNames) + { + bool const add = targetName.find("+") != std::string::npos; + bool const remove = targetName.find("-") != std::string::npos; + if (!add && !remove) + { + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_add_remove_syntax", + "Please specify a + for add or - to remove a target", + {}); + botAI->TellMasterNoFacing(text); + continue; + } + + std::string const playerName = targetName.substr(1); + Player* target = FindGroupPlayerByName(bot, playerName); + if (!target) + { + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_not_in_group_with", + "I'm not in a group with %player_name", + placeholders); + botAI->TellMasterNoFacing(text); + continue; + } + + ObjectGuid const& targetGuid = target->GetGUID(); + if (add) + { + if (std::find(focusHealTargets.begin(), focusHealTargets.end(), targetGuid) == + focusHealTargets.end()) + focusHealTargets.push_back(targetGuid); + + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_added", + "Added %player_name to focus heal targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + else + { + focusHealTargets.remove(targetGuid); + std::map placeholders; + placeholders["%player_name"] = playerName; + std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "focus_heal_removed", + "Removed %player_name from focus heal targets", + placeholders); + botAI->TellMasterNoFacing(text); + } + } + + SET_AI_VALUE(std::list, "focus heal targets", focusHealTargets); + + if (focusHealTargets.empty()) + botAI->ChangeStrategy("-focus heal targets", BOT_STATE_COMBAT); + else + botAI->ChangeStrategy("+focus heal targets", BOT_STATE_COMBAT); + + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.h new file mode 100644 index 000000000..a1f1a8598 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetFocusHealTargetsAction.h @@ -0,0 +1,22 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SETFOCUSHEALTARGETSACTION_H +#define PLAYERBOTS_SETFOCUSHEALTARGETSACTION_H + +#include "Action.h" + +class PlayerbotAI; + +class SetFocusHealTargetsAction : public Action +{ +public: + SetFocusHealTargetsAction(PlayerbotAI* botAI) : Action(botAI, "focus heal targets") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp index aaccf9cfe..b4498eca3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp @@ -1,11 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SetHomeAction.h" #include "Event.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool SetHomeAction::Execute(Event /*event*/) @@ -26,20 +28,11 @@ bool SetHomeAction::Execute(Event /*event*/) if (Unit* unit = botAI->GetUnit(selection)) if (unit->HasNpcFlag(UNIT_NPC_FLAG_INNKEEPER)) { - if (isRpgAction) - { - Creature* creature = botAI->GetCreature(selection); - bot->GetSession()->SendBindPoint(creature); - botAI->TellMaster("Эта таверна теперь мой дом"); - return true; - } - else - { - Creature* creature = botAI->GetCreature(selection); - bot->GetSession()->SendBindPoint(creature); - botAI->TellMaster("Эта таверна теперь мой дом"); - return true; - } + Creature* creature = botAI->GetCreature(selection); + bot->GetSession()->SendBindPoint(creature); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "set_home_success", "This inn is my new home", {})); + return true; } GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); @@ -50,10 +43,12 @@ bool SetHomeAction::Execute(Event /*event*/) continue; bot->GetSession()->SendBindPoint(unit); - botAI->TellMaster("Эта таверна теперь мой дом"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "set_home_success", "This inn is my new home", {})); return true; } - botAI->TellError("Поблизости нет хозяина таверны"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "set_home_no_innkeeper_error", "Can't find any innkeeper around", {})); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.h index ee499bf8a..fd343e74a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SETHOMEACTION_H -#define _PLAYERBOT_SETHOMEACTION_H +#ifndef PLAYERBOTS_SETHOMEACTION_H +#define PLAYERBOTS_SETHOMEACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp index 21f0a32b1..9b948e637 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp @@ -1,12 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ShareQuestAction.h" #include "Event.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" +#include "QuestPackets.h" bool ShareQuestAction::Execute(Event event) { @@ -29,10 +32,13 @@ bool ShareQuestAction::Execute(Event event) uint32 logQuest = bot->GetQuestSlotQuestId(slot); if (logQuest == entry) { - WorldPacket p; + WorldPacket p(CMSG_PUSHQUESTTOPARTY); p << entry; - bot->GetSession()->HandlePushQuestToParty(p); - botAI->TellMaster("Задание передано группе"); + WorldPackets::Quest::PushQuestToParty pushQuest(std::move(p)); + pushQuest.Read(); + bot->GetSession()->HandlePushQuestToParty(pushQuest); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_shared", "Quest shared", {})); return true; } } @@ -40,9 +46,8 @@ bool ShareQuestAction::Execute(Event event) return false; } -bool AutoShareQuestAction::Execute(Event event) +bool AutoShareQuestAction::Execute(Event /*event*/) { - Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); bool shared = false; for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot) @@ -96,10 +101,13 @@ bool AutoShareQuestAction::Execute(Event event) if (!partyNeedsQuest) continue; - WorldPacket p; + WorldPacket p(CMSG_PUSHQUESTTOPARTY); p << logQuest; - bot->GetSession()->HandlePushQuestToParty(p); - botAI->TellMaster("Задание передано группе"); + WorldPackets::Quest::PushQuestToParty pushQuest(std::move(p)); + pushQuest.Read(); + bot->GetSession()->HandlePushQuestToParty(pushQuest); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "quest_shared", "Quest shared", {})); shared = true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.h index 033ed606b..ae62a4f7c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHAREQUESTACTION_H -#define _PLAYERBOT_SHAREQUESTACTION_H +#ifndef PLAYERBOTS_SHAREQUESTACTION_H +#define PLAYERBOTS_SHAREQUESTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp index 2096ff1fb..c55097823 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SkipSpellsListAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.h index 578be9a9c..eafc7a4a1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SKIPSPELLSLISTACTION_H -#define _PLAYERBOT_SKIPSPELLSLISTACTION_H +#ifndef PLAYERBOTS_SKIPSPELLSLISTACTION_H +#define PLAYERBOTS_SKIPSPELLSLISTACTION_H #include "Action.h" #include "ChatHelper.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.cpp index aaf8d1bd1..ffa8f2de2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "StatsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.h index 0dbaf38b8..56cba2bd6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/StatsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STATSACTION_H -#define _PLAYERBOT_STATSACTION_H +#ifndef PLAYERBOTS_STATSACTION_H +#define PLAYERBOTS_STATSACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.cpp index ad10e8990..7798cc877 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "StayActions.h" @@ -47,7 +48,6 @@ bool StayAction::isUseful() PositionInfo stayPosition = AI_VALUE(PositionMap&, "position")["stay"]; if (stayPosition.isSet()) { - const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z); if (sPlayerbotAIConfig.followDistance) return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.h index d050b9661..ac2581f33 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/StayActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STAYACTIONS_H -#define _PLAYERBOT_STAYACTIONS_H +#ifndef PLAYERBOTS_STAYACTIONS_H +#define PLAYERBOTS_STAYACTIONS_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.cpp index 5bd191140..1569edb58 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include @@ -63,7 +64,6 @@ bool SuggestWhatToDoAction::Execute(Event /*event*/) fnct_ptr(); std::string const qualifier = "suggest what to do"; - time_t lastSaid = AI_VALUE2(time_t, "last said", qualifier); botAI->GetAiObjectContext()->GetValue("last said", qualifier)->Set(time(nullptr) + urand(1, 60)); return true; @@ -221,7 +221,7 @@ void SuggestWhatToDoAction::thunderfury() class FindTradeItemsVisitor : public IterateItemsVisitor { public: - FindTradeItemsVisitor(uint32 quality) : quality(quality), IterateItemsVisitor() {} + FindTradeItemsVisitor(uint32 quality) : IterateItemsVisitor(), quality(quality) {} bool Visit(Item* item) override { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.h index 238ce7e89..85cb20d3c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SuggestWhatToDoAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SUGGESTWHATTODOACTION_H -#define _PLAYERBOT_SUGGESTWHATTODOACTION_H +#ifndef PLAYERBOTS_SUGGESTWHATTODOACTION_H +#define PLAYERBOTS_SUGGESTWHATTODOACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp index 42ed28388..f2b424dbf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TalkToQuestGiverAction.h" @@ -171,7 +172,7 @@ void TalkToQuestGiverAction::RewardMultipleItem(Quest const* quest, Object* ques std::set bestIds; std::ostringstream outid; - if (!botAI->IsAlt() || sPlayerbotAIConfig.autoPickReward == "yes") + if (!botAI->IsAltBot() || sPlayerbotAIConfig.autoPickReward == "yes") { bestIds = BestRewards(quest); if (!bestIds.empty()) @@ -231,7 +232,6 @@ void TalkToQuestGiverAction::AskToSelectReward(Quest const* quest, std::ostrings for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) { ItemTemplate const* item = sObjectMgr->GetItemTemplate(quest->RewardChoiceItemId[i]); - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]); if (!forEquip || BestRewards(quest).count(i) > 0) { @@ -248,7 +248,6 @@ bool TurnInQueryQuestAction::Execute(Event event) WorldPacket pakcet = event.getPacket(); ObjectGuid guid; uint32 questId; - ObjectGuid unk1; pakcet >> guid >> questId; Object* object = ObjectAccessor::GetObjectByTypeMask(*bot, guid, TYPEMASK_UNIT | TYPEMASK_GAMEOBJECT | TYPEMASK_ITEM); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.h index edde3272b..f3b3f6ddf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TALKTOQUESTGIVERACTION_H -#define _PLAYERBOT_TALKTOQUESTGIVERACTION_H +#ifndef PLAYERBOTS_TALKTOQUESTGIVERACTION_H +#define PLAYERBOTS_TALKTOQUESTGIVERACTION_H #include "QuestAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp index 15ef800fd..0c7468219 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TameAction.h" @@ -15,6 +16,7 @@ #include "Player.h" #include "PlayerbotAI.h" #include "PlayerbotFactory.h" +#include "PlayerbotTextMgr.h" #include "SpellMgr.h" #include "WorldSession.h" @@ -51,7 +53,6 @@ bool TameAction::Execute(Event event) { std::set normalFamilies; std::set exoticFamilies; - Player* bot = botAI->GetBot(); // Loop over all creature templates and collect tameable families CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates(); @@ -124,7 +125,8 @@ bool TameAction::Execute(Event event) } catch (...) { - botAI->TellError("Неверный ID приручения."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_invalid_id_error", "Invalid tame id.", {})); } } else if (mode == "family" && !value.empty()) @@ -138,8 +140,10 @@ bool TameAction::Execute(Event event) else { // Unrecognized command or missing argument; show usage - botAI->TellError( - "Usage: tame name | tame id | tame family | tame rename | tame abandon"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_usage_error", + "Usage: tame name | tame id | tame family | tame rename | tame abandon", + {})); return false; } @@ -158,12 +162,15 @@ bool TameAction::Execute(Event event) if (!lastPetName.empty() && lastPetId != 0) { std::ostringstream oss; - oss << "Pet changed to " << lastPetName << ", ID: " << lastPetId << "."; - botAI->TellMaster(oss.str()); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_changed", + "Pet changed to %name, ID: %id.", + {{"%name", lastPetName}, {"%id", std::to_string(lastPetId)}})); } else { - botAI->TellMaster("Питомец заменён и подготовлен!"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_changed_initialized", "Pet changed and initialized!", {})); } } @@ -198,7 +205,10 @@ bool TameAction::SetPetByName(const std::string& name) // If the creature is exotic and the bot doesn't have Beast Mastery, show error and fail if (IsExoticPet(&creature) && !HasBeastMastery(bot)) { - botAI->TellError("Я не могу использовать экзотических питомцев без таланта «Повелитель зверей»."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_exotic_requires_beast_mastery", + "I cannot use exotic pets unless I have the Beast Mastery talent.", + {})); return false; } @@ -215,7 +225,8 @@ bool TameAction::SetPetByName(const std::string& name) } // If no suitable pet found, show an error and return failure - botAI->TellError("Не найден приручаемый питомец с именем: " + name); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_pet_by_name", "No tameable pet found with name: %name", {{"%name", name}})); return false; } @@ -232,21 +243,26 @@ bool TameAction::SetPetById(uint32 id) if (!creature->IsTameable(true)) { // If not tameable at all, show an error and fail - botAI->TellError("Не найден приручаемый питомец с ID: " + std::to_string(id)); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_pet_by_id", "No tameable pet found with id: %id", {{"%id", std::to_string(id)}})); return false; } // If it's an exotic pet, make sure the bot has the Beast Mastery talent if (IsExoticPet(creature) && !HasBeastMastery(bot)) { - botAI->TellError("Я не могу использовать экзотических питомцев без таланта «Повелитель зверей»."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_exotic_requires_beast_mastery", + "I cannot use exotic pets unless I have the Beast Mastery talent.", + {})); return false; } // Check if the bot is actually allowed to tame this pet (honoring exotic pet rules) if (!creature->IsTameable(bot->CanTameExoticPets())) { - botAI->TellError("Не найден приручаемый питомец с ID: " + std::to_string(id)); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_pet_by_id", "No tameable pet found with id: %id", {{"%id", std::to_string(id)}})); return false; } @@ -258,7 +274,8 @@ bool TameAction::SetPetById(uint32 id) } // If no valid creature was found by id, show an error - botAI->TellError("Не найден приручаемый питомец с ID: " + std::to_string(id)); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_pet_by_id", "No tameable pet found with id: %id", {{"%id", std::to_string(id)}})); return false; } @@ -316,9 +333,13 @@ bool TameAction::SetPetByFamily(const std::string& family) if (candidates.empty()) { if (foundExotic && !HasBeastMastery(bot)) - botAI->TellError("Я не могу использовать экзотических питомцев без таланта «Повелитель зверей»."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_exotic_requires_beast_mastery", + "I cannot use exotic pets unless I have the Beast Mastery talent.", + {})); else - botAI->TellError("Не найден приручаемый питомец семейства: " + family); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_pet_by_family", "No tameable pet found with family: %family", {{"%family", family}})); return false; } @@ -343,14 +364,18 @@ bool TameAction::RenamePet(const std::string& newName) // Check if the bot currently has a pet if (!pet) { - botAI->TellError("У вас нет питомца для переименования."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_pet_to_rename", "You have no pet to rename.", {})); return false; } // Validate the new name: must not be empty and max 12 characters if (newName.empty() || newName.length() > 12) { - botAI->TellError("Имя питомца должно содержать от 1 до 12 букв."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_name_length_error", + "Pet name must be between 1 and 12 alphabetic characters.", + {})); return false; } @@ -359,7 +384,10 @@ bool TameAction::RenamePet(const std::string& newName) { if (!std::isalpha(static_cast(c))) { - botAI->TellError("Имя питомца должно состоять только из букв."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_name_alpha_error", + "Pet name must only contain alphabetic characters (A-Z, a-z).", + {})); return false; } } @@ -373,7 +401,10 @@ bool TameAction::RenamePet(const std::string& newName) // Check if the new name is reserved or forbidden if (sObjectMgr->IsReservedName(normalized)) { - botAI->TellError("Это имя питомца запрещено. Выберите другое."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_name_forbidden_error", + "That pet name is forbidden. Please choose another name.", + {})); return false; } @@ -383,15 +414,18 @@ bool TameAction::RenamePet(const std::string& newName) bot->GetSession()->SendPetNameQuery(pet->GetGUID(), pet->GetEntry()); // Notify the master about the rename and give a tip to update the client name display - botAI->TellMaster("Ваш питомец переименован в " + normalized + "!"); - botAI->TellMaster("Если новое имя не появилось, отпустите и снова призовите питомца."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_renamed", "Your pet has been renamed to %name!", {{"%name", normalized}})); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_rename_refresh_hint", + "If you do not see the new name, please dismiss and recall your pet.", + {})); // Remove the current pet and (re-)cast Call Pet spell if the bot is a hunter bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT, true); - if (bot->getClass() == CLASS_HUNTER && bot->HasSpell(883)) - { - bot->CastSpell(bot, 883, true); - } + constexpr uint32 SPELL_CALL_PET = 883; + if (bot->getClass() == CLASS_HUNTER && bot->HasSpell(SPELL_CALL_PET)) + bot->CastSpell(bot, SPELL_CALL_PET, true); return true; } @@ -402,7 +436,8 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry) // Ensure the player is a hunter and at least level 10 (required for pets) if (bot->getClass() != CLASS_HUNTER || bot->GetLevel() < 10) { - botAI->TellError("Питомцы доступны охотникам не ниже 10-го уровня."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_only_hunters_level_10", "Only level 10+ hunters can have pets.", {})); return false; } @@ -410,7 +445,8 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry) CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(creatureEntry); if (!creature) { - botAI->TellError("Шаблон существа не найден."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_creature_template_not_found", "Creature template not found.", {})); return false; } @@ -431,7 +467,8 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry) Pet* pet = bot->CreateTamedPetFrom(creatureEntry, 0); if (!pet) { - botAI->TellError("Не удалось создать питомца."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_create_pet_failed", "Failed to create pet.", {})); return false; } @@ -486,13 +523,15 @@ bool TameAction::AbandonPet() // Remove the pet from the bot and mark it as deleted in the database bot->RemovePet(pet, PET_SAVE_AS_DELETED); // Inform the bot's master/player that the pet was abandoned - botAI->TellMaster("Питомец отпущен."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_pet_abandoned", "Your pet has been abandoned.", {})); return true; } else { // If there is no hunter pet, show an error message - botAI->TellError("У вас нет питомца охотника, которого можно отпустить."); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "tame_no_hunter_pet_to_abandon", "You have no hunter pet to abandon.", {})); return false; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.h index 234eaec2a..c2fdd1301 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TAMEACTION_H -#define _PLAYERBOT_TAMEACTION_H +#ifndef PLAYERBOTS_TAMEACTION_H +#define PLAYERBOTS_TAMEACTION_H #include #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp index f6c628f8c..76ccf34d4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp @@ -7,6 +7,7 @@ #include "Event.h" #include "LastMovementValue.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "PlayerbotAIConfig.h" #include "Config.h" @@ -24,17 +25,25 @@ bool TaxiAction::Execute(Event event) { movement.taxiNodes.clear(); movement.Set(nullptr); - botAI->TellMaster("Можно отправляться в следующий полёт"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "taxi_ready_next_flight", "I am ready for the next flight", {})); return true; } GuidVector units = *context->GetValue("nearest npcs"); for (ObjectGuid const guid : units) { - Creature* npc = bot->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_FLIGHTMASTER); - if (!npc) + Creature* npc = ObjectAccessor::GetCreature(*bot, guid); + if (!npc || !npc->IsAlive()) continue; + if (!(npc->GetNpcFlags() & UNIT_NPC_FLAG_FLIGHTMASTER)) + continue; + + if (bot->GetDistance(npc) > sPlayerbotAIConfig.farDistance) + continue; + + bot->GetSession()->SendLearnNewTaxiNode(npc); uint32 curloc = sObjectMgr->GetNearestTaxiNode(npc->GetPositionX(), npc->GetPositionY(), npc->GetPositionZ(), npc->GetMapId(), bot->GetTeamId()); @@ -50,21 +59,17 @@ bool TaxiAction::Execute(Event event) } } - // stagger bot takeoff - uint32 delayMin = sConfigMgr->GetOption("AiPlayerbot.BotTaxiDelayMinMs", 350u, false); - uint32 delayMax = sConfigMgr->GetOption("AiPlayerbot.BotTaxiDelayMaxMs", 5000u, false); - uint32 gapMs = sConfigMgr->GetOption("AiPlayerbot.BotTaxiGapMs", 200u, false); - uint32 gapJitterMs = sConfigMgr->GetOption("AiPlayerbot.BotTaxiGapJitterMs", 100u, false); - // Only for follower bots if (botAI->HasRealPlayerMaster()) { uint32 index = botAI->GetGroupSlotIndex(bot); - uint32 delay = delayMin + index * gapMs + urand(0, gapJitterMs); + uint32 delay = sPlayerbotAIConfig.botTaxiDelayMin + + index * sPlayerbotAIConfig.botTaxiGapMs + + urand(0, sPlayerbotAIConfig.botTaxiGapJitterMs); - delay = std::min(delay, delayMax); + delay = std::min(delay, sPlayerbotAIConfig.botTaxiDelayMax); - // Store the npc’s GUID so we can re-acquire the pointer later + // Store the NPC's GUID so we can re-acquire the pointer later ObjectGuid npcGuid = npc->GetGUID(); // schedule the take-off @@ -118,13 +123,15 @@ bool TaxiAction::Execute(Event event) { movement.taxiNodes.clear(); movement.Set(nullptr); - botAI->TellError("Я не могу лететь вместе с вами"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "taxi_cant_fly_with_you", "I can't fly with you", {})); return false; } return true; } - botAI->TellError("Не удалось найти распорядителя полётов"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "taxi_no_flightmaster_nearby", "Cannot find any flightmaster to talk", {})); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.h index 97cbda6f5..5d9ed0376 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TAXIACTION_H -#define _PLAYERBOT_TAXIACTION_H +#ifndef PLAYERBOTS_TAXIACTION_H +#define PLAYERBOTS_TAXIACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp index 17a6b7ca2..570d2d418 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TeleportAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.h index 46e316f8e..3ab1e4308 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELEPORTACTION_H -#define _PLAYERBOT_TELEPORTACTION_H +#ifndef PLAYERBOTS_TELEPORTACTION_H +#define PLAYERBOTS_TELEPORTACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp index 1f58d2069..329d7bd2a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellCastFailedAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.h index 5eec7a43a..0a1330ae9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLCASTFAILEDACTION_H -#define _PLAYERBOT_TELLCASTFAILEDACTION_H +#ifndef PLAYERBOTS_TELLCASTFAILEDACTION_H +#define PLAYERBOTS_TELLCASTFAILEDACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.cpp new file mode 100644 index 000000000..587d96aca --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.cpp @@ -0,0 +1,40 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TellEmblemsAction.h" + +#include + +#include "Event.h" +#include "Playerbots.h" + +bool TellEmblemsAction::Execute(Event /*event*/) +{ + static std::array const emblemIds = { + 29434, // Badge of Justice + 40752, // Emblem of Heroism + 40753, // Emblem of Valor + 45624, // Emblem of Conquest + 47241, // Emblem of Triumph + 49426 // Emblem of Frost + }; + + botAI->TellMaster("=== Emblems ==="); + + for (uint32 itemId : emblemIds) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + continue; + + uint32 count = bot->GetItemCount(itemId, false); + std::ostringstream out; + out << chat->FormatItem(proto, count); + botAI->TellMaster(out); + } + + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.h new file mode 100644 index 000000000..3d8a515d7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellEmblemsAction.h @@ -0,0 +1,22 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TELLEMBLEMSACTION_H +#define PLAYERBOTS_TELLEMBLEMSACTION_H + +#include "InventoryAction.h" + +class PlayerbotAI; + +class TellEmblemsAction : public InventoryAction +{ +public: + TellEmblemsAction(PlayerbotAI* botAI) : InventoryAction(botAI, "emblems") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp index fd961b5d4..abe4c1315 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellGlyphsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.h index 77bb5b1ca..a710e4c5b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLGLYPHSACTION_H -#define _PLAYERBOT_TELLGLYPHSACTION_H +#ifndef PLAYERBOTS_TELLGLYPHSACTION_H +#define PLAYERBOTS_TELLGLYPHSACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp index 89a5baf4a..757edbc5e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellItemCountAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.h index 3bc4cf79d..c81af829a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLITEMCOUNTACTION_H -#define _PLAYERBOT_TELLITEMCOUNTACTION_H +#ifndef PLAYERBOTS_TELLITEMCOUNTACTION_H +#define PLAYERBOTS_TELLITEMCOUNTACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp index dc8ae4c91..8f7a3c508 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellLosAction.h" @@ -95,7 +96,6 @@ bool TellAuraAction::Execute(Event /*event*/) std::string caster_name = caster ? caster->GetName() : "unknown"; bool is_area = aura->IsArea(); int32 duration = aura->GetDuration(); - const SpellInfo* spellInfo = aura->GetSpellInfo(); int32 spellId = aura->GetSpellInfo()->Id; bool isPositive = aura->GetSpellInfo()->IsPositive(); sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.h index d4ae928e6..7d3a32593 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLLOSACTION_H -#define _PLAYERBOT_TELLLOSACTION_H +#ifndef PLAYERBOTS_TELLLOSACTION_H +#define PLAYERBOTS_TELLLOSACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp index 714c5ede1..373ef120b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellMasterAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.h index 5a8821206..34009eb55 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLMASTERACTION_H -#define _PLAYERBOT_TELLMASTERACTION_H +#ifndef PLAYERBOTS_TELLMASTERACTION_H +#define PLAYERBOTS_TELLMASTERACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.cpp index 943cf415c..a2ac1ea6a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellPvpStatsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.h index 025cbd0d8..9f4ca0a27 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellPvpStatsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLPVPSTATSACTION_H -#define _PLAYERBOT_TELLPVPSTATSACTION_H +#ifndef PLAYERBOTS_TELLPVPSTATSACTION_H +#define PLAYERBOTS_TELLPVPSTATSACTION_H #include "Action.h" @@ -17,4 +18,4 @@ public: bool Execute(Event event) override; }; -#endif \ No newline at end of file +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp index 4750d2b84..c16edcc81 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp @@ -1,38 +1,28 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellReputationAction.h" +#include + #include "Event.h" #include "PlayerbotAI.h" #include "ReputationMgr.h" -bool TellReputationAction::Execute(Event /*event*/) +#include "SharedDefines.h" + +std::string TellReputationAction::BuildReputationLine(FactionEntry const* entry) { - Player* master = GetMaster(); - if (!master) - return false; - - ObjectGuid selection = master->GetTarget(); - if (selection.IsEmpty()) - return false; - - Unit* unit = ObjectAccessor::GetUnit(*master, selection); - if (!unit) - return false; - - FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry(); - uint32 faction = factionTemplate->faction; - FactionEntry const* entry = sFactionStore.LookupEntry(faction); - int32 reputation = bot->GetReputationMgr().GetReputation(faction); + ReputationMgr& repMgr = bot->GetReputationMgr(); + ReputationRank rank = repMgr.GetRank(entry); + int32 reputation = repMgr.GetReputation(entry->ID); std::ostringstream out; - out << entry->name[0] << ": "; - out << "|cff"; + out << entry->name[0] << ": |cff"; - ReputationRank rank = bot->GetReputationMgr().GetRank(entry); switch (rank) { case REP_HATED: @@ -71,7 +61,65 @@ bool TellReputationAction::Execute(Event /*event*/) base -= ReputationMgr::PointsInRank[i]; out << " (" << (reputation - base) << "/" << ReputationMgr::PointsInRank[rank] << ")"; - botAI->TellMaster(out); + return out.str(); +} + +bool TellReputationAction::Execute(Event event) +{ + std::string const param = event.getParam(); + if (param == "all") + { + ReputationMgr& repMgr = bot->GetReputationMgr(); + std::vector lines; + + FactionStateList const& stateList = repMgr.GetStateList(); + lines.reserve(stateList.size()); + + for (auto const& itr : stateList) + { + FactionState const& faction = itr.second; + if (!(faction.Flags & FACTION_FLAG_VISIBLE)) + continue; + + if (faction.Flags & (FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED) && + !(faction.Flags & FACTION_FLAG_SPECIAL)) + continue; + + FactionEntry const* entry = sFactionStore.LookupEntry(faction.ID); + if (!entry) + continue; + + lines.push_back(BuildReputationLine(entry)); + } + + std::sort(lines.begin(), lines.end()); + + botAI->TellMaster("=== Reputations ==="); + for (auto const& line : lines) + botAI->TellMaster(line); + + return true; + } + + Player* master = GetMaster(); + if (!master) + return false; + + ObjectGuid selection = master->GetTarget(); + if (selection.IsEmpty()) + return false; + + Unit* unit = ObjectAccessor::GetUnit(*master, selection); + if (!unit) + return false; + + FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry(); + + FactionEntry const* entry = sFactionStore.LookupEntry(factionTemplate->faction); + if (!entry) + return false; + + botAI->TellMaster(BuildReputationLine(entry)); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.h index 3adaa66d5..26a5b44f6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.h @@ -1,13 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLREPUTATIONACTION_H -#define _PLAYERBOT_TELLREPUTATIONACTION_H +#ifndef PLAYERBOTS_TELLREPUTATIONACTION_H +#define PLAYERBOTS_TELLREPUTATIONACTION_H + +#include #include "Action.h" +struct FactionEntry; class PlayerbotAI; class TellReputationAction : public Action @@ -16,6 +20,9 @@ public: TellReputationAction(PlayerbotAI* botAI) : Action(botAI, "reputation") {} bool Execute(Event event) override; + +private: + std::string BuildReputationLine(FactionEntry const* entry); }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp index 60e3f379c..c7619330c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellTargetAction.h" #include "Event.h" -#include "ThreatMgr.h" +#include "CombatManager.h" +#include "ThreatManager.h" #include "AiObjectContext.h" #include "PlayerbotAI.h" @@ -42,21 +44,21 @@ bool TellAttackersAction::Execute(Event /*event*/) botAI->TellMaster("--- Угроза ---"); - HostileReference* ref = bot->getHostileRefMgr().getFirst(); - if (!ref) + auto const& threatenedByMe = bot->GetThreatMgr().GetThreatenedByMeList(); + if (threatenedByMe.empty()) return true; - while (ref) + for (auto const& [guid, ref] : threatenedByMe) { - ThreatMgr* threatMgr = ref->GetSource(); - Unit* unit = threatMgr->GetOwner(); + Unit* unit = ref->GetOwner(); + if (!unit) + continue; + float threat = ref->GetThreat(); std::ostringstream out; out << unit->GetName() << " (" << threat << ")"; botAI->TellMaster(out); - - ref = ref->next(); } return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.h index 440e42f21..2438867d4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLTARGETACTION_H -#define _PLAYERBOT_TELLTARGETACTION_H +#ifndef PLAYERBOTS_TELLTARGETACTION_H +#define PLAYERBOTS_TELLTARGETACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp index ab4f62463..9d4a961a3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TradeAction.h" @@ -77,7 +78,7 @@ bool TradeAction::Execute(Event event) continue; int8 slot = item->CanBeTraded() ? -1 : TRADE_SLOT_NONTRADED; - if (TradeItem(item, slot) && slot != TRADE_SLOT_NONTRADED && ++traded >= count) + if (TradeItem(item, slot) && slot != TRADE_SLOT_NONTRADED && ++traded >= uint32(count)) break; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.h index 4b81d24dd..c44baebf6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRADEACTION_H -#define _PLAYERBOT_TRADEACTION_H +#ifndef PLAYERBOTS_TRADEACTION_H +#define PLAYERBOTS_TRADEACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp index ed2af344f..e087eda95 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TradeStatusAction.h" @@ -12,6 +13,7 @@ #include "ItemVisitors.h" #include "PlayerbotMgr.h" #include "PlayerbotSecurity.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "RandomPlayerbotMgr.h" #include "SetCraftAction.h" @@ -67,13 +69,17 @@ bool TradeStatusAction::Execute(Event event) // Allow the master and group members to trade if (trader != master && !traderBotAI && (!bot->GetGroup() || !bot->GetGroup()->IsMember(trader->GetGUID()))) { - bot->Whisper("Сейчас мне не до обмена", LANG_UNIVERSAL, trader); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_busy_now", "I'm kind of busy now", {}), + LANG_UNIVERSAL, trader); return false; } if (sPlayerbotAIConfig.enableRandomBotTrading == 0 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot))) { - bot->Whisper("Обмен отключён", LANG_UNIVERSAL, trader); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_disabled", "Trading is disabled", {}), + LANG_UNIVERSAL, trader); return false; } @@ -103,8 +109,6 @@ bool TradeStatusAction::Execute(Event event) uint32 discount = sRandomPlayerbotMgr.GetTradeDiscount(bot, trader); if (CheckTrade()) { - int32 botMoney = CalculateCost(bot, true); - std::map givenItemIds, takenItemIds; for (uint32 slot = 0; slot < TRADE_SLOT_TRADED_COUNT; ++slot) { @@ -200,15 +204,11 @@ bool TradeStatusAction::CheckTrade() if (!botAI->HasActivePlayerMaster() && GET_PLAYERBOT_AI(bot->GetTrader())) { - bool isGivingItem = false; for (uint32 slot = 0; slot < TRADE_SLOT_TRADED_COUNT; ++slot) { Item* item = bot->GetTradeData()->GetItem((TradeSlots)slot); if (item) - { - isGivingItem = true; break; - } } bool isGettingItem = false; for (uint32 slot = 0; slot < TRADE_SLOT_TRADED_COUNT; ++slot) @@ -225,9 +225,15 @@ bool TradeStatusAction::CheckTrade() { if (bot->GetGroup() && bot->GetGroup()->IsMember(bot->GetTrader()->GetGUID()) && botAI->HasRealPlayerMaster()) - botAI->TellMasterNoFacing("Спасибо, " + chat->FormatWorldobject(bot->GetTrader())); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_thank_you_player", + "Thank you %player", + {{"%player", chat->FormatWorldobject(bot->GetTrader())}})); else - bot->Say("Спасибо, " + chat->FormatWorldobject(bot->GetTrader()), + bot->Say(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_thank_you_player", + "Thank you %player", + {{"%player", chat->FormatWorldobject(bot->GetTrader())}}), (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); } return isGettingItem; @@ -255,12 +261,16 @@ bool TradeStatusAction::CheckTrade() int32 playerMoney = trader->GetTradeData()->GetMoney() + playerItemsMoney; if (botItemsMoney > 0 && sPlayerbotAIConfig.enableRandomBotTrading == 2 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot))) { - bot->Whisper("Продажа отключена.", LANG_UNIVERSAL, trader); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_selling_disabled", "Selling is disabled.", {}), + LANG_UNIVERSAL, trader); return false; } if (playerItemsMoney && sPlayerbotAIConfig.enableRandomBotTrading == 3 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot))) { - bot->Whisper("Покупка отключена.", LANG_UNIVERSAL, trader); + bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_buying_disabled", "Buying is disabled.", {}), + LANG_UNIVERSAL, trader); return false; } for (uint32 slot = 0; slot < TRADE_SLOT_TRADED_COUNT; ++slot) @@ -269,8 +279,10 @@ bool TradeStatusAction::CheckTrade() if (item && !item->GetTemplate()->SellPrice && !item->GetTemplate()->IsConjuredConsumable()) { std::ostringstream out; - out << chat->FormatItem(item->GetTemplate()) << " - This is not for sale"; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_item_not_for_sale", + "%item - This is not for sale", + {{"%item", chat->FormatItem(item->GetTemplate())}})); botAI->PlaySound(TEXT_EMOTE_NO); return false; } @@ -284,8 +296,10 @@ bool TradeStatusAction::CheckTrade() if ((botMoney && !item->GetTemplate()->BuyPrice) || usage == ITEM_USAGE_NONE) { std::ostringstream out; - out << chat->FormatItem(item->GetTemplate()) << " - I don't need this"; - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_item_not_needed", + "%item - I don't need this", + {{"%item", chat->FormatItem(item->GetTemplate())}})); botAI->PlaySound(TEXT_EMOTE_NO); return false; } @@ -297,7 +311,8 @@ bool TradeStatusAction::CheckTrade() if (!botItemsMoney && !playerItemsMoney) { - botAI->TellError("В обмене нет предметов"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_no_items_error", "There are no items to trade", {})); return false; } @@ -311,7 +326,8 @@ bool TradeStatusAction::CheckTrade() { if (moneyDelta < 0) { - botAI->TellError("Скидку можно использовать только для покупки предметов"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_discount_buy_only", "You can use discount to buy items only", {})); botAI->PlaySound(TEXT_EMOTE_NO); return false; } @@ -327,16 +343,20 @@ bool TradeStatusAction::CheckTrade() switch (urand(0, 4)) { case 0: - botAI->TellMaster("Сделка состоялась"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_success_pleasure", "A pleasure doing business with you", {})); break; case 1: - botAI->TellMaster("Честный обмен"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_success_fair_trade", "Fair trade", {})); break; case 2: - botAI->TellMaster("Спасибо"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_success_thanks", "Thanks", {})); break; case 3: - botAI->TellMaster("До встречи"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_success_off_with_you", "Off with you", {})); break; } @@ -345,8 +365,10 @@ bool TradeStatusAction::CheckTrade() } std::ostringstream out; - out << "За это нужно " << FormatMoneyRussian(-(delta + discount)); - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "trade_want_money_for_this", + "I want %money for this", + {{"%money", chat->formatMoney(-(delta + discount))}})); botAI->PlaySound(TEXT_EMOTE_NO); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.h index 4313b18d0..b0f220ead 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRADESTATUSACTION_H -#define _PLAYERBOT_TRADESTATUSACTION_H +#ifndef PLAYERBOTS_TRADESTATUSACTION_H +#define PLAYERBOTS_TRADESTATUSACTION_H #include "QueryItemUsageAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp index d74cbf159..1cbe6ac0f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "TradeStatusExtendedAction.h" #include "Event.h" #include "Player.h" @@ -67,9 +73,10 @@ bool TradeStatusExtendedAction::Execute(Event event) return false; } - if (bot->getClass() == CLASS_ROGUE && bot->HasSpell(1804) && lockbox->IsLocked()) // Pick Lock spell + constexpr uint32 SPELL_PICK_LOCK = 1804; + if (bot->getClass() == CLASS_ROGUE && bot->HasSpell(SPELL_PICK_LOCK) && lockbox->IsLocked()) { - // botAI->CastSpell(1804, bot, lockbox); // Attempt to cast Pick Lock on the lockbox + // botAI->CastSpell(SPELL_PICK_LOCK, bot, lockbox); // Attempt to cast Pick Lock on the lockbox botAI->DoSpecificAction("unlock traded item"); botAI->SetNextCheckDelay(4000); // Delay before accepting trade } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.h index 8797d8dc8..7d9b892e7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_TRADESTATUSEXTENDEDACTION_H -#define _PLAYERBOT_TRADESTATUSEXTENDEDACTION_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TRADESTATUSEXTENDEDACTION_H +#define PLAYERBOTS_TRADESTATUSEXTENDEDACTION_H #include "QueryItemUsageAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.cpp index 47a86e10d..69d4535bb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TradeValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.h b/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.h index 3f7d3e530..086a6926a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRADEVALUES_H -#define _PLAYERBOT_TRADEVALUES_H +#ifndef PLAYERBOTS_TRADEVALUES_H +#define PLAYERBOTS_TRADEVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp index 7849c0f5d..fac00c7e4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp @@ -1,85 +1,133 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TrainerAction.h" +#include "AiFactory.h" +#include "BisListMgr.h" #include "BudgetValues.h" #include "Event.h" #include "PlayerbotFactory.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" +#include "ReputationMgr.h" +#include "Trainer.h" -void TrainerAction::Learn(uint32 cost, const Trainer::Spell tSpell, std::ostringstream& msg) +bool TrainerAction::Execute(Event event) { - if (sPlayerbotAIConfig.autoTrainSpells != "free" && !botAI->HasCheat(BotCheatMask::gold)) - { - if (AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::spells) < cost) - { - msg << " - too expensive"; - return; - } + std::string const param = event.getParam(); - bot->ModifyMoney(-int32(cost)); - } + Creature* target = GetCreatureTarget(); + if (!target) + return false; - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(tSpell.SpellId); - if (!spellInfo) - return; + Trainer::Trainer* trainer = sObjectMgr->GetTrainer(target->GetEntry()); + if (!trainer) + return false; - bool learned = false; - for (uint8 j = 0; j < 3; ++j) - { - if (spellInfo->Effects[j].Effect == SPELL_EFFECT_LEARN_SPELL) - { - uint32 learnedSpell = spellInfo->Effects[j].TriggerSpell; - if (!bot->HasSpell(learnedSpell)) - { - bot->learnSpell(learnedSpell); - learned = true; - } - } - } + // NOTE: Original version uses SpellIds here, but occasionally only inserts + // a single spell ID value from parameters. If someone wants to impl multiple + // spells as parameters, check SkipSpellsListAction::parseIds as an example. + uint32 spellId = chat->parseSpell(param); - if (!learned && !bot->HasSpell(tSpell.SpellId)) - bot->learnSpell(tSpell.SpellId); + bool learnSpells = param.find("learn") != std::string::npos || sRandomPlayerbotMgr.IsRandomBot(bot) || + (sPlayerbotAIConfig.allowLearnTrainerSpells && + // TODO: Rewrite to only exclude start primary profession skills and make config dependent. + (trainer->GetTrainerType() != Trainer::Type::Tradeskill || !botAI->HasActivePlayerMaster())); - msg << " - learned"; + Iterate(target, learnSpells, spellId); + + return true; } -void TrainerAction::Iterate(Creature* creature, TrainerSpellAction action, SpellIds& spells) +bool TrainerAction::isUseful() +{ + Creature* target = GetCreatureTarget(); + if (!target) + return false; + + if (!target->IsInWorld() || target->IsDuringRemoveFromWorld() || !target->IsAlive()) + return false; + + return target->IsTrainer(); +} + +bool TrainerAction::isPossible() +{ + Creature* target = GetCreatureTarget(); + if (!target) + return false; + + Trainer::Trainer* trainer = sObjectMgr->GetTrainer(target->GetEntry()); + if (!trainer) + return false; + + if (!trainer->IsTrainerValidForPlayer(bot)) + return false; + + if (trainer->GetSpells().empty()) + return false; + + return true; +} + +Unit* TrainerAction::GetTarget() +{ + // There are just two scenarios: the bot has a master or it doesn't. If the + // bot has a master, the master should target a unit; otherwise, the bot + // should target the unit itself. + if (Player* master = GetMaster()) + return master->GetSelectedUnit(); + + return bot->GetSelectedUnit(); +} + +Creature* TrainerAction::GetCreatureTarget() +{ + Unit* target = GetTarget(); + return target ? target->ToCreature() : nullptr; +} + +void TrainerAction::Iterate(Creature* creature, bool learnSpells, uint32 spellId) { TellHeader(creature); Trainer::Trainer* trainer = sObjectMgr->GetTrainer(creature->GetEntry()); - if (!trainer) return; - float fDiscountMod = bot->GetReputationPriceDiscount(creature); + float reputationDiscount = bot->GetReputationPriceDiscount(creature); uint32 totalCost = 0; for (auto& spell : trainer->GetSpells()) { - if (!trainer->CanTeachSpell(bot, trainer->GetSpell(spell.SpellId))) + // simplified version of Trainer::TeachSpell method + + Trainer::Spell const* trainerSpell = trainer->GetSpell(spell.SpellId); + if (!trainerSpell) continue; - if (!spells.empty() && spells.find(spell.SpellId) == spells.end()) + if (!trainer->CanTeachSpell(bot, trainerSpell)) continue; - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell.SpellId); + if (spellId && trainerSpell->SpellId != spellId) + continue; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(trainerSpell->SpellId); if (!spellInfo) continue; - uint32 cost = uint32(floor(spell.MoneyCost * fDiscountMod)); + uint32 cost = static_cast(floor(trainerSpell->MoneyCost * reputationDiscount)); totalCost += cost; std::ostringstream out; out << chat->FormatSpell(spellInfo) << chat->formatMoney(cost); - if (action) - (this->*action)(cost, spell, out); + if (learnSpells) + Learn(spellInfo, cost, out); botAI->TellMaster(out); } @@ -87,55 +135,25 @@ void TrainerAction::Iterate(Creature* creature, TrainerSpellAction action, Spell TellFooter(totalCost); } -bool TrainerAction::Execute(Event event) +void TrainerAction::Learn(SpellInfo const* spellInfo, uint32 cost, std::ostringstream& out) { - std::string const text = event.getParam(); - - Player* master = GetMaster(); - - Creature* creature = botAI->GetCreature(bot->GetTarget()); - - if (master) + if (!botAI->HasCheat(BotCheatMask::gold)) { - creature = master->GetSelectedUnit() ? master->GetSelectedUnit()->ToCreature() : nullptr; - } - // if (AI_VALUE(GuidPosition, "rpg target") != bot->GetTarget()) - // if (master) - // creature = botAI->GetCreature(master->GetTarget()); - // else - // return false; + if (AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::spells) < cost) + { + out << " - too expensive"; + return; + } - if (!creature || !creature->IsTrainer()) - return false; - - Trainer::Trainer* trainer = sObjectMgr->GetTrainer(creature->GetEntry()); - - if (!trainer || !trainer->IsTrainerValidForPlayer(bot)) - return false; - - std::vector trainer_spells = trainer->GetSpells(); - - if (trainer_spells.empty()) - { - botAI->TellError("У этого тренера нечему учиться"); - return false; + bot->ModifyMoney(-static_cast(cost)); } - uint32 spell = chat->parseSpell(text); - SpellIds spells; - if (spell) - spells.insert(spell); - - if (text.find("learn") != std::string::npos || sRandomPlayerbotMgr.IsRandomBot(bot) || - (sPlayerbotAIConfig.autoTrainSpells != "no" && - (trainer->GetTrainerType() != Trainer::Type::Tradeskill || - !botAI->HasActivePlayerMaster()))) // Todo rewrite to only exclude start primary profession skills and make - // config dependent. - Iterate(creature, &TrainerAction::Learn, spells); + if (spellInfo->HasEffect(SPELL_EFFECT_LEARN_SPELL)) + bot->CastSpell(bot, spellInfo->Id, true); else - Iterate(creature, nullptr, spells); + bot->learnSpell(spellInfo->Id, false); - return true; + out << " - learned"; } void TrainerAction::TellHeader(Creature* creature) @@ -166,7 +184,7 @@ bool MaintenanceAction::Execute(Event /*event*/) botAI->TellMaster("Выполняю обслуживание"); PlayerbotFactory factory(bot, bot->GetLevel()); - if (!botAI->IsAlt()) + if (!botAI->IsAltBot()) { factory.InitAttunementQuests(); factory.InitBags(false); @@ -245,7 +263,8 @@ bool MaintenanceAction::Execute(Event /*event*/) if (sPlayerbotAIConfig.altMaintenanceKeyring) factory.InitKeyring(); - if (sPlayerbotAIConfig.altMaintenanceGemsEnchants && bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel) + if (sPlayerbotAIConfig.altMaintenanceGemsEnchants && + bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel) factory.ApplyEnchantAndGemsNew(); } @@ -255,6 +274,282 @@ bool MaintenanceAction::Execute(Event /*event*/) return true; } +bool BisGearAction::RunAutogearFallback(uint16 effectiveIlvl) +{ + if (!sPlayerbotAIConfig.autoGearCommand) + { + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_autogear_unavailable_error", + "autogear command is not allowed, please check the configuration.", {})); + return false; + } + + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_no_rows_fallback", + "No BiS for your tier/spec/level, check cfg, running autogear instead", {})); + + // Wipe all equipped slots so autogear gears from scratch at the requested ilvl + // (avoids old high-tier items surviving the incremental 1.2x threshold). + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY) + continue; + if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); + } + + uint32 gs = effectiveIlvl == 0 + ? 0 + : PlayerbotFactory::CalcMixedGearScore(effectiveIlvl, sPlayerbotAIConfig.autoGearQualityLimit); + PlayerbotFactory factory(bot, bot->GetLevel(), sPlayerbotAIConfig.autoGearQualityLimit, gs); + factory.InitEquipment(false, sPlayerbotAIConfig.twoRoundsGearInit); + factory.InitAmmo(); + if (bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel) + factory.ApplyEnchantAndGemsNew(); + bot->DurabilityRepairAll(false, 1.0f, false); + return true; +} + +bool BisGearAction::Execute(Event event) +{ + if (!sPlayerbotAIConfig.autoGearBisCommand) + { + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_command_unavailable_error", + "bis command is not allowed, please check the configuration.", {})); + return false; + } + + if (!sPlayerbotAIConfig.autoGearCommandAltBots && + !sPlayerbotAIConfig.IsInRandomAccountList(bot->GetSession()->GetAccountId())) + { + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_altbot_refused_error", "You cannot use bis on alt bots.", {})); + return false; + } + + if (sPlayerbotAIConfig.autoGearQualityLimit < 4) + { + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_quality_floor_error", "AutoGearQualityLimit must be 4 for BiS.", {})); + return false; + } + + if (sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass())) + { + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_pvp_refused_error", "bis is PvE only, bot is configured as PvP.", {})); + return false; + } + + uint16 ilvl = static_cast(sPlayerbotAIConfig.autoGearScoreLimit); + + // Optional explicit ilvl override: `/p autogear bis 55`. + // Garbage or out-of-range args are hard-rejected: no autogear fallback, no gear change. + std::string const param = event.getParam(); + if (!param.empty()) + { + unsigned long parsed = 0; + size_t pos = 0; + bool valid = false; + try + { + parsed = std::stoul(param, &pos); + valid = (parsed > 0 && pos == param.size() && parsed <= 0xFFFFu); + } + catch (...) + { + valid = false; + } + + if (!valid) + { + std::map phs; + phs["%param"] = param; + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_invalid_arg_error", + "Invalid BiS ilvl argument '%param'. Use a positive integer.", phs)); + return false; + } + if (parsed > static_cast(sPlayerbotAIConfig.autoGearScoreLimit)) + { + std::map phs; + phs["%requested"] = std::to_string(parsed); + phs["%limit"] = std::to_string(sPlayerbotAIConfig.autoGearScoreLimit); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_arg_above_limit_error", + "BiS ilvl %requested exceeds AutoGearScoreLimit %limit, refusing", phs)); + return false; + } + ilvl = static_cast(parsed); + } + uint8 cls = bot->getClass(); + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + uint8 faction = bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 2; + + // Druid Bear (Feral Tank) shares tab 1 with Cat. Use sentinel tab 10 when tank strategy active. + constexpr uint8 BIS_TAB_DRUID_BEAR = 10; + constexpr uint16 BIS_ILVL_FALLBACK_WINDOW = 20; + uint16 resolvedIlvl = 0; + std::map bisMap; + if (cls == CLASS_DRUID && tab == DRUID_TAB_FERAL && PlayerbotAI::IsTank(bot)) + bisMap = sBisListMgr->GetBisForNearest(ilvl, BIS_ILVL_FALLBACK_WINDOW, cls, BIS_TAB_DRUID_BEAR, faction, + &resolvedIlvl); + if (bisMap.empty()) + bisMap = sBisListMgr->GetBisForNearest(ilvl, BIS_ILVL_FALLBACK_WINDOW, cls, tab, faction, &resolvedIlvl); + + // No rows within fallback window -> full autogear fallback at the effective ilvl. + if (bisMap.empty()) + { + std::map phs; + phs["%ilvl"] = std::to_string(ilvl); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_no_rows_autogear_msg", + "No BiS at ilvl %ilvl, using Autogear %ilvl instead", phs)); + return RunAutogearFallback(ilvl); + } + + if (resolvedIlvl != ilvl) + { + std::map phs; + phs["%requested"] = std::to_string(ilvl); + phs["%resolved"] = std::to_string(resolvedIlvl); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_closest_match_msg", + "No BiS at ilvl %requested, using closest match at ilvl %resolved", phs)); + } + + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_applying_msg", "Applying BiS gear", {})); + + // 1. Wipe everything currently equipped so autogear starts from a clean slate. + // Old items linger in inventory otherwise and autogear leaves slots empty on bag conflicts. + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY) + continue; + if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); + } + + // Wipe equippable items from bags too. Autogear can shove old equipped items into bags + // (HandleAutoStoreBagItemOpcode), and a unique-equipped duplicate stuck in a bag blocks + // CanEquipNewItem on subsequent BiS runs. Spare consumables/reagents. + auto destroyIfEquippable = [&](uint8 bag, uint8 slot) + { + Item* item = bot->GetItemByPos(bag, slot); + if (!item) + return; + ItemTemplate const* tmpl = item->GetTemplate(); + if (!tmpl) + return; + if (tmpl->Class == ITEM_CLASS_WEAPON || tmpl->Class == ITEM_CLASS_ARMOR) + bot->DestroyItem(bag, slot, true); + }; + for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + destroyIfEquippable(INVENTORY_SLOT_BAG_0, slot); + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + if (Bag* container = bot->GetBagByPos(bag)) + for (uint32 slot = 0; slot < container->GetBagSize(); ++slot) + destroyIfEquippable(bag, slot); + } + + // 2. Run full autogear on the empty bot so every slot gets a best-available pick. + // Uncovered slots will keep the autogear pick; BiS overwrites the rest below. + if (sPlayerbotAIConfig.autoGearCommand) + { + uint32 fillGs = ilvl == 0 + ? 0 + : PlayerbotFactory::CalcMixedGearScore(ilvl, sPlayerbotAIConfig.autoGearQualityLimit); + PlayerbotFactory fillFactory(bot, bot->GetLevel(), sPlayerbotAIConfig.autoGearQualityLimit, fillGs); + fillFactory.InitEquipment(false, sPlayerbotAIConfig.twoRoundsGearInit); + } + + // 2b. Pre-destroy autogear picks that would conflict with any BiS item by entry. + // Autogear may have placed the exact item BiS wants into trinket2/finger2 (or vice versa); + // unique-equipped enforcement would then make BiS's equip silently drop one copy. + std::set bisEntries; + for (auto const& kv : bisMap) + bisEntries.insert(kv.second); + for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot) + { + if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + if (bisEntries.count(item->GetEntry())) + bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); + } + + // 3. Apply BiS: only touch slots where the bot can actually equip the BiS item. + // If item requires reputation, grant the required rank first. If CanUseItem still + // fails (class/race/skill/level), keep autogear's pick for that slot. + for (auto const& kv : bisMap) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(kv.second); + if (!proto) + continue; + + // Grant required reputation rank if the item gates on it. + if (proto->RequiredReputationFaction && proto->RequiredReputationRank > 0) + { + if (FactionEntry const* fac = sFactionStore.LookupEntry(proto->RequiredReputationFaction)) + { + ReputationRank requiredRank = static_cast(proto->RequiredReputationRank); + if (bot->GetReputationRank(proto->RequiredReputationFaction) < requiredRank) + { + int32 standing = ReputationMgr::ReputationRankToStanding( + static_cast(requiredRank - 1)) + 1; + bot->GetReputationMgr().SetReputation(fac, standing); + } + } + } + + if (bot->CanUseItem(proto) != EQUIP_ERR_OK) + continue; + + uint8 slot = kv.first; + if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); + + uint16 dest = 0; + InventoryResult eqResult = bot->CanEquipNewItem(slot, dest, kv.second, false); + + // Paired slots (finger 10<->11, trinket 12<->13): destroy paired slot and retry once + // when unique-equipped or autogear residue blocks the first attempt. + if (eqResult != EQUIP_ERR_OK) + { + uint8 pairedSlot = 0xFF; + if (slot == EQUIPMENT_SLOT_FINGER1) pairedSlot = EQUIPMENT_SLOT_FINGER2; + else if (slot == EQUIPMENT_SLOT_FINGER2) pairedSlot = EQUIPMENT_SLOT_FINGER1; + else if (slot == EQUIPMENT_SLOT_TRINKET1) pairedSlot = EQUIPMENT_SLOT_TRINKET2; + else if (slot == EQUIPMENT_SLOT_TRINKET2) pairedSlot = EQUIPMENT_SLOT_TRINKET1; + + if (pairedSlot != 0xFF) + { + if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, pairedSlot)) + bot->DestroyItem(INVENTORY_SLOT_BAG_0, pairedSlot, true); + eqResult = bot->CanEquipNewItem(slot, dest, kv.second, false); + } + } + + if (eqResult == EQUIP_ERR_OK) + { + bot->EquipNewItem(dest, kv.second, true); + bot->AutoUnequipOffhandIfNeed(); + } + } + + PlayerbotFactory factory(bot, bot->GetLevel(), ITEM_QUALITY_EPIC, 0); + factory.InitAmmo(); + if (bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel) + factory.ApplyEnchantAndGemsNew(); + + bot->DurabilityRepairAll(false, 1.0f, false); + + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bis_applied_msg", "BiS applied", {})); + return true; +} + bool RemoveGlyphAction::Execute(Event /*event*/) { for (uint32 slotIndex = 0; slotIndex < MAX_GLYPH_SLOT_INDEX; ++slotIndex) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.h index e6046c3a2..2664f77ad 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.h @@ -1,14 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRAINERACTION_H -#define _PLAYERBOT_TRAINERACTION_H +#ifndef PLAYERBOTS_TRAINERACTION_H +#define PLAYERBOTS_TRAINERACTION_H #include "Action.h" #include "ChatHelper.h" -#include "Trainer.h" class Creature; class PlayerbotAI; @@ -21,11 +21,14 @@ public: TrainerAction(PlayerbotAI* botAI) : Action(botAI, "trainer") {} bool Execute(Event event) override; + bool isUseful() override; + bool isPossible() override; + Unit* GetTarget() override; private: - typedef void (TrainerAction::*TrainerSpellAction)(uint32, const Trainer::Spell, std::ostringstream& msg); - void Iterate(Creature* creature, TrainerSpellAction action, SpellIds& spells); - void Learn(uint32 cost, const Trainer::Spell tSpell, std::ostringstream& msg); + Creature* GetCreatureTarget(); + void Iterate(Creature* creature, bool learnSpells, uint32 spellId); + void Learn(SpellInfo const* spellInfo, uint32 cost, std::ostringstream& out); void TellHeader(Creature* creature); void TellFooter(uint32 totalCost); }; @@ -51,4 +54,14 @@ public: bool Execute(Event event) override; }; +class BisGearAction : public Action +{ +public: + BisGearAction(PlayerbotAI* botAI) : Action(botAI, "autogear bis") {} + bool Execute(Event event) override; + +private: + bool RunAutogearFallback(uint16 effectiveIlvl); +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.cpp index 3516426ce..243548652 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TravelAction.h" @@ -37,7 +38,7 @@ bool TravelAction::Execute(Event /*event*/) if (!newTarget->IsAlive()) continue; - if (newTarget->GetEntry() == target->getDestination()->getEntry()) + if (newTarget->GetEntry() == uint32(target->getDestination()->getEntry())) continue; if (newTarget->IsInCombat()) @@ -134,8 +135,6 @@ bool MoveFromDarkPortalAction::Execute(Event /*event*/) if (bot->GetTeamId() == TEAM_ALLIANCE) return MoveTo(530, -319.261f, 1027.213, 54.172638f, false, true); - else - return MoveTo(530, -180.444f, 1027.947, 54.181538f, false, true); - return false; + return MoveTo(530, -180.444f, 1027.947, 54.181538f, false, true); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.h index d84b0c243..76c297d01 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TravelAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRAVELACTION_H -#define _PLAYERBOT_TRAVELACTION_H +#ifndef PLAYERBOTS_TRAVELACTION_H +#define PLAYERBOTS_TRAVELACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.cpp index f5b5f6285..a8d89cff8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "UnequipAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.h index 56004bebe..c3dc6b985 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnequipAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_UNEQUIPACTION_H -#define _PLAYERBOT_UNEQUIPACTION_H +#ifndef PLAYERBOTS_UNEQUIPACTION_H +#define PLAYERBOTS_UNEQUIPACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp index baf09d542..319ff5324 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "UnlockItemAction.h" #include "PlayerbotAI.h" #include "ItemTemplate.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.h index a4738da7c..6c8e3640a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_UNLOCKITEMACTION_H -#define _PLAYERBOT_UNLOCKITEMACTION_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UNLOCKITEMACTION_H +#define PLAYERBOTS_UNLOCKITEMACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp index 7173a7f71..80bda0123 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "UnlockTradedItemAction.h" #include "PlayerbotAI.h" #include "TradeData.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.h index 6349ab56b..3bee60f7a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_UNLOCKTRADEDITEMACTION_H -#define _PLAYERBOT_UNLOCKTRADEDITEMACTION_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UNLOCKTRADEDITEMACTION_H +#define PLAYERBOTS_UNLOCKTRADEDITEMACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp index 17eade154..c6753be64 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "UseItemAction.h" @@ -9,6 +10,7 @@ #include "Event.h" #include "ItemPackets.h" #include "ItemUsageValue.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" bool UseItemAction::Execute(Event event) @@ -35,7 +37,8 @@ bool UseItemAction::Execute(Event event) return UseItemOnGameObject(*items.begin(), *gos.begin()); } - botAI->TellError("Нет доступных предметов или игровых объектов"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "use_item_none_available", "No items (or game objects) available", {})); return false; } @@ -48,8 +51,10 @@ bool UseItemAction::UseGameObject(ObjectGuid guid) go->Use(bot); std::ostringstream out; - out << "Использую: " << chat->FormatGameobject(go); - botAI->TellMasterNoFacing(out.str()); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "use_gameobject", + "Using %gameobject", + {{"%gameobject", chat->FormatGameobject(go)}})); return true; } @@ -69,7 +74,6 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni uint8 bagIndex = item->GetBagSlot(); uint8 slot = item->GetSlot(); - uint8 spell_index = 0; uint8 cast_count = 1; ObjectGuid item_guid = item->GetGUID(); uint32 glyphIndex = 0; @@ -93,16 +97,16 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni bool targetSelected = false; - std::ostringstream out; - out << "Использую: " << chat->FormatItem(item->GetTemplate()); + std::string itemText = chat->FormatItem(item->GetTemplate()); + std::string targetText; if (item->GetTemplate()->Stackable > 1) { uint32 count = item->GetCount(); if (count > 1) - out << " (" << count << " available) "; + itemText += " (" + std::to_string(count) + " available)"; else - out << " (the last one!)"; + itemText += " (the last one!)"; } if (goGuid) @@ -115,7 +119,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni packet << targetFlag; packet << goGuid.WriteAsPacked(); - out << " on " << chat->FormatGameobject(go); + targetText = chat->FormatGameobject(go); targetSelected = true; } @@ -125,7 +129,8 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni { bool fit = SocketItem(itemTarget, item) || SocketItem(itemTarget, item, true); if (!fit) - botAI->TellMaster("Предмет не подходит для этой ячейки"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "socket_does_not_fit", "Socket does not fit", {})); return fit; } @@ -134,7 +139,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni targetFlag = TARGET_FLAG_ITEM; packet << targetFlag; packet << itemTarget->GetGUID().WriteAsPacked(); - out << " on " << chat->FormatItem(itemTarget->GetTemplate()); + targetText = chat->FormatItem(itemTarget->GetTemplate()); targetSelected = true; } } @@ -150,7 +155,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni { targetFlag = TARGET_FLAG_UNIT; packet << targetFlag << masterSelection.WriteAsPacked(); - out << " on " << unit->GetName(); + targetText = unit->GetName(); targetSelected = true; } } @@ -160,7 +165,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni { targetFlag = TARGET_FLAG_UNIT; packet << targetFlag << unitTarget->GetGUID().WriteAsPacked(); - out << " on " << unitTarget->GetName(); + targetText = unitTarget->GetName(); targetSelected = true; } @@ -174,9 +179,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni packet << uint32(0); bot->GetSession()->HandleQuestgiverAcceptQuestOpcode(packet); - std::ostringstream out; - out << "Получено задание: " << chat->FormatQuest(qInfo); - botAI->TellMasterNoFacing(out.str()); + botAI->TellMasterNoFacing("Got quest " + chat->FormatQuest(qInfo)); return true; } } @@ -218,7 +221,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni targetFlag = TARGET_FLAG_TRADE_ITEM; packet << targetFlag << (uint8)1 << ObjectGuid((uint64)TRADE_SLOT_NONTRADED).WriteAsPacked(); targetSelected = true; - out << " on traded item"; + targetText = "traded item"; } else { @@ -226,7 +229,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni packet << targetFlag; packet << itemForSpell->GetGUID().WriteAsPacked(); targetSelected = true; - out << " on " << chat->FormatItem(itemForSpell->GetTemplate()); + targetText = chat->FormatItem(itemForSpell->GetTemplate()); } uint32 castTime = spellInfo->CalcCastTime(); botAI->SetNextCheckDelay(castTime + sPlayerbotAIConfig.reactDelay); @@ -247,17 +250,17 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni targetSelected = true; if (unitTarget == bot || !unitTarget->IsInWorld() || unitTarget->IsDuringRemoveFromWorld()) - out << " on self"; + targetText = "self"; else if (unitTarget->IsHostileTo(bot)) - out << " on self"; + targetText = "self"; else - out << " on " << unitTarget->GetName(); + targetText = unitTarget->GetName(); } else { packet << bot->GetPackGUID(); targetSelected = true; - out << " on self"; + targetText = "self"; } } @@ -308,7 +311,12 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni return false; // botAI->SetNextCheckDelay(sPlayerbotAIConfig.globalCoolDown); - botAI->TellMasterNoFacing(out.str()); + std::string useText = targetSelected + ? PlayerbotTextMgr::instance().GetBotTextOrDefault( + "use_item_on_target", "Using %item on %target", {{"%item", itemText}, {"%target", targetText}}) + : PlayerbotTextMgr::instance().GetBotTextOrDefault( + "use_item", "Using %item", {{"%item", itemText}}); + botAI->TellMasterNoFacing(useText); bot->GetSession()->HandleUseItemOpcode(packet); return true; } @@ -373,10 +381,10 @@ bool UseItemAction::SocketItem(Item* item, Item* gem, bool replace) if (fits) { - std::ostringstream out; - out << "Вставляю самоцвет в: " << chat->FormatItem(item->GetTemplate()); - out << " with " << chat->FormatItem(gem->GetTemplate()); - botAI->TellMaster(out); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "socketing_item_with_gem", + "Socketing %item with %gem", + {{"%item", chat->FormatItem(item->GetTemplate())}, {"%gem", chat->FormatItem(gem->GetTemplate())}})); WorldPackets::Item::SocketGems nicePacket(std::move(packet)); nicePacket.Read(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.h index 263bc29dc..38d1eaa36 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_USEITEMACTION_H -#define _PLAYERBOT_USEITEMACTION_H +#ifndef PLAYERBOTS_USEITEMACTION_H +#define PLAYERBOTS_USEITEMACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp index 24292b9dd..562cf2e76 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "UseMeetingStoneAction.h" @@ -11,6 +12,7 @@ #include "GridNotifiersImpl.h" #include "NearestGameObjects.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "PositionValue.h" @@ -36,7 +38,8 @@ bool UseMeetingStoneAction::Execute(Event event) if (bot->IsInCombat()) { - botAI->TellError("Я в бою"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_in_combat", "I am in combat", {})); return false; } @@ -61,7 +64,7 @@ bool SummonAction::Execute(Event /*event*/) if (!master) return false; - if (Pet* pet = bot->GetPet()) + if (bot->GetPet()) botAI->PetFollow(); if (master->GetSession()->GetSecurity() >= SEC_PLAYER) @@ -73,13 +76,15 @@ bool SummonAction::Execute(Event /*event*/) if (SummonUsingGos(master, bot, true) || SummonUsingNpcs(master, bot, true)) { - botAI->TellMasterNoFacing("Здравствуйте!"); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "hello", "Hello!", {})); return true; } if (SummonUsingGos(bot, master, true) || SummonUsingNpcs(bot, master, true)) { - botAI->TellMasterNoFacing("Добро пожаловать!"); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_welcome", "Welcome!", {})); return true; } @@ -99,7 +104,10 @@ bool SummonAction::SummonUsingGos(Player* summoner, Player* player, bool preserv return Teleport(summoner, player, preserveAuras); } - botAI->TellError(summoner == bot ? "There is no meeting stone nearby" : "There is no meeting stone near you"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + summoner == bot ? "meeting_stone_none_nearby" : "meeting_stone_none_near_you", + summoner == bot ? "There is no meeting stone nearby" : "There is no meeting stone near you", + {})); return false; } @@ -119,13 +127,19 @@ bool SummonAction::SummonUsingNpcs(Player* summoner, Player* player, bool preser { if (!player->HasItemCount(6948, 1, false)) { - botAI->TellError(player == bot ? "I have no hearthstone" : "You have no hearthstone"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + player == bot ? "meeting_stone_no_hearthstone_self" : "meeting_stone_no_hearthstone_you", + player == bot ? "I have no hearthstone" : "You have no hearthstone", + {})); return false; } if (player->HasSpellCooldown(8690)) { - botAI->TellError(player == bot ? "My hearthstone is not ready" : "Your hearthstone is not ready"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + player == bot ? "meeting_stone_hearthstone_not_ready_self" : "meeting_stone_hearthstone_not_ready_you", + player == bot ? "My hearthstone is not ready" : "Your hearthstone is not ready", + {})); return false; } @@ -141,7 +155,10 @@ bool SummonAction::SummonUsingNpcs(Player* summoner, Player* player, bool preser } } - botAI->TellError(summoner == bot ? "There are no innkeepers nearby" : "There are no innkeepers near you"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + summoner == bot ? "meeting_stone_no_innkeepers_nearby" : "meeting_stone_no_innkeepers_near_you", + summoner == bot ? "There are no innkeepers nearby" : "There are no innkeepers near you", + {})); return false; } @@ -153,7 +170,8 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras if (player->GetVehicle()) { - botAI->TellError("Нельзя призвать меня, пока я нахожусь в транспорте"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_cannot_summon_vehicle", "You cannot summon me while I'm on a vehicle", {})); return false; } @@ -174,20 +192,29 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras if (summoner->IsInCombat() && !sPlayerbotAIConfig.allowSummonInCombat) { - botAI->TellError("Нельзя призвать меня, пока вы в бою"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_cannot_summon_master_in_combat", + "You cannot summon me while you're in combat", + {})); return false; } if (!summoner->IsAlive() && !sPlayerbotAIConfig.allowSummonWhenMasterIsDead) { - botAI->TellError("Нельзя призвать меня, пока вы мертвы"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_cannot_summon_master_dead", + "You cannot summon me while you're dead", + {})); return false; } if (bot->isDead() && !bot->HasPlayerFlag(PLAYER_FLAGS_GHOST) && !sPlayerbotAIConfig.allowSummonWhenBotIsDead) { - botAI->TellError("Нельзя призвать меня до воскрешения. Сначала освободите мой дух"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_cannot_summon_bot_dead", + "You cannot summon me while I'm dead, you need to release my spirit first", + {})); return false; } @@ -199,7 +226,8 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras { bot->ResurrectPlayer(1.0f, false); bot->SpawnCorpseBones(); - botAI->TellMasterNoFacing("Я снова жив!"); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_revived", "I live, again!", {})); botAI->GetAiObjectContext()->GetValue("prioritized targets")->Reset(); } @@ -229,6 +257,7 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras } if (summoner != player) - botAI->TellError("Недостаточно места для призыва"); + botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "meeting_stone_not_enough_space", "Not enough place to summon", {})); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.h index fd2a2c5f2..8ad98efd1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_USEMEETINGSTONEACTION_H -#define _PLAYERBOT_USEMEETINGSTONEACTION_H +#ifndef PLAYERBOTS_USEMEETINGSTONEACTION_H +#define PLAYERBOTS_USEMEETINGSTONEACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.cpp index 71963690d..b3684ea80 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "VehicleActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.h b/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.h index 1a0e0a300..2f8fecef6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/VehicleActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_VEHICLEACTIONS_H -#define _PLAYERBOT_VEHICLEACTIONS_H +#ifndef PLAYERBOTS_VEHICLEACTIONS_H +#define PLAYERBOTS_VEHICLEACTIONS_H #include "Event.h" #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.cpp new file mode 100644 index 000000000..185a2f21c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.cpp @@ -0,0 +1,184 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + * Sebastiaan Keek (mostlikely4r) + * Cyberium + */ + +#include "WaitForAttackAction.h" + +#include +#include + +#include "ObjectAccessor.h" +#include "PlayerbotAI.h" +#include "PlayerbotTextMgr.h" +#include "Playerbots.h" +#include "ServerFacade.h" +#include "TravelMgr.h" +#include "WaitForAttackStrategy.h" + +namespace +{ + +WorldPosition GetBestPoint(AiObjectContext* context, Player* bot, Unit* target, + float minDistance, float maxDistance) +{ + WorldPosition botPosition(bot); + WorldPosition targetPosition(target); + + int8 startDir = urand(0, 1) * 2 - 1; + float const radiansIncrement = (5.0f / 180.0f) * static_cast(M_PI); + float startAngle = targetPosition.getAngleTo(botPosition) + + frand(0.0f, radiansIncrement) * startDir; + float distance = frand(minDistance, maxDistance); + + GuidVector enemies = AI_VALUE(GuidVector, "possible targets no los"); + + for (float tryAngle = 0.0f; tryAngle < static_cast(M_PI); tryAngle += radiansIncrement) + { + for (int8 tryDir = -1; tryAngle && tryDir < 1; tryDir += 2) + { + float pointAngle = startAngle + tryAngle * startDir * tryDir; + + float x = targetPosition.GetPositionX() + distance * cos(pointAngle); + float y = targetPosition.GetPositionY() + distance * sin(pointAngle); + float z = targetPosition.GetPositionZ() + 1.0f; + + WorldPosition point(targetPosition.GetMapId(), x, y, z); + + float groundZ = bot->GetMapHeight(x, y, z); + if (groundZ == INVALID_HEIGHT || groundZ == VMAP_INVALID_HEIGHT_VALUE) + continue; + + point.setZ(groundZ); + + // Check line of sight to target + if (!target->IsWithinLOS(point.GetPositionX(), point.GetPositionY(), + point.GetPositionZ() + bot->GetCollisionHeight())) + continue; + + // Check if enemies are close to this point + bool enemyClose = false; + for (ObjectGuid const& enemyGUID : enemies) + { + Unit* enemy = ObjectAccessor::GetUnit(*bot, enemyGUID); + if (enemy && enemy->IsWithinLOSInMap(bot) && enemy->IsHostileTo(bot)) + { + float enemyAttackRange = enemy->GetCombatReach() + ATTACK_DISTANCE; + WorldPosition enemyPos(enemy); + if (enemyPos.sqDistance(point) <= (enemyAttackRange * enemyAttackRange)) + { + enemyClose = true; + break; + } + } + } + + if (enemyClose) + continue; + + // Check if bot can path to this point + if (!botPosition.canPathTo(point, bot)) + continue; + + return point; + } + } + + return botPosition; +} + +} // namespace + +bool WaitForAttackKeepSafeDistanceAction::Execute(Event /*event*/) +{ + Unit* target = AI_VALUE(Unit*, "current target"); + + if (!target) + return false; + + // If our target is moving towards a stationary unit, use that unit as anchor + if (!target->IsStopped()) + { + ObjectGuid targetGuid = target->GetTarget(); + if (targetGuid) + { + Unit* targetsTarget = ObjectAccessor::GetUnit(*target, targetGuid); + if (targetsTarget && targetsTarget->IsStopped()) + target = targetsTarget; + } + } + + if (target->IsAlive()) + { + float safeDistance = std::max( + target->GetCombatReach() + ATTACK_DISTANCE, + WaitForAttackStrategy::GetSafeDistance()); + float safeDistanceThreshold = WaitForAttackStrategy::GetSafeDistanceThreshold(); + + WorldPosition bestPoint = GetBestPoint(context, bot, target, + safeDistance - safeDistanceThreshold, safeDistance); + + if (bestPoint) + return MoveTo(bestPoint.GetMapId(), bestPoint.GetPositionX(), + bestPoint.GetPositionY(), bestPoint.GetPositionZ()); + } + + return false; +} + +bool SetWaitForAttackTimeAction::Execute(Event event) +{ + std::string newTimeStr = event.getParam(); + + if (newTimeStr.empty()) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_provide_time", + "Please provide a time to set (in seconds)", + std::map()); + botAI->TellMaster(text); + return false; + } + + if (!std::all_of(newTimeStr.begin(), newTimeStr.end(), ::isdigit)) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_invalid_time", + "Please provide valid time to set (in seconds) between 0 and 99", + std::map()); + botAI->TellMaster(text); + return false; + } + + int newTime = std::stoi(newTimeStr); + if (newTime < 0 || newTime > 99) + { + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_invalid_time", + "Please provide valid time to set (in seconds) between 0 and 99", + std::map()); + botAI->TellMaster(text); + return false; + } + + context->GetValue("wait for attack time")->Set(static_cast(newTime)); + + std::map placeholders; + placeholders["%new_time"] = std::to_string(newTime); + std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "wait_for_attack_time_set", + "Wait for attack time set to %new_time seconds", + placeholders); + botAI->TellMaster(text); + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.h new file mode 100644 index 000000000..2be4a3795 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.h @@ -0,0 +1,41 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + * Sebastiaan Keek (mostlikely4r) + * Cyberium + */ + +#ifndef PLAYERBOTS_WAITFORATTACKACTION_H +#define PLAYERBOTS_WAITFORATTACKACTION_H + +#include "MovementActions.h" + +class PlayerbotAI; + +class WaitForAttackKeepSafeDistanceAction : public MovementAction +{ +public: + WaitForAttackKeepSafeDistanceAction(PlayerbotAI* botAI) + : MovementAction(botAI, "wait for attack keep safe distance") {} + + bool Execute(Event event) override; +}; + +class SetWaitForAttackTimeAction : public Action +{ +public: + SetWaitForAttackTimeAction(PlayerbotAI* botAI) + : Action(botAI, "wait for attack time") {} + + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp index fdb364752..d5169c3af 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WhoAction.h" @@ -108,7 +109,7 @@ std::string const WhoAction::QuerySkill(std::string const text) return out.str(); } -std::string const WhoAction::QuerySpec(std::string const text) +std::string const WhoAction::QuerySpec(std::string const /*text*/) { std::ostringstream out; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.h index d3ba1756d..147268802 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WHOACTION_H -#define _PLAYERBOT_WHOACTION_H +#ifndef PLAYERBOTS_WHOACTION_H +#define PLAYERBOTS_WHOACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.cpp index bbd1997d6..3aac3807b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlayerbotAI.h" @@ -8,11 +9,13 @@ bool WipeAction::Execute(Event event) { - Player* master = event.getOwner(); + Player* const owner = event.getOwner(); + Player* const master = this->botAI->GetMaster(); - if (botAI->GetMaster()->GetGUID() != event.getOwner()->GetGUID()) + if (owner != nullptr && master != nullptr && master->GetGUID() != owner->GetGUID()) return false; bot->Kill(bot, bot); + return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.h index 868119087..09dfcb4c3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WipeAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WIPEACTION_H -#define _PLAYERBOT_WIPEACTION_H +#ifndef PLAYERBOTS_WIPEACTION_H +#define PLAYERBOTS_WIPEACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.cpp index b2bf4e096..0c9a679ff 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WorldBuffAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.h index e1330e949..1356e4ee8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WorldBuffAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WORLDBUFFACTION_H -#define _PLAYERBOT_WORLDBUFFACTION_H +#ifndef PLAYERBOTS_WORLDBUFFACTION_H +#define PLAYERBOTS_WORLDBUFFACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp index bc215f5f4..0b03e448f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WtsAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.h index 22da503f8..3135a1746 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WTSACTION_H -#define _PLAYERBOT_WTSACTION_H +#ifndef PLAYERBOTS_WTSACTION_H +#define PLAYERBOTS_WTSACTION_H #include "InventoryAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.cpp index fba57180f..162efdc51 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "XpGainAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.h b/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.h index d5935a9fd..acd7c99c5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.h +++ b/modules/mod-playerbots/src/Ai/Base/Actions/XpGainAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_XPGAINACTION_H -#define _PLAYERBOT_XPGAINACTION_H +#ifndef PLAYERBOTS_XPGAINACTION_H +#define PLAYERBOTS_XPGAINACTION_H #include "Action.h" diff --git a/modules/mod-playerbots/src/Ai/Base/ChatActionContext.h b/modules/mod-playerbots/src/Ai/Base/ChatActionContext.h index 4bfdcb043..cc48528af 100644 --- a/modules/mod-playerbots/src/Ai/Base/ChatActionContext.h +++ b/modules/mod-playerbots/src/Ai/Base/ChatActionContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATACTIONCONTEXTACTION_H -#define _PLAYERBOT_CHATACTIONCONTEXTACTION_H +#ifndef PLAYERBOTS_CHATACTIONCONTEXT_H +#define PLAYERBOTS_CHATACTIONCONTEXT_H #include "AddLootAction.h" #include "AttackAction.h" @@ -37,11 +38,13 @@ #include "LogLevelAction.h" #include "LootStrategyAction.h" #include "LootRollAction.h" +#include "SetFocusHealTargetsAction.h" #include "MailAction.h" #include "NamedObjectContext.h" #include "NewRpgAction.h" #include "PassLeadershipToMasterAction.h" #include "PositionAction.h" +#include "PullActions.h" #include "QueryItemUsageAction.h" #include "QueryQuestAction.h" #include "RangeAction.h" @@ -64,6 +67,7 @@ #include "TaxiAction.h" #include "TeleportAction.h" #include "TellCastFailedAction.h" +#include "TellEmblemsAction.h" #include "TellItemCountAction.h" #include "TellLosAction.h" #include "TellReputationAction.h" @@ -84,6 +88,7 @@ #include "TellGlyphsAction.h" #include "EquipGlyphsAction.h" #include "PetsAction.h" +#include "WaitForAttackAction.h" class ChatActionContext : public NamedObjectContext { @@ -117,6 +122,7 @@ public: creators["teleport"] = &ChatActionContext::teleport; creators["taxi"] = &ChatActionContext::taxi; creators["repair"] = &ChatActionContext::repair; + creators["emblems"] = &ChatActionContext::emblems; creators["use"] = &ChatActionContext::use; creators["item count"] = &ChatActionContext::item_count; creators["equip"] = &ChatActionContext::equip; @@ -134,8 +140,11 @@ public: creators["maintenance"] = &ChatActionContext::maintenance; creators["remove glyph"] = &ChatActionContext::remove_glyph; creators["autogear"] = &ChatActionContext::autogear; + creators["autogear bis"] = &ChatActionContext::autogear_bis; 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; @@ -187,6 +196,7 @@ public: creators["guild leave"] = &ChatActionContext::guild_leave; creators["rtsc"] = &ChatActionContext::rtsc; creators["bwl chat shortcut"] = &ChatActionContext::bwl_chat_shortcut; + creators["naxx chat shortcut"] = &ChatActionContext::naxx_chat_shortcut; creators["tell estimated dps"] = &ChatActionContext::tell_estimated_dps; creators["join"] = &ChatActionContext::join; creators["lfg"] = &ChatActionContext::lfg; @@ -198,6 +208,8 @@ public: creators["pet"] = &ChatActionContext::pet; creators["pet attack"] = &ChatActionContext::pet_attack; creators["roll"] = &ChatActionContext::roll_action; + creators["wait for attack time"] = &ChatActionContext::wait_for_attack_time; + creators["focus heal targets"] = &ChatActionContext::focus_heal_targets; } private: @@ -245,10 +257,13 @@ 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); } static Action* autogear(PlayerbotAI* botAI) { return new AutoGearAction(botAI); } + static Action* autogear_bis(PlayerbotAI* botAI) { return new BisGearAction(botAI); } static Action* equip_upgrade(PlayerbotAI* botAI) { return new EquipUpgradeAction(botAI); } static Action* co(PlayerbotAI* botAI) { return new ChangeCombatStrategyAction(botAI); } static Action* nc(PlayerbotAI* botAI) { return new ChangeNonCombatStrategyAction(botAI); } @@ -266,6 +281,7 @@ private: static Action* item_count(PlayerbotAI* botAI) { return new TellItemCountAction(botAI); } static Action* use(PlayerbotAI* botAI) { return new UseItemAction(botAI); } static Action* repair(PlayerbotAI* botAI) { return new RepairAllAction(botAI); } + static Action* emblems(PlayerbotAI* botAI) { return new TellEmblemsAction(botAI); } static Action* taxi(PlayerbotAI* botAI) { return new TaxiAction(botAI); } static Action* teleport(PlayerbotAI* botAI) { return new TeleportAction(botAI); } static Action* release(PlayerbotAI* botAI) { return new ReleaseSpiritAction(botAI); } @@ -298,6 +314,7 @@ private: static Action* guild_remove(PlayerbotAI* botAI) { return new GuildRemoveAction(botAI); } static Action* guild_leave(PlayerbotAI* botAI) { return new GuildLeaveAction(botAI); } static Action* rtsc(PlayerbotAI* botAI) { return new RTSCAction(botAI); } + static Action* naxx_chat_shortcut(PlayerbotAI* ai) { return new NaxxChatShortcutAction(ai); } static Action* bwl_chat_shortcut(PlayerbotAI* ai) { return new BwlChatShortcutAction(ai); } static Action* tell_estimated_dps(PlayerbotAI* ai) { return new TellEstimatedDpsAction(ai); } static Action* join(PlayerbotAI* ai) { return new JoinGroupAction(ai); } @@ -309,6 +326,8 @@ private: static Action* pet(PlayerbotAI* botAI) { return new PetsAction(botAI); } static Action* pet_attack(PlayerbotAI* botAI) { return new PetsAction(botAI, "attack"); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } + static Action* wait_for_attack_time(PlayerbotAI* botAI) { return new SetWaitForAttackTimeAction(botAI); } + static Action* focus_heal_targets(PlayerbotAI* botAI) { return new SetFocusHealTargetsAction(botAI); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/ChatTriggerContext.h b/modules/mod-playerbots/src/Ai/Base/ChatTriggerContext.h index 3e71839bd..b8945b666 100644 --- a/modules/mod-playerbots/src/Ai/Base/ChatTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Base/ChatTriggerContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATTRIGGERCONTEXT_H -#define _PLAYERBOT_CHATTRIGGERCONTEXT_H +#ifndef PLAYERBOTS_CHATTRIGGERCONTEXT_H +#define PLAYERBOTS_CHATTRIGGERCONTEXT_H #include "ChatCommandTrigger.h" #include "NamedObjectContext.h" @@ -41,6 +42,7 @@ public: creators["teleport"] = &ChatTriggerContext::teleport; creators["taxi"] = &ChatTriggerContext::taxi; creators["repair"] = &ChatTriggerContext::repair; + creators["emblems"] = &ChatTriggerContext::emblems; creators["u"] = &ChatTriggerContext::use; creators["use"] = &ChatTriggerContext::use; creators["c"] = &ChatTriggerContext::item_count; @@ -64,8 +66,12 @@ public: creators["maintenance"] = &ChatTriggerContext::maintenance; creators["remove glyph"] = &ChatTriggerContext::remove_glyph; creators["autogear"] = &ChatTriggerContext::autogear; + creators["autogear bis"] = &ChatTriggerContext::autogear_bis; 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; @@ -104,6 +110,13 @@ public: creators["target"] = &ChatTriggerContext::target; creators["formation"] = &ChatTriggerContext::formation; creators["stance"] = &ChatTriggerContext::stance; + creators["cancel tree form"] = &ChatTriggerContext::cancel_tree_form; + creators["cancel travel form"] = &ChatTriggerContext::cancel_travel_form; + creators["cancel bear form"] = &ChatTriggerContext::cancel_bear_form; + creators["cancel dire bear form"] = &ChatTriggerContext::cancel_dire_bear_form; + creators["cancel cat form"] = &ChatTriggerContext::cancel_cat_form; + creators["cancel moonkin form"] = &ChatTriggerContext::cancel_moonkin_form; + creators["cancel aquatic form"] = &ChatTriggerContext::cancel_aquatic_form; creators["sendmail"] = &ChatTriggerContext::sendmail; creators["mail"] = &ChatTriggerContext::mail; creators["outfit"] = &ChatTriggerContext::outfit; @@ -127,7 +140,6 @@ public: creators["guild leave"] = &ChatTriggerContext::guild_leave; creators["rtsc"] = &ChatTriggerContext::rtsc; creators["drink"] = &ChatTriggerContext::drink; - // creators["bwl"] = &ChatTriggerContext::bwl; creators["dps"] = &ChatTriggerContext::dps; creators["disperse"] = &ChatTriggerContext::disperse; creators["calc"] = &ChatTriggerContext::calc; @@ -139,6 +151,8 @@ public: creators["pet"] = &ChatTriggerContext::pet; creators["pet attack"] = &ChatTriggerContext::pet_attack; creators["roll"] = &ChatTriggerContext::roll_action; + creators["wait for attack time"] = &ChatTriggerContext::wait_for_attack_time; + creators["focus heal"] = &ChatTriggerContext::focus_heal; } private: @@ -160,6 +174,13 @@ private: static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); } static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); } static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); } + static Trigger* cancel_tree_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel tree form"); } + static Trigger* cancel_travel_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel travel form"); } + static Trigger* cancel_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel bear form"); } + static Trigger* cancel_dire_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel dire bear form"); } + static Trigger* cancel_cat_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel cat form"); } + static Trigger* cancel_moonkin_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel moonkin form"); } + static Trigger* cancel_aquatic_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel aquatic form"); } static Trigger* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); } static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); } static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); } @@ -194,10 +215,14 @@ 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"); } static Trigger* autogear(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "autogear"); } + static Trigger* autogear_bis(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "autogear bis"); } static Trigger* equip_upgrade(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "equip upgrade"); } static Trigger* co(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "co"); } static Trigger* nc(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "nc"); } @@ -214,6 +239,7 @@ private: static Trigger* item_count(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "c"); } static Trigger* use(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "use"); } static Trigger* repair(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "repair"); } + static Trigger* emblems(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "emblems"); } static Trigger* taxi(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "taxi"); } static Trigger* teleport(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "teleport"); } static Trigger* q(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "q"); } @@ -245,7 +271,6 @@ private: static Trigger* guild_leave(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "guild leave"); } static Trigger* rtsc(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "rtsc"); } static Trigger* drink(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "drink"); } - // static Trigger* bwl(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "bwl"); } static Trigger* dps(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "dps"); } static Trigger* disperse(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "disperse"); } static Trigger* calc(PlayerbotAI* ai) { return new ChatCommandTrigger(ai, "calc"); } @@ -257,6 +282,8 @@ private: static Trigger* pet(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet"); } static Trigger* pet_attack(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pet attack"); } static Trigger* roll_action(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "roll"); } + static Trigger* wait_for_attack_time(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "wait for attack time"); } + static Trigger* focus_heal(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "focus heal"); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/SharedValueContext.h b/modules/mod-playerbots/src/Ai/Base/SharedValueContext.h index 94653aeab..3bb2a75f2 100644 --- a/modules/mod-playerbots/src/Ai/Base/SharedValueContext.h +++ b/modules/mod-playerbots/src/Ai/Base/SharedValueContext.h @@ -1,14 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHAREDVALUECONTEXT_H -#define _PLAYERBOT_SHAREDVALUECONTEXT_H +#ifndef PLAYERBOTS_SHAREDVALUECONTEXT_H +#define PLAYERBOTS_SHAREDVALUECONTEXT_H #include "LootValues.h" #include "NamedObjectContext.h" -#include "Playerbots.h" +#include "PlayerbotAI.h" #include "PvpValues.h" #include "QuestValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.cpp new file mode 100644 index 000000000..acea9ae51 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.cpp @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AggressiveStrategy.h" + +#include "Playerbots.h" + +void AggressiveStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode( + "no target", + { + NextAction("aggressive target", 4.0f) + } + ) + ); +} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.h new file mode 100644 index 000000000..5d5cdb042 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/AggressiveStrategy.h @@ -0,0 +1,23 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AGGRESSIVESTRATEGY_H +#define PLAYERBOTS_AGGRESSIVESTRATEGY_H + +#include "NonCombatStrategy.h" + +class PlayerbotAI; + +class AggressiveStrategy : public NonCombatStrategy +{ +public: + AggressiveStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + + std::string const getName() override { return "aggressive"; } + void InitTriggers(std::vector& triggers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.cpp index a43269976..7cf1f4819 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AttackEnemyPlayersStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.h index aa29d90c1..ca7aadb38 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/AttackEnemyPlayersStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ATTACKENEMYPAYERSSTRATEGYACTION_H -#define _PLAYERBOT_ATTACKENEMYPAYERSSTRATEGYACTION_H +#ifndef PLAYERBOTS_ATTACKENEMYPLAYERSSTRATEGY_H +#define PLAYERBOTS_ATTACKENEMYPLAYERSSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.cpp index 41c726cc2..241aeef54 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BattlegroundStrategy.h" @@ -14,7 +15,7 @@ void BGStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("timer", { NextAction("bg strategy check", relevance)})); } -BGStrategy::BGStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) {} +BGStrategy::BGStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) {} void BattlegroundStrategy::InitTriggers(std::vector& triggers) { diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.h index e36895e2c..96867264e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/BattlegroundStrategy.h @@ -1,14 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BATTLEGROUNDSTRATEGY_H -#define _PLAYERBOT_BATTLEGROUNDSTRATEGY_H +#ifndef PLAYERBOTS_BATTLEGROUNDSTRATEGY_H +#define PLAYERBOTS_BATTLEGROUNDSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" -class BGStrategy : public PassTroughStrategy +class BGStrategy : public PassThroughStrategy { public: BGStrategy(PlayerbotAI* botAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.cpp index 7c70923d4..dcbf4c648 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CastTimeStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.h index f5eddc1ca..f777e829b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/CastTimeStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CASTTIMESTRATEGY_H -#define _PLAYERBOT_CASTTIMESTRATEGY_H +#ifndef PLAYERBOTS_CASTTIMESTRATEGY_H +#define PLAYERBOTS_CASTTIMESTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index f67ae49ca..63b954f5a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChatCommandHandlerStrategy.h" @@ -11,7 +12,7 @@ public: ChatCommandActionNodeFactoryInternal() { creators["tank attack chat shortcut"] = &tank_attack_chat_shortcut; } private: - static ActionNode* tank_attack_chat_shortcut(PlayerbotAI* botAI) + static ActionNode* tank_attack_chat_shortcut(PlayerbotAI* /*botAI*/) { return new ActionNode("tank attack chat shortcut", /*P*/ {}, @@ -20,20 +21,23 @@ private: } }; +// Commands where trigger name =/= action name. void ChatCommandHandlerStrategy::InitTriggers(std::vector& triggers) { - PassTroughStrategy::InitTriggers(triggers); + PassThroughStrategy::InitTriggers(triggers); + // Keep single action triggers on one line, and multi-action triggers on multiple lines. triggers.push_back(new TriggerNode("rep", { NextAction("reputation", relevance) })); triggers.push_back(new TriggerNode("pvp stats", { NextAction("tell pvp stats", relevance) })); - triggers.push_back(new TriggerNode("q", { NextAction("query quest", relevance), - NextAction("query item usage", relevance) })); - triggers.push_back(new TriggerNode("add all loot", { NextAction("add all loot", relevance), - NextAction("loot", relevance) })); + triggers.push_back(new TriggerNode("q", + { NextAction("query quest", relevance), + NextAction("query item usage", relevance) })); + triggers.push_back(new TriggerNode("add all loot", + { NextAction("add all loot", relevance), + NextAction("loot", relevance) })); triggers.push_back(new TriggerNode("u", { NextAction("use", relevance) })); triggers.push_back(new TriggerNode("c", { NextAction("item count", relevance) })); - triggers.push_back( - new TriggerNode("items", { NextAction("item count", relevance) })); + triggers.push_back(new TriggerNode("items", { NextAction("item count", relevance) })); triggers.push_back(new TriggerNode("inv", { NextAction("item count", relevance) })); triggers.push_back(new TriggerNode("e", { NextAction("equip", relevance) })); triggers.push_back(new TriggerNode("ue", { NextAction("unequip", relevance) })); @@ -42,72 +46,41 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back(new TriggerNode("s", { NextAction("sell", relevance) })); triggers.push_back(new TriggerNode("b", { NextAction("buy", relevance) })); triggers.push_back(new TriggerNode("r", { NextAction("reward", relevance) })); - triggers.push_back( - new TriggerNode("attack", { NextAction("attack my target", relevance) })); - triggers.push_back( - new TriggerNode("accept", { NextAction("accept quest", relevance) })); - triggers.push_back( - new TriggerNode("follow", { NextAction("follow chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("stay", { NextAction("stay chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("move from group", { NextAction("move from group chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("flee", { NextAction("flee chat shortcut", relevance) })); - triggers.push_back(new TriggerNode( - "tank attack", { NextAction("tank attack chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("grind", { NextAction("grind chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("talk", { NextAction("gossip hello", relevance), - NextAction("talk to quest giver", relevance) })); - triggers.push_back( - new TriggerNode("enter vehicle", { NextAction("enter vehicle", relevance) })); - triggers.push_back( - new TriggerNode("leave vehicle", { NextAction("leave vehicle", relevance) })); - triggers.push_back( - new TriggerNode("cast", { NextAction("cast custom spell", relevance) })); - triggers.push_back( - new TriggerNode("castnc", { NextAction("cast custom nc spell", relevance) })); - triggers.push_back( - new TriggerNode("revive", { NextAction("spirit healer", relevance) })); - triggers.push_back( - new TriggerNode("runaway", { NextAction("runaway chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("warning", { NextAction("runaway chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("max dps", { NextAction("max dps chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("attackers", { NextAction("tell attackers", relevance) })); - triggers.push_back( - new TriggerNode("target", { NextAction("tell target", relevance) })); - triggers.push_back( - new TriggerNode("ready", { NextAction("ready check", relevance) })); - triggers.push_back( - new TriggerNode("bwl", { NextAction("bwl chat shortcut", relevance) })); - triggers.push_back( - new TriggerNode("dps", { NextAction("tell estimated dps", relevance) })); - triggers.push_back( - new TriggerNode("disperse", { NextAction("disperse set", relevance) })); - triggers.push_back( - new TriggerNode("open items", { NextAction("open items", relevance) })); - triggers.push_back( - new TriggerNode("qi", { NextAction("query item usage", relevance) })); - triggers.push_back( - new TriggerNode("unlock items", { NextAction("unlock items", relevance) })); - triggers.push_back( - new TriggerNode("unlock traded item", { NextAction("unlock traded item", relevance) })); - triggers.push_back( - new TriggerNode("wipe", { NextAction("wipe", relevance) })); - triggers.push_back(new TriggerNode("tame", { NextAction("tame", relevance) })); - triggers.push_back(new TriggerNode("glyphs", { NextAction("glyphs", relevance) })); // Added for custom Glyphs - triggers.push_back(new TriggerNode("glyph equip", { NextAction("glyph equip", relevance) })); // Added for custom Glyphs - triggers.push_back(new TriggerNode("pet", { NextAction("pet", relevance) })); - triggers.push_back(new TriggerNode("pet attack", { NextAction("pet attack", relevance) })); - triggers.push_back(new TriggerNode("roll", { NextAction("roll", relevance) })); + triggers.push_back(new TriggerNode("attack", { NextAction("attack my target", relevance) })); + triggers.push_back(new TriggerNode("accept", { NextAction("accept quest", relevance) })); + triggers.push_back(new TriggerNode("follow", { NextAction("follow chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("stay", { NextAction("stay chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("move from group", { NextAction("move from group chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("flee", { NextAction("flee chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("tank attack", { NextAction("tank attack chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("grind", { NextAction("grind chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("talk", + { NextAction("gossip hello", relevance), + NextAction("talk to quest giver", relevance) })); + triggers.push_back(new TriggerNode("enter vehicle", { NextAction("enter vehicle", relevance) })); + triggers.push_back(new TriggerNode("leave vehicle", { NextAction("leave vehicle", relevance) })); + triggers.push_back(new TriggerNode("cast", { NextAction("cast custom spell", relevance) })); + triggers.push_back(new TriggerNode("castnc", { NextAction("cast custom nc spell", relevance) })); + triggers.push_back(new TriggerNode("revive", { NextAction("spirit healer", relevance) })); + triggers.push_back(new TriggerNode("runaway", { NextAction("runaway chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("warning", { NextAction("runaway chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("max dps", { NextAction("max dps chat shortcut", relevance) })); + triggers.push_back(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(new TriggerNode("naxx", {NextAction("naxx chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("bwl", { NextAction("bwl chat shortcut", relevance) })); + triggers.push_back(new TriggerNode("dps", { NextAction("tell estimated dps", relevance) })); + triggers.push_back(new TriggerNode("disperse", { NextAction("disperse set", relevance) })); + triggers.push_back(new TriggerNode("qi", { NextAction("query item usage", relevance) })); + triggers.push_back(new TriggerNode("focus heal", { NextAction("focus heal targets", relevance) })); } -ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) +// Commands where trigger name == action name. +ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) { actionNodeFactories.Add(new ChatCommandActionNodeFactoryInternal()); @@ -129,6 +102,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("teleport"); supported.push_back("taxi"); supported.push_back("repair"); + supported.push_back("emblems"); supported.push_back("talents"); supported.push_back("spells"); supported.push_back("co"); @@ -138,6 +112,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("maintenance"); supported.push_back("remove glyph"); supported.push_back("autogear"); + supported.push_back("autogear bis"); supported.push_back("equip upgrade"); supported.push_back("chat"); supported.push_back("home"); @@ -158,6 +133,13 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("save mana"); supported.push_back("formation"); supported.push_back("stance"); + supported.push_back("cancel tree form"); + supported.push_back("cancel travel form"); + supported.push_back("cancel bear form"); + supported.push_back("cancel dire bear form"); + supported.push_back("cancel cat form"); + supported.push_back("cancel moonkin form"); + supported.push_back("cancel aquatic form"); supported.push_back("sendmail"); supported.push_back("mail"); supported.push_back("outfit"); @@ -181,13 +163,15 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("rtsc"); supported.push_back("drink"); supported.push_back("calc"); + supported.push_back("roll"); supported.push_back("open items"); - supported.push_back("qi"); supported.push_back("unlock items"); supported.push_back("unlock traded item"); + supported.push_back("wipe"); supported.push_back("tame"); - supported.push_back("glyphs"); // Added for custom Glyphs - supported.push_back("glyph equip"); // Added for custom Glyphs + supported.push_back("glyphs"); + supported.push_back("glyph equip"); supported.push_back("pet"); supported.push_back("pet attack"); + supported.push_back("wait for attack time"); } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.h index 87eab747d..cc80d3957 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.h @@ -1,16 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATCOMMANDHANDLERSTRATEGY_H -#define _PLAYERBOT_CHATCOMMANDHANDLERSTRATEGY_H +#ifndef PLAYERBOTS_CHATCOMMANDHANDLERSTRATEGY_H +#define PLAYERBOTS_CHATCOMMANDHANDLERSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" class PlayerbotAI; -class ChatCommandHandlerStrategy : public PassTroughStrategy +class ChatCommandHandlerStrategy : public PassThroughStrategy { public: ChatCommandHandlerStrategy(PlayerbotAI* botAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.cpp index 62972d4b1..d9f44fd8e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CombatStrategy.h" @@ -64,11 +65,11 @@ std::vector AvoidAoeStrategy::getDefaultActions() }; } -void AvoidAoeStrategy::InitTriggers(std::vector& triggers) +void AvoidAoeStrategy::InitTriggers(std::vector& /*triggers*/) { } -void AvoidAoeStrategy::InitMultipliers(std::vector& multipliers) +void AvoidAoeStrategy::InitMultipliers(std::vector& /*multipliers*/) { } @@ -81,7 +82,7 @@ std::vector TankFaceStrategy::getDefaultActions() }; } -void TankFaceStrategy::InitTriggers(std::vector& triggers) +void TankFaceStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.h index 3296d8bc7..de640a212 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/CombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_COMBATSTRATEGY_H -#define _PLAYERBOT_COMBATSTRATEGY_H +#ifndef PLAYERBOTS_COMBATSTRATEGY_H +#define PLAYERBOTS_COMBATSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.cpp index 3acabcd4d..1d6c29640 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ConserveManaStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.h index 000399f19..c3258d1fe 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ConserveManaStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CONSERVEMANASTRATEGY_H -#define _PLAYERBOT_CONSERVEMANASTRATEGY_H +#ifndef PLAYERBOTS_CONSERVEMANASTRATEGY_H +#define PLAYERBOTS_CONSERVEMANASTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.cpp index 61399fa10..0979c927c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DeadStrategy.h" @@ -9,7 +10,7 @@ void DeadStrategy::InitTriggers(std::vector& triggers) { - PassTroughStrategy::InitTriggers(triggers); + PassThroughStrategy::InitTriggers(triggers); triggers.push_back( new TriggerNode("often", { NextAction("auto release", relevance) })); @@ -29,4 +30,4 @@ void DeadStrategy::InitTriggers(std::vector& triggers) "can self resurrect", { NextAction("self resurrect", relevance + 2.0f) })); } -DeadStrategy::DeadStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) {} +DeadStrategy::DeadStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) {} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.h index 4ae33d4c5..d90eb0124 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DeadStrategy.h @@ -1,16 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DEADSTRATEGY_H -#define _PLAYERBOT_DEADSTRATEGY_H +#ifndef PLAYERBOTS_DEADSTRATEGY_H +#define PLAYERBOTS_DEADSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" class PlayerbotAI; -class DeadStrategy : public PassTroughStrategy +class DeadStrategy : public PassThroughStrategy { public: DeadStrategy(PlayerbotAI* botAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.cpp index bae6b419d..e4250f273 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DebugStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.h index 95118f737..ea2d25e30 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DebugStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DEBUGSTRATEGY_H -#define _PLAYERBOT_DEBUGSTRATEGY_H +#ifndef PLAYERBOTS_DEBUGSTRATEGY_H +#define PLAYERBOTS_DEBUGSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.cpp index 8ead279bd..8dd49fdb2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DpsAssistStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.h index 2f97e1656..bbe46c1aa 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DpsAssistStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DPSASSISTSTRATEGY_H -#define _PLAYERBOT_DPSASSISTSTRATEGY_H +#ifndef PLAYERBOTS_DPSASSISTSTRATEGY_H +#define PLAYERBOTS_DPSASSISTSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.cpp index c7f7e9d23..cfd441ab4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.cpp @@ -1,13 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DuelStrategy.h" void DuelStrategy::InitTriggers(std::vector& triggers) { - PassTroughStrategy::InitTriggers(triggers); + PassThroughStrategy::InitTriggers(triggers); triggers.push_back( new TriggerNode("duel requested", { NextAction("accept duel", relevance) })); @@ -15,8 +16,8 @@ void DuelStrategy::InitTriggers(std::vector& triggers) new TriggerNode("no attackers", { NextAction("attack duel opponent", 70.0f) })); } -DuelStrategy::DuelStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) {} +DuelStrategy::DuelStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) {} -void StartDuelStrategy::InitTriggers(std::vector& triggers) {} +void StartDuelStrategy::InitTriggers(std::vector& /*triggers*/) {} StartDuelStrategy::StartDuelStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.h index fc08030a7..587a1abe7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/DuelStrategy.h @@ -1,16 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DUELSTRATEGY_H -#define _PLAYERBOT_DUELSTRATEGY_H +#ifndef PLAYERBOTS_DUELSTRATEGY_H +#define PLAYERBOTS_DUELSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" class PlayerbotAI; -class DuelStrategy : public PassTroughStrategy +class DuelStrategy : public PassThroughStrategy { public: DuelStrategy(PlayerbotAI* botAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.cpp index f2e002e8d..a31acbeb3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EmoteStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.h index 4f1f5efa4..c691c84d9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/EmoteStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EMOTESTRATEGY_H -#define _PLAYERBOT_EMOTESTRATEGY_H +#ifndef PLAYERBOTS_EMOTESTRATEGY_H +#define PLAYERBOTS_EMOTESTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.cpp index 663738306..d48ead47f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FleeStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.h index 315719840..44ee2fa2b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/FleeStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FLEESTRATEGY_H -#define _PLAYERBOT_FLEESTRATEGY_H +#ifndef PLAYERBOTS_FLEESTRATEGY_H +#define PLAYERBOTS_FLEESTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/FocusTargetStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/FocusTargetStrategy.h new file mode 100644 index 000000000..40281e59f --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/FocusTargetStrategy.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOCUSTARGETSTRATEGY_H +#define PLAYERBOTS_FOCUSTARGETSTRATEGY_H + +#include "Strategy.h" + +class PlayerbotAI; + +class FocusHealTargetsStrategy : public Strategy +{ +public: + FocusHealTargetsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + std::string const getName() override { return "focus heal targets"; } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.cpp index 6701fdcac..cb9bc9f15 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FollowMasterStrategy.h" @@ -12,6 +13,6 @@ std::vector FollowMasterStrategy::getDefaultActions() }; } -void FollowMasterStrategy::InitTriggers(std::vector& triggers) +void FollowMasterStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.h index 9d7e3431f..5e28f898e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/FollowMasterStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FOLLOWMASTERSTRATEGY_H -#define _PLAYERBOT_FOLLOWMASTERSTRATEGY_H +#ifndef PLAYERBOTS_FOLLOWMASTERSTRATEGY_H +#define PLAYERBOTS_FOLLOWMASTERSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.cpp index 2db8cd87d..dc695c673 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GrindingStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.h index e093c7a58..cf216284a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GrindingStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GRINDINGSTRATEGY_H -#define _PLAYERBOT_GRINDINGSTRATEGY_H +#ifndef PLAYERBOTS_GRINDINGSTRATEGY_H +#define PLAYERBOTS_GRINDINGSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.cpp index 1b81faa9f..427b02574 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GroupStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.h index 0a2bd5086..d60238d13 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GroupStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GROUPSTRATEGY_H -#define _PLAYERBOT_GROUPSTRATEGY_H +#ifndef PLAYERBOTS_GROUPSTRATEGY_H +#define PLAYERBOTS_GROUPSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.cpp index 96017365f..b71e0f5fe 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuardStrategy.h" @@ -12,4 +13,4 @@ std::vector GuardStrategy::getDefaultActions() }; } -void GuardStrategy::InitTriggers(std::vector& triggers) {} +void GuardStrategy::InitTriggers(std::vector& /*triggers*/) {} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.h index 98c6b2a6f..c618e7d2e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GuardStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUARDSTRATEGY_H -#define _PLAYERBOT_GUARDSTRATEGY_H +#ifndef PLAYERBOTS_GUARDSTRATEGY_H +#define PLAYERBOTS_GUARDSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.cpp index d2594504d..75e405b1c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.h index 1e46bf9b0..73c2822ad 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/GuildStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDSTRATEGY_H -#define _PLAYERBOT_GUILDSTRATEGY_H +#ifndef PLAYERBOTS_GUILDSTRATEGY_H +#define PLAYERBOTS_GUILDSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.cpp index 2d4964460..8407270ce 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "KiteStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.h index 4e77774c6..057d92940 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/KiteStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_KITESTRATEGY_H -#define _PLAYERBOT_KITESTRATEGY_H +#ifndef PLAYERBOTS_KITESTRATEGY_H +#define PLAYERBOTS_KITESTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.cpp index b6057881c..0f0a81e08 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LfgStrategy.h" @@ -16,4 +17,4 @@ void LfgStrategy::InitTriggers(std::vector& triggers) "unknown dungeon", { NextAction("give leader in dungeon", relevance) })); } -LfgStrategy::LfgStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) {} +LfgStrategy::LfgStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) {} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.h index 6626725b5..cfe166d3d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/LfgStrategy.h @@ -1,14 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LFGSTRATEGY_H -#define _PLAYERBOT_LFGSTRATEGY_H +#ifndef PLAYERBOTS_LFGSTRATEGY_H +#define PLAYERBOTS_LFGSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" -class LfgStrategy : public PassTroughStrategy +class LfgStrategy : public PassThroughStrategy { public: LfgStrategy(PlayerbotAI* botAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.cpp index 5ecbb6959..b6a9c89cc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootNonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.h index fef564f8b..f3af7d168 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/LootNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTNONCOMBATSTRATEGY_H -#define _PLAYERBOT_LOOTNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_LOOTNONCOMBATSTRATEGY_H +#define PLAYERBOTS_LOOTNONCOMBATSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.cpp index 855555edd..bc18714b0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MaintenanceStrategy.h" @@ -13,7 +14,7 @@ void MaintenanceStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode( - "random", + "seldom", { NextAction("clean quest log", 6.0f) } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.h index 44bfadd03..ed0bb9671 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MaintenanceStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MAINTANCESTRATEGY_H -#define _PLAYERBOT_MAINTANCESTRATEGY_H +#ifndef PLAYERBOTS_MAINTENANCESTRATEGY_H +#define PLAYERBOTS_MAINTENANCESTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.cpp index 8d49c9a8a..504d2e08e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MarkRtiStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.h index 734f3d8e6..44cb13430 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MarkRtiStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MARKRTISTRATEGY_H -#define _PLAYERBOT_MARKRTISTRATEGY_H +#ifndef PLAYERBOTS_MARKRTISTRATEGY_H +#define PLAYERBOTS_MARKRTISTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.cpp index bc2d18913..44686c5ed 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MeleeCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.h index 9cad2bb3e..30c09ab0c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MeleeCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MELEECOMBATSTRATEGY_H -#define _PLAYERBOT_MELEECOMBATSTRATEGY_H +#ifndef PLAYERBOTS_MELEECOMBATSTRATEGY_H +#define PLAYERBOTS_MELEECOMBATSTRATEGY_H #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.cpp index 247abc88e..f74199eda 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MoveFromGroupStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.h index 53aa1c99a..c63915d58 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/MoveFromGroupStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MOVEFROMGROUPSTRATEGY_H -#define _PLAYERBOT_MOVEFROMGROUPSTRATEGY_H +#ifndef PLAYERBOTS_MOVEFROMGROUPSTRATEGY_H +#define PLAYERBOTS_MOVEFROMGROUPSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.cpp index cb32233f4..ca21d8c86 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NonCombatStrategy.h" @@ -17,7 +18,7 @@ void CollisionStrategy::InitTriggers(std::vector& triggers) new TriggerNode("collision", { NextAction("move out of collision", 2.0f) })); } -void MountStrategy::InitTriggers(std::vector& triggers) +void MountStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.h index 18549ac11..719dae319 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/NonCombatStrategy.h @@ -1,12 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NONCOMBATSTRATEGY_H -#define _PLAYERBOT_NONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_NONCOMBATSTRATEGY_H +#define PLAYERBOTS_NONCOMBATSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" class PlayerbotAI; diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.cpp new file mode 100644 index 000000000..5e0b05327 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.cpp @@ -0,0 +1,16 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PassThroughStrategy.h" + +#include "Playerbots.h" + +void PassThroughStrategy::InitTriggers(std::vector& triggers) +{ + for (std::vector::iterator i = supported.begin(); i != supported.end(); i++) + triggers.push_back( + new TriggerNode(i->c_str(), { NextAction(i->c_str(), relevance) })); +} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.h new file mode 100644 index 000000000..7f9d24c4a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/PassThroughStrategy.h @@ -0,0 +1,26 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PASSTHROUGHSTRATEGY_H +#define PLAYERBOTS_PASSTHROUGHSTRATEGY_H + +#include "Strategy.h" + +class PlayerbotAI; + +class PassThroughStrategy : public Strategy +{ +public: + PassThroughStrategy(PlayerbotAI* botAI, float relevance = 100.0f) : Strategy(botAI), relevance(relevance) {} + + void InitTriggers(std::vector& triggers) override; + +protected: + std::vector supported; + float relevance; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.cpp deleted file mode 100644 index 228106df4..000000000 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.cpp +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 "PassTroughStrategy.h" - -#include "Playerbots.h" - -void PassTroughStrategy::InitTriggers(std::vector& triggers) -{ - for (std::vector::iterator i = supported.begin(); i != supported.end(); i++) - triggers.push_back( - new TriggerNode(i->c_str(), { NextAction(i->c_str(), relevance) })); -} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.h deleted file mode 100644 index b3f897e2c..000000000 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/PassTroughStrategy.h +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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_PASSTHROUGHSTRATEGY_H -#define _PLAYERBOT_PASSTHROUGHSTRATEGY_H - -#include "Strategy.h" - -class PlayerbotAI; - -class PassTroughStrategy : public Strategy -{ -public: - PassTroughStrategy(PlayerbotAI* botAI, float relevance = 100.0f) : Strategy(botAI), relevance(relevance) {} - - void InitTriggers(std::vector& triggers) override; - -protected: - std::vector supported; - float relevance; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.cpp index 700b9e677..237a3e769 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PassiveStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.h index 097ecd0a0..8a1c5077c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/PassiveStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PASSIVESTRATEGY_H -#define _PLAYERBOT_PASSIVESTRATEGY_H +#ifndef PLAYERBOTS_PASSIVESTRATEGY_H +#define PLAYERBOTS_PASSIVESTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.cpp index d0c7c9eac..90808eb74 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.cpp @@ -1,12 +1,202 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * ike3 - original author + * Sebastiaan Keek (mostlikely4r) + * David Parra Ausina (Flekz) */ #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 strategy 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 strategy 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 +214,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 +232,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 +273,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/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.h index bdd7332f3..5be568669 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/PullStrategy.h @@ -1,27 +1,80 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PULLSTRATEGY_H -#define _PLAYERBOT_PULLSTRATEGY_H +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * ike3 - original author + * Sebastiaan Keek (mostlikely4r) + * David Parra Ausina (Flekz) + */ -#include "CombatStrategy.h" +#ifndef PLAYERBOTS_PULLSTRATEGY_H +#define PLAYERBOTS_PULLSTRATEGY_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 +86,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/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.cpp index adcc4b22b..b4901eda2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.cpp @@ -1,17 +1,18 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "QuestStrategies.h" #include "Playerbots.h" -QuestStrategy::QuestStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) { supported.push_back("accept quest"); } +QuestStrategy::QuestStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) { supported.push_back("accept quest"); } void QuestStrategy::InitTriggers(std::vector& triggers) { - PassTroughStrategy::InitTriggers(triggers); + PassThroughStrategy::InitTriggers(triggers); triggers.push_back( new TriggerNode("quest share", { NextAction("accept quest share", relevance) })); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.h b/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.h index 232d19428..c78af83f3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/QuestStrategies.h @@ -1,16 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_QUESTSTRATEGIES_H -#define _PLAYERBOT_QUESTSTRATEGIES_H +#ifndef PLAYERBOTS_QUESTSTRATEGIES_H +#define PLAYERBOTS_QUESTSTRATEGIES_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" class PlayerbotAI; -class QuestStrategy : public PassTroughStrategy +class QuestStrategy : public PassThroughStrategy { public: QuestStrategy(PlayerbotAI* botAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.cpp index 525338c15..697978b20 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.cpp @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RTSCStrategy.h" RTSCStrategy::RTSCStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} -void RTSCStrategy::InitTriggers(std::vector& triggers) {} +void RTSCStrategy::InitTriggers(std::vector& /*triggers*/) {} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.h index c61aee2a9..90773ed28 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RTSCStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTSCSTRATEGY_H -#define _PLAYERBOT_RTSCSTRATEGY_H +#ifndef PLAYERBOTS_RTSCSTRATEGY_H +#define PLAYERBOTS_RTSCSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.cpp index dc6d5bc48..1d45d78be 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -1,42 +1,108 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RacialsStrategy.h" +#include "Playerbots.h" -class RacialsStrategyActionNodeFactory : public NamedObjectFactory +namespace { -public: - RacialsStrategyActionNodeFactory() { creators["lifeblood"] = &lifeblood; } - -private: - static ActionNode* lifeblood(PlayerbotAI* botAI) - { - return new ActionNode("lifeblood", - /*P*/ {}, - /*A*/ { NextAction("gift of the naaru") }, - /*C*/ {}); - } -}; - -void RacialsStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode("low health", { NextAction("lifeblood", ACTION_NORMAL + 5) })); - triggers.push_back( - new TriggerNode("medium aoe", { NextAction("war stomp", ACTION_NORMAL + 5) })); - triggers.push_back(new TriggerNode( - "low mana", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); - - triggers.push_back(new TriggerNode( - "generic boost", { NextAction("blood fury", ACTION_NORMAL + 5), - NextAction("berserking", ACTION_NORMAL + 5), - NextAction("use trinket", ACTION_NORMAL + 4) })); - + constexpr uint32 SPELL_ARCANE_TORRENT_ENERGY = 25046; + constexpr uint32 SPELL_ARCANE_TORRENT_MANA = 28730; + constexpr uint32 SPELL_ARCANE_TORRENT_RUNIC_POWER = 50613; + constexpr uint32 SPELL_WAR_STOMP = 20549; + constexpr uint32 SPELL_BERSERKING = 26297; + constexpr uint32 SPELL_EVERY_MAN_FOR_HIMSELF = 59752; + constexpr uint32 SPELL_WILL_OF_THE_FORSAKEN = 7744; + constexpr uint32 SPELL_STONEFORM = 20594; + constexpr uint32 SPELL_ESCAPE_ARTIST = 20589; } RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI) { - actionNodeFactories.Add(new RacialsStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed +} + +void RacialsStrategy::InitTriggers(std::vector& triggers) +{ + Player* bot = botAI->GetBot(); + + if (bot->HasSpell(SPELL_ARCANE_TORRENT_MANA)) + { + triggers.push_back(new TriggerNode( + "low mana", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_ARCANE_TORRENT_ENERGY)) + { + triggers.push_back(new TriggerNode( + "low energy", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_ARCANE_TORRENT_RUNIC_POWER)) + { + // No low runic power trigger exists; this trigger should be modified if one is added + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("arcane torrent", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_WAR_STOMP)) + { + triggers.push_back(new TriggerNode( + "medium aoe", { NextAction("war stomp", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_BERSERKING)) + { + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("berserking", ACTION_NORMAL + 5) })); + } + + if (bot->HasSpell(SPELL_EVERY_MAN_FOR_HIMSELF)) + { + triggers.push_back(new TriggerNode( + "loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) })); + } + + if (bot->HasSpell(SPELL_WILL_OF_THE_FORSAKEN)) + { + triggers.push_back(new TriggerNode( + "fear charm sleep", { NextAction("will of the forsaken", ACTION_EMERGENCY + 1) })); + } + + if (bot->HasSpell(SPELL_STONEFORM)) + { + triggers.push_back(new TriggerNode( + "poison disease bleed", { NextAction("stoneform", ACTION_DISPEL) })); + } + + if (bot->HasSpell(SPELL_ESCAPE_ARTIST)) + { + triggers.push_back(new TriggerNode( + "movement impaired", { NextAction("escape artist", ACTION_EMERGENCY + 1) })); + } + + if (botAI->HasSpell("blood fury")) + { + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("blood fury", ACTION_NORMAL + 5) })); + } + + if (botAI->HasSpell("gift of the naaru")) + { + // Currently targets self only + triggers.push_back(new TriggerNode( + "medium health", { NextAction("gift of the naaru", ACTION_LIGHT_HEAL + 5) })); + } + + if (botAI->HasSpell("lifeblood")) + { + triggers.push_back(new TriggerNode( + "medium health", { NextAction("lifeblood", ACTION_LIGHT_HEAL + 5) })); + } + + triggers.push_back(new TriggerNode( + "generic boost", { NextAction("use trinket", ACTION_NORMAL + 4) })); } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.h index 1837890f5..1b4cebe6c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RacialsStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RACIALSSTRATEGY_H -#define _PLAYERBOT_RACIALSSTRATEGY_H +#ifndef PLAYERBOTS_RACIALSSTRATEGY_H +#define PLAYERBOTS_RACIALSSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.cpp index 73f26423a..cc4b15cc3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RangedCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.h index cb5adad7a..ab748f5f3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RangedCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANGEDCOMBATSTRATEGY_H -#define _PLAYERBOT_RANGEDCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_RANGEDCOMBATSTRATEGY_H +#define PLAYERBOTS_RANGEDCOMBATSTRATEGY_H #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.cpp index 2c88e64fd..1e14b7e70 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ReturnStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.h index 1d260b33a..b6e452b93 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ReturnStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RETURNSTRATEGY_H -#define _PLAYERBOT_RETURNSTRATEGY_H +#ifndef PLAYERBOTS_RETURNSTRATEGY_H +#define PLAYERBOTS_RETURNSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.cpp index 28868a48e..f72a3cca1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RunawayStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.h index 4ab802ac3..5f0edc5b1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/RunawayStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RUNAWAYSTRATEGY_H -#define _PLAYERBOT_RUNAWAYSTRATEGY_H +#ifndef PLAYERBOTS_RUNAWAYSTRATEGY_H +#define PLAYERBOTS_RUNAWAYSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.cpp index 71a72b79f..36893f1e5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SayStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.h index f83ece59c..04004c6e8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/SayStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SAYSTRATEGY_H -#define _PLAYERBOT_SAYSTRATEGY_H +#ifndef PLAYERBOTS_SAYSTRATEGY_H +#define PLAYERBOTS_SAYSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.cpp index a59f9461a..5dd0584c3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "StayStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.h index 0ee1c7d1c..da187a449 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/StayStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STAYSTRATEGY_H -#define _PLAYERBOT_STAYSTRATEGY_H +#ifndef PLAYERBOTS_STAYSTRATEGY_H +#define PLAYERBOTS_STAYSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.cpp index 4d4aae1a8..ea42fbb6b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TankAssistStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.h index 8efcf0fe4..583296c24 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/TankAssistStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TANKASSISTSTRATEGY_H -#define _PLAYERBOT_TANKASSISTSTRATEGY_H +#ifndef PLAYERBOTS_TANKASSISTSTRATEGY_H +#define PLAYERBOTS_TANKASSISTSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.cpp index c4c387f4f..9ea4fba91 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TellTargetStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.h index 4b3663494..c2e7edef2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/TellTargetStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TELLTARGETSTRATEGY_H -#define _PLAYERBOT_TELLTARGETSTRATEGY_H +#ifndef PLAYERBOTS_TELLTARGETSTRATEGY_H +#define PLAYERBOTS_TELLTARGETSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.cpp index 4509eaa0b..4985e1c07 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ThreatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.h index d1815e816..975a9d1e6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/ThreatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_THREATSTRATEGY_H -#define _PLAYERBOT_THREATSTRATEGY_H +#ifndef PLAYERBOTS_THREATSTRATEGY_H +#define PLAYERBOTS_THREATSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.cpp index 3b5c77ff3..5e442b00e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TravelStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.h index c0b0bd2d1..a1c80d524 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/TravelStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRAVELSTRATEGY_H -#define _PLAYERBOT_TRAVELSTRATEGY_H +#ifndef PLAYERBOTS_TRAVELSTRATEGY_H +#define PLAYERBOTS_TRAVELSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.cpp index 939d655ff..307ee8727 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "UseFoodStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.h index 9eb7a94d5..dbc912533 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/UseFoodStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_USEFOODSTRATEGY_H -#define _PLAYERBOT_USEFOODSTRATEGY_H +#ifndef PLAYERBOTS_USEFOODSTRATEGY_H +#define PLAYERBOTS_USEFOODSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.cpp index 55827fbd2..1390b4dff 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "UsePotionsStrategy.h" @@ -11,7 +12,7 @@ public: UsePotionsStrategyActionNodeFactory() { creators["healthstone"] = &healthstone; } private: - static ActionNode* healthstone(PlayerbotAI* botAI) + static ActionNode* healthstone(PlayerbotAI* /*botAI*/) { return new ActionNode("healthstone", /*P*/ {}, @@ -32,5 +33,5 @@ void UsePotionsStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "critical health", { NextAction("healthstone", ACTION_MEDIUM_HEAL + 1) })); triggers.push_back( - new TriggerNode("low mana", { NextAction("mana potion", ACTION_EMERGENCY) })); + new TriggerNode("medium mana", { NextAction("mana potion", ACTION_EMERGENCY) })); } diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.h index 36cf2dc29..022805acc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/UsePotionsStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_USEPOTIONSSTRATEGY_H -#define _PLAYERBOT_USEPOTIONSSTRATEGY_H +#ifndef PLAYERBOTS_USEPOTIONSSTRATEGY_H +#define PLAYERBOTS_USEPOTIONSSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp new file mode 100644 index 000000000..e572081f2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.cpp @@ -0,0 +1,95 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "WaitForAttackStrategy.h" + +#include "Action.h" +#include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "Strategy.h" + +void WaitForAttackStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "wait for attack safe distance", + { + NextAction("wait for attack keep safe distance", ACTION_RAID) + } + )); +} + +void WaitForAttackStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new WaitForAttackMultiplier(botAI)); +} + +bool WaitForAttackStrategy::ShouldWait(PlayerbotAI* botAI) +{ + if (botAI->HasStrategy("wait for attack", BOT_STATE_COMBAT)) + { + Player* bot = botAI->GetBot(); + if (bot->GetGroup() && botAI->HasRealPlayerMaster()) + { + // Don't wait if the current target is an enemy player + Unit* target = botAI->GetAiObjectContext()->GetValue("current target")->Get(); + if (target && target->IsPlayer()) + return false; + + AiObjectContext* context = botAI->GetAiObjectContext(); + time_t combatStartTime = context->GetValue("combat start time")->Get(); + + if (bot->IsInCombat()) + { + if (combatStartTime == 0) + { + combatStartTime = time(nullptr); + context->GetValue("combat start time")->Set(combatStartTime); + } + + time_t elapsedTime = time(nullptr) - combatStartTime; + return elapsedTime < GetWaitTime(botAI); + } + else + { + if (combatStartTime != 0) + context->GetValue("combat start time")->Set(0); + } + } + } + + return false; +} + +uint8 WaitForAttackStrategy::GetWaitTime(PlayerbotAI* botAI) +{ + return botAI->GetAiObjectContext()->GetValue("wait for attack time")->Get(); +} + +float WaitForAttackStrategy::GetSafeDistance() +{ + return sPlayerbotAIConfig.spellDistance; +} + +float WaitForAttackMultiplier::GetValue(Action* action) +{ + std::string const& actionName = action->getName(); + + if (actionName != "wait for attack keep safe distance" && + actionName != "dps assist" && + actionName != "set facing" && + actionName != "pull my target" && + actionName != "pull rti target" && + actionName != "reach pull" && + actionName != "pull start" && + actionName != "pull action" && + actionName != "pull end") + { + return WaitForAttackStrategy::ShouldWait(botAI) ? 0.0f : 1.0f; + } + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.h new file mode 100644 index 000000000..4f675ab54 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/WaitForAttackStrategy.h @@ -0,0 +1,40 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_WAITFORATTACKSTRATEGY_H +#define PLAYERBOTS_WAITFORATTACKSTRATEGY_H + +#include "Multiplier.h" +#include "Strategy.h" + +class PlayerbotAI; + +class WaitForAttackStrategy : public Strategy +{ +public: + WaitForAttackStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "wait for attack"; } + + static bool ShouldWait(PlayerbotAI* botAI); + static uint8 GetWaitTime(PlayerbotAI* botAI); + static float GetSafeDistance(); + static float GetSafeDistanceThreshold() { return 2.5f; } + +private: + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +class WaitForAttackMultiplier : public Multiplier +{ +public: + WaitForAttackMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "wait for attack") {} + + float GetValue(Action* action) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp b/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp index ad31c2c39..63a6a4373 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.cpp @@ -1,13 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WorldPacketHandlerStrategy.h" void WorldPacketHandlerStrategy::InitTriggers(std::vector& triggers) { - PassTroughStrategy::InitTriggers(triggers); + PassThroughStrategy::InitTriggers(triggers); triggers.push_back( new TriggerNode("group invite", { NextAction("accept invitation", relevance) })); @@ -16,7 +17,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back( new TriggerNode("uninvite guid", { NextAction("uninvite", relevance) })); triggers.push_back( - new TriggerNode("group set leader", { /*NextAction("leader", relevance),*/ })); + new TriggerNode("group set leader", { NextAction("reset botAI", relevance) })); triggers.push_back(new TriggerNode( "not enough money", { NextAction("tell not enough money", relevance) })); triggers.push_back( @@ -71,7 +72,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger triggers.push_back(new TriggerNode("very often", { NextAction("loot roll", relevance) })); } -WorldPacketHandlerStrategy::WorldPacketHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) +WorldPacketHandlerStrategy::WorldPacketHandlerStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) { supported.push_back("loot roll"); supported.push_back("check mount state"); diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.h b/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.h index 52b6e83e3..9d0717f84 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.h +++ b/modules/mod-playerbots/src/Ai/Base/Strategy/WorldPacketHandlerStrategy.h @@ -1,16 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WORLDPACKETHANDLERSTRATEGY_H -#define _PLAYERBOT_WORLDPACKETHANDLERSTRATEGY_H +#ifndef PLAYERBOTS_WORLDPACKETHANDLERSTRATEGY_H +#define PLAYERBOTS_WORLDPACKETHANDLERSTRATEGY_H -#include "PassTroughStrategy.h" +#include "PassThroughStrategy.h" class PlayerbotAI; -class WorldPacketHandlerStrategy : public PassTroughStrategy +class WorldPacketHandlerStrategy : public PassThroughStrategy { public: WorldPacketHandlerStrategy(PlayerbotAI* botAI); @@ -19,10 +20,10 @@ public: std::string const getName() override { return "default"; } }; -class ReadyCheckStrategy : public PassTroughStrategy +class ReadyCheckStrategy : public PassThroughStrategy { public: - ReadyCheckStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI) { } + ReadyCheckStrategy(PlayerbotAI* botAI) : PassThroughStrategy(botAI) { } void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "ready check"; } diff --git a/modules/mod-playerbots/src/Ai/Base/StrategyContext.h b/modules/mod-playerbots/src/Ai/Base/StrategyContext.h index 0cc6855f3..f60c00dbd 100644 --- a/modules/mod-playerbots/src/Ai/Base/StrategyContext.h +++ b/modules/mod-playerbots/src/Ai/Base/StrategyContext.h @@ -1,11 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STRATEGYCONTEXT_H -#define _PLAYERBOT_STRATEGYCONTEXT_H +#ifndef PLAYERBOTS_STRATEGYCONTEXT_H +#define PLAYERBOTS_STRATEGYCONTEXT_H +#include "AggressiveStrategy.h" #include "AttackEnemyPlayersStrategy.h" #include "BattlegroundStrategy.h" #include "CastTimeStrategy.h" @@ -18,6 +20,7 @@ #include "DuelStrategy.h" #include "EmoteStrategy.h" #include "FleeStrategy.h" +#include "FocusTargetStrategy.h" #include "FollowMasterStrategy.h" #include "GrindingStrategy.h" #include "GroupStrategy.h" @@ -49,6 +52,7 @@ #include "TravelStrategy.h" #include "UseFoodStrategy.h" #include "UsePotionsStrategy.h" +#include "WaitForAttackStrategy.h" #include "WorldPacketHandlerStrategy.h" class StrategyContext : public NamedObjectContext @@ -61,6 +65,7 @@ public: creators["gather"] = &StrategyContext::gather; creators["emote"] = &StrategyContext::emote; creators["passive"] = &StrategyContext::passive; + creators["aggressive"] = &StrategyContext::aggressive; creators["save mana"] = &StrategyContext::auto_save_mana; creators["food"] = &StrategyContext::food; creators["chat"] = &StrategyContext::chat; @@ -91,6 +96,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; @@ -122,6 +128,8 @@ public: creators["worldbuff"] = &StrategyContext::world_buff; creators["use bobber"] = &StrategyContext::bobber_strategy; creators["master fishing"] = &StrategyContext::master_fishing; + creators["wait for attack"] = &StrategyContext::wait_for_attack; + creators["focus heal targets"] = &StrategyContext::focus_heal_targets; } private: @@ -144,6 +152,7 @@ private: static Strategy* gather(PlayerbotAI* botAI) { return new GatherStrategy(botAI); } static Strategy* emote(PlayerbotAI* botAI) { return new EmoteStrategy(botAI); } static Strategy* passive(PlayerbotAI* botAI) { return new PassiveStrategy(botAI); } + static Strategy* aggressive(PlayerbotAI* botAI) { return new AggressiveStrategy(botAI); } // static Strategy* conserve_mana(PlayerbotAI* botAI) { return new ConserveManaStrategy(botAI); } static Strategy* auto_save_mana(PlayerbotAI* botAI) { return new HealerAutoSaveManaStrategy(botAI); } static Strategy* food(PlayerbotAI* botAI) { return new UseFoodStrategy(botAI); } @@ -164,6 +173,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); } @@ -192,6 +202,8 @@ private: static Strategy* world_buff(PlayerbotAI* botAI) { return new WorldBuffStrategy(botAI); } static Strategy* bobber_strategy(PlayerbotAI* botAI) { return new UseBobberStrategy(botAI); } static Strategy* master_fishing(PlayerbotAI* botAI) { return new MasterFishingStrategy(botAI); } + static Strategy* wait_for_attack(PlayerbotAI* botAI) { return new WaitForAttackStrategy(botAI); } + static Strategy* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetsStrategy(botAI); } }; class MovementStrategyContext : public NamedObjectContext diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.cpp index 8d2520cf6..211770882 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BossAuraTriggers.h" @@ -23,9 +24,7 @@ bool BossFireResistanceTrigger::IsActive() return false; // Check if bot have fire resistance aura - if (bot->HasAura(SPELL_FIRE_RESISTANCE_AURA_RANK_5) || bot->HasAura(SPELL_FIRE_RESISTANCE_AURA_RANK_4) || - bot->HasAura(SPELL_FIRE_RESISTANCE_AURA_RANK_3) || bot->HasAura(SPELL_FIRE_RESISTANCE_AURA_RANK_2) || - bot->HasAura(SPELL_FIRE_RESISTANCE_AURA_RANK_1)) + if (botAI->HasAura("fire resistance aura", bot)) return false; // Check if bot dont have already have fire resistance strategy @@ -76,9 +75,7 @@ bool BossFrostResistanceTrigger::IsActive() return false; // Check if bot have frost resistance aura - if (bot->HasAura(SPELL_FROST_RESISTANCE_AURA_RANK_5) || bot->HasAura(SPELL_FROST_RESISTANCE_AURA_RANK_4) || - bot->HasAura(SPELL_FROST_RESISTANCE_AURA_RANK_3) || bot->HasAura(SPELL_FROST_RESISTANCE_AURA_RANK_2) || - bot->HasAura(SPELL_FROST_RESISTANCE_AURA_RANK_1)) + if (botAI->HasAura("frost resistance aura", bot)) return false; // Check if bot dont have already have frost resistance strategy @@ -133,8 +130,7 @@ bool BossNatureResistanceTrigger::IsActive() return false; // Check if bot have nature resistance aura - if (bot->HasAura(SPELL_ASPECT_OF_THE_WILD_RANK_4) || bot->HasAura(SPELL_ASPECT_OF_THE_WILD_RANK_3) || - bot->HasAura(SPELL_ASPECT_OF_THE_WILD_RANK_2) || bot->HasAura(SPELL_ASPECT_OF_THE_WILD_RANK_1)) + if (botAI->HasAura("aspect of the wild", bot)) return false; // Check if bot dont have already setted nature resistance aura @@ -184,11 +180,7 @@ bool BossShadowResistanceTrigger::IsActive() return false; // Check if bot have shadow resistance aura - if (bot->HasAura(SPELL_SHADOW_RESISTANCE_AURA_RANK_5) || - bot->HasAura(SPELL_SHADOW_RESISTANCE_AURA_RANK_4) || - bot->HasAura(SPELL_SHADOW_RESISTANCE_AURA_RANK_3) || - bot->HasAura(SPELL_SHADOW_RESISTANCE_AURA_RANK_2) || - bot->HasAura(SPELL_SHADOW_RESISTANCE_AURA_RANK_1)) + if (botAI->HasAura("shadow resistance aura", bot)) return false; // Check if bot dont have already have shadow resistance strategy diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.h index 86a0fa31f..77798c77a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/BossAuraTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BOSSAURATRIGGERS_H -#define _PLAYERBOT_BOSSAURATRIGGERS_H +#ifndef PLAYERBOTS_BOSSAURATRIGGERS_H +#define PLAYERBOTS_BOSSAURATRIGGERS_H #include "GenericTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.cpp index 3fd6d7325..8db91ee32 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChatCommandTrigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.h b/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.h index 4131b0e42..7c49969c9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/ChatCommandTrigger.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATCOMMANDTRIGGER_H -#define _PLAYERBOT_CHATCOMMANDTRIGGER_H +#ifndef PLAYERBOTS_CHATCOMMANDTRIGGER_H +#define PLAYERBOTS_CHATCOMMANDTRIGGER_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.cpp index f119441e1..d62496cf3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CureTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.h index 208ee5ad8..ca7b86054 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/CureTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CURETRIGGERS_H -#define _PLAYERBOT_CURETRIGGERS_H +#ifndef PLAYERBOTS_CURETRIGGERS_H +#define PLAYERBOTS_CURETRIGGERS_H #include "GenericTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.cpp index dae877574..5549fc0e2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FishingTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.h index ed3c6962b..af7196bfb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/FishingTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FISHING_TRIGGER_H -#define _PLAYERBOT_FISHING_TRIGGER_H +#ifndef PLAYERBOTS_FISHINGTRIGGERS_H +#define PLAYERBOTS_FISHINGTRIGGERS_H #include "GenericTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.cpp index 209766be7..2801435e0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericTriggers.h" #include +#include "GenericBuffUtils.h" #include "CreatureAI.h" #include "ItemVisitors.h" #include "LastSpellCastValue.h" @@ -16,7 +18,7 @@ #include "PositionValue.h" #include "SharedDefines.h" #include "TemporarySummon.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" #include "Timer.h" #include "PlayerbotAI.h" #include "Player.h" @@ -34,54 +36,57 @@ bool MediumManaTrigger::IsActive() AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.mediumMana; } +bool LowEnergyTrigger::IsActive() +{ + return AI_VALUE2(uint8, "energy", "self target") < threshold; +} + bool NoPetTrigger::IsActive() { - return (bot->GetMinionGUID().IsEmpty()) && (!AI_VALUE(Unit*, "pet target")) && (!bot->GetGuardianPet()) && - (!bot->GetFirstControlled()) && (!AI_VALUE2(bool, "mounted", "self target")); + return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() && + !bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target"); } bool HasPetTrigger::IsActive() { - return (AI_VALUE(Unit*, "pet target")) && !AI_VALUE2(bool, "mounted", "self target"); - ; + return AI_VALUE(Unit*, "pet target") && !AI_VALUE2(bool, "mounted", "self target"); } bool PetAttackTrigger::IsActive() { Guardian* pet = bot->GetGuardianPet(); if (!pet) - { return false; - } + Unit* target = AI_VALUE(Unit*, "current target"); if (!target) - { return false; - } + if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack()) - { return false; - } + if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat()) - { return false; - } + return true; } bool HighManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana; + return AI_VALUE2(bool, "has mana", "self target") && + AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana; } bool AlmostFullManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > 85; + return AI_VALUE2(bool, "has mana", "self target") && + AI_VALUE2(uint8, "mana", "self target") > 85; } bool EnoughManaTrigger::IsActive() { - return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana; + return AI_VALUE2(bool, "has mana", "self target") && + AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana; } bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; } @@ -96,9 +101,8 @@ bool TargetWithComboPointsLowerHealTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } + return ComboPointsAvailableTrigger::IsActive() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime; } @@ -159,19 +163,27 @@ bool BuffTrigger::IsActive() Unit* target = GetTarget(); if (!target) return false; - if (!SpellTrigger::IsActive()) - return false; + Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration); - if (!aura) - return true; - if (beforeDuration && aura->GetDuration() < beforeDuration) + if (!aura || (beforeDuration && uint32(aura->GetDuration()) < beforeDuration)) return true; + return false; } Value* BuffOnPartyTrigger::GetTargetValue() { - return context->GetValue("party member without aura", spell); + return context->GetValue( + "party member without aura", ai::buff::MakeAuraQualifierForBuff(spell)); +} + +bool BuffOnPartyTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (ai::buff::ShouldDeferPartyBuffEvaluationForRecentLogin(bot, target, spell)) + return false; + + return BuffTrigger::IsActive(); } bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); } @@ -204,22 +216,23 @@ bool MediumThreatTrigger::IsActive() { if (!AI_VALUE(Unit*, "main tank")) return false; + return MyAttackerCountTrigger::IsActive(); } bool LowTankThreatTrigger::IsActive() { - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) return false; Unit* current_target = AI_VALUE(Unit*, "current target"); if (!current_target) return false; - ThreatMgr& mgr = current_target->GetThreatMgr(); + ThreatManager& mgr = current_target->GetThreatMgr(); float threat = mgr.GetThreat(bot); - float tankThreat = mgr.GetThreat(mt); + float tankThreat = mgr.GetThreat(mainTank); return tankThreat == 0.0f || threat > tankThreat * 0.5f; } @@ -227,9 +240,8 @@ bool AoeTrigger::IsActive() { Unit* current_target = AI_VALUE(Unit*, "current target"); if (!current_target) - { return false; - } + GuidVector attackers = context->GetValue("attackers")->Get(); int attackers_count = 0; for (ObjectGuid const guid : attackers) @@ -237,10 +249,9 @@ bool AoeTrigger::IsActive() Unit* unit = botAI->GetUnit(guid); if (!unit || !unit->IsAlive()) continue; + if (unit->GetDistance(current_target->GetPosition()) <= range) - { attackers_count++; - } } return attackers_count >= amount; } @@ -269,20 +280,19 @@ bool DebuffTrigger::IsActive() { Unit* target = GetTarget(); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } - return BuffTrigger::IsActive() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; + + return BuffTrigger::IsActive() && + (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; } bool DebuffOnBossTrigger::IsActive() { if (!DebuffTrigger::IsActive()) - { return false; - } - Creature* c = GetTarget()->ToCreature(); - return c && ((c->IsDungeonBoss()) || (c->isWorldBoss())); + + Creature* creature = GetTarget()->ToCreature(); + return creature && (creature->IsDungeonBoss() || creature->isWorldBoss()); } bool SpellTrigger::IsActive() { return GetTarget(); } @@ -312,9 +322,7 @@ bool SpellCooldownTrigger::IsActive() } RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability) - : Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) -{ -} + : Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) {} bool RandomTrigger::IsActive() { @@ -325,6 +333,7 @@ bool RandomTrigger::IsActive() int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier); if (k < 1) k = 1; + return (rand() % k) == 0; } @@ -363,9 +372,11 @@ bool BoostTrigger::IsActive() { if (!BuffTrigger::IsActive()) return false; + Unit* target = AI_VALUE(Unit*, "current target"); if (target && target->ToPlayer()) return true; + return AI_VALUE(uint8, "balance") <= balance; } @@ -374,20 +385,19 @@ bool GenericBoostTrigger::IsActive() Unit* target = AI_VALUE(Unit*, "current target"); if (target && target->ToPlayer()) return true; + return AI_VALUE(uint8, "balance") <= balance; } bool HealerShouldAttackTrigger::IsActive() { - // nobody can help me if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1) return true; if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth) return false; - // special check for resto druid (dont remove tree of life frequently) - if (bot->GetAura(33891)) + if (bot->GetAura(33891)) // Tree of Life { LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue("last spell cast")->Get(); if (lastSpell.timer + 5 > time(nullptr)) @@ -396,7 +406,6 @@ bool HealerShouldAttackTrigger::IsActive() int manaThreshold; int balance = AI_VALUE(uint8, "balance"); - // higher threshold in higher pressure if (balance <= 50) manaThreshold = 85; else if (balance <= 100) @@ -410,7 +419,7 @@ bool HealerShouldAttackTrigger::IsActive() return true; } -bool ItemCountTrigger::IsActive() { return AI_VALUE2(uint32, "item count", item) < count; } +bool ItemCountTrigger::IsActive() { return AI_VALUE2(uint32, "item count", item) < uint32(count); } bool InterruptSpellTrigger::IsActive() { @@ -420,13 +429,7 @@ bool InterruptSpellTrigger::IsActive() bool DeflectSpellTrigger::IsActive() { Unit* target = GetTarget(); - if (!target) - return false; - - if (!target->IsNonMeleeSpellCast(true)) - return false; - - if (target->GetTarget() != bot->GetGUID()) + if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID()) return false; uint32 spellid = context->GetValue("spell id", spell)->Get(); @@ -457,6 +460,7 @@ bool DeflectSpellTrigger::IsActive() return true; } } + return false; } @@ -464,19 +468,55 @@ bool AttackerCountTrigger::IsActive() { return AI_VALUE(uint8, "attacker count") bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); } +bool LossOfControlTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_STUN) || + bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_ROOT) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM); +} + +bool FearCharmSleepTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM) || + bot->HasAuraType(SPELL_AURA_AOE_CHARM) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP); +} + +bool FearSleepSapTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) || + bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED); +} + +bool PoisonDiseaseBleedTrigger::IsActive() +{ + return botAI->HasAuraToDispel(bot, DISPEL_POISON) || + botAI->HasAuraToDispel(bot, DISPEL_DISEASE) || + bot->HasAuraWithMechanic(1 << MECHANIC_BLEED); +} + +bool MovementImpairedTrigger::IsActive() +{ + return botAI->IsMovementImpaired(bot) && + !botAI->HasAnyAuraOf(bot, "stealth", "prowl", nullptr); +} + bool HasAuraStackTrigger::IsActive() { - Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); - // sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "HasAuraStackTrigger::IsActive %s %d", getName(), aura ? - // aura->GetStackAmount() : -1); - return aura; + return botAI->GetAura(getName(), GetTarget(), false, true, stack); } bool TimerTrigger::IsActive() { - if (time(nullptr) != lastCheck) + time_t now = time(nullptr); + + if (now != lastCheck) { - lastCheck = time(nullptr); + lastCheck = now; return true; } @@ -523,9 +563,8 @@ bool IsBehindTargetTrigger::IsActive() bool IsNotBehindTargetTrigger::IsActive() { if (botAI->HasStrategy("stay", botAI->GetState())) - { return false; - } + Unit* target = AI_VALUE(Unit*, "current target"); return target && !AI_VALUE2(bool, "behind", "current target"); } @@ -533,9 +572,8 @@ bool IsNotBehindTargetTrigger::IsActive() bool IsNotFacingTargetTrigger::IsActive() { if (botAI->HasStrategy("stay", botAI->GetState())) - { return false; - } + return !AI_VALUE2(bool, "facing", "current target"); } @@ -552,12 +590,14 @@ bool NoPossibleTargetsTrigger::IsActive() return !targets.size(); } -bool PossibleAddsTrigger::IsActive() { return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); } +bool PossibleAddsTrigger::IsActive() +{ + return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); +} bool NotDpsTargetActiveTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); - // do not switch if enemy target if (target && target->IsAlive()) { Unit* enemy = AI_VALUE(Unit*, "enemy player target"); @@ -575,7 +615,6 @@ bool NotDpsAoeTargetActiveTrigger::IsActive() Unit* target = AI_VALUE(Unit*, "current target"); Unit* enemy = AI_VALUE(Unit*, "enemy player target"); - // do not switch if enemy target if (target && target == enemy && target->IsAlive()) return false; @@ -609,7 +648,10 @@ Value* InterruptEnemyHealerTrigger::GetTargetValue() return context->GetValue("enemy healer target", spell); } -bool RandomBotUpdateTrigger::IsActive() { return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); } +bool RandomBotUpdateTrigger::IsActive() +{ + return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); +} bool NoNonBotPlayersAroundTrigger::IsActive() { @@ -689,43 +731,24 @@ bool AmmoCountTrigger::IsActive() bool NewPetTrigger::IsActive() { - // Get the bot player object from the AI - Player* bot = botAI->GetBot(); - if (!bot) - return false; - - // Try to get the current pet; initialize guardian and GUID to null/empty - Pet* pet = bot->GetPet(); - Guardian* guardian = nullptr; ObjectGuid currentPetGuid = ObjectGuid::Empty; - // If bot has a pet, get its GUID - if (pet) - { + if (Pet* pet = bot->GetPet()) currentPetGuid = pet->GetGUID(); - } - else - { - // If no pet, try to get a guardian pet and its GUID - guardian = bot->GetGuardianPet(); - if (guardian) - currentPetGuid = guardian->GetGUID(); - } + else if (Guardian* guardian = bot->GetGuardianPet()) + currentPetGuid = guardian->GetGUID(); - // If the current pet or guardian GUID has changed (including becoming empty), reset the trigger state if (currentPetGuid != lastPetGuid) { triggered = false; lastPetGuid = currentPetGuid; } - // If there's a valid current pet/guardian (non-empty GUID) and we haven't triggered yet, activate trigger if (currentPetGuid != ObjectGuid::Empty && !triggered) { triggered = true; return true; } - // Otherwise, do not activate return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.h index 7b884fbf0..33a668ed4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/GenericTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICTRIGGERS_H -#define _PLAYERBOT_GENERICTRIGGERS_H +#ifndef PLAYERBOTS_GENERICTRIGGERS_H +#define PLAYERBOTS_GENERICTRIGGERS_H #include @@ -20,9 +21,7 @@ class StatAvailable : public Trigger { public: StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available") - : Trigger(botAI, name), amount(amount) - { - } + : Trigger(botAI, name), amount(amount) {} protected: int32 amount; @@ -118,8 +117,8 @@ public: class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger { public: - TargetWithComboPointsLowerHealTrigger(PlayerbotAI* ai, int32 combo_point = 5, float lifeTime = 8.0f) - : ComboPointsAvailableTrigger(ai, combo_point), lifeTime(lifeTime) + TargetWithComboPointsLowerHealTrigger(PlayerbotAI* botAI, int32 combo_point = 5, float lifeTime = 8.0f) + : ComboPointsAvailableTrigger(botAI, combo_point), lifeTime(lifeTime) { } bool IsActive() override; @@ -196,7 +195,6 @@ public: bool IsActive() override; }; -// TODO: check other targets class InterruptSpellTrigger : public SpellTrigger { public: @@ -217,9 +215,7 @@ class AttackerCountTrigger : public Trigger { public: AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance) - : Trigger(botAI), amount(amount), distance(distance) - { - } + : Trigger(botAI), amount(amount), distance(distance) {} bool IsActive() override; std::string const getName() override { return "attacker count"; } @@ -269,9 +265,7 @@ class AoeTrigger : public AttackerCountTrigger { public: AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f) - : AttackerCountTrigger(botAI, amount), range(range) - { - } + : AttackerCountTrigger(botAI, amount), range(range) {} bool IsActive() override; std::string const getName() override { return "aoe"; } @@ -317,7 +311,8 @@ public: class BuffTrigger : public SpellTrigger { public: - BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0) + BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, + bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0) : SpellTrigger(botAI, spell, checkInterval) { this->checkIsOwner = checkIsOwner; @@ -339,11 +334,10 @@ class BuffOnPartyTrigger : public BuffTrigger { public: BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1) - : BuffTrigger(botAI, spell, checkInterval) - { - } + : BuffTrigger(botAI, spell, checkInterval) {} Value* GetTargetValue() override; + bool IsActive() override; std::string const getName() override { return spell + " on party"; } }; @@ -393,9 +387,7 @@ class DebuffTrigger : public BuffTrigger public: DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, float needLifeTime = 8.0f, uint32 beforeDuration = 0) - : BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) - { - } + : BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) {} std::string const GetTargetName() override { return "current target"; } bool IsActive() override; @@ -408,9 +400,7 @@ class DebuffOnBossTrigger : public DebuffTrigger { public: DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false) - : DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) - { - } + : DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) {} bool IsActive() override; }; @@ -419,9 +409,7 @@ class DebuffOnAttackerTrigger : public DebuffTrigger public: DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true, float needLifeTime = 8.0f) - : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) - { - } + : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -432,9 +420,7 @@ class DebuffOnMeleeAttackerTrigger : public DebuffTrigger public: DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true, float needLifeTime = 8.0f) - : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) - { - } + : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {} Value* GetTargetValue() override; std::string const getName() override { return spell + " on attacker"; } @@ -444,9 +430,7 @@ class BoostTrigger : public BuffTrigger { public: BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f) - : BuffTrigger(botAI, spell, 1), balance(balance) - { - } + : BuffTrigger(botAI, spell, 1), balance(balance) {} bool IsActive() override; @@ -458,9 +442,7 @@ class GenericBoostTrigger : public Trigger { public: GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f) - : Trigger(botAI, "generic boost", 1), balance(balance) - { - } + : Trigger(botAI, "generic boost", 1), balance(balance) {} bool IsActive() override; @@ -472,9 +454,7 @@ class HealerShouldAttackTrigger : public Trigger { public: HealerShouldAttackTrigger(PlayerbotAI* botAI) - : Trigger(botAI, "healer should attack", 1) - { - } + : Trigger(botAI, "healer should attack", 1) {} bool IsActive() override; }; @@ -550,6 +530,17 @@ public: bool IsActive() override; }; +class LowEnergyTrigger : public Trigger +{ +public: + LowEnergyTrigger(PlayerbotAI* botAI, uint8 threshold = 30) : Trigger(botAI, "low energy"), threshold(threshold) {} + + bool IsActive() override; + +private: + uint8 threshold; +}; + BEGIN_TRIGGER(PanicTrigger, Trigger) // cppcheck-suppress unknownMacro std::string const getName() override { return "panic"; } END_TRIGGER() @@ -569,7 +560,7 @@ public: class HasPetTrigger : public Trigger { public: - HasPetTrigger(PlayerbotAI* ai) : Trigger(ai, "has pet", 5 * 1000) {} + HasPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "has pet", 5 * 1000) {} virtual bool IsActive() override; }; @@ -577,7 +568,7 @@ public: class PetAttackTrigger : public Trigger { public: - PetAttackTrigger(PlayerbotAI* ai) : Trigger(ai, "pet attack") {} + PetAttackTrigger(PlayerbotAI* botAI) : Trigger(botAI, "pet attack") {} virtual bool IsActive() override; }; @@ -586,9 +577,7 @@ class ItemCountTrigger : public Trigger { public: ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000) - : Trigger(botAI, item, interval), item(item), count(count) - { - } + : Trigger(botAI, item, interval), item(item), count(count) {} bool IsActive() override; std::string const getName() override { return "item count"; } @@ -602,9 +591,7 @@ class AmmoCountTrigger : public ItemCountTrigger { public: AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000) - : ItemCountTrigger(botAI, item, count, interval) - { - } + : ItemCountTrigger(botAI, item, count, interval) {} bool IsActive() override; }; @@ -612,9 +599,7 @@ class HasAuraTrigger : public Trigger { public: HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1) - : Trigger(botAI, spell, checkInterval) - { - } + : Trigger(botAI, spell, checkInterval) {} std::string const GetTargetName() override { return "self target"; } bool IsActive() override; @@ -623,10 +608,8 @@ public: class HasAuraStackTrigger : public Trigger { public: - HasAuraStackTrigger(PlayerbotAI* ai, std::string spell, int stack, int checkInterval = 1) - : Trigger(ai, spell, checkInterval), stack(stack) - { - } + HasAuraStackTrigger(PlayerbotAI* botAI, std::string spell, int stack, int checkInterval = 1) + : Trigger(botAI, spell, checkInterval), stack(stack) {} std::string const GetTargetName() override { return "self target"; } bool IsActive() override; @@ -746,6 +729,46 @@ public: bool IsActive() override; }; +class LossOfControlTrigger : public Trigger +{ +public: + LossOfControlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "loss of control", 1) {} + + bool IsActive() override; +}; + +class FearCharmSleepTrigger : public Trigger +{ +public: + FearCharmSleepTrigger(PlayerbotAI* botAI) : Trigger(botAI, "fear charm sleep", 1) {} + + bool IsActive() override; +}; + +class FearSleepSapTrigger : public Trigger +{ +public: + FearSleepSapTrigger(PlayerbotAI* botAI) : Trigger(botAI, "fear sleep sap", 1) {} + + bool IsActive() override; +}; + +class PoisonDiseaseBleedTrigger : public Trigger +{ +public: + PoisonDiseaseBleedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "poison disease bleed", 1) {} + + bool IsActive() override; +}; + +class MovementImpairedTrigger : public Trigger +{ +public: + MovementImpairedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "movement impaired", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: @@ -823,9 +846,7 @@ class StayTimeTrigger : public Trigger { public: StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name) - : Trigger(botAI, name, 5 * 1000), delay(delay) - { - } + : Trigger(botAI, name, 5 * 1000), delay(delay) {} bool IsActive() override; @@ -842,7 +863,7 @@ public: class ReturnToStayPositionTrigger : public Trigger { public: - ReturnToStayPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "return to stay position", 2) {} + ReturnToStayPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to stay position", 2) {} virtual bool IsActive() override; }; @@ -857,9 +878,7 @@ class GiveItemTrigger : public Trigger { public: GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item) - : Trigger(botAI, name, 2 * 1000), item(item) - { - } + : Trigger(botAI, name, 2 * 1000), item(item) {} bool IsActive() override; @@ -927,9 +946,7 @@ class BuffOnMainTankTrigger : public BuffTrigger { public: BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1) - : BuffTrigger(botAI, spell, checkInterval, checkIsOwner) - { - } + : BuffTrigger(botAI, spell, checkInterval, checkIsOwner) {} public: virtual Value* GetTargetValue(); @@ -938,7 +955,7 @@ public: class SelfResurrectTrigger : public Trigger { public: - SelfResurrectTrigger(PlayerbotAI* ai) : Trigger(ai, "can self resurrect") {} + SelfResurrectTrigger(PlayerbotAI* botAI) : Trigger(botAI, "can self resurrect") {} bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); } }; @@ -946,7 +963,7 @@ public: class NewPetTrigger : public Trigger { public: - NewPetTrigger(PlayerbotAI* ai) : Trigger(ai, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {} + NewPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {} bool IsActive() override; diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.cpp index afa405581..ef36140a2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildTriggers.h" @@ -27,7 +28,7 @@ bool LeaveLargeGuildTrigger::IsActive() if (botAI->IsRealPlayer()) return false; - if (botAI->IsAlt()) + if (botAI->IsAltBot()) return false; if (botAI->IsInRealGuild()) @@ -39,11 +40,8 @@ bool LeaveLargeGuildTrigger::IsActive() Player* leader = ObjectAccessor::FindPlayer(guild->GetLeaderGUID()); - // Only leave the guild if we know the leader is not a real player. - if (!leader || !GET_PLAYERBOT_AI(leader) || !GET_PLAYERBOT_AI(leader)->IsRealPlayer()) - return false; - - PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(leader); + // Only leave the guild if the leader is an online bot (not a real player). + PlayerbotAI* leaderBotAI = leader ? GET_PLAYERBOT_AI(leader) : nullptr; if (!leaderBotAI || leaderBotAI->IsRealPlayer()) return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.h index adbf730e7..f87f8d775 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/GuildTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDTRIGGER_H -#define _PLAYERBOT_GUILDTRIGGER_H +#ifndef PLAYERBOTS_GUILDTRIGGERS_H +#define PLAYERBOTS_GUILDTRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.cpp index a2b36962d..981a9de7e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HealthTriggers.h" @@ -22,6 +23,15 @@ bool DeadTrigger::IsActive() { return AI_VALUE2(bool, "dead", GetTargetName()); bool AoeHealTrigger::IsActive() { return AI_VALUE2(uint8, "aoe heal", type) >= count; } +bool HealerLowManaTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + return target->GetPowerPct(POWER_MANA) < sPlayerbotAIConfig.lowMana; +} + bool AoeInGroupTrigger::IsActive() { int32 member = botAI->GetNearGroupMemberCount(); diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.h index 0fbd403d7..15b72ff9d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/HealthTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HEALTHTRIGGERS_H -#define _PLAYERBOT_HEALTHTRIGGERS_H +#ifndef PLAYERBOTS_HEALTHTRIGGERS_H +#define PLAYERBOTS_HEALTHTRIGGERS_H #include @@ -143,6 +144,15 @@ public: TargetCriticalHealthTrigger(PlayerbotAI* botAI) : TargetLowHealthTrigger(botAI, 20) {} }; +class HealerLowManaTrigger : public Trigger +{ +public: + HealerLowManaTrigger(PlayerbotAI* botAI) : Trigger(botAI, "healer low mana") {} + + std::string const GetTargetName() override { return "healer low mana"; } + bool IsActive() override; +}; + class PartyMemberDeadTrigger : public Trigger { public: diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.cpp index 7aa2bc426..b0a635e53 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LfgTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.h index c1f79055a..1d97a3fd1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/LfgTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LFGTRIGGERS_H -#define _PLAYERBOT_LFGTRIGGERS_H +#ifndef PLAYERBOTS_LFGTRIGGERS_H +#define PLAYERBOTS_LFGTRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.cpp index 0a4b7e411..f0c62b026 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootTriggers.h" @@ -11,6 +12,9 @@ bool LootAvailableTrigger::IsActive() { + if (!AI_VALUE(bool, "has available loot")) + return false; + bool distanceCheck = false; if (botAI->HasStrategy("stay", BOT_STATE_NON_COMBAT)) { @@ -23,9 +27,16 @@ bool LootAvailableTrigger::IsActive() INTERACTION_DISTANCE - 2.0f); } - // if loot target if empty, always pass distance check - return AI_VALUE(bool, "has available loot") && - (distanceCheck || AI_VALUE(GuidVector, "all targets").empty()); + // Loot target in range, or no hostile targets to deal with first. + if (distanceCheck || AI_VALUE(GuidVector, "all targets").empty()) + return true; + + // A target that became not loot-possible after being selected - the bot + // was pulled into combat while approaching it, or it despawned - never + // returns to range, so without this it stays selected forever and the + // loot action never gets to pick another. Report active so it can. + LootObject lootTarget = AI_VALUE(LootObject, "loot target"); + return !lootTarget.IsEmpty() && !lootTarget.IsLootPossible(bot); } bool FarFromCurrentLootTrigger::IsActive() diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.h index f7c580b21..2710e673d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/LootTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTTRIGGERS_H -#define _PLAYERBOT_LOOTTRIGGERS_H +#ifndef PLAYERBOTS_LOOTTRIGGERS_H +#define PLAYERBOTS_LOOTTRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.cpp new file mode 100644 index 000000000..4a7db13a4 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.cpp @@ -0,0 +1,72 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + * Sebastiaan Keek (mostlikely4r) + * Cyberium + */ + +#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/modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.h new file mode 100644 index 000000000..8f75382aa --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/PullTriggers.h @@ -0,0 +1,45 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + * Sebastiaan Keek (mostlikely4r) + * Cyberium + */ + +#ifndef PLAYERBOTS_PULLTRIGGERS_H +#define PLAYERBOTS_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/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.cpp index 31fcd0357..77e93545b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PvpTriggers.h" @@ -297,7 +298,7 @@ bool PlayerWantsInBattlegroundTrigger::IsActive() if (bot->GetBattleground() && bot->GetBattleground()->GetStatus() == STATUS_IN_PROGRESS) return false; - if (!bot->CanJoinToBattleground()) + if (bot->IsDeserter()) return false; return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.h index 46e489a63..ba60c5bef 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/PvpTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PVPTRIGGERS_H -#define _PLAYERBOT_PVPTRIGGERS_H +#ifndef PLAYERBOTS_PVPTRIGGERS_H +#define PLAYERBOTS_PVPTRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.cpp index 01a4d0f61..1277d27aa 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RangeTriggers.h" @@ -11,11 +12,6 @@ #include "ServerFacade.h" #include "SharedDefines.h" -static float GetSpeedInMotion(Unit* target) -{ - return target->GetSpeed(Movement::SelectSpeedType(target->GetUnitMovementFlags())); -} - bool EnemyTooCloseForSpellTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.h index 62e1ae6e0..64cbe1408 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/RangeTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANGETRIGGERS_H -#define _PLAYERBOT_RANGETRIGGERS_H +#ifndef PLAYERBOTS_RANGETRIGGERS_H +#define PLAYERBOTS_RANGETRIGGERS_H #include "PlayerbotAIConfig.h" #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.cpp index 0151c6c5e..3c916a186 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RpgTriggers.h" @@ -31,7 +32,7 @@ bool RpgTrigger::IsActive() { return true; } Event RpgTrigger::Check() { - if (!NoRpgTargetTrigger::IsActive() && (AI_VALUE(std::string, "next rpg action") == "choose rpg target") || + if ((!NoRpgTargetTrigger::IsActive() && (AI_VALUE(std::string, "next rpg action") == "choose rpg target")) || !FarFromRpgTargetTrigger::IsActive()) return Trigger::Check(); @@ -163,52 +164,19 @@ bool RpgRepairTrigger::IsActive() return false; } -bool RpgTrainTrigger::IsTrainerOf(CreatureTemplate const* cInfo, Player* pPlayer) -{ - Trainer::Trainer* trainer = sObjectMgr->GetTrainer(cInfo->Entry); - - if (trainer->GetTrainerType() == Trainer::Type::Mount && trainer->GetTrainerRequirement() != pPlayer->getRace()) - { - if (FactionTemplateEntry const* faction_template = sFactionTemplateStore.LookupEntry(cInfo->faction)) - if (pPlayer->GetReputationRank(faction_template->faction) == REP_EXALTED) - return true; - - return false; - } - - return trainer->IsTrainerValidForPlayer(pPlayer); -} - bool RpgTrainTrigger::IsActive() { - GuidPosition guidP(getGuidP()); - - if (!guidP.HasNpcFlag(UNIT_NPC_FLAG_TRAINER)) + GuidPosition gp = getGuidP(); + if (!gp) return false; - CreatureTemplate const* cInfo = guidP.GetCreatureTemplate(); - - if (!IsTrainerOf(cInfo, bot)) + if (!gp.HasNpcFlag(UNIT_NPC_FLAG_TRAINER)) return false; - Trainer::Trainer* trainer = sObjectMgr->GetTrainer(cInfo->Entry); - FactionTemplateEntry const* factionTemplate = sFactionTemplateStore.LookupEntry(cInfo->faction); - float fDiscountMod = bot->GetReputationPriceDiscount(factionTemplate); + if (!AI_VALUE(bool, "can train")) + return false; - for (auto& spell : trainer->GetSpells()) - { - if (!trainer->CanTeachSpell(bot, trainer->GetSpell(spell.SpellId))) - continue; - - uint32 cost = uint32(floor(spell.MoneyCost * fDiscountMod)); - - if (cost > AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::spells)) - continue; - - return true; - } - - return false; + return true; } bool RpgHealTrigger::IsActive() diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.h index ce8cd2c1a..2fccd15db 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/RpgTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RPGTRIGGERS_H -#define _PLAYERBOT_RPGTRIGGERS_H +#ifndef PLAYERBOTS_RPGTRIGGERS_H +#define PLAYERBOTS_RPGTRIGGERS_H #include "Trigger.h" @@ -134,8 +135,6 @@ class RpgTrainTrigger : public RpgTrigger public: RpgTrainTrigger(PlayerbotAI* botAI, std::string const name = "rpg train") : RpgTrigger(botAI, name) {} - static bool IsTrainerOf(CreatureTemplate const* cInfo, Player* pPlayer); - bool IsActive() override; }; diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.cpp index 6158bc4d6..1ddfd09b3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RtiTriggers.h" @@ -16,5 +17,21 @@ bool NoRtiTrigger::IsActive() return false; Unit* target = AI_VALUE(Unit*, "rti target"); - return target != nullptr; + return target == nullptr; +} + +// Fires when the RTI CC target should be crowd controlled by this spell. +// Standard path: the target is already in the attackers list and "cc target" matches the RTI +// mark — delegates to HasCcTargetTrigger to confirm no one else is already CCing it. +bool RtiCcTrigger::IsActive() +{ + Unit* rtiCcTarget = AI_VALUE(Unit*, "rti cc target"); + if (!rtiCcTarget) + return false; + + Unit* ccTarget = AI_VALUE2(Unit*, "cc target", getName()); + if (ccTarget && ccTarget == rtiCcTarget) + return HasCcTargetTrigger::IsActive(); + + return botAI->CanCastSpell(getName(), rtiCcTarget); } diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.h index a8ecab4d9..627151774 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/RtiTriggers.h @@ -1,11 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTITRIGGERS_H -#define _PLAYERBOT_RTITRIGGERS_H +#ifndef PLAYERBOTS_RTITRIGGERS_H +#define PLAYERBOTS_RTITRIGGERS_H +#include "GenericTriggers.h" #include "Trigger.h" class PlayerbotAI; @@ -18,4 +20,12 @@ public: bool IsActive() override; }; +class RtiCcTrigger : public HasCcTargetTrigger +{ +public: + RtiCcTrigger(PlayerbotAI* botAI, std::string const name) : HasCcTargetTrigger(botAI, name) {} + + bool IsActive() override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.cpp index b6ade75d8..07875324b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "StuckTriggers.h" @@ -8,7 +9,7 @@ #include "CellImpl.h" #include "PathGenerator.h" #include "Playerbots.h" -#include "MMapFactory.h" +#include "MapCollisionData.h" bool MoveStuckTrigger::IsActive() { @@ -89,8 +90,7 @@ bool MoveLongStuckTrigger::IsActive() return true; } - if (cell.GridX() > 0 && cell.GridY() > 0 && - !MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(botPos.GetMapId(), cell.GridX(), cell.GridY())) + if (bot->GetMap()->IsGridCreated(GridCoord(cell.GridX(), cell.GridY()))) { // LOG_INFO("playerbots", "Bot {} {}:{} <{}> was in unloaded grid {},{} on map {}", // bot->GetGUID().ToString().c_str(), bot->GetTeamId() == TEAM_ALLIANCE ? "A" : "H", bot->GetLevel(), diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.h index 42edc9d16..cd375bfcf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/StuckTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STUCKTRIGGERS_H -#define _PLAYERBOT_STUCKTRIGGERS_H +#ifndef PLAYERBOTS_STUCKTRIGGERS_H +#define PLAYERBOTS_STUCKTRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.cpp index ce036027c..dbd97c997 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TravelTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.h index ce2e1467c..2db41d8c4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/TravelTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRAVELTRIGGERS_H -#define _PLAYERBOT_TRAVELTRIGGERS_H +#ifndef PLAYERBOTS_TRAVELTRIGGERS_H +#define PLAYERBOTS_TRAVELTRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/WaitForAttackTriggers.h b/modules/mod-playerbots/src/Ai/Base/Trigger/WaitForAttackTriggers.h new file mode 100644 index 000000000..7e0d55726 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/WaitForAttackTriggers.h @@ -0,0 +1,50 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_WAITFORATTACKTRIGGERS_H +#define PLAYERBOTS_WAITFORATTACKTRIGGERS_H + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "ServerFacade.h" +#include "Trigger.h" +#include "WaitForAttackStrategy.h" + +class PlayerbotAI; + +class WaitForAttackSafeDistanceTrigger : public Trigger +{ +public: + WaitForAttackSafeDistanceTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "wait for attack safe distance") {} + + bool IsActive() override + { + if (!WaitForAttackStrategy::ShouldWait(botAI)) + return false; + + // Do not move if stay strategy is set + if (botAI->HasStrategy("stay", botAI->GetState())) + return false; + + // Do not move if currently being targeted + if (!bot->getAttackers().empty()) + return false; + + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) + return false; + + float safeDistance = WaitForAttackStrategy::GetSafeDistance(); + float safeDistanceThreshold = WaitForAttackStrategy::GetSafeDistanceThreshold(); + float distanceToTarget = ServerFacade::instance().GetDistance2d(bot, target); + + return (distanceToTarget > (safeDistance + safeDistanceThreshold)) || + (distanceToTarget < (safeDistance - safeDistanceThreshold)); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.cpp index 9baab0829..d21f7317d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WithinAreaTrigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.h b/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.h index 1d428d9df..1b03564c0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/WithinAreaTrigger.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WITHINAREATRIGGER_H -#define _PLAYERBOT_WITHINAREATRIGGER_H +#ifndef PLAYERBOTS_WITHINAREATRIGGER_H +#define PLAYERBOTS_WITHINAREATRIGGER_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.cpp b/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.cpp index 0c9343ca9..689087607 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WorldPacketTrigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.h b/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.h index 6f98e37ac..f85f9045c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.h +++ b/modules/mod-playerbots/src/Ai/Base/Trigger/WorldPacketTrigger.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WORLDPACKETTRIGGER_H -#define _PLAYERBOT_WORLDPACKETTRIGGER_H +#ifndef PLAYERBOTS_WORLDPACKETTRIGGER_H +#define PLAYERBOTS_WORLDPACKETTRIGGER_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Base/TriggerContext.h b/modules/mod-playerbots/src/Ai/Base/TriggerContext.h index ceb7c001c..c823926c3 100644 --- a/modules/mod-playerbots/src/Ai/Base/TriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Base/TriggerContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRIGGERCONTEXT_H -#define _PLAYERBOT_TRIGGERCONTEXT_H +#ifndef PLAYERBOTS_TRIGGERCONTEXT_H +#define PLAYERBOTS_TRIGGERCONTEXT_H #include "CureTriggers.h" #include "FishingTriggers.h" @@ -16,10 +17,12 @@ #include "NewRpgStrategy.h" #include "NewRpgTriggers.h" #include "PvpTriggers.h" +#include "PullTriggers.h" #include "RpgTriggers.h" #include "RtiTriggers.h" #include "StuckTriggers.h" #include "TravelTriggers.h" +#include "WaitForAttackTriggers.h" class PlayerbotAI; @@ -49,6 +52,7 @@ public: creators["low mana"] = &TriggerContext::LowMana; creators["medium mana"] = &TriggerContext::MediumMana; + creators["low energy"] = &TriggerContext::LowEnergy; creators["high mana"] = &TriggerContext::HighMana; creators["almost full mana"] = &TriggerContext::AlmostFullMana; creators["enough mana"] = &TriggerContext::EnoughMana; @@ -57,8 +61,14 @@ public: creators["party member low health"] = &TriggerContext::PartyMemberLowHealth; creators["party member medium health"] = &TriggerContext::PartyMemberMediumHealth; creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth; + creators["healer low mana"] = &TriggerContext::HealerLowMana; creators["generic boost"] = &TriggerContext::generic_boost; + creators["loss of control"] = &TriggerContext::loss_of_control; + creators["fear charm sleep"] = &TriggerContext::fear_charm_sleep; + creators["fear sleep sap"] = &TriggerContext::fear_sleep_sap; + creators["poison disease bleed"] = &TriggerContext::poison_disease_bleed; + creators["movement impaired"] = &TriggerContext::movement_impaired; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -103,7 +113,8 @@ public: creators["enemy within melee"] = &TriggerContext::enemy_within_melee; creators["party member to heal out of spell range"] = &TriggerContext::party_member_to_heal_out_of_spell_range; - creators["combo points available"] = &TriggerContext::ComboPointsAvailable; + creators["combo points 5 available"] = &TriggerContext::ComboPoints5Available; + creators["combo points 4 available"] = &TriggerContext::ComboPoints4Available; creators["combo points 3 available"] = &TriggerContext::ComboPoints3Available; creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead; creators["combo points not full"] = &TriggerContext::ComboPointsNotFull; @@ -124,6 +135,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; @@ -225,10 +239,12 @@ public: creators["wander npc status"] = &TriggerContext::wander_npc_status; creators["do quest status"] = &TriggerContext::do_quest_status; creators["travel flight status"] = &TriggerContext::travel_flight_status; + creators["outdoor pvp status"] = &TriggerContext::outdoor_pvp_status; creators["can self resurrect"] = &TriggerContext::can_self_resurrect; creators["can fish"] = &TriggerContext::can_fish; creators["can use fishing bobber"] = &TriggerContext::can_use_fishing_bobber; creators["new pet"] = &TriggerContext::new_pet; + creators["wait for attack safe distance"] = &TriggerContext::wait_for_attack_safe_distance; } private: @@ -273,6 +289,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); } @@ -298,6 +317,7 @@ private: static Trigger* TargetCriticalHealth(PlayerbotAI* botAI) { return new TargetCriticalHealthTrigger(botAI); } static Trigger* LowMana(PlayerbotAI* botAI) { return new LowManaTrigger(botAI); } static Trigger* MediumMana(PlayerbotAI* botAI) { return new MediumManaTrigger(botAI); } + static Trigger* LowEnergy(PlayerbotAI* botAI) { return new LowEnergyTrigger(botAI); } static Trigger* HighMana(PlayerbotAI* botAI) { return new HighManaTrigger(botAI); } static Trigger* AlmostFullMana(PlayerbotAI* botAI) { return new AlmostFullManaTrigger(botAI); } static Trigger* EnoughMana(PlayerbotAI* botAI) { return new EnoughManaTrigger(botAI); } @@ -338,7 +358,8 @@ private: { return new PartyMemberToHealOutOfSpellRangeTrigger(botAI); } - static Trigger* ComboPointsAvailable(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI); } + static Trigger* ComboPoints5Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 5); } + static Trigger* ComboPoints4Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 4); } static Trigger* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); } static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai) { @@ -361,10 +382,16 @@ private: return new PartyMemberAlmostFullHealthTrigger(botAI); } static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } + static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); } + static Trigger* fear_charm_sleep(PlayerbotAI* botAI) { return new FearCharmSleepTrigger(botAI); } + static Trigger* fear_sleep_sap(PlayerbotAI* botAI) { return new FearSleepSapTrigger(botAI); } + static Trigger* poison_disease_bleed(PlayerbotAI* botAI) { return new PoisonDiseaseBleedTrigger(botAI); } + static Trigger* movement_impaired(PlayerbotAI* botAI) { return new MovementImpairedTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI); } + static Trigger* HealerLowMana(PlayerbotAI* botAI) { return new HealerLowManaTrigger(botAI); } static Trigger* protect_party_member(PlayerbotAI* botAI) { return new ProtectPartyMemberTrigger(botAI); } static Trigger* no_pet(PlayerbotAI* botAI) { return new NoPetTrigger(botAI); } static Trigger* has_pet(PlayerbotAI* botAI) { return new HasPetTrigger(botAI); } @@ -426,10 +453,12 @@ private: static Trigger* wander_npc_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_WANDER_NPC); } static Trigger* do_quest_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_DO_QUEST); } static Trigger* travel_flight_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_TRAVEL_FLIGHT); } + static Trigger* outdoor_pvp_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_OUTDOOR_PVP); } static Trigger* can_self_resurrect(PlayerbotAI* ai) { return new SelfResurrectTrigger(ai); } static Trigger* can_fish(PlayerbotAI* ai) { return new CanFishTrigger(ai); } static Trigger* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberTrigger(ai); } static Trigger* new_pet(PlayerbotAI* ai) { return new NewPetTrigger(ai); } + static Trigger* wait_for_attack_safe_distance(PlayerbotAI* ai) { return new WaitForAttackSafeDistanceTrigger(ai); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.cpp b/modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.cpp new file mode 100644 index 000000000..70e328727 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.cpp @@ -0,0 +1,324 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericBuffUtils.h" + +#include "AiObjectContext.h" + +#include "GameTime.h" +#include "Group.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "PlayerbotAIConfig.h" +#include "SpellMgr.h" +#include "Unit.h" +#include "Value.h" + +namespace ai::buff +{ + namespace + { + // Prevents bots from immediately casting already-present buffs upon logging in + constexpr uint32 POST_LOGIN_BUFF_GRACE_MS = 5 * IN_MILLISECONDS; + + bool IsWithinPostLoginBuffGrace(Player* player) + { + if (!player) + return false; + + return getMSTimeDiff( + player->GetInGameTime(), GameTime::GetGameTimeMS().count()) < POST_LOGIN_BUFF_GRACE_MS; + } + } + + static bool HasEnoughSameMapMissingPlayersForGroupVariant( + Player* bot, PlayerbotAI* botAI, std::string const& baseName, + std::string const& groupName, uint32 requiredCount = 3) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + uint32 missingCount = 0; + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member || !member->IsInWorld() || !member->IsAlive() || + member->GetMap() != bot->GetMap()) + { + continue; + } + + if (botAI->HasAura(baseName, member) || botAI->HasAura(groupName, member)) + continue; + + if (++missingCount >= requiredCount) + return true; + } + + return false; + } + + static bool IsEligibleGroupForPartyBuffs(Group const* group) + { + if (!group) + return false; + + switch (sPlayerbotAIConfig.autoPartyBuffs) + { + case AutoPartyBuffMode::RAID_ONLY: + return group->isRaidGroup(); + case AutoPartyBuffMode::GROUP_OR_RAID: + return true; + case AutoPartyBuffMode::DISABLED: + return false; + } + + return false; + } + + bool IsGroupVariantEnabled(Player* bot, std::string const& name) + { + if (!IsEligibleGroupForPartyBuffs(bot->GetGroup())) + return false; + + return !GroupVariantFor(name).empty(); + } + + std::string MakeAuraQualifierForBuff(std::string const& name) + { + // Paladin + if (name == "blessing of kings") return "blessing of kings,greater blessing of kings"; + if (name == "blessing of might") return "blessing of might,greater blessing of might"; + if (name == "blessing of wisdom") return "blessing of wisdom,greater blessing of wisdom"; + if (name == "blessing of sanctuary") return "blessing of sanctuary,greater blessing of sanctuary"; + // Druid + if (name == "mark of the wild") return "mark of the wild,gift of the wild"; + // Mage + if (name == "arcane intellect") return "arcane intellect,arcane brilliance"; + // Priest + if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude"; + if (name == "divine spirit") return "divine spirit,prayer of spirit"; + if (name == "shadow protection") return "shadow protection,prayer of shadow protection"; + + return name; + } + + std::string GroupVariantFor(std::string const& name) + { + // Druid + if (name == "mark of the wild") return "gift of the wild"; + // Mage + if (name == "arcane intellect") return "arcane brilliance"; + // Priest + if (name == "power word: fortitude") return "prayer of fortitude"; + if (name == "divine spirit") return "prayer of spirit"; + if (name == "shadow protection") return "prayer of shadow protection"; + + // Paladin blessings are intentionally not included here because they are + // coordinated by the auto greater blessing system instead. + return std::string(); + } + + bool NeedsPostLoginBuffGrace(std::string const& name) + { + static char const* const trackedBuffs[] = { + "mark of the wild", + "arcane intellect", + "power word: fortitude", + "prayer of fortitude", + "divine spirit", + "prayer of spirit", + "shadow protection", + "prayer of shadow protection", + "blessing of kings", + "blessing of might", + "blessing of wisdom", + "blessing of sanctuary" + }; + + for (char const* trackedBuff : trackedBuffs) + { + if (name.find(trackedBuff) != std::string::npos) + return true; + } + + return false; + } + + bool ShouldDeferPartyBuffEvaluationForRecentLogin( + Player* bot, Unit* target, std::string const& spell) + { + if (!NeedsPostLoginBuffGrace(spell)) + return false; + + if (IsWithinPostLoginBuffGrace(bot)) + return true; + + Player* playerTarget = target ? target->ToPlayer() : nullptr; + return IsWithinPostLoginBuffGrace(playerTarget); + } + + bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot) + { + if (IsWithinPostLoginBuffGrace(bot)) + return true; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member || !member->IsInWorld()) + continue; + + if (IsWithinPostLoginBuffGrace(member)) + return true; + } + + return false; + } + + bool HasRequiredReagents(Player* bot, uint32 spellId) + { + if (!spellId) + return false; + + if (SpellInfo const* info = sSpellMgr->GetSpellInfo(spellId)) + { + for (int i = 0; i < MAX_SPELL_REAGENTS; ++i) + { + if (info->Reagent[i] > 0) + { + uint32 const itemId = info->Reagent[i]; + int32 const need = info->ReagentCount[i]; + if ((int32)bot->GetItemCount(itemId, false) < need) + return false; + } + } + return true; + } + return false; + } + + void ClearMissingBuffReagentNotice(PlayerbotAI* botAI, std::string const& groupName) + { + if (!botAI || groupName.empty()) + return; + + botAI->GetAiObjectContext() + ->GetValue("missing buff reagent notice")->Get() + .erase(groupName); + } + + bool TryAnnounceMissingBuffReagents( + PlayerbotAI* botAI, std::string const& baseName, std::string const& groupName) + { + if (!sPlayerbotAIConfig.tellWhenMissingBuffReagents) + return false; + + Player* bot = botAI->GetBot(); + if (bot->InBattleground()) + return false; + + auto const cooldownMs = sPlayerbotAIConfig.missingBuffReagentMessageCooldown * IN_MILLISECONDS; + auto const now = GameTime::GetGameTimeMS().count(); + auto& noticeTimes = botAI->GetAiObjectContext() + ->GetValue("missing buff reagent notice")->Get(); + auto const noticeIt = noticeTimes.find(groupName); + + if (cooldownMs && noticeIt != noticeTimes.end() && + getMSTimeDiff(noticeIt->second, now) < cooldownMs) + { + return false; + } + + std::map placeholders = { + {"%base_spell", baseName}, + {"%group_spell", groupName} + }; + + std::string const message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "missing_group_buff_reagent", + "I am out of reagents for %group_spell and am casting %base_spell instead.", + placeholders); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + const bool announced = + group->isRaidGroup() ? botAI->SayToRaid(message) : botAI->SayToParty(message); + + if (announced) + noticeTimes[groupName] = now; + + return announced; + } + + std::string UpgradeToGroupIfAppropriate( + Player* bot, + PlayerbotAI* botAI, + std::string const& baseName, + std::string* outMissingReagentGroupName) + { + if (outMissingReagentGroupName) + outMissingReagentGroupName->clear(); + + if (!IsGroupVariantEnabled(bot, baseName)) + return baseName; + + std::string const groupName = GroupVariantFor(baseName); + if (groupName.empty()) + return baseName; + + // Prefer singles until at least three living, in-world group members on the bot's map + // are missing both the single-target buff and its group variant. + if (!HasEnoughSameMapMissingPlayersForGroupVariant(bot, botAI, baseName, groupName)) + return baseName; + + uint32 const groupSpellId = botAI->GetAiObjectContext() + ->GetValue("spell id", groupName)->Get(); + + if (groupSpellId && HasRequiredReagents(bot, groupSpellId)) + { + ClearMissingBuffReagentNotice(botAI, groupName); + return groupName; + } + + if (groupSpellId && outMissingReagentGroupName) + *outMissingReagentGroupName = groupName; + + return baseName; + } +} + +namespace ai::spell +{ + bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId) + { + if (bot->HasSpellCooldown(spellId)) + return true; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return false; + + uint32 category = spellInfo->GetCategory(); + if (!category) + return false; + + for (auto const& [cooldownSpellId, cooldown] : bot->GetSpellCooldownMap()) + { + if (cooldown.category == category && bot->GetSpellCooldownDelay(cooldownSpellId) > 0) + return true; + } + + return false; + } +} diff --git a/modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.h b/modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.h new file mode 100644 index 000000000..eafcea1c0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Util/GenericBuffUtils.h @@ -0,0 +1,59 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GENERICBUFFUTILS_H +#define PLAYERBOTS_GENERICBUFFUTILS_H + +#include +#include + +#include "Common.h" + +class Player; +class PlayerbotAI; +class Unit; + +namespace ai::buff +{ + +typedef std::unordered_map MissingBuffReagentNoticeMap; + +bool IsGroupVariantEnabled(Player* bot, std::string const& name); + +std::string MakeAuraQualifierForBuff(std::string const& name); + +std::string GroupVariantFor(std::string const& name); + +bool NeedsPostLoginBuffGrace(std::string const& name); + +bool ShouldDeferPartyBuffEvaluationForRecentLogin( + Player* bot, + Unit* target, + std::string const& spell); + +bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot); + +bool HasRequiredReagents(Player* bot, uint32 spellId); + +void ClearMissingBuffReagentNotice(PlayerbotAI* botAI, std::string const& groupName); + +bool TryAnnounceMissingBuffReagents( + PlayerbotAI* botAI, std::string const& baseName, std::string const& groupName); + +std::string UpgradeToGroupIfAppropriate( + Player* bot, + PlayerbotAI* botAI, + std::string const& baseName, + std::string* outMissingReagentGroupName = nullptr); + +} + +namespace ai::spell +{ + bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId); +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.cpp index 132a6f826..3c2df666b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ActiveSpellValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.h b/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.h index 50e1e5891..3fb66fdd0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ActiveSpellValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ACTIVESPELLVALUE_H -#define _PLAYERBOT_ACTIVESPELLVALUE_H +#ifndef PLAYERBOTS_ACTIVESPELLVALUE_H +#define PLAYERBOTS_ACTIVESPELLVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.cpp new file mode 100644 index 000000000..c26869241 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.cpp @@ -0,0 +1,67 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AggressiveTargetValue.h" + +#include "Playerbots.h" +#include "ServerFacade.h" +#include "SharedDefines.h" + +Unit* AggressiveTargetValue::Calculate() +{ + Player* master = GetMaster(); + + if (master && (master == bot || master->GetMapId() != bot->GetMapId() || master->IsBeingTeleported() || + !GET_PLAYERBOT_AI(master))) + master = nullptr; + + GuidVector targets = AI_VALUE(GuidVector, "possible targets"); + if (targets.empty()) + return nullptr; + + float aggroRange = sPlayerbotAIConfig.aggroDistance; + float distance = 0; + Unit* result = nullptr; + + for (ObjectGuid const guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (!unit->IsInWorld() || unit->IsDuringRemoveFromWorld()) + continue; + + if (unit->ToCreature() && !unit->ToCreature()->GetCreatureTemplate()->lootid && + bot->GetReactionTo(unit) >= REP_NEUTRAL) + continue; + + if (!bot->IsHostileTo(unit) && unit->GetNpcFlags() != UNIT_NPC_FLAG_NONE) + continue; + + if (abs(bot->GetPositionZ() - unit->GetPositionZ()) > INTERACTION_DISTANCE) + continue; + + if (!bot->InBattleground() && master && botAI->HasStrategy("follow", BotState::BOT_STATE_NON_COMBAT) && + ServerFacade::instance().GetDistance2d(master, unit) > aggroRange) + continue; + + if (!bot->IsWithinLOSInMap(unit)) + continue; + + if (bot->GetDistance(unit) > aggroRange) + continue; + + float newdistance = bot->GetDistance(unit); + if (!result || (newdistance < distance)) + { + distance = newdistance; + result = unit; + } + } + + return result; +} diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.h new file mode 100644 index 000000000..40110dae8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Value/AggressiveTargetValue.h @@ -0,0 +1,23 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AGGRESSIVETARGETVALUE_H +#define PLAYERBOTS_AGGRESSIVETARGETVALUE_H + +#include "TargetValue.h" + +class PlayerbotAI; +class Unit; + +class AggressiveTargetValue : public TargetValue +{ +public: + AggressiveTargetValue(PlayerbotAI* botAI, std::string const name = "aggressive target") : TargetValue(botAI, name) {} + + Unit* Calculate() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.cpp index 13a6aba2f..1fdc3cf87 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AlwaysLootListValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.h b/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.h index d55df8772..eaf756a2a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AlwaysLootListValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ALWAYSLOOTLISTVALUE_H -#define _PLAYERBOT_ALWAYSLOOTLISTVALUE_H +#ifndef PLAYERBOTS_ALWAYSLOOTLISTVALUE_H +#define PLAYERBOTS_ALWAYSLOOTLISTVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.cpp index 2c5698667..4b7304372 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AoeHealValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.h b/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.h index b4c3ac956..05b76fa75 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AoeHealValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AOEHEALVALUES_H -#define _PLAYERBOT_AOEHEALVALUES_H +#ifndef PLAYERBOTS_AOEHEALVALUES_H +#define PLAYERBOTS_AOEHEALVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.cpp index a51a2f4d3..a68b1bb59 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AoeValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.h b/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.h index cefc1a10c..1a798cfff 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AoeValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AOEVALUES_H -#define _PLAYERBOT_AOEVALUES_H +#ifndef PLAYERBOTS_AOEVALUES_H +#define PLAYERBOTS_AOEVALUES_H #include "AiObjectContext.h" #include "GameObject.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Arrow.cpp b/modules/mod-playerbots/src/Ai/Base/Value/Arrow.cpp index 114b2c058..2cdc60d8c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Arrow.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/Arrow.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Arrow.h" @@ -19,7 +20,8 @@ WorldLocation ArrowFormation::GetLocationInternal() uint32 tankLines = 1 + tanks.Size() / 6; uint32 meleeLines = 1 + melee.Size() / 6; uint32 rangedLines = 1 + ranged.Size() / 6; - uint32 healerLines = 1 + healers.Size() / 6; + //@TODO Implement Healer Lines + //uint32 healerLines = 1 + healers.Size() / 6; float offset = 0.f; Player* master = botAI->GetMaster(); @@ -144,12 +146,10 @@ UnitPosition MultiLineUnitPlacer::Place(FormationUnit* unit, uint32 index, uint3 uint32 lineNo = index / 6; uint32 indexInLine = index % 6; uint32 lineSize = std::max(count - lineNo * 6, uint32(6)); - float x = cos(orientation) * sPlayerbotAIConfig.followDistance * lineNo; - float y = sin(orientation) * sPlayerbotAIConfig.followDistance * lineNo; return placer.Place(unit, indexInLine, lineSize); } -UnitPosition SingleLineUnitPlacer::Place(FormationUnit* unit, uint32 index, uint32 count) +UnitPosition SingleLineUnitPlacer::Place(FormationUnit* /*unit*/, uint32 index, uint32 count) { float angle = orientation - M_PI / 2.0f; float x = cos(angle) * sPlayerbotAIConfig.followDistance * ((float)index - (float)count / 2); @@ -160,17 +160,13 @@ UnitPosition SingleLineUnitPlacer::Place(FormationUnit* unit, uint32 index, uint void FormationSlot::Move(float dx, float dy) { for (FormationUnit* unit : units) - { unit->SetLocation(unit->GetX() + dx, unit->GetY() + dy); - } } FormationSlot::~FormationSlot() { for (FormationUnit* unit : units) - { delete unit; - } units.clear(); } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Arrow.h b/modules/mod-playerbots/src/Ai/Base/Value/Arrow.h index 3e5a76b8f..c525904cf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Arrow.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/Arrow.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ARROW_H -#define _PLAYERBOT_ARROW_H +#ifndef PLAYERBOTS_ARROW_H +#define PLAYERBOTS_ARROW_H #include "Formations.h" #include "TravelMgr.h" @@ -16,11 +17,6 @@ class UnitPosition { public: UnitPosition(float x, float y) : x(x), y(y) {} - UnitPosition(UnitPosition const& other) - { - x = other.x; - y = other.y; - } float x, y; }; @@ -102,7 +98,7 @@ class ArrowFormation : public MoveAheadFormation { public: ArrowFormation(PlayerbotAI* botAI) - : MoveAheadFormation(botAI, "arrow"), built(false), masterUnit(nullptr), botUnit(nullptr) + : MoveAheadFormation(botAI, "arrow"), masterUnit(nullptr), botUnit(nullptr), built(false) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.cpp index 88c3cd1fb..abd0181aa 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AttackerCountValues.h" @@ -22,7 +23,7 @@ bool HasAggroValue::Calculate() { return true; } - bool isMT = botAI->IsMainTank(bot); + bool isMT = botAI->IsExplicitMainTank(bot); if (victim && (victim->GetGUID() == bot->GetGUID() || (!isMT && victim->ToPlayer() && botAI->IsTank(victim->ToPlayer())))) { diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.h b/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.h index 651c34153..efa9c51b7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AttackerCountValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ATTACKERCOUNTVALUES_H -#define _PLAYERBOT_ATTACKERCOUNTVALUES_H +#ifndef PLAYERBOTS_ATTACKERCOUNTVALUES_H +#define PLAYERBOTS_ATTACKERCOUNTVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.cpp index c80ba5f68..4bfd56305 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.cpp @@ -1,31 +1,33 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AttackerWithoutAuraTargetValue.h" #include "Playerbots.h" +#include "Strategy.h" +#include "TargetValue.h" Unit* AttackerWithoutAuraTargetValue::Calculate() { GuidVector attackers = botAI->GetAiObjectContext()->GetValue("attackers")->Get(); + GuidSet const dynamicExclusions = GatherStrategyTargetExclusions(botAI, TargetValueExclusionType::Attacker); // Unit* target = botAI->GetAiObjectContext()->GetValue("current target")->Get(); uint32 max_health = 0; Unit* result = nullptr; for (ObjectGuid const guid : attackers) { Unit* unit = botAI->GetUnit(guid); - if (!unit || !unit->IsAlive()) + if (!unit || !unit->IsAlive() || dynamicExclusions.find(guid) != dynamicExclusions.end()) continue; if (!bot->IsWithinCombatRange(unit, botAI->GetRange(range))) continue; if (unit->GetHealth() < max_health) - { continue; - } if (!botAI->HasAura(qualifier, unit, false, true)) { @@ -40,13 +42,14 @@ Unit* AttackerWithoutAuraTargetValue::Calculate() Unit* MeleeAttackerWithoutAuraTargetValue::Calculate() { GuidVector attackers = botAI->GetAiObjectContext()->GetValue("attackers")->Get(); + GuidSet const dynamicExclusions = GatherStrategyTargetExclusions(botAI, TargetValueExclusionType::Attacker); // Unit* target = botAI->GetAiObjectContext()->GetValue("current target")->Get(); uint32 max_health = 0; Unit* result = nullptr; for (ObjectGuid const guid : attackers) { Unit* unit = botAI->GetUnit(guid); - if (!unit || !unit->IsAlive()) + if (!unit || !unit->IsAlive() || dynamicExclusions.find(guid) != dynamicExclusions.end()) continue; if (!bot->IsWithinMeleeRange(unit)) @@ -56,9 +59,7 @@ Unit* MeleeAttackerWithoutAuraTargetValue::Calculate() continue; if (unit->GetHealth() < max_health) - { continue; - } if (!botAI->HasAura(qualifier, unit, false, true)) { diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.h index 5f1a4aa6c..57e85d505 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AttackerWithoutAuraTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ATTACKERWITHOUTAURATARGETVALUE_H -#define _PLAYERBOT_ATTACKERWITHOUTAURATARGETVALUE_H +#ifndef PLAYERBOTS_ATTACKERWITHOUTAURATARGETVALUE_H +#define PLAYERBOTS_ATTACKERWITHOUTAURATARGETVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.cpp index dbde7ab8d..59b61e2f9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AttackersValue.h" @@ -92,21 +93,15 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set& t if (!player || !player->IsInWorld() || player->IsBeingTeleported()) return; - HostileRefMgr& refManager = player->getHostileRefMgr(); - HostileReference* ref = refManager.getFirst(); - if (!ref) - return; - - while (ref) + for (auto const& [guid, ref] : player->GetThreatMgr().GetThreatenedByMeList()) { - ThreatMgr* threatMgr = ref->GetSource(); - Unit* attacker = threatMgr->GetOwner(); + Unit* attacker = ref->GetOwner(); + if (!attacker) + continue; if (player->IsValidAttackTarget(attacker) && player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance) targets.insert(attacker); - - ref = ref->next(); } } @@ -131,7 +126,6 @@ bool AttackersValue::hasRealThreat(Unit* attacker) return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() && // !attacker->isInRoots() && !attacker->IsFriendlyTo(bot); - (attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast(attacker)); } bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) @@ -161,6 +155,11 @@ bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range if (attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) || attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) return false; + // Skip targets that are immune to all damage (e.g., Ice Block, Divine Shield) + if (attacker->IsImmunedToDamage(SPELL_SCHOOL_MASK_NORMAL) && + attacker->IsImmunedToDamage(SPELL_SCHOOL_MASK_MAGIC)) + return false; + // Relationship checks if (attacker->IsFriendlyTo(bot)) return false; @@ -241,9 +240,6 @@ bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) { return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker); - // (attacker->GetThreatMgr().getCurrentVictim() || attacker->GetGuidValue(UNIT_FIELD_TARGET) || - // attacker->GetGUID().IsPlayer() || attacker->GetGUID() == - // GET_PLAYERBOT_AI(bot)->GetAiObjectContext()->GetValue("pull target")->Get()); } bool PossibleAddsValue::Calculate() @@ -255,27 +251,24 @@ bool PossibleAddsValue::Calculate() { if (find(attackers.begin(), attackers.end(), guid) != attackers.end()) continue; + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsInWorld() || add->IsDuringRemoveFromWorld()) + continue; - if (Unit* add = botAI->GetUnit(guid)) + if (!add->GetTarget() && !add->GetThreatMgr().GetLastVictim() && add->IsHostileTo(bot)) { - if (!add->IsInWorld() || add->IsDuringRemoveFromWorld()) - continue; - - if (!add->GetTarget() && !add->GetThreatMgr().getCurrentVictim() && add->IsHostileTo(bot)) + for (ObjectGuid const attackerGUID : attackers) { - for (ObjectGuid const attackerGUID : attackers) - { - Unit* attacker = botAI->GetUnit(attackerGUID); - if (!attacker) - continue; + Unit* attacker = botAI->GetUnit(attackerGUID); + if (!attacker) + continue; - float dist = ServerFacade::instance().GetDistance2d(attacker, add); - if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) - continue; + float dist = ServerFacade::instance().GetDistance2d(attacker, add); + if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) + continue; - if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) - return true; - } + if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) + return true; } } } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.h b/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.h index 7e9397c86..7c2ba9aff 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AttackersValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ATTACKERSVALUE_H -#define _PLAYERBOT_ATTACKERSVALUE_H +#ifndef PLAYERBOTS_ATTACKERSVALUE_H +#define PLAYERBOTS_ATTACKERSVALUE_H #include "PlayerbotAIConfig.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.cpp index 75b4b599a..31c3e93de 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AvailableLootValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.h b/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.h index 4c2a62a84..cd2c977eb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/AvailableLootValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AVAILABLELOOTVALUE_H -#define _PLAYERBOT_AVAILABLELOOTVALUE_H +#ifndef PLAYERBOTS_AVAILABLELOOTVALUE_H +#define PLAYERBOTS_AVAILABLELOOTVALUE_H #include "LootObjectStack.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.cpp index daa7aca86..6982ba321 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BudgetValues.h" @@ -9,7 +10,8 @@ uint32 MaxGearRepairCostValue::Calculate() { - uint32 TotalCost = 0; + uint32 totalCost = 0; + for (int i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i) { uint16 pos = ((INVENTORY_SLOT_BAG_0 << 8) | i); @@ -24,7 +26,7 @@ uint32 MaxGearRepairCostValue::Calculate() uint32 curDurability = item->GetUInt32Value(ITEM_FIELD_DURABILITY); - if (i >= EQUIPMENT_SLOT_END && curDurability >= maxDurability) // Only count items equiped or already damanged. + if (uint32(i) >= EQUIPMENT_SLOT_END && curDurability >= maxDurability) // Only count items equiped or already damanged. continue; ItemTemplate const* ditemProto = item->GetTemplate(); @@ -43,15 +45,16 @@ uint32 MaxGearRepairCostValue::Calculate() uint32 costs = uint32(maxDurability * dmultiplier * double(dQualitymodEntry->quality_mod)); - TotalCost += costs; + totalCost += costs; } - return TotalCost; + return totalCost; } uint32 RepairCostValue::Calculate() { - uint32 TotalCost = 0; + uint32 totalCost = 0; + for (int i = EQUIPMENT_SLOT_START; i < INVENTORY_SLOT_ITEM_END; ++i) { uint16 pos = ((INVENTORY_SLOT_BAG_0 << 8) | i); @@ -86,45 +89,49 @@ uint32 RepairCostValue::Calculate() dcost->multiplier[ItemSubClassToDurabilityMultiplierId(ditemProto->Class, ditemProto->SubClass)]; uint32 costs = uint32(LostDurability * dmultiplier * double(dQualitymodEntry->quality_mod)); - TotalCost += costs; + totalCost += costs; } - return TotalCost; + return totalCost; } uint32 TrainCostValue::Calculate() { - uint32 TotalCost = 0; + uint32 totalCost = 0; - std::set spells; + std::unordered_set spells; - if (CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates()) + CreatureTemplateContainer const* ctc = sObjectMgr->GetCreatureTemplates(); + for (CreatureTemplateContainer::const_iterator itr = ctc->begin(); itr != ctc->end(); ++itr) { - for (CreatureTemplateContainer::const_iterator itr = creatures->begin(); itr != creatures->end(); ++itr) + if (!(itr->second.npcflag & UNIT_NPC_FLAG_TRAINER)) + continue; + + Trainer::Trainer* trainer = sObjectMgr->GetTrainer(itr->first); + if (!trainer) + continue; + + if (trainer->GetTrainerType() != Trainer::Type::Class || !trainer->IsTrainerValidForPlayer(bot)) + continue; + + for (auto& spell : trainer->GetSpells()) { - Trainer::Trainer* trainer = sObjectMgr->GetTrainer(itr->first); - - if (!trainer) + Trainer::Spell const* trainerSpell = trainer->GetSpell(spell.SpellId); + if (!trainerSpell) continue; - if (trainer->GetTrainerType() != Trainer::Type::Class || !trainer->IsTrainerValidForPlayer(bot)) + if (!trainer->CanTeachSpell(bot, trainerSpell)) continue; - for (auto& spell : trainer->GetSpells()) - { - if (!trainer->CanTeachSpell(bot, trainer->GetSpell(spell.SpellId))) - continue; + if (spells.find(trainerSpell->SpellId) != spells.end()) + continue; - if (spells.find(spell.SpellId) != spells.end()) - continue; - - TotalCost += spell.MoneyCost; - spells.insert(spell.SpellId); - } + totalCost += trainerSpell->MoneyCost; + spells.insert(trainerSpell->SpellId); } } - return TotalCost; + return totalCost; } uint32 MoneyNeededForValue::Calculate() diff --git a/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.h b/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.h index 024d4a4f1..e6b5fa9bf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/BudgetValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BUDGETVALUES_H -#define _PLAYERBOT_BUDGETVALUES_H +#ifndef PLAYERBOTS_BUDGETVALUES_H +#define PLAYERBOTS_BUDGETVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.cpp index 0559656f7..eb2d97ae9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CcTargetValue.h" @@ -20,7 +21,7 @@ public: } public: - void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* creature, ThreatManager* /*threatMgr*/) override { Player* bot = botAI->GetBot(); if (!botAI->CanCastSpell(spell, creature)) diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.h index 8bc0483fc..f175ff41b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/CcTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CCTARGETVALUE_H -#define _PLAYERBOT_CCTARGETVALUE_H +#ifndef PLAYERBOTS_CCTARGETVALUE_H +#define PLAYERBOTS_CCTARGETVALUE_H #include "NamedObjectContext.h" #include "TargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ChatValue.h b/modules/mod-playerbots/src/Ai/Base/Value/ChatValue.h index f341eaca5..cb95b747e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ChatValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ChatValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATVALUE_H -#define _PLAYERBOT_CHATVALUE_H +#ifndef PLAYERBOTS_CHATVALUE_H +#define PLAYERBOTS_CHATVALUE_H #include "Value.h" #include "SharedDefines.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.cpp index 172a909b1..900c6549f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CollisionValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.h b/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.h index 91802f222..6bada455c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/CollisionValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_COLLISIONVALUE_H -#define _PLAYERBOT_COLLISIONVALUE_H +#ifndef PLAYERBOTS_COLLISIONVALUE_H +#define PLAYERBOTS_COLLISIONVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CraftValue.h b/modules/mod-playerbots/src/Ai/Base/Value/CraftValue.h index d3311e6c8..2992817be 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CraftValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/CraftValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CRAFTVALUE_H -#define _PLAYERBOT_CRAFTVALUE_H +#ifndef PLAYERBOTS_CRAFTVALUE_H +#define PLAYERBOTS_CRAFTVALUE_H #include @@ -16,11 +17,6 @@ class CraftData { public: CraftData() : itemId(0) {} - CraftData(CraftData const& other) : itemId(other.itemId) - { - required.insert(other.required.begin(), other.required.end()); - obtained.insert(other.obtained.begin(), other.obtained.end()); - } uint32 itemId; std::map required, obtained; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.cpp index 97f35c1b5..92b7942e7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CurrentCcTargetValue.h" @@ -13,7 +14,7 @@ public: { } - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (botAI->HasAura(spell, attacker)) result = attacker; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.h index 708b1d704..a55a2429c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/CurrentCcTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CURRENTCCTARGETVALUE_H -#define _PLAYERBOT_CURRENTCCTARGETVALUE_H +#ifndef PLAYERBOTS_CURRENTCCTARGETVALUE_H +#define PLAYERBOTS_CURRENTCCTARGETVALUE_H #include "NamedObjectContext.h" #include "TargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.cpp index 2aa2fe110..097d01718 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CurrentTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.h index 3cfbb4863..09bbc48e0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/CurrentTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CURRENTTARGETVALUE_H -#define _PLAYERBOT_CURRENTTARGETVALUE_H +#ifndef PLAYERBOTS_CURRENTTARGETVALUE_H +#define PLAYERBOTS_CURRENTTARGETVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.cpp index a5c6fe857..195d6c603 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DistanceValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.h b/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.h index d5160f8cf..e8eaec1dd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/DistanceValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DISTANCEVALUE_H -#define _PLAYERBOT_DISTANCEVALUE_H +#ifndef PLAYERBOTS_DISTANCEVALUE_H +#define PLAYERBOTS_DISTANCEVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.cpp index 28f308471..fd200dda7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.cpp @@ -1,28 +1,36 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DpsTargetValue.h" #include "PlayerbotAIConfig.h" #include "Playerbots.h" +#include "Strategy.h" -class FindMaxThreatGapTargetStrategy : public FindTargetStrategy +class DpsFindTargetStrategy : public FindTargetStrategy { public: - FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), minThreat(0) {} + DpsFindTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + TargetValueExclusionType GetExclusionType() override { return TargetValueExclusionType::Dps; } +}; + +class FindMaxThreatGapTargetStrategy : public DpsFindTargetStrategy +{ +public: + FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : DpsFindTargetStrategy(botAI), minThreat(0) {} + + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -32,7 +40,7 @@ public: if (!result || CalcThreatGap(attacker, threatMgr) > CalcThreatGap(result, &result->GetThreatMgr())) result = attacker; } - float CalcThreatGap(Unit* attacker, ThreatMgr* threatMgr) + float CalcThreatGap(Unit* attacker, ThreatManager* threatMgr) { Unit* victim = attacker->GetVictim(); return threatMgr->GetThreat(victim) - threatMgr->GetThreat(attacker); @@ -43,16 +51,16 @@ protected: }; // caster -class CasterFindTargetSmartStrategy : public FindTargetStrategy +class CasterFindTargetSmartStrategy : public DpsFindTargetStrategy { public: CasterFindTargetSmartStrategy(PlayerbotAI* botAI, float dps) - : FindTargetStrategy(botAI), dps_(dps), targetExpectedLifeTime(1000000) + : DpsFindTargetStrategy(botAI), dps_(dps), targetExpectedLifeTime(1000000) { result = nullptr; } - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -61,13 +69,11 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -90,24 +96,19 @@ public: int new_level = GetIntervalLevel(new_unit); int old_level = GetIntervalLevel(old_unit); if (new_level != old_level) - { return new_level > old_level; - } + int32_t level = new_level; if (level % 10 == 2 || level % 10 == 0) - { return new_time < old_time; - } // dont switch targets when all of them with low health Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("current target")->Get(); if (currentTarget == new_unit) - { return true; - } + if (currentTarget == old_unit) - { return false; - } + return new_time > old_time; } int32_t GetIntervalLevel(Unit* unit) @@ -119,13 +120,11 @@ public: attackRange += 5.0f; int level = dis < attackRange ? 10 : 0; if (time >= 5 && time <= 30) - { return level + 2; - } + if (time > 30) - { return level; - } + return level + 1; } @@ -135,15 +134,15 @@ protected: }; // General -class GeneralFindTargetSmartStrategy : public FindTargetStrategy +class GeneralFindTargetSmartStrategy : public DpsFindTargetStrategy { public: GeneralFindTargetSmartStrategy(PlayerbotAI* botAI, float dps) - : FindTargetStrategy(botAI), dps_(dps), targetExpectedLifeTime(1000000) + : DpsFindTargetStrategy(botAI), dps_(dps), targetExpectedLifeTime(1000000) { } - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -152,13 +151,11 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -186,9 +183,8 @@ public: // attack enemy in range and with lowest health int level = new_level; if (level == 10) - { return new_time < old_time; - } + // all targets are far away, choose the closest one return botAI->GetBot()->GetDistance(new_unit) < botAI->GetBot()->GetDistance(old_unit); } @@ -208,15 +204,15 @@ protected: }; // combo -class ComboFindTargetSmartStrategy : public FindTargetStrategy +class ComboFindTargetSmartStrategy : public DpsFindTargetStrategy { public: ComboFindTargetSmartStrategy(PlayerbotAI* botAI, float dps) - : FindTargetStrategy(botAI), dps_(dps), targetExpectedLifeTime(1000000) + : DpsFindTargetStrategy(botAI), dps_(dps), targetExpectedLifeTime(1000000) { } - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -225,13 +221,11 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -254,9 +248,8 @@ public: int new_level = GetIntervalLevel(new_unit); int old_level = GetIntervalLevel(old_unit); if (new_level != old_level) - { return new_level > old_level; - } + // attack enemy in range and with lowest health int level = new_level; Player* bot = botAI->GetBot(); @@ -264,9 +257,8 @@ public: { Unit* combo_unit = bot->GetComboTarget(); if (new_unit == combo_unit) - { return true; - } + return new_time < old_time; } // all targets are far away, choose the closest one @@ -314,12 +306,12 @@ Unit* DpsTargetValue::Calculate() return TargetValue::FindTarget(&strategy); } -class FindMaxHpTargetStrategy : public FindTargetStrategy +class FindMaxHpTargetStrategy : public DpsFindTargetStrategy { public: - FindMaxHpTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), maxHealth(0) {} + FindMaxHpTargetStrategy(PlayerbotAI* botAI) : DpsFindTargetStrategy(botAI), maxHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { diff --git a/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.h index f45a122db..5d8155ea1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/DpsTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DPSTARGETVALUE_H -#define _PLAYERBOT_DPSTARGETVALUE_H +#ifndef PLAYERBOTS_DPSTARGETVALUE_H +#define PLAYERBOTS_DPSTARGETVALUE_H #include "RtiTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.cpp index 51c2b8bed..52abe8b72 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DuelTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.h index ca310fd64..cb0a82dca 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/DuelTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DUELTARGETVALUE_H -#define _PLAYERBOT_DUELTARGETVALUE_H +#ifndef PLAYERBOTS_DUELTARGETVALUE_H +#define PLAYERBOTS_DUELTARGETVALUE_H #include "TargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.cpp index 85559f782..903801456 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EnemyHealerTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.h index 57aec6d16..4e5d55960 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/EnemyHealerTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ENEMYHEALERTARGETVALUE_H -#define _PLAYERBOT_ENEMYHEALERTARGETVALUE_H +#ifndef PLAYERBOTS_ENEMYHEALERTARGETVALUE_H +#define PLAYERBOTS_ENEMYHEALERTARGETVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.cpp index c2f6e056a..bae5ba8c7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.cpp @@ -1,10 +1,12 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EnemyPlayerValue.h" +#include "CombatManager.h" #include "Playerbots.h" #include "ServerFacade.h" #include "Vehicle.h" @@ -51,34 +53,21 @@ Unit* EnemyPlayerValue::Calculate() controllingVehicle = true; } - // 1. Check units we are currently in combat with. + // 1. Check units we are currently in PvP combat with. std::vector targets; Unit* pVictim = bot->GetVictim(); - HostileReference* pReference = bot->getHostileRefMgr().getFirst(); - while (pReference) + for (auto const& [guid, combatRef] : bot->GetCombatManager().GetPvPCombatRefs()) { - ThreatMgr* threatMgr = pReference->GetSource(); - if (Unit* pTarget = threatMgr->GetOwner()) - { - if (pTarget != pVictim && pTarget->IsPlayer() && pTarget->CanSeeOrDetect(bot) && - bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) - { - if (bot->GetTeamId() == TEAM_HORDE) - { - if (pTarget->HasAura(23333)) - return pTarget; - } - else - { - if (pTarget->HasAura(23335)) - return pTarget; - } + Unit* pTarget = combatRef->GetOther(bot); + if (!pTarget || pTarget == pVictim || !pTarget->IsPlayer() || !pTarget->CanSeeOrDetect(bot) || + !bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) + continue; - targets.push_back(pTarget); - } - } + if ((bot->GetTeamId() == TEAM_HORDE && pTarget->HasAura(23333)) || + (bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335))) + return pTarget; - pReference = pReference->next(); + targets.push_back(pTarget); } if (!targets.empty()) diff --git a/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.h b/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.h index 2ff56f4eb..7d7659f98 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/EnemyPlayerValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ENEMYPLAYERVALUE_H -#define _PLAYERBOT_ENEMYPLAYERVALUE_H +#ifndef PLAYERBOTS_ENEMYPLAYERVALUE_H +#define PLAYERBOTS_ENEMYPLAYERVALUE_H #include "PlayerbotAIConfig.h" #include "PossibleTargetsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.cpp index 8b7e5163c..774ec7c6e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "EstimatedLifetimeValue.h" #include "AiFactory.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.h b/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.h index c412068ec..0abeef0eb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/EstimatedLifetimeValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EstimatedLifetimeValue_H -#define _PLAYERBOT_EstimatedLifetimeValue_H +#ifndef PLAYERBOTS_ESTIMATEDLIFETIMEVALUE_H +#define PLAYERBOTS_ESTIMATEDLIFETIMEVALUE_H #include "NamedObjectContext.h" #include "PossibleTargetsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/FishValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/FishValues.cpp index 18b304670..10654365a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/FishValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/FishValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FishValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/FishValues.h b/modules/mod-playerbots/src/Ai/Base/Value/FishValues.h index 4304d6f45..abcb2bb83 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/FishValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/FishValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FISHVALUES_H -#define _PLAYERBOT_FISHVALUES_H +#ifndef PLAYERBOTS_FISHVALUES_H +#define PLAYERBOTS_FISHVALUES_H #include "Value.h" #include "TravelMgr.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Formations.cpp b/modules/mod-playerbots/src/Ai/Base/Value/Formations.cpp index 1394b9fbe..b08c3a678 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Formations.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/Formations.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Formations.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Formations.h b/modules/mod-playerbots/src/Ai/Base/Value/Formations.h index 3e5decf7f..aa67ba14c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Formations.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/Formations.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FORMATIONS_H -#define _PLAYERBOT_FORMATIONS_H +#ifndef PLAYERBOTS_FORMATIONS_H +#define PLAYERBOTS_FORMATIONS_H #include "Action.h" #include "NamedObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.cpp index 573de8fb0..091414f87 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GrindTargetValue.h" @@ -30,7 +31,6 @@ Unit* GrindTargetValue::Calculate() Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount) { - uint32 memberCount = 1; Group* group = bot->GetGroup(); Player* master = GetMaster(); @@ -65,7 +65,6 @@ Unit* GrindTargetValue::FindTargetForGrinding(uint32 assistCount) if (!unit->IsInWorld() || unit->IsDuringRemoveFromWorld()) continue; - auto& rep = bot->ToPlayer()->GetReputationMgr(); if (unit->ToCreature() && !unit->ToCreature()->GetCreatureTemplate()->lootid && bot->GetReactionTo(unit) >= REP_NEUTRAL) continue; @@ -184,7 +183,7 @@ bool GrindTargetValue::needForQuest(Unit* target) int required = questTemplate->RequiredNpcOrGoCount[j]; int available = questStatus->CreatureOrGOCount[j]; - if (required && available < required && target->GetEntry() == entry) + if (required && available < required && target->GetEntry() == uint32(entry)) return true; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.h index 587037eb1..6799aed3d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/GrindTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GRINDTARGETVALUE_H -#define _PLAYERBOT_GRINDTARGETVALUE_H +#ifndef PLAYERBOTS_GRINDTARGETVALUE_H +#define PLAYERBOTS_GRINDTARGETVALUE_H #include "TargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.cpp index 18ed00764..a2af97f37 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GroupLeaderValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.h b/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.h index a35962043..9b4481e8d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/GroupLeaderValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GROUPLEADERVALUE_H -#define _PLAYERBOT_GROUPLEADERVALUE_H +#ifndef PLAYERBOTS_GROUPLEADERVALUE_H +#define PLAYERBOTS_GROUPLEADERVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.cpp index 460b9f334..d7452ce8f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GroupValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.h b/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.h index ba0473003..6678a6423 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/GroupValues.h @@ -1,8 +1,12 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ +#ifndef PLAYERBOTS_GROUPVALUES_H +#define PLAYERBOTS_GROUPVALUES_H + #include "NamedObjectContext.h" #include "Value.h" @@ -71,3 +75,5 @@ public: bool Calculate() override; }; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.cpp index 27d54f968..1620445b5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.h b/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.h index ece61bc42..fcb516c1b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/GuildValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDVALUES_H -#define _PLAYERBOT_GUILDVALUES_H +#ifndef PLAYERBOTS_GUILDVALUES_H +#define PLAYERBOTS_GUILDVALUES_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.cpp index 532e06afc..9620555f8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HasAvailableLootValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.h b/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.h index 9b8200b56..c1626819d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/HasAvailableLootValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HASAVAILABLELOOTVALUE_H -#define _PLAYERBOT_HASAVAILABLELOOTVALUE_H +#ifndef PLAYERBOTS_HASAVAILABLELOOTVALUE_H +#define PLAYERBOTS_HASAVAILABLELOOTVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.cpp index 0968b2e01..79d837f3b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HasTotemValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.h b/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.h index a43d8f204..ebd566800 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/HasTotemValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HASTOTEMVALUE_H -#define _PLAYERBOT_HASTOTEMVALUE_H +#ifndef PLAYERBOTS_HASTOTEMVALUE_H +#define PLAYERBOTS_HASTOTEMVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.cpp index 50f7a3864..5085a4971 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "InvalidTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.h index bc105dd7b..1026454c8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/InvalidTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_INVALIDTARGETVALUE_H -#define _PLAYERBOT_INVALIDTARGETVALUE_H +#ifndef PLAYERBOTS_INVALIDTARGETVALUE_H +#define PLAYERBOTS_INVALIDTARGETVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.cpp index e23506288..a1ed88e38 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "IsBehindValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.h b/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.h index bf09898ee..dd065f02b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/IsBehindValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ISBEHINDVALUE_H -#define _PLAYERBOT_ISBEHINDVALUE_H +#ifndef PLAYERBOTS_ISBEHINDVALUE_H +#define PLAYERBOTS_ISBEHINDVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.cpp index aee5aa6eb..0afe9cb7c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "IsFacingValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.h b/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.h index 1e6a36864..c6f08296f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/IsFacingValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ISFACINGVALUE_H -#define _PLAYERBOT_ISFACINGVALUE_H +#ifndef PLAYERBOTS_ISFACINGVALUE_H +#define PLAYERBOTS_ISFACINGVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.cpp index 452bda0e4..5eaff6fcc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "IsMovingValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.h b/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.h index fa68693a6..f3abe291e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/IsMovingValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ISMOVINGVALUE_H -#define _PLAYERBOT_ISMOVINGVALUE_H +#ifndef PLAYERBOTS_ISMOVINGVALUE_H +#define PLAYERBOTS_ISMOVINGVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.cpp index 9ea7da2e0..6a150a19b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ItemCountValue.h" @@ -11,8 +12,6 @@ std::vector InventoryItemValueBase::Find(std::string const qualifier) { std::vector result; - Player* bot = InventoryAction::botAI->GetBot(); - std::vector items = InventoryAction::parseItems(qualifier); for (Item* item : items) result.push_back(item); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.h b/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.h index 6f7c593b2..b17031b90 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ItemCountValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ITEMCOUNTVALUE_H -#define _PLAYERBOT_ITEMCOUNTVALUE_H +#ifndef PLAYERBOTS_ITEMCOUNTVALUE_H +#define PLAYERBOTS_ITEMCOUNTVALUE_H #include "InventoryAction.h" #include "Item.h" @@ -17,7 +18,7 @@ class InventoryItemValueBase : public InventoryAction public: InventoryItemValueBase(PlayerbotAI* botAI) : InventoryAction(botAI, "empty") {} - bool Execute(Event event) override { return false; } + bool Execute(Event /*event*/) override { return false; } protected: std::vector Find(std::string const qualifier); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.cpp index e14549da4..3e44e1e87 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ItemForSpellValue.h" @@ -48,23 +49,6 @@ Item* ItemForSpellValue::Calculate() } } - // Workaround as some spells have no item mask (e.g. shaman weapon enhancements) - if (!strcmpi(spellInfo->SpellName[0], "rockbiter weapon") || - !strcmpi(spellInfo->SpellName[0], "flametongue weapon") || - !strcmpi(spellInfo->SpellName[0], "earthliving weapon") || - !strcmpi(spellInfo->SpellName[0], "frostbrand weapon") || !strcmpi(spellInfo->SpellName[0], "windfury weapon")) - { - itemForSpell = GetItemFitsToSpellRequirements(EQUIPMENT_SLOT_MAINHAND, spellInfo); - if (itemForSpell && itemForSpell->GetTemplate()->Class == ITEM_CLASS_WEAPON) - return itemForSpell; - - itemForSpell = GetItemFitsToSpellRequirements(EQUIPMENT_SLOT_OFFHAND, spellInfo); - if (itemForSpell && itemForSpell->GetTemplate()->Class == ITEM_CLASS_WEAPON) - return itemForSpell; - - return nullptr; - } - if (!(spellInfo->Targets & TARGET_FLAG_ITEM)) return nullptr; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.h b/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.h index ea7a52bcd..04363825a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ItemForSpellValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ITEMFORSPELLVALUE_H -#define _PLAYERBOT_ITEMFORSPELLVALUE_H +#ifndef PLAYERBOTS_ITEMFORSPELLVALUE_H +#define PLAYERBOTS_ITEMFORSPELLVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.cpp index c6183e30f..14625db97 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ItemUsageValue.h" @@ -69,8 +70,10 @@ ItemUsage ItemUsageValue::Calculate() if (proto->Class == ITEM_CLASS_KEY) return ITEM_USAGE_USE; + const uint32_t maxCount = proto->MaxCount; + if (proto->Class == ITEM_CLASS_CONSUMABLE && - (proto->MaxCount == 0 || bot->GetItemCount(itemId, false) < proto->MaxCount)) + (maxCount == 0 || bot->GetItemCount(itemId, false) < maxCount)) { std::string const foodType = GetConsumableType(proto, bot->GetPower(POWER_MANA)); @@ -107,17 +110,13 @@ ItemUsage ItemUsageValue::Calculate() // Retrieve the bot's Enchanting skill level uint32 enchantingSkill = bot->GetSkillValue(SKILL_ENCHANTING); - // Check if the bot has a high enough skill to disenchant this item - if (proto->RequiredDisenchantSkill > 0 && enchantingSkill < proto->RequiredDisenchantSkill) - return ITEM_USAGE_NONE; // Not skilled enough to disenchant - - // BoE (Bind on Equip) items should NOT be disenchanted unless they are already bound - if (proto->Bonding == BIND_WHEN_PICKED_UP || (proto->Bonding == BIND_WHEN_EQUIPPED && isSoulbound)) + // Only disenchant if skilled enough and binding allows it + if (enchantingSkill >= proto->RequiredDisenchantSkill && + (proto->Bonding == BIND_WHEN_PICKED_UP || (proto->Bonding == BIND_WHEN_EQUIPPED && isSoulbound))) return ITEM_USAGE_DISENCHANT; } Player* master = botAI->GetMaster(); - bool isSelfBot = (master == bot); bool botNeedsItemForQuest = IsItemUsefulForQuest(bot, proto); bool masterNeedsItemForQuest = master && sPlayerbotAIConfig.syncQuestWithPlayer && IsItemUsefulForQuest(master, proto); @@ -134,8 +133,8 @@ ItemUsage ItemUsageValue::Calculate() if (isLootFromItem && botNeedsItemForQuest) return ITEM_USAGE_QUEST; - // If the bot is NOT acting alone and the master needs this quest item, defer to the master - if (!isSelfBot && masterNeedsItemForQuest) + // If this is not a self-bot acting alone and the master needs this quest item, defer to the master + if (!botAI->IsRealPlayer() && masterNeedsItemForQuest) return ITEM_USAGE_NONE; // If the bot itself needs the item for a quest, allow looting @@ -151,9 +150,8 @@ ItemUsage ItemUsageValue::Calculate() // Need to add something like free bagspace or item value. if (proto->SellPrice > 0) { - if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) + if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP) return ITEM_USAGE_AH; - else return ITEM_USAGE_VENDOR; } @@ -179,19 +177,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, delete pItem; if (result != EQUIP_ERR_OK && result != EQUIP_ERR_CANT_CARRY_MORE_OF_THIS) - { return ITEM_USAGE_NONE; - } - // Check is unique items are equipped or not - bool needToCheckUnique = false; - if (result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS) - { - needToCheckUnique = true; - } - else if (itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE)) - { - needToCheckUnique = true; - } + + // Check if unique items are equipped or not + bool needToCheckUnique = result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS || + itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE); if (needToCheckUnique) { @@ -205,62 +195,61 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, bool isEquipped = (totalItemCount > bagItemCount); if (isEquipped) - { return ITEM_USAGE_NONE; // Item is already equipped - } // If not equipped, continue processing } - if (itemProto->Class == ITEM_CLASS_QUIVER) - if (bot->getClass() != CLASS_HUNTER) - return ITEM_USAGE_NONE; + if (itemProto->Class == ITEM_CLASS_QUIVER && bot->getClass() != CLASS_HUNTER) + return ITEM_USAGE_NONE; if (itemProto->Class == ITEM_CLASS_CONTAINER) { if (itemProto->SubClass != ITEM_SUBCLASS_CONTAINER) return ITEM_USAGE_NONE; // Todo add logic for non-bag containers. We want to look at professions/class and // only replace if non-bag is larger than bag. - if (GetSmallestBagSize() >= itemProto->ContainerSlots) return ITEM_USAGE_NONE; return ITEM_USAGE_EQUIP; } + if (itemProto->Class == ITEM_CLASS_WEAPON && itemProto->SubClass == ITEM_SUBCLASS_WEAPON_MISC) + return ITEM_USAGE_NONE; + bool shouldEquip = false; // uint32 statWeight = sRandomItemMgr.GetLiveStatWeight(bot, itemProto->ItemId); StatsWeightCalculator calculator(bot); calculator.SetItemSetBonus(false); calculator.SetOverflowPenalty(false); + // Apply PvP weights if the bot is specced for PvP + bool isPvp = sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass()); + if (isPvp) + calculator.SetPvpSpec(true); + float itemScore = calculator.CalculateItem(itemProto->ItemId, randomPropertyId); if (itemScore) shouldEquip = true; - if (itemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(bot->getClass(), itemProto)) + if (itemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(itemProto, bot->getClass())) shouldEquip = false; if (itemProto->Class == ITEM_CLASS_ARMOR && - !sRandomItemMgr.CanEquipArmor(bot->getClass(), bot->GetLevel(), itemProto)) + !sRandomItemMgr.CanEquipArmor(itemProto, bot->getClass(), bot->GetLevel())) shouldEquip = false; uint8 possibleSlots = 1; uint8 dstSlot = botAI->FindEquipSlot(itemProto, NULL_SLOT, true); // Check if dest wasn't set correctly by CanEquipItem and use FindEquipSlot instead // This occurs with unique items that are already in the bots bags when CanEquipItem is called - if (dest == 0) + if (dest == 0 && dstSlot != NULL_SLOT) { - if (dstSlot != NULL_SLOT) - { - // Construct dest from dstSlot - dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot; - } + // Construct dest from dstSlot + dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot; } if (dstSlot == EQUIPMENT_SLOT_FINGER1 || dstSlot == EQUIPMENT_SLOT_TRINKET1) - { possibleSlots = 2; - } // Check weapon case separately to keep things a bit cleaner bool have2HWeapon = false; @@ -277,14 +266,9 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, itemProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD2); // If the bot can Titan Grip, ignore any 2H weapon that isn't a 2H sword, mace, or axe. - if (bot->CanTitanGrip()) - { - // If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all. - if (itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon) - { - return ITEM_USAGE_NONE; - } - } + // If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all. + if (bot->CanTitanGrip() && itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon) + return ITEM_USAGE_NONE; // Now handle the logic for equipping and possible offhand slots // If the bot can Dual Wield and: @@ -311,9 +295,7 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, if (shouldEquipInSlot) return ITEM_USAGE_EQUIP; else - { return ITEM_USAGE_BAD_EQUIP; - } } ItemTemplate const* oldItemProto = oldItem->GetTemplate(); @@ -322,30 +304,24 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto, { // uint32 oldStatWeight = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId); if (itemScore || oldScore) - { shouldEquipInSlot = itemScore > oldScore * sPlayerbotAIConfig.equipUpgradeThreshold; - } } // Bigger quiver if (itemProto->Class == ITEM_CLASS_QUIVER) { if (!oldItem || oldItemProto->ContainerSlots < itemProto->ContainerSlots) - { return ITEM_USAGE_EQUIP; - } else - { return ITEM_USAGE_NONE; - } } bool existingShouldEquip = true; - if (oldItemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(bot->getClass(), oldItemProto)) + if (oldItemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(oldItemProto, bot->getClass())) existingShouldEquip = false; if (oldItemProto->Class == ITEM_CLASS_ARMOR && - !sRandomItemMgr.CanEquipArmor(bot->getClass(), bot->GetLevel(), oldItemProto)) + !sRandomItemMgr.CanEquipArmor(oldItemProto, bot->getClass(), bot->GetLevel())) existingShouldEquip = false; // uint32 oldItemPower = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId); @@ -732,7 +708,7 @@ bool ItemUsageValue::HasItemsNeededForSpell(uint32 spellId, ItemTemplate const* for (uint8 i = 0; i < MAX_SPELL_REAGENTS; i++) if (spellInfo->ReagentCount[i] > 0 && spellInfo->Reagent[i]) { - if (proto && proto->ItemId == spellInfo->Reagent[i] && + if (proto && proto->ItemId == uint32(spellInfo->Reagent[i]) && spellInfo->ReagentCount[i] == 1) // If we only need 1 item then current item does not need to be // checked since we are looting/buying or already have it. continue; @@ -831,7 +807,7 @@ std::vector ItemUsageValue::SpellsUsingItem(uint32 itemId, Player* bot) continue; for (uint8 i = 0; i < MAX_SPELL_REAGENTS; i++) - if (spellInfo->ReagentCount[i] > 0 && spellInfo->Reagent[i] == itemId) + if (spellInfo->ReagentCount[i] > 0 && uint32(spellInfo->Reagent[i]) == itemId) retSpells.push_back(spellId); } @@ -863,8 +839,6 @@ bool ItemUsageValue::SpellGivesSkillUp(uint32 spellId, Player* bot) { uint32 SkillValue = bot->GetPureSkillValue(skill->SkillLine); - uint32 craft_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_CRAFTING); - if (SkillGainChance(SkillValue, skill->TrivialSkillLineRankHigh, (skill->TrivialSkillLineRankHigh + skill->TrivialSkillLineRankLow) / 2, skill->TrivialSkillLineRankLow) > 0) diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.h b/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.h index b759bab6a..6071f7f2e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ItemUsageValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ITEMUSAGEVALUE_H -#define _PLAYERBOT_ITEMUSAGEVALUE_H +#ifndef PLAYERBOTS_ITEMUSAGEVALUE_H +#define PLAYERBOTS_ITEMUSAGEVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.cpp index 22c35450d..4b694443b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LastMovementValue.h" @@ -14,11 +15,11 @@ LastMovement::LastMovement(LastMovement& other) taxiMaster(other.taxiMaster), lastFollow(other.lastFollow), lastAreaTrigger(other.lastAreaTrigger), + lastFlee(other.lastFlee), lastMoveToX(other.lastMoveToX), lastMoveToY(other.lastMoveToY), lastMoveToZ(other.lastMoveToZ), - lastMoveToOri(other.lastMoveToOri), - lastFlee(other.lastFlee) + lastMoveToOri(other.lastMoveToOri) { lastMoveShort = other.lastMoveShort; nextTeleport = other.nextTeleport; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.h index ef09d31fe..bd6596252 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LastMovementValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LASTMOVEMENTVALUE_H -#define _PLAYERBOT_LASTMOVEMENTVALUE_H +#ifndef PLAYERBOTS_LASTMOVEMENTVALUE_H +#define PLAYERBOTS_LASTMOVEMENTVALUE_H #include "ObjectGuid.h" #include "TravelNode.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LastSaidValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LastSaidValue.h index 443055866..4108f2761 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LastSaidValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LastSaidValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LASTSAIDVALUE_H -#define _PLAYERBOT_LASTSAIDVALUE_H +#ifndef PLAYERBOTS_LASTSAIDVALUE_H +#define PLAYERBOTS_LASTSAIDVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastTimeValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastTimeValue.h index 984ed8032..e08fbd3f8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastTimeValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastTimeValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LASTSPELLCASTTIMEVALUE_H -#define _PLAYERBOT_LASTSPELLCASTTIMEVALUE_H +#ifndef PLAYERBOTS_LASTSPELLCASTTIMEVALUE_H +#define PLAYERBOTS_LASTSPELLCASTTIMEVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.cpp index ff8d6eadd..088c86d78 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LastSpellCastValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.h index fda18e8b6..8fc0f1f7b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LastSpellCastValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LASTSPELLCASTVALUE_H -#define _PLAYERBOT_LASTSPELLCASTVALUE_H +#ifndef PLAYERBOTS_LASTSPELLCASTVALUE_H +#define PLAYERBOTS_LASTSPELLCASTVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.cpp index 423db1de9..2459c5606 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.cpp @@ -1,21 +1,21 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LeastHpTargetValue.h" #include "AttackersValue.h" -#include "PlayerbotAI.h" +#include "Playerbots.h" class FindLeastHpTargetStrategy : public FindNonCcTargetStrategy { public: FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { - Player* bot = botAI->GetBot(); if (IsCcTarget(attacker)) return; @@ -30,5 +30,6 @@ protected: Unit* LeastHpTargetValue::Calculate() { FindLeastHpTargetStrategy strategy(botAI); + return FindTarget(&strategy); } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.h index 01f40656b..d7444f691 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LeastHpTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LEASTHPTARGETVALUE_H -#define _PLAYERBOT_LEASTHPTARGETVALUE_H +#ifndef PLAYERBOTS_LEASTHPTARGETVALUE_H +#define PLAYERBOTS_LEASTHPTARGETVALUE_H #include "TargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LfgValues.h b/modules/mod-playerbots/src/Ai/Base/Value/LfgValues.h index d6ff5c990..35e8a9458 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LfgValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LfgValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LFGVALUES_H -#define _PLAYERBOT_LFGVALUES_H +#ifndef PLAYERBOTS_LFGVALUES_H +#define PLAYERBOTS_LFGVALUES_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.cpp index e06311cc8..797e691b7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LineTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.h index 6f1d1c532..13eb82e19 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LineTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LINETARGETVALUE_H -#define _PLAYERBOT_LINETARGETVALUE_H +#ifndef PLAYERBOTS_LINETARGETVALUE_H +#define PLAYERBOTS_LINETARGETVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LogLevelValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LogLevelValue.h index f8515b13b..901608ee8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LogLevelValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LogLevelValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOGLEVELVALUE_H -#define _PLAYERBOT_LOGLEVELVALUE_H +#ifndef PLAYERBOTS_LOGLEVELVALUE_H +#define PLAYERBOTS_LOGLEVELVALUE_H #include "LogCommon.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.cpp index 6a4f9b9b4..32ecc865b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootStrategyValue.h" @@ -60,7 +61,7 @@ public: class AllLootStrategy : public LootStrategy { public: - bool CanLoot(ItemTemplate const* proto, AiObjectContext* context) override { return true; } + bool CanLoot(ItemTemplate const* /*proto*/, AiObjectContext* /*context*/) override { return true; } std::string const GetName() override { return "all"; } }; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.h b/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.h index dc3f94cc7..62dbc0929 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LootStrategyValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTSTRATEGYVALUE_H -#define _PLAYERBOT_LOOTSTRATEGYVALUE_H +#ifndef PLAYERBOTS_LOOTSTRATEGYVALUE_H +#define PLAYERBOTS_LOOTSTRATEGYVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LootValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/LootValues.cpp index 45cb6a649..a74f84596 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LootValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/LootValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/LootValues.h b/modules/mod-playerbots/src/Ai/Base/Value/LootValues.h index 7945f3f40..e8e22ce3d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/LootValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/LootValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTVALUES_H -#define _PLAYERBOT_LOOTVALUES_H +#ifndef PLAYERBOTS_LOOTVALUES_H +#define PLAYERBOTS_LOOTVALUES_H #include "ItemUsageValue.h" #include "LootMgr.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.cpp index 6439bef28..3b70302e9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MaintenanceValues.h" @@ -44,6 +45,11 @@ bool CanSellValue::Calculate() AI_VALUE2(uint32, "item count", "usage " + std::to_string(ITEM_USAGE_AH))) > 1; } +bool CanTrainValue::Calculate() +{ + return AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::spells) > 0; +} + bool CanFightEqualValue::Calculate() { return AI_VALUE(uint8, "durability") > 20; } bool CanFightEliteValue::Calculate() diff --git a/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.h b/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.h index 5916f7aae..e489439b8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/MaintenanceValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MAINTANCEVALUE_H -#define _PLAYERBOT_MAINTANCEVALUE_H +#ifndef PLAYERBOTS_MAINTENANCEVALUES_H +#define PLAYERBOTS_MAINTENANCEVALUES_H #include "Value.h" @@ -58,6 +59,14 @@ public: bool Calculate() override; }; +class CanTrainValue : public BoolCalculatedValue +{ +public: + CanTrainValue(PlayerbotAI* botAI) : BoolCalculatedValue(botAI, "can train", 2 * 2000) {} + + bool Calculate() override; +}; + class CanFightEqualValue : public BoolCalculatedValue { public: diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ManaSaveLevelValue.h b/modules/mod-playerbots/src/Ai/Base/Value/ManaSaveLevelValue.h index f406998f1..536653725 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ManaSaveLevelValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ManaSaveLevelValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MANASAVELEVELVALUE_H -#define _PLAYERBOT_MANASAVELEVELVALUE_H +#ifndef PLAYERBOTS_MANASAVELEVELVALUE_H +#define PLAYERBOTS_MANASAVELEVELVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.cpp index 17e987681..0e3ef5a12 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestAdsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.h index b9e3fbe31..2c76f7d25 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestAdsValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTADSVALUE_H -#define _PLAYERBOT_NEARESTADSVALUE_H +#ifndef PLAYERBOTS_NEARESTADSVALUE_H +#define PLAYERBOTS_NEARESTADSVALUE_H #include "PlayerbotAIConfig.h" #include "PossibleTargetsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.cpp index 6091c3241..3c26ae19c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestCorpsesValue.h" @@ -12,13 +13,12 @@ class AnyDeadUnitInObjectRangeCheck { public: - AnyDeadUnitInObjectRangeCheck(WorldObject const* obj, float range) : i_obj(obj), i_range(range) {} + AnyDeadUnitInObjectRangeCheck(WorldObject const* obj, float /*range*/) : i_obj(obj) {} WorldObject const& GetFocusObject() const { return *i_obj; } bool operator()(Unit* u) { return !u->IsAlive(); } private: WorldObject const* i_obj; - float i_range; }; void NearestCorpsesValue::FindUnits(std::list& targets) @@ -28,4 +28,4 @@ void NearestCorpsesValue::FindUnits(std::list& targets) Cell::VisitObjects(bot, searcher, range); } -bool NearestCorpsesValue::AcceptUnit(Unit* unit) { return true; } +bool NearestCorpsesValue::AcceptUnit(Unit* /*unit*/) { return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.h index f6a067c3f..69e07f142 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestCorpsesValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTCORPSESVALUE_H -#define _PLAYERBOT_NEARESTCORPSESVALUE_H +#ifndef PLAYERBOTS_NEARESTCORPSESVALUE_H +#define PLAYERBOTS_NEARESTCORPSESVALUE_H #include "NearestUnitsValue.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.cpp index b28072cdb..62e916e27 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestFriendlyPlayersValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.h index 258840904..5bc8296a4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestFriendlyPlayersValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTFRIENDLYPLAYERSVALUES_H -#define _PLAYERBOT_NEARESTFRIENDLYPLAYERSVALUES_H +#ifndef PLAYERBOTS_NEARESTFRIENDLYPLAYERSVALUE_H +#define PLAYERBOTS_NEARESTFRIENDLYPLAYERSVALUE_H #include "NearestUnitsValue.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.cpp index b5aa2de78..7b4c94447 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestGameObjects.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.h index 9d1153aad..5663a3ea3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestGameObjects.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTGAMEOBJECTS_H -#define _PLAYERBOT_NEARESTGAMEOBJECTS_H +#ifndef PLAYERBOTS_NEARESTGAMEOBJECTS_H +#define PLAYERBOTS_NEARESTGAMEOBJECTS_H #include "PlayerbotAIConfig.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.cpp index 862a9a7e3..c763bd7f9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestNonBotPlayersValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.h index 1980bcf2b..4b6253c80 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestNonBotPlayersValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTNONBOTPLAYERSVALUE_H -#define _PLAYERBOT_NEARESTNONBOTPLAYERSVALUE_H +#ifndef PLAYERBOTS_NEARESTNONBOTPLAYERSVALUE_H +#define PLAYERBOTS_NEARESTNONBOTPLAYERSVALUE_H #include "NearestUnitsValue.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.cpp index a32c007f1..6ac53e718 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestNpcsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.h index 71e1d9de4..0be1e2267 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestNpcsValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTNPCSVALUE_H -#define _PLAYERBOT_NEARESTNPCSVALUE_H +#ifndef PLAYERBOTS_NEARESTNPCSVALUE_H +#define PLAYERBOTS_NEARESTNPCSVALUE_H #include "NearestUnitsValue.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.cpp index 04505a1d9..242070b3f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NearestUnitsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.h index 5e00cb6d0..4473c4021 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NearestUnitsValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEARESTUNITSVALUE_H -#define _PLAYERBOT_NEARESTUNITSVALUE_H +#ifndef PLAYERBOTS_NEARESTUNITSVALUE_H +#define PLAYERBOTS_NEARESTUNITSVALUE_H #include "PlayerbotAIConfig.h" #include "Unit.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.cpp index 39343ea25..6fc613efb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NewPlayerNearbyValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.h b/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.h index e26ec0dcb..6a8667277 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/NewPlayerNearbyValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEWPLAYERNEARBYVALUE_H -#define _PLAYERBOT_NEWPLAYERNEARBYVALUE_H +#ifndef PLAYERBOTS_NEWPLAYERNEARBYVALUE_H +#define PLAYERBOTS_NEWPLAYERNEARBYVALUE_H #include "ObjectGuid.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.cpp index 19eab18b6..042f83917 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "OutfitListValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.h b/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.h index fbb5984fa..a45366be1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/OutfitListValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_OUTFITLISTVALUE_H -#define _PLAYERBOT_OUTFITLISTVALUE_H +#ifndef PLAYERBOTS_OUTFITLISTVALUE_H +#define PLAYERBOTS_OUTFITLISTVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp new file mode 100644 index 000000000..1a9915f48 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp @@ -0,0 +1,65 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PartyMemberSnaredTargetValue.h" + +#include + +#include "PlayerbotAIAware.h" +#include "Playerbots.h" + +class PartyMemberSnaredTargetPredicate : public FindPlayerPredicate, public PlayerbotAIAware +{ +public: + PartyMemberSnaredTargetPredicate(PlayerbotAI* botAI) + : PlayerbotAIAware(botAI) + { + } + + bool Check(Unit* unit) override + { + if (!unit || !unit->IsAlive() || !unit->IsInWorld() || unit == botAI->GetBot()) + return false; + + if (unit->GetMapId() != botAI->GetBot()->GetMapId()) + return false; + + if (!botAI->GetBot()->IsWithinLOSInMap(unit)) + return false; + + return botAI->IsMovementImpaired(unit) && !botAI->HasAnyAuraOf(unit, "stealth", "prowl", nullptr); + } +}; + +Unit* PartyMemberSnaredTargetValue::Calculate() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + PartyMemberSnaredTargetPredicate predicate(botAI); + Player* bestTarget = nullptr; + float closestDistanceSq = std::numeric_limits::max(); + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (!member) + continue; + + if (!predicate.Check(member)) + continue; + + float const distanceSq = bot->GetExactDist2dSq(member->GetPositionX(), member->GetPositionY()); + if (distanceSq < closestDistanceSq) + { + closestDistanceSq = distanceSq; + bestTarget = member; + } + } + + return bestTarget; +} diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h new file mode 100644 index 000000000..7807a79ab --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberSnaredTargetValue.h @@ -0,0 +1,25 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PARTYMEMBERSNAREDTARGETVALUE_H +#define PLAYERBOTS_PARTYMEMBERSNAREDTARGETVALUE_H + +#include "NamedObjectContext.h" +#include "PartyMemberValue.h" + +class PartyMemberSnaredTargetValue : public PartyMemberValue +{ +public: + PartyMemberSnaredTargetValue(PlayerbotAI* botAI, std::string const name = "party member snared target") + : PartyMemberValue(botAI, name) + { + } + +protected: + Unit* Calculate() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.cpp index d3bf4e9ab..56c29cbe2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PartyMemberToDispel.h" @@ -10,7 +11,7 @@ class PartyMemberToDispelPredicate : public FindPlayerPredicate, public Playerbo { public: PartyMemberToDispelPredicate(PlayerbotAI* botAI, uint32 dispelType) - : PlayerbotAIAware(botAI), FindPlayerPredicate(), dispelType(dispelType) + : FindPlayerPredicate(), PlayerbotAIAware(botAI), dispelType(dispelType) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.h index 04e7e9b13..e1f8f680c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToDispel.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PARTYMEMBERTODISPEL_H -#define _PLAYERBOT_PARTYMEMBERTODISPEL_H +#ifndef PLAYERBOTS_PARTYMEMBERTODISPEL_H +#define PLAYERBOTS_PARTYMEMBERTODISPEL_H #include "NamedObjectContext.h" #include "PartyMemberValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.cpp index b49c1cd41..61683e18d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PartyMemberToHeal.h" @@ -38,6 +39,36 @@ Unit* PartyMemberToHeal::Calculate() bool isRaid = bot->GetGroup()->isRaidGroup(); MinValueCalculator calc(100); + // If focus heal targets strategy is active, only heal those targets + if (botAI->HasStrategy("focus heal targets", BOT_STATE_COMBAT)) + { + std::list const focusHealTargets = + AI_VALUE(std::list, "focus heal targets"); + + for (ObjectGuid const& focusHealTarget : focusHealTargets) + { + Player* player = ObjectAccessor::FindPlayer(focusHealTarget); + if (!player || !player->IsInWorld() || !player->IsAlive() || !player->IsInSameGroupWith(bot)) + continue; + + float health = player->GetHealthPct(); + if (isRaid || health < sPlayerbotAIConfig.mediumHealth || + !IsTargetOfSpellCast(player, predicate)) + { + float probeValue = 100.0f; + if (player->GetDistance2d(bot) > sPlayerbotAIConfig.healDistance) + probeValue = health + 30.0f; + else + probeValue = health + player->GetDistance2d(bot) / 10.0f; + + if (probeValue < calc.minValue && Check(player)) + calc.probe(probeValue, player); + } + } + + return (Unit*)calc.param; + } + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* player = gref->GetSource(); @@ -45,17 +76,17 @@ Unit* PartyMemberToHeal::Calculate() continue; if (player && player->IsAlive()) { - uint8 health = player->GetHealthPct(); + float health = player->GetHealthPct(); if (isRaid || health < sPlayerbotAIConfig.mediumHealth || !IsTargetOfSpellCast(player, predicate)) { - uint32 probeValue = 100; + float probeValue = 100.0f; if (player->GetDistance2d(bot) > sPlayerbotAIConfig.healDistance) { - probeValue = health + 30; + probeValue = health + 30.0f; } else { - probeValue = health + player->GetDistance2d(bot) / 10; + probeValue = health + player->GetDistance2d(bot) / 10.0f; } // delay Check player to here for better performance if (probeValue < calc.minValue && Check(player)) @@ -68,10 +99,10 @@ Unit* PartyMemberToHeal::Calculate() Pet* pet = player->GetPet(); if (pet && pet->IsAlive()) { - uint8 health = ((Unit*)pet)->GetHealthPct(); - uint32 probeValue = 100; + float health = ((Unit*)pet)->GetHealthPct(); + float probeValue = 100.0f; if (isRaid || health < sPlayerbotAIConfig.mediumHealth) - probeValue = health + 30; + probeValue = health + 30.0f; // delay Check pet to here for better performance if (probeValue < calc.minValue && Check(pet)) { @@ -82,10 +113,10 @@ Unit* PartyMemberToHeal::Calculate() Unit* charm = player->GetCharm(); if (charm && charm->IsAlive()) { - uint8 health = charm->GetHealthPct(); - uint32 probeValue = 100; + float health = charm->GetHealthPct(); + float probeValue = 100.0f; if (isRaid || health < sPlayerbotAIConfig.mediumHealth) - probeValue = health + 30; + probeValue = health + 30.0f; // delay Check charm to here for better performance if (probeValue < calc.minValue && Check(charm)) { @@ -105,6 +136,32 @@ bool PartyMemberToHeal::Check(Unit* player) bot->GetDistance2d(player) < sPlayerbotAIConfig.healDistance * 2 && bot->IsWithinLOSInMap(player); } +Unit* HealerLowMana::Calculate() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + MinValueCalculator calc(100); + + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* player = gref->GetSource(); + if (!player || player == bot) + continue; + if (player->IsGameMaster() || !player->IsAlive()) + continue; + if (!botAI->IsHeal(player)) + continue; + + float mana = player->GetPowerPct(POWER_MANA); + if (mana < calc.minValue) + calc.probe(mana, player); + } + + return (Unit*)calc.param; +} + Unit* PartyMemberToProtect::Calculate() { return nullptr; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.h index f86035c09..8116dfa1a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToHeal.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PARTYMEMBERTOHEAL_H -#define _PLAYERBOT_PARTYMEMBERTOHEAL_H +#ifndef PLAYERBOTS_PARTYMEMBERTOHEAL_H +#define PLAYERBOTS_PARTYMEMBERTOHEAL_H #include "PartyMemberValue.h" @@ -37,4 +38,13 @@ protected: Unit* Calculate() override; }; +class HealerLowMana : public PartyMemberValue +{ +public: + HealerLowMana(PlayerbotAI* botAI) : PartyMemberValue(botAI, "healer low mana") {} + +protected: + Unit* Calculate() override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.cpp index e98fdb3ed..8bd2ddc21 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PartyMemberToResurrect.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.h index 14e94b735..741e424a6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberToResurrect.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PARTYMEMBERTORESURRECT_H -#define _PLAYERBOT_PARTYMEMBERTORESURRECT_H +#ifndef PLAYERBOTS_PARTYMEMBERTORESURRECT_H +#define PLAYERBOTS_PARTYMEMBERTORESURRECT_H #include "PartyMemberValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.cpp index 53a93b2b4..aac6bd236 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PartyMemberValue.h" @@ -27,7 +28,7 @@ Unit* PartyMemberValue::FindPartyMember(std::vector* party, FindPlayerP return nullptr; } -Unit* PartyMemberValue::FindPartyMember(FindPlayerPredicate& predicate, bool ignoreOutOfGroup) +Unit* PartyMemberValue::FindPartyMember(FindPlayerPredicate& predicate, bool /*ignoreOutOfGroup*/) { Player* master = GetMaster(); // GuidVector nearestPlayers; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.h index 4d52ac61d..b53a76258 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PARTYMEMBERVALUE_H -#define _PLAYERBOT_PARTYMEMBERVALUE_H +#ifndef PLAYERBOTS_PARTYMEMBERVALUE_H +#define PLAYERBOTS_PARTYMEMBERVALUE_H #include "Player.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.cpp index f195a899b..575ed10b0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PartyMemberWithoutAuraValue.h" @@ -13,7 +14,7 @@ class PlayerWithoutAuraPredicate : public FindPlayerPredicate, public PlayerbotA { public: PlayerWithoutAuraPredicate(PlayerbotAI* botAI, std::string const aura) - : PlayerbotAIAware(botAI), FindPlayerPredicate(), auras(split(aura, ',')) + : FindPlayerPredicate(), PlayerbotAIAware(botAI), auras(split(aura, ',')) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.h index 4ba9558ff..2deb98ab7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutAuraValue.h @@ -1,14 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PARTYMEMBERWITHOUTAURAVALUE_H -#define _PLAYERBOT_PARTYMEMBERWITHOUTAURAVALUE_H +#ifndef PLAYERBOTS_PARTYMEMBERWITHOUTAURAVALUE_H +#define PLAYERBOTS_PARTYMEMBERWITHOUTAURAVALUE_H #include "NamedObjectContext.h" #include "PartyMemberValue.h" -#include "PlayerbotAIConfig.h" class PlayerbotAI; class Unit; @@ -16,8 +16,7 @@ class Unit; class PartyMemberWithoutAuraValue : public PartyMemberValue, public Qualified { public: - PartyMemberWithoutAuraValue(PlayerbotAI* botAI, std::string const name = "party member without aura", - float range = sPlayerbotAIConfig.sightDistance) + PartyMemberWithoutAuraValue(PlayerbotAI* botAI, std::string const name = "party member without aura") : PartyMemberValue(botAI, name) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.cpp index c9136c61a..c2bed91db 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PartyMemberWithoutItemValue.h" @@ -11,7 +12,7 @@ class PlayerWithoutItemPredicate : public FindPlayerPredicate, public PlayerbotA { public: PlayerWithoutItemPredicate(PlayerbotAI* botAI, std::string const item) - : PlayerbotAIAware(botAI), FindPlayerPredicate(), item(item) + : FindPlayerPredicate(), PlayerbotAIAware(botAI), item(item) { } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.h index 379835543..9b8b2e59d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PartyMemberWithoutItemValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PARTYMEMBERWITHOUTITEMVALUE_H -#define _PLAYERBOT_PARTYMEMBERWITHOUTITEMVALUE_H +#ifndef PLAYERBOTS_PARTYMEMBERWITHOUTITEMVALUE_H +#define PLAYERBOTS_PARTYMEMBERWITHOUTITEMVALUE_H #include "NamedObjectContext.h" #include "PartyMemberValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.cpp index d8d64d1c0..71253a4d9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PetTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.h index 368acdc9d..f4b78bda5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PetTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PETTARGETVALUE_H -#define _PLAYERBOT_PETTARGETVALUE_H +#ifndef PLAYERBOTS_PETTARGETVALUE_H +#define PLAYERBOTS_PETTARGETVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.cpp index 8583cdd77..527d1307f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PositionValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.h index a0123dafc..990089755 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PositionValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_POSITIONVALUE_H -#define _PLAYERBOT_POSITIONVALUE_H +#ifndef PLAYERBOTS_POSITIONVALUE_H +#define PLAYERBOTS_POSITIONVALUE_H #include "NamedObjectContext.h" #include "TravelMgr.h" @@ -20,10 +21,6 @@ public: : x(x), y(y), z(z), mapId(mapId), valueSet(valueSet) { } - PositionInfo(PositionInfo const& other) - : x(other.x), y(other.y), z(other.z), mapId(other.mapId), valueSet(other.valueSet) - { - } void Set(float newX, float newY, float newZ, uint32 newMapId) { diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp index fbcb8fb2d..42bb421ac 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.cpp @@ -1,19 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PossibleRpgTargetsValue.h" -#include "AiObjectContext.h" +#include "CellImpl.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" #include "ObjectGuid.h" +#include "Playerbots.h" #include "ServerFacade.h" #include "SharedDefines.h" #include "NearestGameObjects.h" -#include "GridNotifiers.h" -#include "GridNotifiersImpl.h" -#include "CellImpl.h" -#include "TravelMgr.h" +#include std::vector PossibleRpgTargetsValue::allowedNpcFlags; @@ -74,15 +75,9 @@ bool PossibleRpgTargetsValue::AcceptUnit(Unit* unit) } TravelTarget* travelTarget = context->GetValue("travel target")->Get(); - - if ( - travelTarget != nullptr - && travelTarget->getDestination() - && (uint32_t)travelTarget->getDestination()->getEntry() == unit->GetEntry() - ) - { + if (travelTarget && travelTarget->getDestination() && + static_cast(travelTarget->getDestination()->getEntry()) == unit->GetEntry()) return true; - } if (urand(1, 100) < 25 && unit->IsFriendlyTo(bot)) return true; @@ -95,8 +90,11 @@ bool PossibleRpgTargetsValue::AcceptUnit(Unit* unit) std::vector PossibleNewRpgTargetsValue::allowedNpcFlags; +// Sparse starting zones where the default scan range is insufficient for WANDER_NPC (requires >= 3 NPCs) +static const std::unordered_set rpgRangeOverrideAreaIds = { 3526 /* Ammen Vale */, 2117 /* Deathknell */ }; + PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float range) - : NearestUnitsValue(botAI, "possible new rpg targets", range, true) + : NearestUnitsValue(botAI, "possible new rpg targets", range, true), defaultRange(range) { if (allowedNpcFlags.empty()) { @@ -126,6 +124,11 @@ PossibleNewRpgTargetsValue::PossibleNewRpgTargetsValue(PlayerbotAI* botAI, float GuidVector PossibleNewRpgTargetsValue::Calculate() { + if (rpgRangeOverrideAreaIds.count(bot->GetAreaId()) && defaultRange < 200.0f) + range = 200.0f; + else + range = defaultRange; + std::list targets; FindUnits(targets); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.h index ae5b596a0..1dd4c64af 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PossibleRpgTargetsValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_POSSIBLERPGTARGETSVALUE_H -#define _PLAYERBOT_POSSIBLERPGTARGETSVALUE_H +#ifndef PLAYERBOTS_POSSIBLERPGTARGETSVALUE_H +#define PLAYERBOTS_POSSIBLERPGTARGETSVALUE_H #include "NearestGameObjects.h" #include "NearestUnitsValue.h" @@ -35,6 +36,8 @@ public: protected: void FindUnits(std::list& targets) override; bool AcceptUnit(Unit* unit) override; +private: + float defaultRange; }; class PossibleNewRpgGameObjectsValue : public ObjectGuidListCalculatedValue diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.cpp index 7db1dd39b..2d6ed163a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PossibleTargetsValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.h b/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.h index c710c2676..bb771c1c2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PossibleTargetsValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_POSSIBLETARGETSVALUE_H -#define _PLAYERBOT_POSSIBLETARGETSVALUE_H +#ifndef PLAYERBOTS_POSSIBLETARGETSVALUE_H +#define PLAYERBOTS_POSSIBLETARGETSVALUE_H #include "NearestUnitsValue.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.cpp index c8caf1441..d0299beca 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PvpValues.h" @@ -24,11 +25,11 @@ Unit* FlagCarrierValue::Calculate() if (!bg) return nullptr; - if ((!sameTeam && bot->GetTeamId() == TEAM_HORDE || (sameTeam && bot->GetTeamId() == TEAM_ALLIANCE)) && + if (((!sameTeam && bot->GetTeamId() == TEAM_HORDE) || (sameTeam && bot->GetTeamId() == TEAM_ALLIANCE)) && !bg->GetFlagPickerGUID(TEAM_HORDE).IsEmpty()) carrier = ObjectAccessor::GetPlayer(bg->GetBgMap(), bg->GetFlagPickerGUID(TEAM_HORDE)); - if ((!sameTeam && bot->GetTeamId() == TEAM_ALLIANCE || (sameTeam && bot->GetTeamId() == TEAM_HORDE)) && + if (((!sameTeam && bot->GetTeamId() == TEAM_ALLIANCE) || (sameTeam && bot->GetTeamId() == TEAM_HORDE)) && !bg->GetFlagPickerGUID(TEAM_ALLIANCE).IsEmpty()) carrier = ObjectAccessor::GetPlayer(bg->GetBgMap(), bg->GetFlagPickerGUID(TEAM_ALLIANCE)); @@ -136,7 +137,7 @@ CreatureData const* BgMasterValue::NearestBm(bool allowDead) if (rbmPair && rDist <= dist) continue; - CreatureTemplate const* bmTemplate = sObjectMgr->GetCreatureTemplate(bmPair->id1); + CreatureTemplate const* bmTemplate = sObjectMgr->GetCreatureTemplate(bmPair->id); if (!bmTemplate) continue; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.h b/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.h index bad46b951..4354b9d0a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/PvpValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PVPVALUES_H -#define _PLAYERBOT_PVPVALUES_H +#ifndef PLAYERBOTS_PVPVALUES_H +#define PLAYERBOTS_PVPVALUES_H #include "NamedObjectContext.h" #include "SharedDefines.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.cpp index f556f443d..6d085a76b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "QuestValues.h" @@ -64,7 +65,7 @@ void FindQuestObjectData::GetObjectiveEntries() // quest map. void FindQuestObjectData::operator()(CreatureData const& creData) { - uint32 entry = creData.id1; + uint32 entry = creData.id; for (auto& relation : relationMap[entry]) { diff --git a/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.h b/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.h index a3ece1fd2..1f186731b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/QuestValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_QUESTVALUES_H -#define _PLAYERBOT_QUESTVALUES_H +#ifndef PLAYERBOTS_QUESTVALUES_H +#define PLAYERBOTS_QUESTVALUES_H #include "NamedObjectContext.h" #include "TravelMgr.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.cpp index 4756dae09..300df4dd4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RTSCValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.h b/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.h index d5ca7b58d..583792f53 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/RTSCValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTSCVALUE_H -#define _PLAYERBOT_RTSCVALUE_H +#ifndef PLAYERBOTS_RTSCVALUES_H +#define PLAYERBOTS_RTSCVALUES_H #include "NamedObjectContext.h" #include "TravelMgr.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RandomBotUpdateValue.h b/modules/mod-playerbots/src/Ai/Base/Value/RandomBotUpdateValue.h index ee3c50105..97ca16ce9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RandomBotUpdateValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/RandomBotUpdateValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANDOMBOTUPDATEVALUE_H -#define _PLAYERBOT_RANDOMBOTUPDATEVALUE_H +#ifndef PLAYERBOTS_RANDOMBOTUPDATEVALUE_H +#define PLAYERBOTS_RANDOMBOTUPDATEVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.cpp index 6b5b0013b..ac8d95dfd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RangeValues.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.h b/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.h index 0728be2a2..ab16daeb1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/RangeValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANGEVALUES_H -#define _PLAYERBOT_RANGEVALUES_H +#ifndef PLAYERBOTS_RANGEVALUES_H +#define PLAYERBOTS_RANGEVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.cpp index 4240e91c7..41bc299dc 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RtiTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.h index c2f3d607a..90bf87195 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/RtiTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTITARGETVALUE_H -#define _PLAYERBOT_RTITARGETVALUE_H +#ifndef PLAYERBOTS_RTITARGETVALUE_H +#define PLAYERBOTS_RTITARGETVALUE_H #include "TargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.cpp index be8cee0ae..935ffadee 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RtiValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.h b/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.h index f4a2efbe7..451dd3f5a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/RtiValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RTIVALUE_H -#define _PLAYERBOT_RTIVALUE_H +#ifndef PLAYERBOTS_RTIVALUE_H +#define PLAYERBOTS_RTIVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.cpp index 7e15a5c4f..71c7751d7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SelfTargetValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.h index 1de04e1cd..e27e72005 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/SelfTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SELFTARGETVALUE_H -#define _PLAYERBOT_SELFTARGETVALUE_H +#ifndef PLAYERBOTS_SELFTARGETVALUE_H +#define PLAYERBOTS_SELFTARGETVALUE_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.cpp index 80893974b..b8926247c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SkipSpellsListValue.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.h b/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.h index 55eb745a9..642a364b3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/SkipSpellsListValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SKIPSPELLSLISTVALUE_H -#define _PLAYERBOT_SKIPSPELLSLISTVALUE_H +#ifndef PLAYERBOTS_SKIPSPELLSLISTVALUE_H +#define PLAYERBOTS_SKIPSPELLSLISTVALUE_H #include diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.cpp index f2440fcec..4f15b86d0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SnareTargetValue.h" @@ -14,7 +15,6 @@ Unit* SnareTargetValue::Calculate() std::string const spell = qualifier; GuidVector attackers = botAI->GetAiObjectContext()->GetValue("attackers")->Get(); - Unit* target = botAI->GetAiObjectContext()->GetValue("current target")->Get(); for (ObjectGuid const guid : attackers) { Unit* unit = botAI->GetUnit(guid); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.h index 5ff976daf..49df38dd5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/SnareTargetValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SNARETARGETVALUE_H -#define _PLAYERBOT_SNARETARGETVALUE_H +#ifndef PLAYERBOTS_SNARETARGETVALUE_H +#define PLAYERBOTS_SNARETARGETVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.cpp index 9fa85b3a1..638b06610 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SpellCastUsefulValue.h" @@ -40,13 +41,13 @@ bool SpellCastUsefulValue::Calculate() return false; } - // TODO: workaround - if (qualifier == "windfury weapon" || qualifier == "flametongue weapon" || qualifier == "frostbrand weapon" || - qualifier == "rockbiter weapon" || qualifier == "earthliving weapon" || qualifier == "spellstone") + if (qualifier == "windfury weapon" || qualifier == "flametongue weapon" || + qualifier == "frostbrand weapon" || qualifier == "rockbiter weapon" || + qualifier == "earthliving weapon" || qualifier == "spellstone") { - if (Item* item = AI_VALUE2(Item*, "item for spell", spellid)) - if (item->IsInWorld() && item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) - return false; + if (Item* item = AI_VALUE2(Item*, "item for spell", spellid); + item && item->IsInWorld() && item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)) + return false; } std::set& skipSpells = AI_VALUE(std::set&, "skip spells list"); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.h b/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.h index 789243341..aee29b8bb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/SpellCastUsefulValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SPELLCASTUSEFULVALUE_H -#define _PLAYERBOT_SPELLCASTUSEFULVALUE_H +#ifndef PLAYERBOTS_SPELLCASTUSEFULVALUE_H +#define PLAYERBOTS_SPELLCASTUSEFULVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.cpp index 244634bcd..cfb81142c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SpellIdValue.h" @@ -30,7 +31,7 @@ uint32 SpellIdValue::Calculate() wstrToLower(wnamepart); char firstSymbol = tolower(namepart[0]); - int spellLength = wnamepart.length(); + size_t spellLength = wnamepart.length(); LocaleConstant loc = LOCALE_enUS; @@ -133,13 +134,13 @@ uint32 SpellIdValue::Calculate() continue; } - if (!highestRank || id > highestRank) + if (!highestRank || (uint32)id > highestRank) { highestRank = id; highestSpellId = spellId; } - if (!lowestRank || (lowestRank && id < lowestRank)) + if (!lowestRank || (lowestRank && (uint32)id < lowestRank)) { lowestRank = id; lowestSpellId = spellId; @@ -153,7 +154,7 @@ uint32 SpellIdValue::Calculate() auto spellId = *it; if (!highestSpellId) highestSpellId = spellId; - if (saveMana == rank) + if (saveMana == (int32)rank) return spellId; lowestSpellId = spellId; rank++; @@ -192,7 +193,7 @@ uint32 VehicleSpellIdValue::Calculate() wstrToLower(wnamepart); char firstSymbol = tolower(namepart[0]); - int spellLength = wnamepart.length(); + size_t spellLength = wnamepart.length(); const int loc = LocaleConstant::LOCALE_enUS; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.h b/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.h index 3df8e8229..28dbf8072 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/SpellIdValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SPELLIDVALUE_H -#define _PLAYERBOT_SPELLIDVALUE_H +#ifndef PLAYERBOTS_SPELLIDVALUE_H +#define PLAYERBOTS_SPELLIDVALUE_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp b/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp index 280bb5706..ce33bfac0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Stances.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Stances.h b/modules/mod-playerbots/src/Ai/Base/Value/Stances.h index 16ad14de2..f53590e6b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Stances.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/Stances.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STANCES_H -#define _PLAYERBOT_STANCES_H +#ifndef PLAYERBOTS_STANCES_H +#define PLAYERBOTS_STANCES_H #include "Action.h" #include "Formations.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.cpp index 5205ec05b..95c262160 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "StatsValues.h" @@ -208,7 +209,6 @@ uint8 BagSpaceValue::Calculate() ++totalused; } - uint32 totalfree = 16 - totalused; for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) { const Bag* const pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bag); @@ -218,7 +218,6 @@ uint8 BagSpaceValue::Calculate() if (pBagProto->Class == ITEM_CLASS_CONTAINER && pBagProto->SubClass == ITEM_SUBCLASS_CONTAINER) { total += pBag->GetBagSize(); - totalfree += pBag->GetFreeSlots(); totalused += pBag->GetBagSize() - pBag->GetFreeSlots(); } } diff --git a/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.h b/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.h index 27cad01f1..4c153f3bf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/StatsValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STATSVALUE_H -#define _PLAYERBOT_STATSVALUE_H +#ifndef PLAYERBOTS_STATSVALUES_H +#define PLAYERBOTS_STATSVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.cpp index 90c759a7d..d2259f256 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TankTargetValue.h" @@ -9,18 +10,19 @@ #include "AttackersValue.h" #include "Group.h" #include "PlayerbotAI.h" +#include "Playerbots.h" +#include "Strategy.h" class FindTargetForTankStrategy : public FindNonCcTargetStrategy { public: FindTargetForTankStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minThreat(0) {} - void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override { if (!creature || !creature->IsAlive()) - { return; - } + Player* bot = botAI->GetBot(); float threat = threatMgr->GetThreat(bot); if (!result) @@ -29,14 +31,10 @@ public: result = creature; } // neglect if victim is main tank, or no victim (for untauntable target) - if (threatMgr->getCurrentVictim()) + if (Unit* victim = threatMgr->GetCurrentVictim()) { - // float max_threat = threatMgr->GetThreat(threatMgr->getCurrentVictim()->getTarget()); - Unit* victim = threatMgr->getCurrentVictim()->getTarget(); - if (victim && victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer())) - { + if (victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer())) return; - } } if (minThreat >= threat) { @@ -54,7 +52,9 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy public: FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + TargetValueExclusionType GetExclusionType() override { return TargetValueExclusionType::Tank; } + + void CheckAttacker(Unit* attacker, ThreatManager* /*threatMgr*/) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -62,24 +62,23 @@ public: if (guid && attacker->GetGUID() == guid) return; } + if (!attacker->IsAlive()) - { return; - } + if (!result || IsBetter(attacker, result)) - { result = attacker; - } } bool IsBetter(Unit* new_unit, Unit* old_unit) { Player* bot = botAI->GetBot(); - // if group has multiple tanks, main tank just focus on the current target + // if group has multiple tanks, explicit main tank just focus on the current target Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("current target")->Get(); - if (currentTarget && botAI->IsMainTank(bot) && botAI->GetGroupTankNum(bot) > 1) + if (currentTarget && botAI->IsExplicitMainTank(bot) && botAI->GetGroupTankNum(bot) > 1) { if (old_unit == currentTarget) return false; + if (new_unit == currentTarget) return true; } @@ -89,32 +88,49 @@ public: float old_dis = bot->GetDistance(old_unit); // hasAggro? -> withinMelee? -> threat if (GetIntervalLevel(new_unit) != GetIntervalLevel(old_unit)) - { return GetIntervalLevel(new_unit) > GetIntervalLevel(old_unit); - } + int32_t interval = GetIntervalLevel(new_unit); if (interval == 2) - { return new_dis < old_dis; - } + return new_threat < old_threat; } int32_t GetIntervalLevel(Unit* unit) { if (!botAI->HasAggro(unit)) - { return 2; - } + if (botAI->GetBot()->IsWithinMeleeRange(unit)) - { return 1; - } + return 0; } }; Unit* TankTargetValue::Calculate() { + std::string const rti = botAI->GetAiObjectContext()->GetValue("rti")->Get(); + Unit* rtiTarget = RtiTargetValue::Calculate(); + if (rtiTarget) + { + Unit* victim = rtiTarget->GetVictim(); + + if (victim && victim != bot) + { + if (Player* victimPlayer = victim->ToPlayer()) + { + // rti target is attacking a non-tank player + if (!PlayerbotAI::IsTank(victimPlayer)) + return rtiTarget; + // rti target is attacking a tank player, check if the tank is a bot and has the same rti setting + PlayerbotAI* victimBotAI = GET_PLAYERBOT_AI(victimPlayer); + if (!victimBotAI || victimBotAI->GetAiObjectContext()->GetValue("rti")->Get() != rti) + return rtiTarget; + } + } + } + // FindTargetForTankStrategy strategy(botAI); FindTankTargetSmartStrategy strategy(botAI); return FindTarget(&strategy); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.h index 8aee6182e..401ccfaae 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/TankTargetValue.h @@ -1,19 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TANKTARGETVALUE_H -#define _PLAYERBOT_TANKTARGETVALUE_H +#ifndef PLAYERBOTS_TANKTARGETVALUE_H +#define PLAYERBOTS_TANKTARGETVALUE_H -#include "TargetValue.h" +#include "RtiTargetValue.h" class PlayerbotAI; -class TankTargetValue : public TargetValue +class TankTargetValue : public RtiTargetValue { public: - TankTargetValue(PlayerbotAI* botAI, std::string const name = "tank target") : TargetValue(botAI, name) {} + TankTargetValue(PlayerbotAI* botAI, std::string const name = "tank target") : RtiTargetValue(botAI, "rti", name) {} Unit* Calculate() override; }; diff --git a/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.cpp b/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.cpp index 21621545e..c3186d58a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.cpp @@ -1,30 +1,54 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TargetValue.h" +#include "CombatManager.h" #include "LastMovementValue.h" #include "ObjectGuid.h" #include "Playerbots.h" #include "RtiTargetValue.h" #include "ScriptedCreature.h" -#include "ThreatMgr.h" +#include "Strategy.h" +#include "ThreatManager.h" + +GuidSet GatherStrategyTargetExclusions(PlayerbotAI* botAI, TargetValueExclusionType type) +{ + GuidSet exclusions; + if (!botAI || type == TargetValueExclusionType::None || !botAI->HasTargetExclusions()) + return exclusions; + + for (auto const& strategyName : botAI->GetStrategies(BOT_STATE_COMBAT)) + { + Strategy* strategy = botAI->GetStrategy(strategyName, BOT_STATE_COMBAT); + if (!strategy) + continue; + + strategy->AppendTargetExclusions(exclusions, type); + } + + return exclusions; +} Unit* FindTargetStrategy::GetResult() { return result; } +TargetValueExclusionType FindTargetStrategy::GetExclusionType() { return TargetValueExclusionType::None; } + Unit* TargetValue::FindTarget(FindTargetStrategy* strategy) { GuidVector attackers = botAI->GetAiObjectContext()->GetValue("attackers")->Get(); + GuidSet const dynamicExclusions = GatherStrategyTargetExclusions(botAI, strategy->GetExclusionType()); for (ObjectGuid const guid : attackers) { Unit* unit = botAI->GetUnit(guid); - if (!unit) + if (!unit || dynamicExclusions.find(guid) != dynamicExclusions.end()) continue; - ThreatMgr& ThreatMgr = unit->GetThreatMgr(); - strategy->CheckAttacker(unit, &ThreatMgr); + ThreatManager& threatMgr = unit->GetThreatMgr(); + strategy->CheckAttacker(unit, &threatMgr); } return strategy->GetResult(); @@ -144,24 +168,23 @@ Unit* FindTargetValue::Calculate() { return nullptr; } - HostileReference* ref = bot->getHostileRefMgr().getFirst(); - while (ref) + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) { - ThreatMgr* threatManager = ref->GetSource(); - Unit* unit = threatManager->GetOwner(); + Unit* unit = ref->GetOwner(); + if (!unit) + continue; + std::wstring wnamepart; Utf8toWStr(unit->GetName(), wnamepart); wstrToLower(wnamepart); if (!qualifier.empty() && qualifier.length() == wnamepart.length() && Utf8FitTo(qualifier, wnamepart)) - { return unit; - } - ref = ref->next(); } + return nullptr; } -void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatMgr* threatManager) +void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* /*threatManager*/) { UnitAI* unitAI = attacker->GetAI(); BossAI* bossAI = dynamic_cast(unitAI); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.h b/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.h index fcd7a5889..ce5e75778 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/TargetValue.h @@ -1,18 +1,22 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TARGETVALUE_H -#define _PLAYERBOT_TARGETVALUE_H +#ifndef PLAYERBOTS_TARGETVALUE_H +#define PLAYERBOTS_TARGETVALUE_H #include "NamedObjectContext.h" #include "TravelMgr.h" #include "Value.h" class PlayerbotAI; -class ThreatMgr; +class ThreatManager; class Unit; +enum class TargetValueExclusionType : uint8; + +GuidSet GatherStrategyTargetExclusions(PlayerbotAI* botAI, TargetValueExclusionType type); class FindTargetStrategy { @@ -20,7 +24,8 @@ public: FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {} Unit* GetResult(); - virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) = 0; + virtual TargetValueExclusionType GetExclusionType(); + virtual void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) = 0; void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount); bool IsHighPriority(Unit* attacker); @@ -116,10 +121,19 @@ public: } }; +class PullStrategyTargetValue : public ManualSetValue +{ +public: + PullStrategyTargetValue(PlayerbotAI* botAI, std::string const name = "pull strategy target") + : ManualSetValue(botAI, ObjectGuid::Empty, name) + { + } +}; + class FindTargetValue : public UnitCalculatedValue, public Qualified { public: - FindTargetValue(PlayerbotAI* ai) : UnitCalculatedValue(ai, "find target", /*2 * 1000*/ 1) {} + FindTargetValue(PlayerbotAI* botAI) : UnitCalculatedValue(botAI, "find target", /*2 * 1000*/ 1) {} public: Unit* Calculate(); @@ -128,16 +142,23 @@ public: class FindBossTargetStrategy : public FindTargetStrategy { public: - FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {} - virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatManager); + FindBossTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} + virtual void CheckAttacker(Unit* attacker, ThreatManager* threatManager); }; class BossTargetValue : public TargetValue, public Qualified { public: - BossTargetValue(PlayerbotAI* ai) : TargetValue(ai, "boss target", 2 * 1000) {} + BossTargetValue(PlayerbotAI* botAI) : TargetValue(botAI, "boss target", 2 * 1000) {} public: Unit* Calculate(); }; + +class FocusHealTargetValue : public ManualSetValue> +{ +public: + FocusHealTargetValue(PlayerbotAI* botAI) : ManualSetValue>(botAI, {}, "focus heal targets") {} +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.cpp b/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.cpp index d95b00142..d25b217b1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.cpp @@ -1,12 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ThreatValues.h" #include "Playerbots.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" uint8 ThreatValue::Calculate() { diff --git a/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.h b/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.h index 845a8c958..d2b93bba2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.h +++ b/modules/mod-playerbots/src/Ai/Base/Value/ThreatValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_THREATVALUES_H -#define _PLAYERBOT_THREATVALUES_H +#ifndef PLAYERBOTS_THREATVALUES_H +#define PLAYERBOTS_THREATVALUES_H #include "NamedObjectContext.h" #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Value/WaitForAttackTimeValue.h b/modules/mod-playerbots/src/Ai/Base/Value/WaitForAttackTimeValue.h new file mode 100644 index 000000000..befd2b4e6 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Base/Value/WaitForAttackTimeValue.h @@ -0,0 +1,33 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * David Parra Ausina (davidonete/Flekz) - original author + */ + +#ifndef PLAYERBOTS_WAITFORATTACKTIMEVALUE_H +#define PLAYERBOTS_WAITFORATTACKTIMEVALUE_H + +#include "Value.h" + +class PlayerbotAI; + +class WaitForAttackTimeValue : public ManualSetValue +{ +public: + WaitForAttackTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 10, "wait for attack time") {} +}; + +class CombatStartTimeValue : public ManualSetValue +{ +public: + CombatStartTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 0, "combat start time") {} +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/ValueContext.h b/modules/mod-playerbots/src/Ai/Base/ValueContext.h index b67aee9bb..12a7c6f36 100644 --- a/modules/mod-playerbots/src/Ai/Base/ValueContext.h +++ b/modules/mod-playerbots/src/Ai/Base/ValueContext.h @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_VALUECONTEXT_H -#define _PLAYERBOT_VALUECONTEXT_H +#ifndef PLAYERBOTS_VALUECONTEXT_H +#define PLAYERBOTS_VALUECONTEXT_H #include "ActiveSpellValue.h" +#include "AggressiveTargetValue.h" #include "AlwaysLootListValue.h" #include "AoeHealValues.h" #include "AoeValues.h" @@ -51,6 +53,7 @@ #include "LineTargetValue.h" #include "LogLevelValue.h" #include "LootStrategyValue.h" +#include "LootValues.h" #include "MaintenanceValues.h" #include "ManaSaveLevelValue.h" #include "NearestAdsValue.h" @@ -64,6 +67,7 @@ #include "PartyMemberToDispel.h" #include "PartyMemberToHeal.h" #include "PartyMemberToResurrect.h" +#include "PartyMemberSnaredTargetValue.h" #include "PartyMemberWithoutAuraValue.h" #include "PartyMemberWithoutItemValue.h" #include "PetTargetValue.h" @@ -89,6 +93,7 @@ #include "ThreatValues.h" #include "TradeValues.h" #include "Value.h" +#include "WaitForAttackTimeValue.h" class PlayerbotAI; @@ -128,6 +133,7 @@ public: creators["attacker without aura"] = &ValueContext::attacker_without_aura; creators["melee attacker without aura"] = &ValueContext::melee_attacker_without_aura; creators["party member to heal"] = &ValueContext::party_member_to_heal; + creators["healer low mana"] = &ValueContext::healer_low_mana; creators["party member to resurrect"] = &ValueContext::party_member_to_resurrect; creators["current target"] = &ValueContext::current_target; creators["self target"] = &ValueContext::self_target; @@ -143,11 +149,13 @@ public: creators["pet target"] = &ValueContext::pet_target; creators["old target"] = &ValueContext::old_target; creators["grind target"] = &ValueContext::grind_target; + creators["aggressive target"] = &ValueContext::aggressive_target; creators["rti target"] = &ValueContext::rti_target; creators["rti cc target"] = &ValueContext::rti_cc_target; creators["duel target"] = &ValueContext::duel_target; creators["party member to dispel"] = &ValueContext::party_member_to_dispel; creators["party member to protect"] = &ValueContext::party_member_to_protect; + creators["party member snared target"] = &ValueContext::party_member_snared_target; creators["health"] = &ValueContext::health; creators["rage"] = &ValueContext::rage; creators["energy"] = &ValueContext::energy; @@ -185,6 +193,7 @@ public: creators["spell id"] = &ValueContext::spell_id; creators["vehicle spell id"] = &ValueContext::vehicle_spell_id; creators["item for spell"] = &ValueContext::item_for_spell; + creators["missing buff reagent notice"] = &ValueContext::missing_buff_reagent_notice; creators["spell cast useful"] = &ValueContext::spell_cast_useful; creators["last spell cast"] = &ValueContext::last_spell_cast; creators["last spell cast time"] = &ValueContext::last_spell_cast_time; @@ -235,6 +244,8 @@ public: creators["travel target"] = &ValueContext::travel_target; creators["talk target"] = &ValueContext::talk_target; creators["pull target"] = &ValueContext::pull_target; + creators["pull strategy target"] = &ValueContext::pull_strategy_target; + creators["focus heal targets"] = &ValueContext::focus_heal_targets; creators["group"] = &ValueContext::group; creators["range"] = &ValueContext::range; creators["inside target"] = &ValueContext::inside_target; @@ -272,6 +283,7 @@ public: creators["can repair"] = &ValueContext::can_repair; creators["should sell"] = &ValueContext::should_sell; creators["can sell"] = &ValueContext::can_sell; + creators["can train"] = &ValueContext::can_train; creators["can fight equal"] = &ValueContext::can_fight_equal; creators["can fight elite"] = &ValueContext::can_fight_elite; creators["can fight boss"] = &ValueContext::can_fight_boss; @@ -318,6 +330,8 @@ public: creators["can fish"] = &ValueContext::can_fish; creators["can use fishing bobber"] = &ValueContext::can_use_fishing_bobber; creators["fishing spot"] = &ValueContext::fishing_spot; + creators["wait for attack time"] = &ValueContext::wait_for_attack_time; + creators["combat start time"] = &ValueContext::combat_start_time; } private: @@ -351,7 +365,7 @@ private: static UntypedValue* attackers(PlayerbotAI* botAI) { return new AttackersValue(botAI); } static UntypedValue* position(PlayerbotAI* botAI) { return new PositionValue(botAI); } - static UntypedValue* pos(PlayerbotAI* ai) { return new SinglePositionValue(ai); } + static UntypedValue* pos(PlayerbotAI* botAI) { return new SinglePositionValue(botAI); } static UntypedValue* current_position(PlayerbotAI* botAI) { return new CurrentPositionValue(botAI); } static UntypedValue* rti(PlayerbotAI* botAI) { return new RtiValue(botAI); } static UntypedValue* rti_cc(PlayerbotAI* botAI) { return new RtiCcValue(botAI); } @@ -363,6 +377,7 @@ private: static UntypedValue* last_spell_cast_time(PlayerbotAI* botAI) { return new LastSpellCastTimeValue(botAI); } static UntypedValue* spell_cast_useful(PlayerbotAI* botAI) { return new SpellCastUsefulValue(botAI); } static UntypedValue* item_for_spell(PlayerbotAI* botAI) { return new ItemForSpellValue(botAI); } + static UntypedValue* missing_buff_reagent_notice(PlayerbotAI* botAI) { return new MissingBuffReagentNoticeValue(botAI); } static UntypedValue* spell_id(PlayerbotAI* botAI) { return new SpellIdValue(botAI); } static UntypedValue* vehicle_spell_id(PlayerbotAI* botAI) { return new VehicleSpellIdValue(botAI); } static UntypedValue* inventory_item(PlayerbotAI* botAI) { return new InventoryItemValue(botAI); } @@ -440,9 +455,11 @@ private: return new MeleeAttackerWithoutAuraTargetValue(botAI); } static UntypedValue* party_member_to_heal(PlayerbotAI* botAI) { return new PartyMemberToHeal(botAI); } + static UntypedValue* healer_low_mana(PlayerbotAI* botAI) { return new HealerLowMana(botAI); } static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); } static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); } static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); } + static UntypedValue* party_member_snared_target(PlayerbotAI* botAI) { return new PartyMemberSnaredTargetValue(botAI); } static UntypedValue* current_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* old_target(PlayerbotAI* botAI) { return new CurrentTargetValue(botAI); } static UntypedValue* self_target(PlayerbotAI* botAI) { return new SelfTargetValue(botAI); } @@ -457,6 +474,7 @@ private: static UntypedValue* current_cc_target(PlayerbotAI* botAI) { return new CurrentCcTargetValue(botAI); } static UntypedValue* pet_target(PlayerbotAI* botAI) { return new PetTargetValue(botAI); } static UntypedValue* grind_target(PlayerbotAI* botAI) { return new GrindTargetValue(botAI); } + static UntypedValue* aggressive_target(PlayerbotAI* botAI) { return new AggressiveTargetValue(botAI); } static UntypedValue* rti_target(PlayerbotAI* botAI) { return new RtiTargetValue(botAI); } static UntypedValue* rti_cc_target(PlayerbotAI* botAI) { return new RtiCcTargetValue(botAI); } static UntypedValue* duel_target(PlayerbotAI* botAI) { return new DuelTargetValue(botAI); } @@ -486,6 +504,8 @@ private: static UntypedValue* next_rpg_action(PlayerbotAI* botAI) { return new NextRpgActionValue(botAI); } static UntypedValue* travel_target(PlayerbotAI* botAI) { return new TravelTargetValue(botAI); } static UntypedValue* pull_target(PlayerbotAI* botAI) { return new PullTargetValue(botAI); } + static UntypedValue* pull_strategy_target(PlayerbotAI* botAI) { return new PullStrategyTargetValue(botAI); } + static UntypedValue* focus_heal_targets(PlayerbotAI* botAI) { return new FocusHealTargetValue(botAI); } static UntypedValue* bg_master(PlayerbotAI* botAI) { return new BgMasterValue(botAI); } static UntypedValue* bg_role(PlayerbotAI* botAI) { return new BgRoleValue(botAI); } @@ -519,6 +539,7 @@ private: static UntypedValue* can_repair(PlayerbotAI* botAI) { return new CanRepairValue(botAI); } static UntypedValue* should_sell(PlayerbotAI* botAI) { return new ShouldSellValue(botAI); } static UntypedValue* can_sell(PlayerbotAI* botAI) { return new CanSellValue(botAI); } + static UntypedValue* can_train(PlayerbotAI* botAI) { return new CanTrainValue(botAI); } static UntypedValue* can_fight_equal(PlayerbotAI* botAI) { return new CanFightEqualValue(botAI); } static UntypedValue* can_fight_elite(PlayerbotAI* botAI) { return new CanFightEliteValue(botAI); } static UntypedValue* can_fight_boss(PlayerbotAI* botAI) { return new CanFightBossValue(botAI); } @@ -549,29 +570,28 @@ private: static UntypedValue* has_area_debuff(PlayerbotAI* botAI) { return new HasAreaDebuffValue(botAI); } - static UntypedValue* main_tank(PlayerbotAI* ai) { return new PartyMemberMainTankValue(ai); } - static UntypedValue* find_target(PlayerbotAI* ai) { return new FindTargetValue(ai); } - static UntypedValue* boss_target(PlayerbotAI* ai) { return new BossTargetValue(ai); } - static UntypedValue* nearest_triggers(PlayerbotAI* ai) { return new NearestTriggersValue(ai); } - static UntypedValue* neglect_threat(PlayerbotAI* ai) { return new NeglectThreatResetValue(ai); } - static UntypedValue* expected_lifetime(PlayerbotAI* ai) { return new EstimatedLifetimeValue(ai); } - static UntypedValue* expected_group_dps(PlayerbotAI* ai) { return new EstimatedGroupDpsValue(ai); } - static UntypedValue* area_debuff(PlayerbotAI* ai) { return new AreaDebuffValue(ai); } - static UntypedValue* nearest_trap_with_damange(PlayerbotAI* ai) { return new NearestTrapWithDamageValue(ai); } - static UntypedValue* disperse_distance(PlayerbotAI* ai) { return new DisperseDistanceValue(ai); } - static UntypedValue* last_flee_angle(PlayerbotAI* ai) { return new LastFleeAngleValue(ai); } - static UntypedValue* last_flee_timestamp(PlayerbotAI* ai) { return new LastFleeTimestampValue(ai); } - static UntypedValue* recently_flee_info(PlayerbotAI* ai) { return new RecentlyFleeInfo(ai); } - static UntypedValue* can_fish(PlayerbotAI* ai) { return new CanFishValue(ai); } - static UntypedValue* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberValue(ai); } - static UntypedValue* fishing_spot(PlayerbotAI* ai) { return new FishingSpotValue(ai); } - // ------------------------------------------------------- - // Flag for cutom glyphs : true when /w bot glyph equip - // ------------------------------------------------------- - static UntypedValue* custom_glyphs(PlayerbotAI* ai) + static UntypedValue* main_tank(PlayerbotAI* botAI) { return new PartyMemberMainTankValue(botAI); } + static UntypedValue* find_target(PlayerbotAI* botAI) { return new FindTargetValue(botAI); } + static UntypedValue* boss_target(PlayerbotAI* botAI) { return new BossTargetValue(botAI); } + static UntypedValue* nearest_triggers(PlayerbotAI* botAI) { return new NearestTriggersValue(botAI); } + static UntypedValue* neglect_threat(PlayerbotAI* botAI) { return new NeglectThreatResetValue(botAI); } + static UntypedValue* expected_lifetime(PlayerbotAI* botAI) { return new EstimatedLifetimeValue(botAI); } + static UntypedValue* expected_group_dps(PlayerbotAI* botAI) { return new EstimatedGroupDpsValue(botAI); } + static UntypedValue* area_debuff(PlayerbotAI* botAI) { return new AreaDebuffValue(botAI); } + static UntypedValue* nearest_trap_with_damange(PlayerbotAI* botAI) { return new NearestTrapWithDamageValue(botAI); } + static UntypedValue* disperse_distance(PlayerbotAI* botAI) { return new DisperseDistanceValue(botAI); } + static UntypedValue* last_flee_angle(PlayerbotAI* botAI) { return new LastFleeAngleValue(botAI); } + static UntypedValue* last_flee_timestamp(PlayerbotAI* botAI) { return new LastFleeTimestampValue(botAI); } + static UntypedValue* recently_flee_info(PlayerbotAI* botAI) { return new RecentlyFleeInfo(botAI); } + static UntypedValue* can_fish(PlayerbotAI* botAI) { return new CanFishValue(botAI); } + static UntypedValue* can_use_fishing_bobber(PlayerbotAI* botAI) { return new CanUseFishingBobberValue(botAI); } + static UntypedValue* fishing_spot(PlayerbotAI* botAI) { return new FishingSpotValue(botAI); } + static UntypedValue* custom_glyphs(PlayerbotAI* botAI) { - return new ManualSetValue(ai, false, "custom_glyphs"); + return new ManualSetValue(botAI, false, "custom_glyphs"); } + static UntypedValue* wait_for_attack_time(PlayerbotAI* botAI) { return new WaitForAttackTimeValue(botAI); } + static UntypedValue* combat_start_time(PlayerbotAI* botAI) { return new CombatStartTimeValue(botAI); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Base/WorldPacketActionContext.h b/modules/mod-playerbots/src/Ai/Base/WorldPacketActionContext.h index 839a88655..f94ed6ebe 100644 --- a/modules/mod-playerbots/src/Ai/Base/WorldPacketActionContext.h +++ b/modules/mod-playerbots/src/Ai/Base/WorldPacketActionContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WORLDPACKETACTIONCONTEXT_H -#define _PLAYERBOT_WORLDPACKETACTIONCONTEXT_H +#ifndef PLAYERBOTS_WORLDPACKETACTIONCONTEXT_H +#define PLAYERBOTS_WORLDPACKETACTIONCONTEXT_H #include "AcceptBattlegroundInvitationAction.h" #include "AcceptDuelAction.h" diff --git a/modules/mod-playerbots/src/Ai/Base/WorldPacketTriggerContext.h b/modules/mod-playerbots/src/Ai/Base/WorldPacketTriggerContext.h index 62afafd1a..cf8445bf8 100644 --- a/modules/mod-playerbots/src/Ai/Base/WorldPacketTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Base/WorldPacketTriggerContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WORLDPACKETTRIGGERCONTEXT_H -#define _PLAYERBOT_WORLDPACKETTRIGGERCONTEXT_H +#ifndef PLAYERBOTS_WORLDPACKETTRIGGERCONTEXT_H +#define PLAYERBOTS_WORLDPACKETTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "WithinAreaTrigger.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.cpp deleted file mode 100644 index 7788e4819..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 "DKActions.h" - -#include "Duration.h" -#include "GenericSpellActions.h" -#include "Playerbots.h" -#include "SpellInfo.h" -#include "SpellMgr.h" - -std::vector CastDeathchillAction::getPrerequisites() -{ - return NextAction::merge({ NextAction("frost presence") }, - CastSpellAction::getPrerequisites()); -} - -std::vector CastUnholyMeleeSpellAction::getPrerequisites() -{ - return NextAction::merge({ NextAction("unholy presence") }, - CastMeleeSpellAction::getPrerequisites()); -} - -std::vector CastFrostMeleeSpellAction::getPrerequisites() -{ - return NextAction::merge({ NextAction("frost presence") }, - CastMeleeSpellAction::getPrerequisites()); -} - -std::vector CastBloodMeleeSpellAction::getPrerequisites() -{ - return NextAction::merge({ NextAction("blood presence") }, - CastMeleeSpellAction::getPrerequisites()); -} - -bool CastRaiseDeadAction::Execute(Event event) -{ - const bool result = CastBuffSpellAction::Execute(event); - - if (!result) - return false; - - const uint32_t spellId = AI_VALUE2(uint32_t, "spell id", spell); - - bot->AddSpellCooldown(spellId, 0, 3 * 60 * 1000); - - return true; -} diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/DKActions.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/DKActions.cpp new file mode 100644 index 000000000..f034893e8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Dk/DKActions.cpp @@ -0,0 +1,103 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DKActions.h" + +#include "Duration.h" +#include "GenericSpellActions.h" +#include "Playerbots.h" +#include "SpellInfo.h" +#include "SpellMgr.h" + +std::vector CastDeathchillAction::getPrerequisites() +{ + return NextAction::merge({ NextAction("frost presence") }, + CastSpellAction::getPrerequisites()); +} + +std::vector CastUnholyMeleeSpellAction::getPrerequisites() +{ + return NextAction::merge({ NextAction("unholy presence") }, + CastMeleeSpellAction::getPrerequisites()); +} + +std::vector CastFrostMeleeSpellAction::getPrerequisites() +{ + return NextAction::merge({ NextAction("frost presence") }, + CastMeleeSpellAction::getPrerequisites()); +} + +std::vector CastBloodMeleeSpellAction::getPrerequisites() +{ + return NextAction::merge({ NextAction("blood presence") }, + CastMeleeSpellAction::getPrerequisites()); +} + +bool CastRaiseDeadAction::Execute(Event event) +{ + const bool result = CastBuffSpellAction::Execute(event); + + if (!result) + return false; + + const uint32_t spellId = AI_VALUE2(uint32_t, "spell id", spell); + + bot->AddSpellCooldown(spellId, 0, 3 * 60 * 1000); + + return true; +} + +Unit* CastHysteriaAction::GetTarget() +{ + Group* group = bot->GetGroup(); + if (!group) + { + if (!bot->HasAura(49016)) + return bot; + return nullptr; + } + + Unit* rangedDps = nullptr; + Unit* tank = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->GetMap() != bot->GetMap() || bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance) + continue; + + // Skip if already has hysteria + if (member->HasAura(49016)) + continue; + + // Priority 1: Melee DPS + if (botAI->IsMelee(member) && botAI->IsDps(member)) + return member; + + // Priority 2: Ranged DPS (physical, not casters) + if (!rangedDps && botAI->IsRanged(member) && botAI->IsDps(member) && !botAI->IsCaster(member)) + rangedDps = member; + + // Priority 3: Tank + if (!tank && botAI->IsTank(member)) + tank = member; + } + + if (rangedDps) + return rangedDps; + + if (tank) + return tank; + + // Fallback to self if no hysteria + if (!bot->HasAura(49016)) + return bot; + + return nullptr; +} diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.h b/modules/mod-playerbots/src/Ai/Class/Dk/DKActions.h similarity index 95% rename from modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.h rename to modules/mod-playerbots/src/Ai/Class/Dk/DKActions.h index 74e066cd5..85d3fa702 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Action/DKActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/DKActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DKACTIONS_H -#define _PLAYERBOT_DKACTIONS_H +#ifndef PLAYERBOTS_DKACTIONS_H +#define PLAYERBOTS_DKACTIONS_H #include "Event.h" #include "GenericSpellActions.h" @@ -340,4 +341,11 @@ public: CastBloodTapAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "blood tap") {} }; +class CastHysteriaAction : public CastSpellAction +{ +public: + CastHysteriaAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hysteria") {} + Unit* GetTarget() override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.cpp index ac80c7dc4..14be64ef6 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DKAiObjectContext.h" @@ -8,11 +9,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 +29,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); } }; @@ -51,18 +52,6 @@ private: static Strategy* blood(PlayerbotAI* botAI) { return new BloodDKStrategy(botAI); } }; -class DeathKnightDKBuffStrategyFactoryInternal : public NamedObjectContext -{ -public: - DeathKnightDKBuffStrategyFactoryInternal() : NamedObjectContext(false, true) - { - creators["bdps"] = &DeathKnightDKBuffStrategyFactoryInternal::bdps; - } - -private: - static Strategy* bdps(PlayerbotAI* botAI) { return new DKBuffDpsStrategy(botAI); } -}; - class DeathKnightTriggerFactoryInternal : public NamedObjectContext { public: @@ -100,6 +89,7 @@ public: creators["dd cd and no desolation"] = &DeathKnightTriggerFactoryInternal::dd_cd_and_no_desolation; creators["death and decay cooldown"] = &DeathKnightTriggerFactoryInternal::death_and_decay_cooldown; creators["army of the dead"] = &DeathKnightTriggerFactoryInternal::army_of_the_dead; + creators["hysteria no cd"] = &DeathKnightTriggerFactoryInternal::hysteria_no_cd; } private: @@ -152,6 +142,7 @@ private: } static Trigger* death_and_decay_cooldown(PlayerbotAI* botAI) { return new DeathAndDecayCooldownTrigger(botAI); } static Trigger* army_of_the_dead(PlayerbotAI* botAI) { return new ArmyOfTheDeadTrigger(botAI); } + static Trigger* hysteria_no_cd(PlayerbotAI* botAI) { return new HysteriaNoCooldownTrigger(botAI); } }; class DeathKnightAiObjectContextInternal : public NamedObjectContext @@ -209,7 +200,7 @@ public: creators["vampiric blood"] = &DeathKnightAiObjectContextInternal::vampiric_blood; creators["death pact"] = &DeathKnightAiObjectContextInternal::death_pact; creators["death rune_mastery"] = &DeathKnightAiObjectContextInternal::death_rune_mastery; - // creators["hysteria"] = &DeathKnightAiObjectContextInternal::hysteria; + creators["hysteria"] = &DeathKnightAiObjectContextInternal::hysteria; creators["dancing rune weapon"] = &DeathKnightAiObjectContextInternal::dancing_rune_weapon; creators["dark command"] = &DeathKnightAiObjectContextInternal::dark_command; } @@ -265,7 +256,7 @@ private: static Action* vampiric_blood(PlayerbotAI* botAI) { return new CastVampiricBloodAction(botAI); } static Action* death_pact(PlayerbotAI* botAI) { return new CastDeathPactAction(botAI); } static Action* death_rune_mastery(PlayerbotAI* botAI) { return new CastDeathRuneMasteryAction(botAI); } - // static Action* hysteria(PlayerbotAI* botAI) { return new CastHysteriaAction(botAI); } + static Action* hysteria(PlayerbotAI* botAI) { return new CastHysteriaAction(botAI); } static Action* dancing_rune_weapon(PlayerbotAI* botAI) { return new CastDancingRuneWeaponAction(botAI); } static Action* dark_command(PlayerbotAI* botAI) { return new CastDarkCommandAction(botAI); } static Action* mind_freeze_on_enemy_healer(PlayerbotAI* botAI) @@ -297,7 +288,6 @@ void DKAiObjectContext::BuildSharedStrategyContexts(SharedNamedObjectContextList AiObjectContext::BuildSharedStrategyContexts(strategyContexts); strategyContexts.Add(new DeathKnightStrategyFactoryInternal()); strategyContexts.Add(new DeathKnightCombatStrategyFactoryInternal()); - strategyContexts.Add(new DeathKnightDKBuffStrategyFactoryInternal()); } void DKAiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.h index 8921df718..8fd3c7fe1 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/DKAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DKAIOBJECTCONTEXT_H -#define _PLAYERBOT_DKAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_DKAIOBJECTCONTEXT_H +#define PLAYERBOTS_DKAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Trigger/DKTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/DKTriggers.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Class/Dk/Trigger/DKTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Dk/DKTriggers.cpp index 4cad0c047..ccd134f78 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Trigger/DKTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/DKTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DKTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Trigger/DKTriggers.h b/modules/mod-playerbots/src/Ai/Class/Dk/DKTriggers.h similarity index 91% rename from modules/mod-playerbots/src/Ai/Class/Dk/Trigger/DKTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Dk/DKTriggers.h index c46fbfe37..5417f0a79 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Trigger/DKTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/DKTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DKTRIGGERS_H -#define _PLAYERBOT_DKTRIGGERS_H +#ifndef PLAYERBOTS_DKTRIGGERS_H +#define PLAYERBOTS_DKTRIGGERS_H #include "GenericTriggers.h" @@ -198,4 +199,10 @@ public: ArmyOfTheDeadTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "army of the dead") {} }; +class HysteriaNoCooldownTrigger : public SpellNoCooldownTrigger +{ +public: + HysteriaNoCooldownTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "hysteria") {} +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp index 344b3dc09..26f154132 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BloodDKStrategy.h" @@ -50,7 +51,9 @@ private: { NextAction("frost presence") }, - /*A*/ {}, + /*A*/ { + NextAction("blood strike") + }, /*C*/ {} ); } @@ -89,13 +92,11 @@ BloodDKStrategy::BloodDKStrategy(PlayerbotAI* botAI) : GenericDKStrategy(botAI) std::vector BloodDKStrategy::getDefaultActions() { return { - NextAction("rune strike", ACTION_DEFAULT + 0.8f), - NextAction("icy touch", ACTION_DEFAULT + 0.7f), - NextAction("heart strike", ACTION_DEFAULT + 0.6f), - NextAction("blood strike", ACTION_DEFAULT + 0.5f), - NextAction("dancing rune weapon", ACTION_DEFAULT + 0.4f), - NextAction("death coil", ACTION_DEFAULT + 0.3f), - NextAction("plague strike", ACTION_DEFAULT + 0.2f), + NextAction("rune strike", ACTION_DEFAULT + 0.6f), + NextAction("icy touch", ACTION_DEFAULT + 0.5f), + NextAction("heart strike", ACTION_DEFAULT + 0.4f), + NextAction("dancing rune weapon", ACTION_DEFAULT + 0.3f), + NextAction("death coil", ACTION_DEFAULT + 0.2f), NextAction("horn of winter", ACTION_DEFAULT + 0.1f), NextAction("melee", ACTION_DEFAULT) }; @@ -105,6 +106,14 @@ void BloodDKStrategy::InitTriggers(std::vector& triggers) { GenericDKStrategy::InitTriggers(triggers); + triggers.push_back( + new TriggerNode( + "hysteria no cd", + { + NextAction("hysteria", ACTION_NORMAL + 4) + } + ) + ); triggers.push_back( new TriggerNode( "rune strike", @@ -162,4 +171,12 @@ void BloodDKStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back( + new TriggerNode( + "high unholy rune", + { + NextAction("death strike", ACTION_HIGH + 1) + } + ) + ); } diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.h b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.h index 590727c93..03557af40 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/BloodDKStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BLOODDKSTRATEGY_H -#define _PLAYERBOT_BLOODDKSTRATEGY_H +#ifndef PLAYERBOTS_BLOODDKSTRATEGY_H +#define PLAYERBOTS_BLOODDKSTRATEGY_H #include "GenericDKStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp new file mode 100644 index 000000000..c3178af82 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.cpp @@ -0,0 +1,32 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DeathKnightPullStrategy.h" + +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string DeathKnightPullStrategy::GetPullActionName() const +{ + Unit* target = GetTarget(); + if (!target || + (!botAI->HasStrategy("blood", BOT_STATE_COMBAT) && !botAI->HasStrategy("blood", BOT_STATE_NON_COMBAT))) + { + return PullStrategy::GetPullActionName(); + } + + if (botAI->CanCastSpell("death grip", target)) + return "death grip"; + + if (!botAI->CanCastSpell("icy touch", target) && + botAI->CanCastSpell("dark command", target)) + { + return "dark command"; + } + + return PullStrategy::GetPullActionName(); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h new file mode 100644 index 000000000..c31358813 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/DeathKnightPullStrategy.h @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DEATHKNIGHTPULLSTRATEGY_H +#define PLAYERBOTS_DEATHKNIGHTPULLSTRATEGY_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/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp index d0b0ee203..9e84a0a55 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FrostDKStrategy.h" @@ -91,7 +92,6 @@ std::vector FrostDKStrategy::getDefaultActions() return { NextAction("obliterate", ACTION_DEFAULT + 0.7f), NextAction("frost strike", ACTION_DEFAULT + 0.4f), - NextAction("empower rune weapon", ACTION_DEFAULT + 0.3f), NextAction("horn of winter", ACTION_DEFAULT + 0.1f), NextAction("melee", ACTION_DEFAULT) }; diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.h b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.h index 948339193..c314af2fc 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/FrostDKStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FROSTDKSTRATEGY_H -#define _PLAYERBOT_FROSTDKSTRATEGY_H +#ifndef PLAYERBOTS_FROSTDKSTRATEGY_H +#define PLAYERBOTS_FROSTDKSTRATEGY_H #include "GenericDKStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp index d358d4370..0b99083f6 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.cpp @@ -1,59 +1,23 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericDKNonCombatStrategy.h" -class GenericDKNonCombatStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - GenericDKNonCombatStrategyActionNodeFactory() - { - creators["bone shield"] = &bone_shield; - creators["horn of winter"] = &horn_of_winter; - } - -private: - static ActionNode* bone_shield([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("bone shield", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* horn_of_winter([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("horn of winter", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } -}; - GenericDKNonCombatStrategy::GenericDKNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) { - actionNodeFactories.Add(new GenericDKNonCombatStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } void GenericDKNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 1) })); + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); triggers.push_back( new TriggerNode("horn of winter", { NextAction("horn of winter", 21.0f) })); triggers.push_back( new TriggerNode("bone shield", { NextAction("bone shield", 21.0f) })); - triggers.push_back( - new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); - triggers.push_back( - new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); -} - -void DKBuffDpsStrategy::InitTriggers(std::vector& triggers) -{ - } diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.h index 37e8c74bc..f016f6729 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICDKNONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICDKNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICDKNONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICDKNONCOMBATSTRATEGY_H #include "GenericDKStrategy.h" #include "NonCombatStrategy.h" @@ -20,13 +21,4 @@ public: void InitTriggers(std::vector& triggers) override; }; -class DKBuffDpsStrategy : public Strategy -{ -public: - DKBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bdps"; } -}; - #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp index 61ff8c748..aab442677 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.cpp @@ -1,5 +1,7 @@ -#/* - * 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. +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericDKStrategy.h" @@ -12,53 +14,12 @@ class GenericDKStrategyActionNodeFactory : public NamedObjectFactory public: GenericDKStrategyActionNodeFactory() { - // blood - // creators["rune tap"] = &rune_tap; cd - // creators["vampiric blood"] = &vampiric_blood; - // creators["death pact"] = &death_pact; - // creators["hysteria"] = &hysteria; boost party - // creators["dancing rune weapon"] = &dancing_rune_weapon; //cd - // creators["dark command"] = &dark_command; taunt - - // frost - // creators["chains of ice"] = &chains_of_ice; - // creators["icy clutch"] = &icy_clutch; - creators["horn of winter"] = &horn_of_winter; - creators["killing machine"] = &killing_machine; // buff - // creators["deathchill"] = &deathchill; //boost - creators["icebound fortitude"] = &icebound_fortitude; - // creators["mind freeze"] = &mind_freeze; interrupt - // creators["empower rune weapon"] = &empower_rune_weapon; boost - // creators["hungering cold"] = &hungering_cold; snare - // creators["unbreakable armor"] = &unbreakable_armor; boost +cd - // creators["improved icy talons"] = &improved_icy_talons; boost party - - // unholy - creators["death and decay"] = &death_and_decay; - // creators["raise dead"] = &raise_dead; - // creators["army of the dead"] = &army of the dead; - // creators["summon gargoyle"] = &army of the dead; - // creators["anti magic shell"] = &anti_magic_shell; cd + creators["killing machine"] = &killing_machine; creators["anti magic zone"] = &anti_magic_zone; - // creators["ghoul frenzy"] = &ghoul_frenzy; - creators["corpse explosion"] = &corpse_explosion; - creators["bone shield"] = &bone_shield; - creators["heart strike"] = &heart_strike; creators["death grip"] = &death_grip; - creators["plague strike"] = &plague_strike; - creators["pestilence"] = &pestilence; - creators["icy touch"] = &icy_touch; } private: - static ActionNode* death_coil([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("death coil", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* death_grip([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("death grip", @@ -67,54 +28,6 @@ private: /*C*/ {}); } - static ActionNode* plague_strike([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("plague strike", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* icy_touch([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("icy touch", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* heart_strike([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("heart strike", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* pestilence([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("pestilence", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* horn_of_winter([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("horn of winter", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* bone_shield([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("bone shield", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* killing_machine([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("killing machine", @@ -123,22 +36,6 @@ private: /*C*/ {}); } - static ActionNode* corpse_explosion([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("corpse explosion", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* death_and_decay([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("death and decay", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* anti_magic_zone([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("anti magic zone", @@ -146,14 +43,6 @@ private: /*A*/ { NextAction("anti magic shell") }, /*C*/ {}); } - - static ActionNode* icebound_fortitude([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("icebound fortitude", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } }; GenericDKStrategy::GenericDKStrategy(PlayerbotAI* botAI) : MeleeCombatStrategy(botAI) @@ -165,12 +54,6 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) { MeleeCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 5) })); - triggers.push_back( - new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); - triggers.push_back( - new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); triggers.push_back( new TriggerNode("mind freeze", { NextAction("mind freeze", ACTION_HIGH + 1) })); triggers.push_back( @@ -179,7 +62,8 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode( "horn of winter", { NextAction("horn of winter", ACTION_NORMAL + 1) })); triggers.push_back(new TriggerNode("critical health", - { NextAction("death pact", ACTION_HIGH + 5) })); + { NextAction("raise dead", ACTION_HIGH + 6), + NextAction("death pact", ACTION_HIGH + 5) })); triggers.push_back( new TriggerNode("low health", { NextAction("icebound fortitude", ACTION_HIGH + 5), @@ -190,4 +74,11 @@ void GenericDKStrategy::InitTriggers(std::vector& triggers) NextAction("blood boil", ACTION_NORMAL + 3) })); triggers.push_back( new TriggerNode("pestilence glyph", { NextAction("pestilence", ACTION_HIGH + 9) })); + triggers.push_back( + new TriggerNode("no rune", + { + NextAction("empower rune weapon", ACTION_HIGH + 1) + } + ) + ); } diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.h b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.h index dabe74281..685cd5701 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/GenericDKStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICDKSTRATEGY_H -#define _PLAYERBOT_GENERICDKSTRATEGY_H +#ifndef PLAYERBOTS_GENERICDKSTRATEGY_H +#define PLAYERBOTS_GENERICDKSTRATEGY_H #include "MeleeCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp index d94a94ec3..7157d6b90 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.cpp @@ -1,5 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "UnholyDKStrategy.h" @@ -87,6 +89,13 @@ void UnholyDKStrategy::InitTriggers(std::vector& triggers) { GenericDKStrategy::InitTriggers(triggers); + triggers.push_back( + new TriggerNode("no pet", { NextAction("raise dead", ACTION_NORMAL + 5) })); + triggers.push_back( + new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); + triggers.push_back( + new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + triggers.push_back( new TriggerNode( "death and decay cooldown", @@ -146,13 +155,6 @@ void UnholyDKStrategy::InitTriggers(std::vector& triggers) } ) ); - triggers.push_back( - new TriggerNode("no rune", - { - NextAction("empower rune weapon", ACTION_HIGH + 1) - } - ) - ); triggers.push_back( new TriggerNode( "army of the dead", diff --git a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.h b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.h index 65ee65d92..11ebb9272 100644 --- a/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Dk/Strategy/UnholyDKStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_UNHOLYDKSTRATEGY_H -#define _PLAYERBOT_UNHOLYDKSTRATEGY_H +#ifndef PLAYERBOTS_UNHOLYDKSTRATEGY_H +#define PLAYERBOTS_UNHOLYDKSTRATEGY_H #include "GenericDKStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.cpp index d6a950b17..6dce676eb 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DruidActions.h" @@ -11,6 +12,25 @@ #include "AoeValues.h" #include "TargetValue.h" +constexpr uint32 SPELL_ECLIPSE_SOLAR = 48517; +constexpr uint32 SPELL_ECLIPSE_LUNAR = 48518; + +namespace +{ + bool PrepareThornsTarget(PlayerbotAI* botAI, Unit* target) + { + if (!target) + return false; + + Aura* existingThorns = botAI->GetAura("thorns", target, true); + if (!existingThorns) + return true; + + target->RemoveOwnedAura(existingThorns, AURA_REMOVE_BY_CANCEL); + return true; + } +} + std::vector CastAbolishPoisonAction::getAlternatives() { return NextAction::merge({ NextAction("cure poison") }, @@ -23,16 +43,114 @@ std::vector CastAbolishPoisonOnPartyAction::getAlternatives() CastSpellAction::getPrerequisites()); } -Value* CastEntanglingRootsCcAction::GetTargetValue() +bool CastLifebloomOnMainTankAction::isUseful() { - return context->GetValue("cc target", "entangling roots"); + Unit* target = GetTarget(); + if (!target || !target->IsAlive() || !CastSpellAction::isUseful()) + return false; + + Aura* lifebloom = botAI->GetAura("lifebloom", target, true, true); + return !lifebloom || lifebloom->GetStackAmount() < 3 || lifebloom->GetDuration() < 2000; } -bool CastEntanglingRootsCcAction::Execute(Event /*event*/) { return botAI->CastSpell("entangling roots", GetTarget()); } +bool CastThornsAction::Execute(Event event) +{ + return PrepareThornsTarget(botAI, GetTarget()) && CastBuffSpellAction::Execute(event); +} -Value* CastHibernateCcAction::GetTargetValue() { return context->GetValue("cc target", "hibernate"); } +bool CastThornsOnPartyAction::Execute(Event event) +{ + return PrepareThornsTarget(botAI, GetTarget()) && BuffOnPartyAction::Execute(event); +} + +bool CastThornsOnMainTankAction::Execute(Event event) +{ + return PrepareThornsTarget(botAI, GetTarget()) && BuffOnMainTankAction::Execute(event); +} + +bool CastWrathAction::isUseful() +{ + time_t now = time(nullptr); + time_t solarTime = context->GetValue("eclipse solar proc time")->Get(); + time_t lunarTime = context->GetValue("eclipse lunar proc time")->Get(); + + // --- Update Solar Eclipse tracking --- + // Wrath is selected during Solar Eclipse (eclipse trigger at 20.0f), so we reliably see it here. + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && !solarTime) + context->GetValue("eclipse solar proc time")->Set(now); + // Lunar procced — Solar fishing window is over, new cycle begins + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && solarTime) + context->GetValue("eclipse solar proc time")->Set(0); + // 30 s cooldown window expired + if (solarTime && (now - solarTime) >= 30) + context->GetValue("eclipse solar proc time")->Set(0); + + // --- Update Lunar Eclipse tracking (belt-and-suspenders in case Starfire isn't evaluated) --- + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && !lunarTime) + context->GetValue("eclipse lunar proc time")->Set(now); + // Solar procced — Lunar fishing window is over + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && lunarTime) + context->GetValue("eclipse lunar proc time")->Set(0); + if (lunarTime && (now - lunarTime) >= 30) + context->GetValue("eclipse lunar proc time")->Set(0); + + // Block Wrath while in Lunar Eclipse / post-Lunar fishing window + if (context->GetValue("eclipse lunar proc time")->Get()) + return false; + + return CastSpellAction::isUseful(); +} + +bool CastStarfireAction::isUseful() +{ + time_t now = time(nullptr); + time_t solarTime = context->GetValue("eclipse solar proc time")->Get(); + time_t lunarTime = context->GetValue("eclipse lunar proc time")->Get(); + + // --- Update Lunar Eclipse tracking --- + // Starfire is selected during Lunar Eclipse (eclipse trigger at 20.0f), so we reliably see it here. + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && !lunarTime) + context->GetValue("eclipse lunar proc time")->Set(now); + // Solar procced — Lunar fishing window is over, new cycle begins + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && lunarTime) + context->GetValue("eclipse lunar proc time")->Set(0); + // 30 s cooldown window expired + if (lunarTime && (now - lunarTime) >= 30) + context->GetValue("eclipse lunar proc time")->Set(0); + + // --- Update Solar Eclipse tracking (belt-and-suspenders in case Wrath isn't evaluated) --- + if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && !solarTime) + context->GetValue("eclipse solar proc time")->Set(now); + // Lunar procced — Solar fishing window is over + if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && solarTime) + context->GetValue("eclipse solar proc time")->Set(0); + if (solarTime && (now - solarTime) >= 30) + context->GetValue("eclipse solar proc time")->Set(0); + + // Block Starfire while in Solar Eclipse / post-Solar fishing window + if (context->GetValue("eclipse solar proc time")->Get()) + return false; + + return CastSpellAction::isUseful(); +} + +Value* CastEntanglingRootsCcAction::GetTargetValue() +{ + return context->GetValue("rti cc target"); +} + +Value* CastHibernateCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } + +Value* CastCycloneCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } + +bool CastTyphoonAction::isUseful() +{ + bool facingTarget = AI_VALUE2(bool, "facing", "current target"); + bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan( + AI_VALUE2(float, "distance", GetTargetName()), 15.f); + return facingTarget && targetClose; +} -bool CastHibernateCcAction::Execute(Event /*event*/) { return botAI->CastSpell("hibernate", GetTarget()); } bool CastStarfallAction::isUseful() { if (!CastSpellAction::isUseful()) @@ -48,12 +166,26 @@ bool CastStarfallAction::isUseful() return false; } - // Avoid single-target usage on initial pull - uint8 aoeCount = *context->GetValue("aoe count"); - if (aoeCount < 2) + // Suppress if any unengaged hostile unit is within 40 yards — Starfall's 36-yard radius would pull them. + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + GuidVector const& nearbyNpcs = AI_VALUE(GuidVector, "possible targets"); + for (ObjectGuid const& guid : nearbyNpcs) { - Unit* target = context->GetValue("current target")->Get(); - if (!target || (!botAI->HasAura("moonfire", target) && !botAI->HasAura("insect swarm", target))) + Unit* unit = botAI->GetUnit(guid); + // Standard null/world-state guard before touching the unit. + if (!unit || !unit->IsAlive() || !unit->IsInWorld()) + continue; + // Already our target — its in-combat flag covers it. + if (unit == currentTarget) + continue; + // Safety net for any hostile-faction trigger creature that carries NON_ATTACKABLE flags. + if (!bot->IsValidAttackTarget(unit)) + continue; + // Outside Starfall's actual radius; no pull risk. + if (ServerFacade::instance().GetDistance2d(bot, unit) > 40.0f) + continue; + // Unengaged mob within range — casting would pull it. + if (!unit->IsInCombat()) return false; } @@ -78,6 +210,24 @@ bool CastRebirthAction::isUseful() AI_VALUE2(float, "distance", GetTargetName()) <= sPlayerbotAIConfig.spellDistance; } +bool CastInnervateOnHealerAction::isPossible() +{ + Unit* target = GetTarget(); + if (!target || !target->IsInWorld()) + return false; + + if (botAI->HasAura("innervate", target)) + return false; + + uint32 spellId = AI_VALUE2(uint32, "spell id", "innervate"); + return spellId && !bot->HasSpellCooldown(spellId); +} + +std::vector CastInnervateOnHealerAction::getPrerequisites() +{ + return { NextAction("caster form") }; +} + Unit* CastRejuvenationOnNotFullAction::GetTarget() { Group* group = bot->GetGroup(); @@ -108,3 +258,63 @@ bool CastRejuvenationOnNotFullAction::isUseful() { return GetTarget(); } + +// --- Blanket HoT actions --- + +Unit* CastBlanketHotAction::GetBlanketTarget(std::string const& auraName) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + auto eligible = [&](Player* member) -> bool + { + return member && member->IsAlive() && + !member->IsGameMaster() && + bot->GetDistance2d(member) <= sPlayerbotAIConfig.spellDistance && + !botAI->HasAura(auraName, member, false, true); + }; + + Player* firstMelee = nullptr; + Player* firstRanged = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!eligible(member)) + continue; + + if (PlayerbotAI::IsTank(member)) + return member; + else if (!firstMelee && PlayerbotAI::IsMelee(member) && !PlayerbotAI::IsTank(member)) + firstMelee = member; + else if (!firstRanged && PlayerbotAI::IsRanged(member)) + firstRanged = member; + + if (firstMelee && firstRanged) + break; + } + + if (firstMelee) return firstMelee; + return firstRanged; +} + +Unit* CastRejuvenationBlanketAction::GetTarget() +{ + return GetBlanketTarget("rejuvenation"); +} + +bool CastRejuvenationBlanketAction::isUseful() +{ + return GetTarget() != nullptr; +} + +Unit* CastWildGrowthBlanketAction::GetTarget() +{ + return GetBlanketTarget("wild growth"); +} + +bool CastWildGrowthBlanketAction::isUseful() +{ + return GetTarget() != nullptr; +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.h b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.h index e3e177590..efb5b4b9d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidActions.h @@ -1,13 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DRUIDACTIONS_H -#define _PLAYERBOT_DRUIDACTIONS_H +#ifndef PLAYERBOTS_DRUIDACTIONS_H +#define PLAYERBOTS_DRUIDACTIONS_H #include "GenericSpellActions.h" #include "SharedDefines.h" +#include "Value.h" class PlayerbotAI; class Unit; @@ -64,7 +66,7 @@ class CastHealingTouchOnPartyAction : public HealPartyMemberAction { public: CastHealingTouchOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "healing touch", 50.0f, HealingManaEfficiency::LOW) + : HealPartyMemberAction(botAI, "healing touch", 50.0f, HealingManaEfficiency::MEDIUM) { } }; @@ -86,16 +88,16 @@ public: bool isUseful() override; }; -class CastMarkOfTheWildAction : public CastBuffSpellAction +class CastMarkOfTheWildAction : public GroupBuffSpellAction { public: - CastMarkOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mark of the wild") {} + CastMarkOfTheWildAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "mark of the wild") {} }; -class CastMarkOfTheWildOnPartyAction : public BuffOnPartyAction +class CastMarkOfTheWildOnPartyAction : public GroupBuffOnPartyAction { public: - CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "mark of the wild") {} + CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "mark of the wild") {} }; class CastSurvivalInstinctsAction : public CastBuffSpellAction @@ -114,30 +116,39 @@ class CastThornsAction : public CastBuffSpellAction { public: CastThornsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "thorns") {} + + bool Execute(Event event) override; }; class CastThornsOnPartyAction : public BuffOnPartyAction { public: CastThornsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "thorns") {} + + bool Execute(Event event) override; }; class CastThornsOnMainTankAction : public BuffOnMainTankAction { public: CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {} + + bool Execute(Event event) override; }; -class CastOmenOfClarityAction : public CastBuffSpellAction +class CastLifebloomOnMainTankAction : public BuffOnMainTankAction { public: - CastOmenOfClarityAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "omen of clarity") {} + CastLifebloomOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "lifebloom", true) {} + + bool isUseful() override; }; class CastWrathAction : public CastSpellAction { public: CastWrathAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wrath") {} + bool isUseful() override; }; class CastStarfallAction : public CastSpellAction @@ -155,6 +166,14 @@ public: ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } }; +class CastTyphoonAction : public CastSpellAction +{ +public: + CastTyphoonAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "typhoon") {} + ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } + bool isUseful() override; +}; + class CastMoonfireAction : public CastDebuffSpellAction { public: @@ -171,6 +190,7 @@ class CastStarfireAction : public CastSpellAction { public: CastStarfireAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "starfire") {} + bool isUseful() override; }; class CastEntanglingRootsAction : public CastSpellAction @@ -179,12 +199,11 @@ public: CastEntanglingRootsAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "entangling roots") {} }; -class CastEntanglingRootsCcAction : public CastSpellAction +class CastEntanglingRootsCcAction : public CastCrowdControlSpellAction { public: - CastEntanglingRootsCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "entangling roots on cc") {} + CastEntanglingRootsCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "entangling roots") {} Value* GetTargetValue() override; - bool Execute(Event event) override; }; class CastHibernateAction : public CastSpellAction @@ -193,12 +212,18 @@ public: CastHibernateAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hibernate") {} }; -class CastHibernateCcAction : public CastSpellAction +class CastHibernateCcAction : public CastCrowdControlSpellAction { public: - CastHibernateCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hibernate on cc") {} + CastHibernateCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "hibernate") {} + Value* GetTargetValue() override; +}; + +class CastCycloneCcAction : public CastCrowdControlSpellAction +{ +public: + CastCycloneCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "cyclone") {} Value* GetTargetValue() override; - bool Execute(Event event) override; }; class CastNaturesGraspAction : public CastBuffSpellAction @@ -250,6 +275,16 @@ public: std::string const GetTargetName() override { return "self target"; } }; +class CastInnervateOnHealerAction : public CastSpellAction +{ +public: + CastInnervateOnHealerAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "innervate") {} + + std::string const GetTargetName() override { return "healer low mana"; } + bool isPossible() override; + std::vector getPrerequisites() override; +}; + class CastTranquilityAction : public CastAoeHealSpellAction { public: @@ -298,13 +333,15 @@ public: class CastInsectSwarmOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastInsectSwarmOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "insect swarm") {} + CastInsectSwarmOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "insect swarm", true, 0.0f) {} + bool isUseful() override { return CastAuraSpellAction::isUseful(); } }; class CastMoonfireOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastMoonfireOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "moonfire") {} + CastMoonfireOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "moonfire", true, 0.0f) {} + bool isUseful() override { return CastAuraSpellAction::isUseful(); } }; class CastEnrageAction : public CastBuffSpellAction @@ -330,4 +367,48 @@ public: CastForceOfNatureAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "force of nature") {} }; +// Base for blanket HoT actions. Provides GetBlanketTarget() as a member so +// subclasses can use AI_VALUE and the standard context machinery. +class CastBlanketHotAction : public CastSpellAction +{ +public: + CastBlanketHotAction(PlayerbotAI* ai, std::string const& spell) : CastSpellAction(ai, spell) + { + range = botAI->GetRange("heal"); + } + +protected: + Unit* GetBlanketTarget(std::string const& auraName); +}; + +class CastRejuvenationBlanketAction : public CastBlanketHotAction +{ +public: + CastRejuvenationBlanketAction(PlayerbotAI* ai) : CastBlanketHotAction(ai, "rejuvenation") {} + bool isUseful() override; + Unit* GetTarget() override; + std::string const getName() override { return "rejuvenation blanket"; } +}; + +class CastWildGrowthBlanketAction : public CastBlanketHotAction +{ +public: + CastWildGrowthBlanketAction(PlayerbotAI* ai) : CastBlanketHotAction(ai, "wild growth") {} + bool isUseful() override; + Unit* GetTarget() override; + std::string const getName() override { return "wild growth blanket"; } +}; + +class EclipseSolarProcTimeValue : public ManualSetValue +{ +public: + EclipseSolarProcTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 0) {} +}; + +class EclipseLunarProcTimeValue : public ManualSetValue +{ +public: + EclipseLunarProcTimeValue(PlayerbotAI* botAI) : ManualSetValue(botAI, 0) {} +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.cpp index e6ff2a86c..b5f3ac69b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DruidBearActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.h b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.h index caf369be3..fb6ba7c75 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidBearActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DRUIDBEARACTIONS_H -#define _PLAYERBOT_DRUIDBEARACTIONS_H +#ifndef PLAYERBOTS_DRUIDBEARACTIONS_H +#define PLAYERBOTS_DRUIDBEARACTIONS_H #include "GenericSpellActions.h" #include "ReachTargetActions.h" @@ -23,6 +24,12 @@ public: CastGrowlAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "growl") {} }; +class CastChallengingRoarAction : public CastMeleeDebuffSpellAction +{ +public: + CastChallengingRoarAction(PlayerbotAI* botAI) : CastMeleeDebuffSpellAction(botAI, "challenging roar") {} +}; + class CastMaulAction : public CastMeleeSpellAction { public: @@ -37,12 +44,6 @@ public: CastBashAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "bash") {} }; -class CastSwipeAction : public CastMeleeSpellAction -{ -public: - CastSwipeAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "swipe") {} -}; - class CastDemoralizingRoarAction : public CastMeleeDebuffSpellAction { public: diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.cpp index 03571a8ec..87e9a257b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.cpp @@ -1 +1,7 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "DruidCatActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.h b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.h index f6d8f2320..43ea6e6ee 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidCatActions.h @@ -1,20 +1,32 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DRUIDCATACTIONS_H -#define _PLAYERBOT_DRUIDCATACTIONS_H +#ifndef PLAYERBOTS_DRUIDCATACTIONS_H +#define PLAYERBOTS_DRUIDCATACTIONS_H #include "GenericSpellActions.h" #include "ReachTargetActions.h" +constexpr uint32 SPELL_POUNCE_RANK_1 = 9005; +constexpr uint32 SPELL_RAVAGE_RANK_1 = 6785; + class PlayerbotAI; class CastFeralChargeCatAction : public CastReachTargetSpellAction { public: CastFeralChargeCatAction(PlayerbotAI* botAI) : CastReachTargetSpellAction(botAI, "feral charge - cat", 1.5f) {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot)) + return false; + + return CastReachTargetSpellAction::isUseful(); + } }; class CastCowerAction : public CastBuffSpellAction @@ -48,28 +60,47 @@ public: CastRakeAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "rake", true, 6.0f) {} }; -class CastRakeOnMeleeAttackersAction : public CastDebuffSpellOnMeleeAttackerAction -{ -public: - CastRakeOnMeleeAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnMeleeAttackerAction(botAI, "rake", true, 6.0f) {} -}; - class CastClawAction : public CastMeleeSpellAction { public: CastClawAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "claw") {} + + bool isUseful() override + { + // Block Claw once Pounce is learned; Claw remains available as the stealth opener before then. + if (botAI->HasAura("prowl", bot) && bot->HasSpell(SPELL_POUNCE_RANK_1)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastMangleCatAction : public CastMeleeSpellAction { public: CastMangleCatAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "mangle (cat)") {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastSwipeCatAction : public CastMeleeSpellAction { public: CastSwipeCatAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "swipe (cat)") {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastFerociousBiteAction : public CastMeleeSpellAction @@ -78,6 +109,21 @@ public: CastFerociousBiteAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "ferocious bite") {} }; +class CastMaimAction : public CastMeleeSpellAction +{ +public: + CastMaimAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "maim") {} + + bool isUseful() override + { + Unit* target = GetTarget(); + if (!target || !target->ToPlayer()) + return false; + + return CastMeleeSpellAction::isUseful(); + } +}; + class CastRipAction : public CastMeleeDebuffSpellAction { public: @@ -88,6 +134,14 @@ class CastShredAction : public CastMeleeSpellAction { public: CastShredAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shred") {} + + bool isUseful() override + { + if (botAI->HasAura("prowl", bot) && bot->HasSpell(SPELL_RAVAGE_RANK_1)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastProwlAction : public CastBuffSpellAction @@ -106,12 +160,28 @@ class CastRavageAction : public CastMeleeSpellAction { public: CastRavageAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "ravage") {} + + bool isUseful() override + { + if (!botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; class CastPounceAction : public CastMeleeSpellAction { public: CastPounceAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "pounce") {} + + bool isUseful() override + { + if (!botAI->HasAura("prowl", bot)) + return false; + + return CastMeleeSpellAction::isUseful(); + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp index a87f22a3b..51cfc3c22 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DruidShapeshiftActions.h" @@ -44,15 +45,16 @@ bool CastCasterFormAction::isUseful() AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; } -bool CastCancelTreeFormAction::Execute(Event /*event*/) +bool CastCancelDruidAction::Execute(Event /*event*/) { - botAI->RemoveAura("tree of life"); + botAI->RemoveAura(auraName); return true; } -bool CastCancelTreeFormAction::isUseful() { return botAI->HasAura(33891, bot); } +bool CastCancelDruidAction::isUseful() { return bot->HasAura(auraId); } bool CastTreeFormAction::isUseful() { - return GetTarget() && CastSpellAction::isUseful() && !botAI->HasAura(33891, bot); + constexpr uint32 SPELL_TREE_OF_LIFE = 33891; + return GetTarget() && CastSpellAction::isUseful() && !bot->HasAura(SPELL_TREE_OF_LIFE); } diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h index 9d75f2682..e685f38ba 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DRUIDSHAPESHIFTACTIONS_H -#define _PLAYERBOT_DRUIDSHAPESHIFTACTIONS_H +#ifndef PLAYERBOTS_DRUIDSHAPESHIFTACTIONS_H +#define PLAYERBOTS_DRUIDSHAPESHIFTACTIONS_H #include "GenericSpellActions.h" @@ -71,14 +72,78 @@ public: bool isPossible() override { return true; } }; -class CastCancelTreeFormAction : public CastBuffSpellAction +class CastCancelDruidAction : public CastBuffSpellAction { public: - CastCancelTreeFormAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cancel tree form") {} + CastCancelDruidAction(PlayerbotAI* botAI, std::string const& actionName, std::string const& auraName, uint32 auraId) + : CastBuffSpellAction(botAI, actionName), auraName(auraName), auraId(auraId) + { + } bool Execute(Event event) override; bool isUseful() override; bool isPossible() override { return true; } + +private: + std::string auraName; + uint32 auraId; +}; + +class CastCancelTreeFormAction : public CastCancelDruidAction +{ +public: + CastCancelTreeFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel tree form", "tree of life", 33891) + { + } +}; + +class CastCancelTravelFormAction : public CastCancelDruidAction +{ +public: + CastCancelTravelFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel travel form", "travel form", 783) + { + } +}; + +class CastCancelBearFormAction : public CastCancelDruidAction +{ +public: + CastCancelBearFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel bear form", "bear form", 5487) {} +}; + +class CastCancelDireBearFormAction : public CastCancelDruidAction +{ +public: + CastCancelDireBearFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel dire bear form", "dire bear form", 9634) + { + } +}; + +class CastCancelCatFormAction : public CastCancelDruidAction +{ +public: + CastCancelCatFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel cat form", "cat form", 768) {} +}; + +class CastCancelMoonkinFormAction : public CastCancelDruidAction +{ +public: + CastCancelMoonkinFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel moonkin form", "moonkin form", 24858) + { + } +}; + +class CastCancelAquaticFormAction : public CastCancelDruidAction +{ +public: + CastCancelAquaticFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel aquatic form", "aquatic form", 1066) + { + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 29d9d4fdc..1d233e4d8 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -1,13 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DruidAiObjectContext.h" -#include "BearTankDruidStrategy.h" -#include "CasterDruidStrategy.h" -#include "CatDpsDruidStrategy.h" +#include "BalanceDruidStrategy.h" +#include "BearDruidStrategy.h" +#include "CatDruidStrategy.h" #include "DruidActions.h" #include "DruidBearActions.h" #include "DruidCatActions.h" @@ -15,10 +16,9 @@ #include "DruidTriggers.h" #include "GenericDruidNonCombatStrategy.h" #include "GenericDruidStrategy.h" -#include "HealDruidStrategy.h" -#include "MeleeDruidStrategy.h" -#include "OffhealDruidCatStrategy.h" +#include "RestoDruidStrategy.h" #include "Playerbots.h" +#include "DruidPullStrategy.h" class DruidStrategyFactoryInternal : public NamedObjectContext { @@ -26,29 +26,32 @@ public: DruidStrategyFactoryInternal() { creators["nc"] = &DruidStrategyFactoryInternal::nc; - creators["cat aoe"] = &DruidStrategyFactoryInternal::cat_aoe; - creators["caster aoe"] = &DruidStrategyFactoryInternal::caster_aoe; - creators["caster debuff"] = &DruidStrategyFactoryInternal::caster_debuff; - creators["dps debuff"] = &DruidStrategyFactoryInternal::caster_debuff; + creators["pull"] = &DruidStrategyFactoryInternal::pull; + creators["aoe"] = &DruidStrategyFactoryInternal::aoe; creators["cure"] = &DruidStrategyFactoryInternal::cure; - creators["melee"] = &DruidStrategyFactoryInternal::melee; creators["buff"] = &DruidStrategyFactoryInternal::buff; creators["boost"] = &DruidStrategyFactoryInternal::boost; creators["cc"] = &DruidStrategyFactoryInternal::cc; creators["healer dps"] = &DruidStrategyFactoryInternal::healer_dps; + creators["offheal"] = &DruidStrategyFactoryInternal::offheal; + creators["blanketing"] = &DruidStrategyFactoryInternal::blanketing; + creators["tranquility"] = &DruidStrategyFactoryInternal::tranquility; + creators["feral charge"] = &DruidStrategyFactoryInternal::feral_charge; } private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericDruidNonCombatStrategy(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); } + static Strategy* pull(PlayerbotAI* botAI) { return new DruidPullStrategy(botAI); } + static Strategy* aoe(PlayerbotAI* botAI) { return new DruidAoeStrategy(botAI); } static Strategy* cure(PlayerbotAI* botAI) { return new DruidCureStrategy(botAI); } - static Strategy* melee(PlayerbotAI* botAI) { return new MeleeDruidStrategy(botAI); } static Strategy* buff(PlayerbotAI* botAI) { return new GenericDruidBuffStrategy(botAI); } static Strategy* boost(PlayerbotAI* botAI) { return new DruidBoostStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new DruidCcStrategy(botAI); } static Strategy* healer_dps(PlayerbotAI* botAI) { return new DruidHealerDpsStrategy(botAI); } + static Strategy* offheal(PlayerbotAI* botAI) { return new CatOffhealStrategy(botAI); } + static Strategy* blanketing(PlayerbotAI* botAI) { return new DruidBlanketStrategy(botAI); } + static Strategy* tranquility(PlayerbotAI* botAI) { return new DruidTranquilityStrategy(botAI); } + static Strategy* feral_charge(PlayerbotAI* botAI) { return new FeralChargeDruidStrategy(botAI); } }; class DruidDruidStrategyFactoryInternal : public NamedObjectContext @@ -59,18 +62,16 @@ public: creators["bear"] = &DruidDruidStrategyFactoryInternal::bear; creators["tank"] = &DruidDruidStrategyFactoryInternal::bear; creators["cat"] = &DruidDruidStrategyFactoryInternal::cat; - creators["caster"] = &DruidDruidStrategyFactoryInternal::caster; + creators["balance"] = &DruidDruidStrategyFactoryInternal::balance; creators["dps"] = &DruidDruidStrategyFactoryInternal::cat; - creators["heal"] = &DruidDruidStrategyFactoryInternal::heal; - creators["offheal"] = &DruidDruidStrategyFactoryInternal::offheal; + creators["resto"] = &DruidDruidStrategyFactoryInternal::heal; } private: - static Strategy* bear(PlayerbotAI* botAI) { return new BearTankDruidStrategy(botAI); } - static Strategy* cat(PlayerbotAI* botAI) { return new CatDpsDruidStrategy(botAI); } - static Strategy* caster(PlayerbotAI* botAI) { return new CasterDruidStrategy(botAI); } - static Strategy* heal(PlayerbotAI* botAI) { return new HealDruidStrategy(botAI); } - static Strategy* offheal(PlayerbotAI* botAI) { return new OffhealDruidCatStrategy(botAI); } + static Strategy* bear(PlayerbotAI* botAI) { return new BearDruidStrategy(botAI); } + static Strategy* cat(PlayerbotAI* botAI) { return new CatDruidStrategy(botAI); } + static Strategy* balance(PlayerbotAI* botAI) { return new BalanceDruidStrategy(botAI); } + static Strategy* heal(PlayerbotAI* botAI) { return new RestoDruidStrategy(botAI); } }; class DruidTriggerFactoryInternal : public NamedObjectContext @@ -78,7 +79,7 @@ class DruidTriggerFactoryInternal : public NamedObjectContext public: DruidTriggerFactoryInternal() { - creators["omen of clarity"] = &DruidTriggerFactoryInternal::omen_of_clarity; + creators["clearcasting"] = &DruidTriggerFactoryInternal::clearcasting; creators["thorns"] = &DruidTriggerFactoryInternal::thorns; creators["thorns on party"] = &DruidTriggerFactoryInternal::thorns_on_party; creators["thorns on main tank"] = &DruidTriggerFactoryInternal::thorns_on_main_tank; @@ -86,10 +87,12 @@ public: creators["faerie fire (feral)"] = &DruidTriggerFactoryInternal::faerie_fire_feral; creators["faerie fire"] = &DruidTriggerFactoryInternal::faerie_fire; creators["insect swarm"] = &DruidTriggerFactoryInternal::insect_swarm; + creators["insect swarm on attacker"] = &DruidTriggerFactoryInternal::insect_swarm_on_attacker; creators["moonfire"] = &DruidTriggerFactoryInternal::moonfire; + creators["moonfire on attacker"] = &DruidTriggerFactoryInternal::moonfire_on_attacker; creators["nature's grasp"] = &DruidTriggerFactoryInternal::natures_grasp; - creators["tiger's fury"] = &DruidTriggerFactoryInternal::tigers_fury; creators["berserk"] = &DruidTriggerFactoryInternal::berserk; + creators["berserk active"] = &DruidTriggerFactoryInternal::berserk_active; creators["savage roar"] = &DruidTriggerFactoryInternal::savage_roar; creators["rake"] = &DruidTriggerFactoryInternal::rake; creators["mark of the wild"] = &DruidTriggerFactoryInternal::mark_of_the_wild; @@ -106,16 +109,35 @@ public: creators["eclipse (lunar)"] = &DruidTriggerFactoryInternal::eclipse_lunar; creators["bash on enemy healer"] = &DruidTriggerFactoryInternal::bash_on_enemy_healer; creators["nature's swiftness"] = &DruidTriggerFactoryInternal::natures_swiftness; + creators["nature's swiftness active"] = &DruidTriggerFactoryInternal::natures_swiftness_active; creators["party member remove curse"] = &DruidTriggerFactoryInternal::party_member_remove_curse; - creators["eclipse (solar) cooldown"] = &DruidTriggerFactoryInternal::eclipse_solar_cooldown; - creators["eclipse (lunar) cooldown"] = &DruidTriggerFactoryInternal::eclipse_lunar_cooldown; + creators["mangle (bear)"] = &DruidTriggerFactoryInternal::mangle_bear_trigger; + creators["lacerate"] = &DruidTriggerFactoryInternal::lacerate_trigger; + creators["demoralizing roar"] = &DruidTriggerFactoryInternal::demoralize_roar; creators["mangle (cat)"] = &DruidTriggerFactoryInternal::mangle_cat; creators["ferocious bite time"] = &DruidTriggerFactoryInternal::ferocious_bite_time; + creators["ferocious bite execute"] = &DruidTriggerFactoryInternal::ferocious_bite_execute; creators["hurricane channel check"] = &DruidTriggerFactoryInternal::hurricane_channel_check; + creators["no healer dps strategy"] = &DruidTriggerFactoryInternal::no_healer_dps_strategy; + creators["starfall"] = &DruidTriggerFactoryInternal::starfall; + creators["force of nature"] = &DruidTriggerFactoryInternal::force_of_nature; + creators["cyclone"] = &DruidTriggerFactoryInternal::cyclone; + creators["predator's swiftness"] = &DruidTriggerFactoryInternal::predators_swiftness; + creators["predator's swiftness and cyclone"] = &DruidTriggerFactoryInternal::predators_swiftness_and_cyclone; + creators["predator's swiftness and hibernate"] = &DruidTriggerFactoryInternal::predators_swiftness_and_hibernate; + creators["predator's swiftness and entangling roots"] = &DruidTriggerFactoryInternal::predators_swiftness_and_entangling_roots; + creators["predator's swiftness and combat party member dead"] = &DruidTriggerFactoryInternal::predators_swiftness_and_combat_party_member_dead; + creators["clearcasting and medium aoe"] = &DruidTriggerFactoryInternal::clearcasting_and_medium_aoe; + creators["prowl"] = &DruidTriggerFactoryInternal::prowl_trigger; + creators["rejuvenation blanket"] = &DruidTriggerFactoryInternal::rejuvenation_blanket; + creators["wild growth blanket"] = &DruidTriggerFactoryInternal::wild_growth_blanket; + creators["aquatic form"] = &DruidTriggerFactoryInternal::aquatic_form; } private: static Trigger* natures_swiftness(PlayerbotAI* botAI) { return new NaturesSwiftnessTrigger(botAI); } + static Trigger* natures_swiftness_active(PlayerbotAI* botAI) { return new NaturesSwiftnessActiveTrigger(botAI); } + static Trigger* clearcasting(PlayerbotAI* botAI) { return new ClearcastingTrigger(botAI); } static Trigger* eclipse_solar(PlayerbotAI* botAI) { return new EclipseSolarTrigger(botAI); } static Trigger* eclipse_lunar(PlayerbotAI* botAI) { return new EclipseLunarTrigger(botAI); } static Trigger* thorns(PlayerbotAI* botAI) { return new ThornsTrigger(botAI); } @@ -124,11 +146,13 @@ private: static Trigger* bash(PlayerbotAI* botAI) { return new BashInterruptSpellTrigger(botAI); } static Trigger* faerie_fire_feral(PlayerbotAI* botAI) { return new FaerieFireFeralTrigger(botAI); } static Trigger* insect_swarm(PlayerbotAI* botAI) { return new InsectSwarmTrigger(botAI); } + static Trigger* insect_swarm_on_attacker(PlayerbotAI* botAI) { return new InsectSwarmOnAttackerTrigger(botAI); } static Trigger* moonfire(PlayerbotAI* botAI) { return new MoonfireTrigger(botAI); } + static Trigger* moonfire_on_attacker(PlayerbotAI* botAI) { return new MoonfireOnAttackerTrigger(botAI); } static Trigger* faerie_fire(PlayerbotAI* botAI) { return new FaerieFireTrigger(botAI); } static Trigger* natures_grasp(PlayerbotAI* botAI) { return new NaturesGraspTrigger(botAI); } - static Trigger* tigers_fury(PlayerbotAI* botAI) { return new TigersFuryTrigger(botAI); } static Trigger* berserk(PlayerbotAI* botAI) { return new BerserkTrigger(botAI); } + static Trigger* berserk_active(PlayerbotAI* botAI) { return new BerserkActiveTrigger(botAI); } static Trigger* savage_roar(PlayerbotAI* botAI) { return new SavageRoarTrigger(botAI); } static Trigger* rake(PlayerbotAI* botAI) { return new RakeTrigger(botAI); } static Trigger* mark_of_the_wild(PlayerbotAI* botAI) { return new MarkOfTheWildTrigger(botAI); } @@ -142,13 +166,28 @@ private: static Trigger* cat_form(PlayerbotAI* botAI) { return new CatFormTrigger(botAI); } static Trigger* tree_form(PlayerbotAI* botAI) { return new TreeFormTrigger(botAI); } static Trigger* bash_on_enemy_healer(PlayerbotAI* botAI) { return new BashInterruptEnemyHealerSpellTrigger(botAI); } - static Trigger* omen_of_clarity(PlayerbotAI* botAI) { return new OmenOfClarityTrigger(botAI); } static Trigger* party_member_remove_curse(PlayerbotAI* ai) { return new DruidPartyMemberRemoveCurseTrigger(ai); } - static Trigger* eclipse_solar_cooldown(PlayerbotAI* ai) { return new EclipseSolarCooldownTrigger(ai); } - static Trigger* eclipse_lunar_cooldown(PlayerbotAI* ai) { return new EclipseLunarCooldownTrigger(ai); } + static Trigger* mangle_bear_trigger(PlayerbotAI* botAI) { return new MangleBearTrigger(botAI); } + static Trigger* lacerate_trigger(PlayerbotAI* botAI) { return new LacerateTrigger(botAI); } + static Trigger* demoralize_roar(PlayerbotAI* botAI) { return new DemoralizeRoarTrigger(botAI); } static Trigger* mangle_cat(PlayerbotAI* ai) { return new MangleCatTrigger(ai); } static Trigger* ferocious_bite_time(PlayerbotAI* ai) { return new FerociousBiteTimeTrigger(ai); } + static Trigger* ferocious_bite_execute(PlayerbotAI* ai) { return new FerociousBiteExecuteTrigger(ai); } static Trigger* hurricane_channel_check(PlayerbotAI* ai) { return new HurricaneChannelCheckTrigger(ai); } + static Trigger* no_healer_dps_strategy(PlayerbotAI* ai) { return new NoHealerDpsStrategyTrigger(ai); } + static Trigger* starfall(PlayerbotAI* ai) { return new StarfallTrigger(ai); } + static Trigger* force_of_nature(PlayerbotAI* ai) { return new ForceOfNatureTrigger(ai); } + static Trigger* cyclone(PlayerbotAI* ai) { return new CycloneTrigger(ai); } + static Trigger* predators_swiftness(PlayerbotAI* ai) { return new PredatorsSwiftnessTrigger(ai); } + static Trigger* predators_swiftness_and_cyclone(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "cyclone"); } + static Trigger* predators_swiftness_and_hibernate(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "hibernate"); } + static Trigger* predators_swiftness_and_entangling_roots(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "entangling roots"); } + static Trigger* predators_swiftness_and_combat_party_member_dead(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "combat party member dead"); } + static Trigger* clearcasting_and_medium_aoe(PlayerbotAI* ai) { return new TwoTriggers(ai, "clearcasting", "medium aoe"); } + static Trigger* prowl_trigger(PlayerbotAI* ai) { return new ProwlTrigger(ai); } + static Trigger* rejuvenation_blanket(PlayerbotAI* ai) { return new BuffOnPartyTrigger(ai, "rejuvenation"); } + static Trigger* wild_growth_blanket(PlayerbotAI* ai) { return new BuffOnPartyTrigger(ai, "wild growth"); } + static Trigger* aquatic_form(PlayerbotAI* ai) { return new AquaticFormTrigger(ai); } }; class DruidAiObjectContextInternal : public NamedObjectContext @@ -170,17 +209,23 @@ public: creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form; creators["caster form"] = &DruidAiObjectContextInternal::caster_form; creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form; + creators["cancel travel form"] = &DruidAiObjectContextInternal::cancel_travel_form; + creators["cancel bear form"] = &DruidAiObjectContextInternal::cancel_bear_form; + creators["cancel dire bear form"] = &DruidAiObjectContextInternal::cancel_dire_bear_form; + creators["cancel cat form"] = &DruidAiObjectContextInternal::cancel_cat_form; + creators["cancel moonkin form"] = &DruidAiObjectContextInternal::cancel_moonkin_form; + creators["cancel aquatic form"] = &DruidAiObjectContextInternal::cancel_aquatic_form; creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear; creators["maul"] = &DruidAiObjectContextInternal::maul; creators["bash"] = &DruidAiObjectContextInternal::bash; - creators["swipe"] = &DruidAiObjectContextInternal::swipe; creators["growl"] = &DruidAiObjectContextInternal::growl; + creators["challenging roar"] = &DruidAiObjectContextInternal::challenging_roar; creators["demoralizing roar"] = &DruidAiObjectContextInternal::demoralizing_roar; creators["hibernate"] = &DruidAiObjectContextInternal::hibernate; creators["entangling roots"] = &DruidAiObjectContextInternal::entangling_roots; creators["entangling roots on cc"] = &DruidAiObjectContextInternal::entangling_roots_on_cc; - creators["hibernate"] = &DruidAiObjectContextInternal::hibernate; creators["hibernate on cc"] = &DruidAiObjectContextInternal::hibernate_on_cc; + creators["cyclone on cc"] = &DruidAiObjectContextInternal::cyclone_on_cc; creators["wrath"] = &DruidAiObjectContextInternal::wrath; creators["starfall"] = &DruidAiObjectContextInternal::starfall; creators["insect swarm"] = &DruidAiObjectContextInternal::insect_swarm; @@ -191,15 +236,16 @@ public: creators["mangle (cat)"] = &DruidAiObjectContextInternal::mangle_cat; creators["swipe (cat)"] = &DruidAiObjectContextInternal::swipe_cat; creators["rake"] = &DruidAiObjectContextInternal::rake; - creators["rake on attacker"] = &DruidAiObjectContextInternal::rake_on_attacker; creators["ferocious bite"] = &DruidAiObjectContextInternal::ferocious_bite; creators["rip"] = &DruidAiObjectContextInternal::rip; + creators["maim"] = &DruidAiObjectContextInternal::maim; creators["cower"] = &DruidAiObjectContextInternal::cower; creators["survival instincts"] = &DruidAiObjectContextInternal::survival_instincts; creators["frenzied regeneration"] = &DruidAiObjectContextInternal::frenzied_regeneration; creators["thorns"] = &DruidAiObjectContextInternal::thorns; creators["thorns on party"] = &DruidAiObjectContextInternal::thorns_on_party; creators["thorns on main tank"] = &DruidAiObjectContextInternal::thorns_on_main_tank; + creators["lifebloom on main tank"] = &DruidAiObjectContextInternal::lifebloom_on_main_tank; creators["cure poison"] = &DruidAiObjectContextInternal::cure_poison; creators["cure poison on party"] = &DruidAiObjectContextInternal::cure_poison_on_party; creators["abolish poison"] = &DruidAiObjectContextInternal::abolish_poison; @@ -222,9 +268,9 @@ public: creators["lacerate"] = &DruidAiObjectContextInternal::lacerate; creators["hurricane"] = &DruidAiObjectContextInternal::hurricane; creators["innervate"] = &DruidAiObjectContextInternal::innervate; + creators["innervate on healer"] = &DruidAiObjectContextInternal::innervate_on_healer; creators["tranquility"] = &DruidAiObjectContextInternal::tranquility; creators["bash on enemy healer"] = &DruidAiObjectContextInternal::bash_on_enemy_healer; - creators["omen of clarity"] = &DruidAiObjectContextInternal::omen_of_clarity; creators["nature's swiftness"] = &DruidAiObjectContextInternal::natures_swiftness; creators["prowl"] = &DruidAiObjectContextInternal::prowl; creators["dash"] = &DruidAiObjectContextInternal::dash; @@ -239,11 +285,13 @@ public: creators["moonfire on attacker"] = &DruidAiObjectContextInternal::moonfire_on_attacker; creators["enrage"] = &DruidAiObjectContextInternal::enrage; creators["force of nature"] = &DruidAiObjectContextInternal::force_of_nature; + creators["typhoon"] = &DruidAiObjectContextInternal::typhoon; + creators["rejuvenation blanket"] = &DruidAiObjectContextInternal::rejuvenation_blanket; + creators["wild growth blanket"] = &DruidAiObjectContextInternal::wild_growth_blanket; } private: static Action* natures_swiftness(PlayerbotAI* botAI) { return new CastNaturesSwiftnessAction(botAI); } - static Action* omen_of_clarity(PlayerbotAI* botAI) { return new CastOmenOfClarityAction(botAI); } static Action* tranquility(PlayerbotAI* botAI) { return new CastTranquilityAction(botAI); } static Action* feral_charge_bear(PlayerbotAI* botAI) { return new CastFeralChargeBearAction(botAI); } static Action* feral_charge_cat(PlayerbotAI* botAI) { return new CastFeralChargeCatAction(botAI); } @@ -258,17 +306,24 @@ private: static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); } static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); } static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); } + static Action* cancel_travel_form(PlayerbotAI* botAI) { return new CastCancelTravelFormAction(botAI); } + static Action* cancel_bear_form(PlayerbotAI* botAI) { return new CastCancelBearFormAction(botAI); } + static Action* cancel_dire_bear_form(PlayerbotAI* botAI) { return new CastCancelDireBearFormAction(botAI); } + static Action* cancel_cat_form(PlayerbotAI* botAI) { return new CastCancelCatFormAction(botAI); } + static Action* cancel_moonkin_form(PlayerbotAI* botAI) { return new CastCancelMoonkinFormAction(botAI); } + static Action* cancel_aquatic_form(PlayerbotAI* botAI) { return new CastCancelAquaticFormAction(botAI); } static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); } static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); } static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } - static Action* swipe(PlayerbotAI* botAI) { return new CastSwipeAction(botAI); } static Action* growl(PlayerbotAI* botAI) { return new CastGrowlAction(botAI); } + static Action* challenging_roar(PlayerbotAI* botAI) { return new CastChallengingRoarAction(botAI); } static Action* demoralizing_roar(PlayerbotAI* botAI) { return new CastDemoralizingRoarAction(botAI); } static Action* moonkin_form(PlayerbotAI* botAI) { return new CastMoonkinFormAction(botAI); } static Action* hibernate(PlayerbotAI* botAI) { return new CastHibernateAction(botAI); } static Action* entangling_roots(PlayerbotAI* botAI) { return new CastEntanglingRootsAction(botAI); } static Action* hibernate_on_cc(PlayerbotAI* botAI) { return new CastHibernateCcAction(botAI); } static Action* entangling_roots_on_cc(PlayerbotAI* botAI) { return new CastEntanglingRootsCcAction(botAI); } + static Action* cyclone_on_cc(PlayerbotAI* botAI) { return new CastCycloneCcAction(botAI); } static Action* wrath(PlayerbotAI* botAI) { return new CastWrathAction(botAI); } static Action* starfall(PlayerbotAI* botAI) { return new CastStarfallAction(botAI); } static Action* insect_swarm(PlayerbotAI* botAI) { return new CastInsectSwarmAction(botAI); } @@ -279,15 +334,16 @@ private: static Action* mangle_cat(PlayerbotAI* botAI) { return new CastMangleCatAction(botAI); } static Action* swipe_cat(PlayerbotAI* botAI) { return new CastSwipeCatAction(botAI); } static Action* rake(PlayerbotAI* botAI) { return new CastRakeAction(botAI); } - static Action* rake_on_attacker(PlayerbotAI* botAI) { return new CastRakeOnMeleeAttackersAction(botAI); } static Action* ferocious_bite(PlayerbotAI* botAI) { return new CastFerociousBiteAction(botAI); } static Action* rip(PlayerbotAI* botAI) { return new CastRipAction(botAI); } + static Action* maim(PlayerbotAI* botAI) { return new CastMaimAction(botAI); } static Action* cower(PlayerbotAI* botAI) { return new CastCowerAction(botAI); } static Action* survival_instincts(PlayerbotAI* botAI) { return new CastSurvivalInstinctsAction(botAI); } static Action* frenzied_regeneration(PlayerbotAI* botAI) { return new CastFrenziedRegenerationAction(botAI); } static Action* thorns(PlayerbotAI* botAI) { return new CastThornsAction(botAI); } static Action* thorns_on_party(PlayerbotAI* botAI) { return new CastThornsOnPartyAction(botAI); } static Action* thorns_on_main_tank(PlayerbotAI* botAI) { return new CastThornsOnMainTankAction(botAI); } + static Action* lifebloom_on_main_tank(PlayerbotAI* botAI) { return new CastLifebloomOnMainTankAction(botAI); } static Action* cure_poison(PlayerbotAI* botAI) { return new CastCurePoisonAction(botAI); } static Action* cure_poison_on_party(PlayerbotAI* botAI) { return new CastCurePoisonOnPartyAction(botAI); } static Action* abolish_poison(PlayerbotAI* botAI) { return new CastAbolishPoisonAction(botAI); } @@ -310,6 +366,7 @@ private: static Action* lacerate(PlayerbotAI* botAI) { return new CastLacerateAction(botAI); } static Action* hurricane(PlayerbotAI* botAI) { return new CastHurricaneAction(botAI); } static Action* innervate(PlayerbotAI* botAI) { return new CastInnervateAction(botAI); } + static Action* innervate_on_healer(PlayerbotAI* botAI) { return new CastInnervateOnHealerAction(botAI); } static Action* bash_on_enemy_healer(PlayerbotAI* botAI) { return new CastBashOnEnemyHealerAction(botAI); } static Action* ravage(PlayerbotAI* botAI) { return new CastRavageAction(botAI); } static Action* pounce(PlayerbotAI* botAI) { return new CastPounceAction(botAI); } @@ -324,6 +381,9 @@ private: static Action* moonfire_on_attacker(PlayerbotAI* ai) { return new CastMoonfireOnAttackerAction(ai); } static Action* enrage(PlayerbotAI* ai) { return new CastEnrageAction(ai); } static Action* force_of_nature(PlayerbotAI* ai) { return new CastForceOfNatureAction(ai); } + static Action* typhoon(PlayerbotAI* ai) { return new CastTyphoonAction(ai); } + static Action* rejuvenation_blanket(PlayerbotAI* ai) { return new CastRejuvenationBlanketAction(ai); } + static Action* wild_growth_blanket(PlayerbotAI* ai) { return new CastWildGrowthBlanketAction(ai); } }; SharedNamedObjectContextList DruidAiObjectContext::sharedStrategyContexts; @@ -363,7 +423,22 @@ void DruidAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextLi triggerContexts.Add(new DruidTriggerFactoryInternal()); } +class DruidValueContextInternal : public NamedObjectContext +{ +public: + DruidValueContextInternal() + { + creators["eclipse solar proc time"] = &DruidValueContextInternal::eclipse_solar_proc_time; + creators["eclipse lunar proc time"] = &DruidValueContextInternal::eclipse_lunar_proc_time; + } + +private: + static UntypedValue* eclipse_solar_proc_time(PlayerbotAI* botAI) { return new EclipseSolarProcTimeValue(botAI); } + static UntypedValue* eclipse_lunar_proc_time(PlayerbotAI* botAI) { return new EclipseLunarProcTimeValue(botAI); } +}; + void DruidAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) { AiObjectContext::BuildSharedValueContexts(valueContexts); + valueContexts.Add(new DruidValueContextInternal()); } diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.h index ee309e79c..a855a6456 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/DruidAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DRUIDAIOBJECTCONTEXT_H -#define _PLAYERBOT_DRUIDAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_DRUIDAIOBJECTCONTEXT_H +#define PLAYERBOTS_DRUIDAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.cpp new file mode 100644 index 000000000..4cebc8832 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.cpp @@ -0,0 +1,126 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DruidTriggers.h" +#include "DynamicObject.h" +#include "Player.h" +#include "Playerbots.h" +#include "ServerFacade.h" + +bool MarkOfTheWildTrigger::IsActive() +{ + return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget()); +} + +bool ThornsOnPartyTrigger::IsActive() +{ + return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget()); +} + +bool EntanglingRootsKiteTrigger::IsActive() +{ + return DebuffTrigger::IsActive() && AI_VALUE(uint8, "attacker count") < 3 && !GetTarget()->GetPower(POWER_MANA); +} + +bool ThornsTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget()); } + +bool BearFormTrigger::IsActive() { return !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr); } + +bool TreeFormTrigger::IsActive() +{ + constexpr uint32 SPELL_TREE_OF_LIFE = 33891; + return !bot->HasAura(SPELL_TREE_OF_LIFE); +} + +bool CatFormTrigger::IsActive() { return !botAI->HasAura("cat form", bot); } + +bool AquaticFormTrigger::IsActive() +{ + return !bot->IsInCombat() && !botAI->HasAura("aquatic form", bot) && + bot->GetLiquidData().Status == LIQUID_MAP_UNDER_WATER; +} + +bool ProwlTrigger::IsActive() +{ + if (botAI->HasAura("prowl", bot) || bot->IsInCombat()) + return false; + + if (!botAI->HasSpell("prowl")) + return false; + + uint32 const prowlId = AI_VALUE2(uint32, "spell id", "prowl"); + if (bot->HasSpellCooldown(prowlId)) + return false; + + float distance = 30.f; + + Unit* target = AI_VALUE(Unit*, "enemy player target"); + if (target && !target->IsInWorld()) + return false; + if (!target) + target = AI_VALUE(Unit*, "grind target"); + if (!target) + target = AI_VALUE(Unit*, "dps target"); + if (!target) + return false; + + if (target && target->GetVictim()) + distance -= 10; + if (target->isMoving() && target->GetVictim()) + distance -= 10; + if (bot->InBattleground()) + distance += 15; + if (bot->InArena()) + distance += 15; + + return target && ServerFacade::instance().GetDistance2d(bot, target) < distance; +} + +const std::set HurricaneChannelCheckTrigger::HURRICANE_SPELL_IDS = { + 16914, // Hurricane Rank 1 + 17401, // Hurricane Rank 2 + 17402, // Hurricane Rank 3 + 27012, // Hurricane Rank 4 + 48467 // Hurricane Rank 5 +}; + +bool HurricaneChannelCheckTrigger::IsActive() +{ + if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + { + if (!HURRICANE_SPELL_IDS.count(spell->m_spellInfo->Id)) + return false; + + // Find this bot's own Hurricane DynamicObject + DynamicObject* dynObj = nullptr; + for (uint32 spellId : HURRICANE_SPELL_IDS) + { + dynObj = bot->GetDynObject(spellId); + if (dynObj) + break; + } + + if (!dynObj) + return false; + + // Count attackers actually inside the Hurricane AoE + float radius = dynObj->GetRadius(); + GuidVector attackers = AI_VALUE(GuidVector, "attackers"); + uint32 count = 0; + for (ObjectGuid const& guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + if (unit->GetDistance(dynObj->GetPosition()) <= radius) + count++; + } + + return count < minEnemies; + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.h b/modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.h new file mode 100644 index 000000000..73db412dd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/DruidTriggers.h @@ -0,0 +1,454 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DRUIDTRIGGERS_H +#define PLAYERBOTS_DRUIDTRIGGERS_H + +#include "CureTriggers.h" +#include "GenericTriggers.h" +#include "RtiTriggers.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "SharedDefines.h" +#include "Trigger.h" +#include + +constexpr uint32 AURA_OMEN_OF_CLARITY = 16864; + +class PlayerbotAI; + +class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 4 * 2000) {} +}; + +class MarkOfTheWildTrigger : public BuffTrigger +{ +public: + MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 4 * 2000) {} + + bool IsActive() override; +}; + +class ThornsOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + ThornsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "thorns", 2 * 2000) {} + + bool IsActive() override; +}; + +class ThornsOnMainTankTrigger : public BuffOnMainTankTrigger +{ +public: + ThornsOnMainTankTrigger(PlayerbotAI* botAI) : BuffOnMainTankTrigger(botAI, "thorns", false, 2 * 2000) {} +}; + +class ThornsTrigger : public BuffTrigger +{ +public: + ThornsTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "thorns", 2 * 2000) {} + + bool IsActive() override; +}; + +class ClearcastingTrigger : public HasAuraTrigger +{ +public: + ClearcastingTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "clearcasting") {} +}; + +class PredatorsSwiftnessTrigger : public HasAuraTrigger +{ +public: + PredatorsSwiftnessTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "predator's swiftness") {} +}; + +class NaturesSwiftnessActiveTrigger : public HasAuraTrigger +{ +public: + NaturesSwiftnessActiveTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "nature's swiftness") {} + bool IsActive() override { return botAI->HasAura("nature's swiftness", bot); } +}; + +class RakeTrigger : public DebuffTrigger +{ +public: + RakeTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "rake", 1, true) {} + bool IsActive() override { return !botAI->HasAura("prowl", bot) && DebuffTrigger::IsActive(); } +}; + +class InsectSwarmTrigger : public DebuffTrigger +{ +public: + InsectSwarmTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "insect swarm", 1, true) {} +}; + +class InsectSwarmOnAttackerTrigger : public DebuffOnAttackerTrigger +{ +public: + InsectSwarmOnAttackerTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "insect swarm", true) {} + bool IsActive() override { return BuffTrigger::IsActive(); } +}; + +class MoonfireTrigger : public DebuffTrigger +{ +public: + MoonfireTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "moonfire", 1, true) {} +}; + +class MoonfireOnAttackerTrigger : public DebuffOnAttackerTrigger +{ +public: + MoonfireOnAttackerTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "moonfire", true) {} + bool IsActive() override { return BuffTrigger::IsActive(); } +}; + +class FaerieFireTrigger : public DebuffTrigger +{ +public: + FaerieFireTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "faerie fire", 1, false, 25.0f) {} +}; + +class FaerieFireFeralTrigger : public DebuffTrigger +{ +public: + FaerieFireFeralTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "faerie fire (feral)") {} + + bool IsActive() override + { + if (!bot->IsInCombat()) + return false; + + // Bear: every cast generates immediate threat/damage for free — spam it + if (botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr)) + { + Unit* target = GetTarget(); + return target && target->IsAlive() && target->IsInWorld(); + } + + if (!botAI->HasAura("cat form", bot)) + return false; + + if (botAI->HasAura("prowl", bot)) + return false; + + // Cat with Omen of Clarity: spam to fish for Clearcasting procs + if (bot->HasAura(AURA_OMEN_OF_CLARITY)) + { + Unit* target = GetTarget(); + return target && target->IsAlive() && target->IsInWorld(); + } + + // Cat without Omen of Clarity: apply as a normal debuff, don't reapply + return DebuffTrigger::IsActive(); + } +}; + +class BashInterruptSpellTrigger : public InterruptSpellTrigger +{ +public: + BashInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "bash") {} +}; + +class BerserkTrigger : public BoostTrigger +{ +public: + BerserkTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "berserk") {} +}; + +class BerserkActiveTrigger : public HasAuraTrigger +{ +public: + BerserkActiveTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "berserk") {} +}; + +class SavageRoarTrigger : public BuffTrigger +{ +public: + SavageRoarTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "savage roar") {} +}; + +class NaturesGraspTrigger : public BuffTrigger +{ +public: + NaturesGraspTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "nature's grasp") {} +}; + +class EntanglingRootsTrigger : public RtiCcTrigger +{ +public: + EntanglingRootsTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "entangling roots") {} +}; + +class EntanglingRootsKiteTrigger : public DebuffTrigger +{ +public: + EntanglingRootsKiteTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "entangling roots") {} + + bool IsActive() override; +}; + +class HibernateTrigger : public RtiCcTrigger +{ +public: + HibernateTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "hibernate") {} +}; + +class CycloneTrigger : public RtiCcTrigger +{ +public: + CycloneTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "cyclone") {} +}; + +class CurePoisonTrigger : public NeedCureTrigger +{ +public: + CurePoisonTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} +}; + +class PartyMemberCurePoisonTrigger : public PartyMemberNeedCureTrigger +{ +public: + PartyMemberCurePoisonTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cure poison", DISPEL_POISON) + { + } +}; + +class BearFormTrigger : public BuffTrigger +{ +public: + BearFormTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "bear form") {} + + bool IsActive() override; +}; + +class TreeFormTrigger : public BuffTrigger +{ +public: + TreeFormTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "tree of life") {} + + bool IsActive() override; +}; + +class CatFormTrigger : public BuffTrigger +{ +public: + CatFormTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "cat form") {} + + bool IsActive() override; +}; + +class AquaticFormTrigger : public Trigger +{ +public: + AquaticFormTrigger(PlayerbotAI* botAI) : Trigger(botAI, "aquatic form") {} + + bool IsActive() override; +}; + +class EclipseSolarTrigger : public HasAuraTrigger +{ +public: + EclipseSolarTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "eclipse (solar)") {} +}; + +class EclipseLunarTrigger : public HasAuraTrigger +{ +public: + EclipseLunarTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "eclipse (lunar)") {} +}; + +class BashInterruptEnemyHealerSpellTrigger : public InterruptEnemyHealerTrigger +{ +public: + BashInterruptEnemyHealerSpellTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "bash") {} +}; + +class NaturesSwiftnessTrigger : public BuffTrigger +{ +public: + NaturesSwiftnessTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "nature's swiftness") {} +}; + +class DruidPartyMemberRemoveCurseTrigger : public PartyMemberNeedCureTrigger +{ +public: + DruidPartyMemberRemoveCurseTrigger(PlayerbotAI* ai) + : PartyMemberNeedCureTrigger(ai, "druid remove curse", DISPEL_CURSE) + { + } +}; + +class StarfallTrigger : public SpellNoCooldownTrigger +{ +public: + StarfallTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "starfall") {} +}; + +class ForceOfNatureTrigger : public BoostTrigger +{ +public: + ForceOfNatureTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "force of nature") {} +}; + +class MangleBearTrigger : public DebuffTrigger +{ +public: + MangleBearTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "mangle (bear)") {} + + bool IsActive() override + { + if (!bot->IsInCombat() || !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr)) + return false; + Unit* target = GetTarget(); + return target && target->IsAlive() && target->IsInWorld(); + } +}; + +class LacerateTrigger : public DebuffTrigger +{ +public: + LacerateTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "lacerate") {} + + bool IsActive() override + { + if (!bot->IsInCombat() || !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr)) + return false; + + Unit* target = GetTarget(); + if (!target || !target->IsAlive() || !target->IsInWorld()) + return false; + + Aura* lacerate = botAI->GetAura("lacerate", target, false, false); + if (!lacerate) + return true; + + if (lacerate->GetStackAmount() < 5) + return true; + + return lacerate->GetDuration() <= 6000; + } +}; + +class DemoralizeRoarTrigger : public DebuffTrigger +{ +public: + DemoralizeRoarTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "demoralizing roar") {} + + bool IsActive() override + { + return DebuffTrigger::IsActive() + && !botAI->HasAura("curse of weakness", GetTarget(), false, false) + && !botAI->HasAura("demoralizing shout", GetTarget(), false, false) + && !botAI->HasAura("vindication", GetTarget(), false, false); + } +}; + +class MangleCatTrigger : public DebuffTrigger +{ +public: + MangleCatTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "mangle (cat)", 1, false, 0.0f) {} + bool IsActive() override + { + if (botAI->HasAura("prowl", bot)) + return false; + return DebuffTrigger::IsActive() && !botAI->HasAura("mangle (bear)", GetTarget(), false, false, -1, true) + && !botAI->HasAura("trauma", GetTarget(), false, false, -1, true); + } +}; + +class FerociousBiteTimeTrigger : public Trigger +{ +public: + FerociousBiteTimeTrigger(PlayerbotAI* ai) : Trigger(ai, "ferocious bite time") {} + bool IsActive() override + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target) + return false; + + uint8 cp = AI_VALUE2(uint8, "combo", "current target"); + if (cp < 5) + return false; + + Aura* roar = botAI->GetAura("savage roar", bot); + bool roarCheck = !roar || roar->GetDuration() > 10000; + if (!roarCheck) + return false; + + Aura* rip = botAI->GetAura("rip", target, true); + bool ripCheck = !rip || rip->GetDuration() > 10000; + if (!ripCheck) + return false; + + return true; + } +}; + +class FerociousBiteExecuteTrigger : public Trigger +{ +public: + FerociousBiteExecuteTrigger(PlayerbotAI* botAI) : Trigger(botAI, "ferocious bite execute") {} + bool IsActive() override + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || !target->IsAlive()) + return false; + + if (!botAI->HasSpell("ferocious bite")) + return false; + + if (AI_VALUE2(uint8, "combo", "current target") < 1) + return false; + + if (target->GetHealthPct() >= 25.0f) + return false; + + if (target->GetHealth() >= 20000) + return false; + + return true; + } +}; + +class HurricaneChannelCheckTrigger : public Trigger +{ +public: + HurricaneChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 3) + : Trigger(botAI, "hurricane channel check"), minEnemies(minEnemies) + { + } + + bool IsActive() override; + +protected: + uint32 minEnemies; + static const std::set HURRICANE_SPELL_IDS; +}; + +class NoHealerDpsStrategyTrigger : public Trigger +{ +public: + NoHealerDpsStrategyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no healer dps strategy") {} + + bool IsActive() override + { + return !botAI->HasStrategy("healer dps", BOT_STATE_COMBAT); + } +}; + +class ProwlTrigger : public Trigger +{ +public: + ProwlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "prowl") {} + + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp new file mode 100644 index 000000000..64fe3c0c9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.cpp @@ -0,0 +1,205 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BalanceDruidStrategy.h" + +#include "AiObjectContext.h" +#include "FeralDruidStrategy.h" + +class BalanceDruidStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + BalanceDruidStrategyActionNodeFactory() + { + creators["faerie fire"] = &faerie_fire; + creators["hibernate"] = &hibernate; + creators["entangling roots"] = &entangling_roots; + creators["entangling roots on cc"] = &entangling_roots_on_cc; + creators["wrath"] = &wrath; + creators["starfall"] = &starfall; + creators["insect swarm"] = &insect_swarm; + creators["moonfire"] = &moonfire; + creators["starfire"] = &starfire; + creators["moonkin form"] = &moonkin_form; + creators["typhoon"] = &typhoon; + creators["hurricane"] = &hurricane; + creators["force of nature"] = &force_of_nature; + creators["cyclone on cc"] = &cyclone_on_cc; + } + +private: + static ActionNode* faerie_fire([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "faerie fire", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* hibernate([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "hibernate", + /*P*/ { NextAction("moonkin form") }, + /*A*/ { NextAction("entangling roots") }, + /*C*/ {} + ); + } + + static ActionNode* entangling_roots([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "entangling roots", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* entangling_roots_on_cc([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "entangling roots on cc", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* wrath([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "wrath", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* starfall([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "starfall", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* insect_swarm([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "insect swarm", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* moonfire([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "moonfire", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* starfire([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "starfire", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* moonkin_form([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "moonkin form", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* typhoon([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "typhoon", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* hurricane([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "hurricane", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* force_of_nature([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "force of nature", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } + + static ActionNode* cyclone_on_cc([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "cyclone on cc", + /*P*/ { NextAction("moonkin form") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + +BalanceDruidStrategy::BalanceDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) +{ + actionNodeFactories.Add(new BalanceDruidStrategyActionNodeFactory()); + actionNodeFactories.Add(new ShapeshiftDruidStrategyActionNodeFactory()); +} + +std::vector BalanceDruidStrategy::getDefaultActions() +{ + return { + NextAction("starfire", 5.4f), + NextAction("wrath", 5.3f), + }; +} + +void BalanceDruidStrategy::InitTriggers(std::vector& triggers) +{ + GenericDruidStrategy::InitTriggers(triggers); + + // Debuffs and DoTs + triggers.push_back(new TriggerNode("faerie fire", { NextAction("faerie fire", 29.5f) })); + triggers.push_back(new TriggerNode("insect swarm", { NextAction("insect swarm", 18.0f) })); + triggers.push_back(new TriggerNode("moonfire", { NextAction("moonfire", 17.5f) })); + + // Eclipse procs + triggers.push_back(new TriggerNode("eclipse (solar)", { NextAction("wrath", 20.0f) })); + triggers.push_back(new TriggerNode("eclipse (lunar)", { NextAction("starfire", 20.0f) })); + + // Utility/Defensive + triggers.push_back(new TriggerNode("medium mana", { NextAction("innervate", 29.0f) })); + triggers.push_back(new TriggerNode("enemy too close for spell", { NextAction("flee", 39.0f) })); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h new file mode 100644 index 000000000..1d65bc540 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BalanceDruidStrategy.h @@ -0,0 +1,26 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BALANCEDRUIDSTRATEGY_H +#define PLAYERBOTS_BALANCEDRUIDSTRATEGY_H + +#include "GenericDruidStrategy.h" + +class PlayerbotAI; + +class BalanceDruidStrategy : public GenericDruidStrategy +{ +public: + BalanceDruidStrategy(PlayerbotAI* botAI); + +public: + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "balance"; } + std::vector getDefaultActions() override; + uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_RANGED; } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp new file mode 100644 index 000000000..998517055 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.cpp @@ -0,0 +1,163 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BearDruidStrategy.h" + +#include "Playerbots.h" + +class BearDruidStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + BearDruidStrategyActionNodeFactory() + { + creators["feral charge - bear"] = &feral_charge_bear; + creators["dire bear form"] = &dire_bear_form; + creators["maul"] = &maul; + creators["bash"] = &bash; + creators["swipe (bear)"] = &swipe_bear; + creators["lacerate"] = &lacerate; + } + +private: + static ActionNode* feral_charge_bear([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "feral charge - bear", + /*P*/ {}, + /*A*/ { NextAction("reach melee") }, + /*C*/ {} + ); + } + + static ActionNode* dire_bear_form([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "dire bear form", + /*P*/ { NextAction("caster form") }, + /*A*/ { NextAction("bear form") }, + /*C*/ {} + ); + } + + static ActionNode* maul([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "maul", + /*P*/ {}, + /*A*/ { NextAction("melee") }, + /*C*/ {} + ); + } + + static ActionNode* bash([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "bash", + /*P*/ {}, + /*A*/ { NextAction("melee") }, + /*C*/ {} + ); + } + + static ActionNode* swipe_bear([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "swipe (bear)", + /*P*/ {}, + /*A*/ { NextAction("melee") }, + /*C*/ {} + ); + } + + static ActionNode* lacerate([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "lacerate", + /*P*/ {}, + /*A*/ { NextAction("maul") }, + /*C*/ {} + ); + } +}; + +BearDruidStrategy::BearDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) +{ + actionNodeFactories.Add(new BearDruidStrategyActionNodeFactory()); +} + +std::vector BearDruidStrategy::getDefaultActions() +{ + return { + NextAction("maul", 5.2f), + NextAction("enrage", 5.1f), + NextAction("melee", 5.0f) + }; +} + +void BearDruidStrategy::InitTriggers(std::vector& triggers) +{ + FeralDruidStrategy::InitTriggers(triggers); + + triggers.push_back( + new TriggerNode( + "bear form", + { NextAction("dire bear form", 28.0f) } + ) + ); + triggers.push_back( + new TriggerNode( + "medium health", + { NextAction("frenzied regeneration", 27.0f) } + ) + ); + triggers.push_back(new TriggerNode( + "mangle (bear)", { NextAction("mangle (bear)", 17.5f) } + )); + triggers.push_back(new TriggerNode( + "faerie fire (feral)", { NextAction("faerie fire (feral)", 17.0f) } + )); + triggers.push_back(new TriggerNode( + "lacerate", { NextAction("lacerate", 16.0f) } + )); + triggers.push_back(new TriggerNode( + "demoralizing roar", { NextAction("demoralizing roar", 15.5f) } + )); + triggers.push_back(new TriggerNode("high aoe", { NextAction("challenging roar", 26.5f) })); + triggers.push_back(new TriggerNode("lose aggro", + { + NextAction("growl", 26.0f), + NextAction("faerie fire (feral)", 25.5f) + } + )); + triggers.push_back(new TriggerNode("berserk active", { NextAction("mangle (bear)", 25.0f) })); + triggers.push_back( + new TriggerNode( + "medium aoe", + { + NextAction("demoralizing roar", 24.5f), + NextAction("swipe (bear)", 24.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "light aoe", + { NextAction("swipe (bear)", 24.0f) } + ) + ); + triggers.push_back( + new TriggerNode( + "bash", + { NextAction("bash", 42.0f) } + ) + ); + triggers.push_back( + new TriggerNode( + "bash on enemy healer", + { NextAction("bash on enemy healer", 41.0f) } + ) + ); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.h new file mode 100644 index 000000000..f590f2524 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearDruidStrategy.h @@ -0,0 +1,25 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BEARDRUIDSTRATEGY_H +#define PLAYERBOTS_BEARDRUIDSTRATEGY_H + +#include "FeralDruidStrategy.h" + +class PlayerbotAI; + +class BearDruidStrategy : public FeralDruidStrategy +{ +public: + BearDruidStrategy(PlayerbotAI* botAI); + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "bear"; } + std::vector getDefaultActions() override; + uint32 GetType() const override { return STRATEGY_TYPE_TANK | STRATEGY_TYPE_MELEE; } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp deleted file mode 100644 index 4a60fbd3e..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.cpp +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 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 "BearTankDruidStrategy.h" - -#include "Playerbots.h" - -class BearTankDruidStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - BearTankDruidStrategyActionNodeFactory() - { - creators["melee"] = &melee; - creators["feral charge - bear"] = &feral_charge_bear; - creators["swipe (bear)"] = &swipe_bear; - creators["faerie fire (feral)"] = &faerie_fire_feral; - creators["bear form"] = &bear_form; - creators["dire bear form"] = &dire_bear_form; - creators["mangle (bear)"] = &mangle_bear; - creators["maul"] = &maul; - creators["bash"] = &bash; - creators["swipe"] = &swipe; - creators["lacerate"] = &lacerate; - creators["demoralizing roar"] = &demoralizing_roar; - creators["taunt spell"] = &growl; - } - -private: - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ { NextAction("feral charge - bear") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* feral_charge_bear([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "feral charge - bear", - /*P*/ {}, - /*A*/ { NextAction("reach melee") }, - /*C*/ {} - ); - } - - static ActionNode* swipe_bear([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "swipe (bear)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire (feral)", - /*P*/ { NextAction("feral charge - bear") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* bear_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "bear form", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* dire_bear_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "dire bear form", - /*P*/ { NextAction("caster form") }, - /*A*/ { NextAction("bear form") }, - /*C*/ {} - ); - } - - static ActionNode* mangle_bear([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mangle (bear)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* maul([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "maul", - /*P*/ {}, - /*A*/ { NextAction("melee") }, - /*C*/ {} - ); - } - - static ActionNode* bash([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "bash", - /*P*/ {}, - /*A*/ { NextAction("melee") }, - /*C*/ {} - ); - } - - static ActionNode* swipe([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "swipe", - /*P*/ {}, - /*A*/ { NextAction("melee") }, - /*C*/ {} - ); - } - - static ActionNode* lacerate([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "lacerate", - /*P*/ {}, - /*A*/ { NextAction("maul") }, - /*C*/ {} - ); - } - - static ActionNode* growl([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "growl", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* demoralizing_roar([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "demoralizing roar", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } -}; - -BearTankDruidStrategy::BearTankDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) -{ - actionNodeFactories.Add(new BearTankDruidStrategyActionNodeFactory()); -} - -std::vector BearTankDruidStrategy::getDefaultActions() -{ - return { - NextAction("mangle (bear)", ACTION_DEFAULT + 0.5f), - NextAction("faerie fire (feral)", ACTION_DEFAULT + 0.4f), - NextAction("lacerate", ACTION_DEFAULT + 0.3f), - NextAction("maul", ACTION_DEFAULT + 0.2f), - NextAction("enrage", ACTION_DEFAULT + 0.1f), - NextAction("melee", ACTION_DEFAULT) - }; -} - -void BearTankDruidStrategy::InitTriggers(std::vector& triggers) -{ - FeralDruidStrategy::InitTriggers(triggers); - - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("feral charge - bear", ACTION_NORMAL + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "bear form", - { - NextAction("dire bear form", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "low health", - { - NextAction("frenzied regeneration", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "faerie fire (feral)", - { - NextAction("faerie fire (feral)", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lose aggro", - { - NextAction("growl", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("demoralizing roar", ACTION_HIGH + 6), - NextAction("swipe (bear)", ACTION_HIGH + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "light aoe", - { - NextAction("swipe (bear)", ACTION_HIGH + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "bash", - { - NextAction("bash", ACTION_INTERRUPT + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "bash on enemy healer", - { - NextAction("bash on enemy healer", ACTION_INTERRUPT + 1) - } - ) - ); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h deleted file mode 100644 index 2019bd0eb..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/BearTankDruidStrategy.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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_BEARTANKDRUIDSTRATEGY_H -#define _PLAYERBOT_BEARTANKDRUIDSTRATEGY_H - -#include "FeralDruidStrategy.h" - -class PlayerbotAI; - -class BearTankDruidStrategy : public FeralDruidStrategy -{ -public: - BearTankDruidStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bear"; } - std::vector getDefaultActions() override; - uint32 GetType() const override { return STRATEGY_TYPE_TANK | STRATEGY_TYPE_MELEE; } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp deleted file mode 100644 index a91f3b540..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.cpp +++ /dev/null @@ -1,254 +0,0 @@ -/* - * 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 "CasterDruidStrategy.h" - -#include "AiObjectContext.h" -#include "FeralDruidStrategy.h" - -class CasterDruidStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - CasterDruidStrategyActionNodeFactory() - { - creators["faerie fire"] = &faerie_fire; - creators["hibernate"] = &hibernate; - creators["entangling roots"] = &entangling_roots; - creators["entangling roots on cc"] = &entangling_roots_on_cc; - creators["wrath"] = &wrath; - creators["starfall"] = &starfall; - creators["insect swarm"] = &insect_swarm; - creators["moonfire"] = &moonfire; - creators["starfire"] = &starfire; - creators["moonkin form"] = &moonkin_form; - } - -private: - static ActionNode* faerie_fire([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* hibernate([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "hibernate", - /*P*/ { NextAction("moonkin form") }, - /*A*/ { NextAction("entangling roots") }, - /*C*/ {} - ); - } - - static ActionNode* entangling_roots([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "entangling roots", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* entangling_roots_on_cc([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "entangling roots on cc", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* wrath([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "wrath", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* starfall([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "starfall", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* insect_swarm([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "insect swarm", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* moonfire([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "moonfire", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* starfire([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "starfire", - /*P*/ { NextAction("moonkin form") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* moonkin_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "moonkin form", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ {} - ); - } -}; - -CasterDruidStrategy::CasterDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) -{ - actionNodeFactories.Add(new CasterDruidStrategyActionNodeFactory()); - actionNodeFactories.Add(new ShapeshiftDruidStrategyActionNodeFactory()); -} - -std::vector CasterDruidStrategy::getDefaultActions() -{ - return { - NextAction("starfall", ACTION_HIGH + 1.0f), - NextAction("force of nature", ACTION_DEFAULT + 1.0f), - NextAction("wrath", ACTION_DEFAULT + 0.1f), - }; -} - -void CasterDruidStrategy::InitTriggers(std::vector& triggers) -{ - GenericDruidStrategy::InitTriggers(triggers); - - triggers.push_back( - new TriggerNode( - "eclipse (lunar) cooldown", - { - NextAction("starfire", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "eclipse (solar) cooldown", - { - NextAction("wrath", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "insect swarm", - { - NextAction("insect swarm", ACTION_NORMAL + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "moonfire", - { - NextAction("moonfire", ACTION_NORMAL + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "eclipse (solar)", - { - NextAction("wrath", ACTION_NORMAL + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "eclipse (lunar)", - { - NextAction("starfire", ACTION_NORMAL + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium mana", - { - NextAction("innervate", ACTION_HIGH + 9) - } - ) - ); - triggers.push_back( - new TriggerNode( - "enemy too close for spell", - { - NextAction("flee", ACTION_MOVE + 9) - } - ) - ); -} - -void CasterDruidAoeStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode( - "hurricane channel check", - { - NextAction("cancel channel", ACTION_HIGH + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("hurricane", ACTION_HIGH + 1) - } - ) - ); - triggers.push_back( - new TriggerNode( - "light aoe", - { - NextAction("insect swarm on attacker", ACTION_NORMAL + 3), - NextAction("moonfire on attacker", ACTION_NORMAL + 3) - } - ) - ); -} - -void CasterDruidDebuffStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode( - "faerie fire", - { - NextAction("faerie fire", ACTION_HIGH) - } - ) - ); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h deleted file mode 100644 index 45bc78dba..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CasterDruidStrategy.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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_CASTERDRUIDSTRATEGY_H -#define _PLAYERBOT_CASTERDRUIDSTRATEGY_H - -#include "GenericDruidStrategy.h" - -class PlayerbotAI; - -class CasterDruidStrategy : public GenericDruidStrategy -{ -public: - CasterDruidStrategy(PlayerbotAI* botAI); - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "caster"; } - std::vector getDefaultActions() override; - uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_RANGED; } -}; - -class CasterDruidAoeStrategy : public CombatStrategy -{ -public: - CasterDruidAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "caster aoe"; } -}; - -class CasterDruidDebuffStrategy : public CombatStrategy -{ -public: - CasterDruidDebuffStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "caster debuff"; } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp deleted file mode 100644 index fda1b5f94..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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 "CatDpsDruidStrategy.h" - -#include "AiObjectContext.h" - -class CatDpsDruidStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - CatDpsDruidStrategyActionNodeFactory() - { - creators["faerie fire (feral)"] = &faerie_fire_feral; - creators["melee"] = &melee; - creators["feral charge - cat"] = &feral_charge_cat; - creators["cat form"] = &cat_form; - creators["claw"] = &claw; - creators["mangle (cat)"] = &mangle_cat; - creators["rake"] = &rake; - creators["ferocious bite"] = &ferocious_bite; - creators["rip"] = &rip; - creators["pounce"] = &pounce; - creators["ravage"] = &ravage; - } - -private: - static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire (feral)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ { NextAction("feral charge - cat") }, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* feral_charge_cat([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "feral charge - cat", - /*P*/ {}, - /*A*/ { NextAction("reach melee") }, - /*C*/ {} - ); - } - - static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "cat form", - /*P*/ { NextAction("caster form") }, - /*A*/ { NextAction("bear form") }, - /*C*/ {} - ); - } - - static ActionNode* claw([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "claw", - /*P*/ {}, - /*A*/ { NextAction("melee") }, - /*C*/ {} - ); - } - - static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mangle (cat)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rake", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "ferocious bite", - /*P*/ {}, - /*A*/ { NextAction("rip") }, - /*C*/ {} - ); - } - - static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rip", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* pounce([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "pounce", - /*P*/ {}, - /*A*/ { NextAction("ravage") }, - /*C*/ {} - ); - } - - static ActionNode* ravage([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "ravage", - /*P*/ {}, - /*A*/ { NextAction("shred") }, - /*C*/ {} - ); - } -}; - -CatDpsDruidStrategy::CatDpsDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) -{ - actionNodeFactories.Add(new CatDpsDruidStrategyActionNodeFactory()); -} - -std::vector CatDpsDruidStrategy::getDefaultActions() -{ - return { - NextAction("tiger's fury", ACTION_DEFAULT + 0.1f) - }; -} - -void CatDpsDruidStrategy::InitTriggers(std::vector& triggers) -{ - FeralDruidStrategy::InitTriggers(triggers); - - // Default priority - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("mangle (cat)", ACTION_DEFAULT + 0.3f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full and high energy", - { - NextAction("mangle (cat)", ACTION_DEFAULT + 0.3f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("claw", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full and high energy", - { - NextAction("claw", ACTION_DEFAULT + 0.2f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "faerie fire (feral)", - { - NextAction("faerie fire (feral)", ACTION_DEFAULT + 0.0f) - } - ) - ); - - // Main spell - triggers.push_back( - new TriggerNode( - "cat form", { - NextAction("cat form", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "savage roar", { - NextAction("savage roar", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points available", - { - NextAction("rip", ACTION_HIGH + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "ferocious bite time", - { - NextAction("ferocious bite", ACTION_HIGH + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "target with combo points almost dead", - { - NextAction("ferocious bite", ACTION_HIGH + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "mangle (cat)", - { - NextAction("mangle (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "rake", - { - NextAction("rake", ACTION_HIGH + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium threat", - { - NextAction("cower", ACTION_HIGH + 1) - } - ) - ); - - // AOE - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("swipe (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "light aoe", - { - NextAction("rake on attacker", ACTION_HIGH + 2) - } - ) - ); - // Reach target - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("feral charge - cat", ACTION_HIGH + 9) - } - ) - ); - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("dash", ACTION_HIGH + 8) - } - ) - ); -} - -void CatAoeDruidStrategy::InitTriggers(std::vector& triggers) {} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h deleted file mode 100644 index 312e94d0f..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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_CATDPSDRUIDSTRATEGY_H -#define _PLAYERBOT_CATDPSDRUIDSTRATEGY_H - -#include "FeralDruidStrategy.h" - -class PlayerbotAI; - -class CatDpsDruidStrategy : public FeralDruidStrategy -{ -public: - CatDpsDruidStrategy(PlayerbotAI* botAI); - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "cat"; } - std::vector getDefaultActions() override; - uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; } -}; - -class CatAoeDruidStrategy : public CombatStrategy -{ -public: - CatAoeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -public: - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "cat aoe"; } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp new file mode 100644 index 000000000..8ea082492 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.cpp @@ -0,0 +1,380 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "CatDruidStrategy.h" + +#include "AiObjectContext.h" + +class CatDruidStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + CatDruidStrategyActionNodeFactory() + { + creators["feral charge - cat"] = &feral_charge_cat; + creators["cat form"] = &cat_form; + creators["claw"] = &claw; + creators["pounce"] = &pounce; + creators["ravage"] = &ravage; + creators["prowl"] = &prowl; + } + +private: + static ActionNode* feral_charge_cat([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "feral charge - cat", + /*P*/ {}, + /*A*/ { NextAction("reach melee") }, + /*C*/ {} + ); + } + + static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "cat form", + /*P*/ { NextAction("caster form") }, + /*A*/ { NextAction("bear form") }, + /*C*/ {} + ); + } + + static ActionNode* claw([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "claw", + /*P*/ {}, + /*A*/ { NextAction("melee") }, + /*C*/ {} + ); + } + + static ActionNode* ravage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "ravage", + /*P*/ {}, + /*A*/ { NextAction("pounce") }, + /*C*/ {} + ); + } + + static ActionNode* pounce([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "pounce", + /*P*/ {}, + /*A*/ { NextAction("shred") }, + /*C*/ {} + ); + } + + static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "prowl", + /*P*/ { NextAction("cat form") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + +CatDruidStrategy::CatDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) +{ + actionNodeFactories.Add(new CatDruidStrategyActionNodeFactory()); +} + +std::vector CatDruidStrategy::getDefaultActions() +{ + return { + NextAction("melee", ACTION_DEFAULT) + }; +} + +void CatDruidStrategy::InitTriggers(std::vector& triggers) +{ + FeralDruidStrategy::InitTriggers(triggers); + + triggers.push_back( + new TriggerNode( + "healer low mana", { + NextAction("innervate on healer", 35.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "prowl", { + NextAction("prowl", 29.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "enemy out of melee", { + NextAction("dash", 28.0f) + } + ) + ); + + triggers.push_back( + new TriggerNode( + "cat form", { + NextAction("cat form", 28.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "low energy", { + NextAction("tiger's fury", 27.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "savage roar", { + NextAction("savage roar", 26.0f) + } + ) + ); + + triggers.push_back( + new TriggerNode( + "combo points 5 available", { + NextAction("rip", 23.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points 5 available", { + NextAction("maim", 23.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "ferocious bite execute", { + NextAction("ferocious bite", 24.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "clearcasting", { + NextAction("shred", 24.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "ferocious bite time", { + NextAction("ferocious bite", 22.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "mangle (cat)", { + NextAction("mangle (cat)", 22.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "rake", { + NextAction("rake", 21.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "medium threat", { + NextAction("cower", 21.0f) + } + ) + ); + + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("ravage", 5.6f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full", { + NextAction("ravage", 5.6f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("pounce", 5.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full", { + NextAction("pounce", 5.5f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("shred", 5.4f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full", { + NextAction("shred", 5.4f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("mangle (cat)", 5.3f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full and high energy", { + NextAction("mangle (cat)", 5.3f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "almost full energy available", { + NextAction("claw", 5.2f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "combo points not full and high energy", { + NextAction("claw", 5.2f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "faerie fire (feral)", { + NextAction("faerie fire (feral)", 5.0f) + } + ) + ); +} + +// ============================================================ +// CatOffhealStrategy +// Additive overlay — only the healing triggers. Designed to be +// stacked on top of "cat" so the bot stays in cat form for DPS +// but shifts out to heal when the party needs it. +// ============================================================ + +class CatOffhealStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + CatOffhealStrategyActionNodeFactory() + { + creators["healing touch on party"] = &healing_touch_on_party; + creators["regrowth on party"] = ®rowth_on_party; + creators["rejuvenation on party"] = &rejuvenation_on_party; + } + +private: + // P: shift to caster form before casting C: shift back to cat form afterwards + static ActionNode* healing_touch_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "healing touch on party", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ { NextAction("cat form") } + ); + } + + static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "regrowth on party", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ { NextAction("cat form") } + ); + } + + static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "rejuvenation on party", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ { NextAction("cat form") } + ); + } +}; + +CatOffhealStrategy::CatOffhealStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) +{ + actionNodeFactories.Add(new CatOffhealStrategyActionNodeFactory()); +} + +void CatOffhealStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode( + "party member critical health", + { + NextAction("regrowth on party", 36.0f), + NextAction("healing touch on party", 35.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "party member low health", + { + NextAction("healing touch on party", 25.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "party member medium health", + { + NextAction("rejuvenation on party", 18.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "party member to heal out of spell range", + { + NextAction("reach party member to heal", 93.0f) + } + ) + ); + triggers.push_back( + new TriggerNode( + "low mana", + { + NextAction("innervate", 24.0f) + } + ) + ); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.h new file mode 100644 index 000000000..5841ef37f --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/CatDruidStrategy.h @@ -0,0 +1,38 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_CATDRUIDSTRATEGY_H +#define PLAYERBOTS_CATDRUIDSTRATEGY_H + +#include "FeralDruidStrategy.h" + +class PlayerbotAI; + +class CatDruidStrategy : public FeralDruidStrategy +{ +public: + CatDruidStrategy(PlayerbotAI* botAI); + +public: + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "cat"; } + std::vector getDefaultActions() override; + uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; } +}; + +// Optional additive strategy. Layers emergency heals on top of the "cat" strategy. +// Enable : co +offheal +// Disable: co -offheal +class CatOffhealStrategy : public CombatStrategy +{ +public: + CatOffhealStrategy(PlayerbotAI* botAI); + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "offheal"; } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp new file mode 100644 index 000000000..6e88815c2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.cpp @@ -0,0 +1,36 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DruidPullStrategy.h" + +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string DruidPullStrategy::GetPullActionName() const +{ + std::string const pullActionName = PullStrategy::GetPullActionName(); + std::string const actionName = + botAI->HasSpell("faerie fire (feral)") && + (botAI->HasStrategy("bear", BOT_STATE_COMBAT) || botAI->HasStrategy("cat", BOT_STATE_COMBAT)) + ? "faerie fire (feral)" : pullActionName; + + Unit* target = GetTarget(); + if (!target) + return actionName; + + if (!botAI->CanCastSpell(actionName, target) && botAI->CanCastSpell("growl", target)) + return "growl"; + + return actionName; +} + +std::string DruidPullStrategy::GetPreActionName() const +{ + if (GetPullActionName() == "faerie fire") + return ""; + + return PullStrategy::GetPreActionName(); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h new file mode 100644 index 000000000..b00a9a88b --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/DruidPullStrategy.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DRUIDPULLSTRATEGY_H +#define PLAYERBOTS_DRUIDPULLSTRATEGY_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/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp index 894c05bff..68dbf5630 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FeralDruidStrategy.h" @@ -14,12 +15,10 @@ public: { creators["survival instincts"] = &survival_instincts; creators["thorns"] = þs; - creators["omen of clarity"] = &omen_of_clarity; creators["cure poison"] = &cure_poison; creators["cure poison on party"] = &cure_poison_on_party; creators["abolish poison"] = &abolish_poison; creators["abolish poison on party"] = &abolish_poison_on_party; - creators["prowl"] = &prowl; } private: @@ -39,14 +38,6 @@ private: /*C*/ {}); } - static ActionNode* omen_of_clarity([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("omen of clarity", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* cure_poison([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("cure poison", @@ -79,13 +70,6 @@ private: /*C*/ {}); } - static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("prowl", - /*P*/ { NextAction("cat form") }, - /*A*/ {}, - /*C*/ {}); - } }; FeralDruidStrategy::FeralDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) @@ -99,15 +83,23 @@ void FeralDruidStrategy::InitTriggers(std::vector& triggers) GenericDruidStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode( - "enemy out of melee", { NextAction("reach melee", ACTION_HIGH + 1) })); + "enemy out of melee", { NextAction("reach melee", 21.0f) })); triggers.push_back(new TriggerNode( - "critical health", { NextAction("survival instincts", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode( - "omen of clarity", { NextAction("omen of clarity", ACTION_HIGH + 9) })); + "low health", { NextAction("survival instincts", 91.0f) })); triggers.push_back(new TriggerNode("player has flag", - { NextAction("dash", ACTION_EMERGENCY + 2) })); + { NextAction("dash", 92.0f) })); triggers.push_back(new TriggerNode("enemy flagcarrier near", - { NextAction("dash", ACTION_EMERGENCY + 2) })); - triggers.push_back( - new TriggerNode("berserk", { NextAction("berserk", ACTION_HIGH + 6) })); + { NextAction("dash", 92.0f) })); +} + +void FeralChargeDruidStrategy::InitTriggers(std::vector& triggers) +{ + Player* bot = botAI->GetBot(); + + if (bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE)) + triggers.push_back(new TriggerNode( + "enemy out of melee", { NextAction("feral charge - cat", 29.0f) })); + else + triggers.push_back(new TriggerNode( + "enemy out of melee", { NextAction("feral charge - bear", 18.0f) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h index abf01c694..283f2ebd2 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/FeralDruidStrategy.h @@ -1,13 +1,17 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FERALRUIDSTRATEGY_H -#define _PLAYERBOT_FERALRUIDSTRATEGY_H +#ifndef PLAYERBOTS_FERALDRUIDSTRATEGY_H +#define PLAYERBOTS_FERALDRUIDSTRATEGY_H #include "GenericDruidStrategy.h" +constexpr uint32 SPELL_CAT_FORM = 768; +constexpr uint32 AURA_THICK_HIDE = 16931; + class PlayerbotAI; class ShapeshiftDruidStrategyActionNodeFactory : public NamedObjectFactory @@ -83,4 +87,18 @@ public: uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; } }; +// Optional strategy — enabled by default for cat and bear. +// Registers the "enemy out of melee" → Feral Charge trigger, spec-gated at +// init time so cats get Feral Charge (Cat) and bears get Feral Charge (Bear). +// Disable with: co -feral charge +// Re-enable with: co +feral charge +class FeralChargeDruidStrategy : public CombatStrategy +{ +public: + FeralChargeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "feral charge"; } +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp index 7d1a5f7ca..ea6b24b09 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericDruidNonCombatStrategy.h" @@ -17,12 +18,12 @@ public: creators["thorns on party"] = þs_on_party; creators["mark of the wild"] = &mark_of_the_wild; creators["mark of the wild on party"] = &mark_of_the_wild_on_party; - // creators["innervate"] = &innervate; creators["regrowth_on_party"] = ®rowth_on_party; creators["rejuvenation on party"] = &rejuvenation_on_party; creators["remove curse on party"] = &remove_curse_on_party; creators["abolish poison on party"] = &abolish_poison_on_party; creators["revive"] = &revive; + creators["aquatic form"] = &aquatic_form; } private: @@ -92,6 +93,14 @@ private: /*A*/ {}, /*C*/ {}); } + + static ActionNode* aquatic_form([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("aquatic form", + /*P*/ { NextAction("caster form") }, + /*A*/ {}, + /*C*/ {}); + } }; GenericDruidNonCombatStrategy::GenericDruidNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) @@ -165,10 +174,12 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector& trig NextAction("remove curse on party", ACTION_DISPEL + 7), })); + triggers.push_back(new TriggerNode("aquatic form", { NextAction("aquatic form", 10.0f) })); + int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot()); - if (specTab == 0 || specTab == 2) // Balance or Restoration + if (specTab == DRUID_TAB_BALANCE || specTab == DRUID_TAB_RESTORATION) triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); - if (specTab == 1) // Feral + if (specTab == DRUID_TAB_FERAL) triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.h index 28e154454..0fb287b9e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICDRUIDNONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICDRUIDNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICDRUIDNONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICDRUIDNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp index d5ac8fcea..d8248cd80 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.cpp @@ -1,10 +1,13 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericDruidStrategy.h" +#include "AiFactory.h" +#include "FeralDruidStrategy.h" #include "Playerbots.h" class GenericDruidStrategyActionNodeFactory : public NamedObjectFactory @@ -12,74 +15,12 @@ class GenericDruidStrategyActionNodeFactory : public NamedObjectFactory& triggers) CombatStrategy::InitTriggers(triggers); triggers.push_back( - new TriggerNode("low health", { NextAction("barkskin", ACTION_HIGH + 7) })); + new TriggerNode("almost full health", { NextAction("barkskin", 40.0f) })); + + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_FERAL) + { + if (!bot->HasAura(16931) /*thick hide — bear spec*/) + { + triggers.push_back(new TriggerNode("predator's swiftness and combat party member dead", + { NextAction("rebirth", 29.0f) })); + triggers.push_back(new TriggerNode("combat party member dead", + { NextAction("rebirth", 28.5f) })); + } + } + else + { + triggers.push_back(new TriggerNode("combat party member dead", + { NextAction("rebirth", 29.0f) })); + } - triggers.push_back(new TriggerNode("combat party member dead", - { NextAction("rebirth", ACTION_HIGH + 9) })); triggers.push_back(new TriggerNode("being attacked", - { NextAction("nature's grasp", ACTION_HIGH + 1) })); - triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + { NextAction("nature's grasp", 39.0f) })); } void DruidCureStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode("party member cure poison", - { NextAction("abolish poison on party", ACTION_DISPEL + 1) })); + { NextAction("abolish poison on party", 51.0f) })); triggers.push_back( new TriggerNode("party member remove curse", - { NextAction("remove curse on party", ACTION_DISPEL + 7) })); + { NextAction("remove curse on party", 57.0f) })); } void DruidBoostStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "nature's swiftness", { NextAction("nature's swiftness", ACTION_HIGH + 9) })); + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_BALANCE) + { + triggers.push_back(new TriggerNode("force of nature", { NextAction("force of nature", 29.0f) })); + triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + } + + if (tab == DRUID_TAB_FERAL) + { + triggers.push_back(new TriggerNode("berserk", { NextAction("berserk", 27.5f) })); + } } void DruidCcStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode( - "entangling roots", { NextAction("entangling roots on cc", ACTION_HIGH + 2) })); - triggers.push_back(new TriggerNode( - "entangling roots kite", { NextAction("entangling roots", ACTION_HIGH + 2) })); - triggers.push_back(new TriggerNode( - "hibernate", { NextAction("hibernate on cc", ACTION_HIGH + 3) })); + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_BALANCE || tab == DRUID_TAB_RESTORATION) + { + triggers.push_back(new TriggerNode( + "cyclone", { NextAction("cyclone on cc", 42.0f) })); + triggers.push_back(new TriggerNode( + "hibernate", { NextAction("hibernate on cc", 41.0f) })); + triggers.push_back(new TriggerNode( + "entangling roots", { NextAction("entangling roots on cc", 40.0f) })); + } + if (tab == DRUID_TAB_FERAL) + { + if (bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE)) + { + triggers.push_back(new TriggerNode( + "predator's swiftness and cyclone", { NextAction("cyclone on cc", 42.0f) })); + triggers.push_back(new TriggerNode( + "predator's swiftness and hibernate", { NextAction("hibernate on cc", 41.0f) })); + triggers.push_back(new TriggerNode( + "predator's swiftness and entangling roots", { NextAction("entangling roots on cc", 40.0f) })); + } + else + { + triggers.push_back(new TriggerNode( + "cyclone", { NextAction("cyclone on cc", 42.0f) })); + triggers.push_back(new TriggerNode( + "hibernate", { NextAction("hibernate on cc", 41.0f) })); + triggers.push_back(new TriggerNode( + "entangling roots", { NextAction("entangling roots on cc", 40.0f) })); + } + } } void DruidHealerDpsStrategy::InitTriggers(std::vector& triggers) @@ -149,10 +152,39 @@ void DruidHealerDpsStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("healer should attack", { - NextAction("cancel tree form", ACTION_DEFAULT + 0.3f), - NextAction("moonfire", ACTION_DEFAULT + 0.2f), - NextAction("wrath", ACTION_DEFAULT + 0.1f), - NextAction("starfire", ACTION_DEFAULT), -})); - + NextAction("cancel tree form", 5.4f), + NextAction("moonfire", 5.3f), + NextAction("wrath", 5.2f), + NextAction("starfire", 5.1f), + })); +} + +void DruidAoeStrategy::InitTriggers(std::vector& triggers) +{ + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + + if (tab == DRUID_TAB_BALANCE) + { + triggers.push_back(new TriggerNode("hurricane channel check", { NextAction("cancel channel", 22.0f) })); + triggers.push_back(new TriggerNode("starfall", { NextAction("starfall", 28.5f) })); + triggers.push_back(new TriggerNode("medium aoe", { NextAction("hurricane", 23.0f) })); + triggers.push_back(new TriggerNode("enemy within melee", { NextAction("typhoon", 40.0f) })); + triggers.push_back(new TriggerNode("insect swarm on attacker", { NextAction("insect swarm on attacker", 5.2f) })); + triggers.push_back(new TriggerNode("moonfire on attacker", { NextAction("moonfire on attacker", 5.1f) })); + } + + if (tab == DRUID_TAB_RESTORATION) + { + triggers.push_back(new TriggerNode("hurricane channel check", { NextAction("cancel channel", 22.0f) })); + triggers.push_back(new TriggerNode("medium aoe", { NextAction("hurricane", 23.0f) })); + triggers.push_back(new TriggerNode("insect swarm on attacker", { NextAction("insect swarm on attacker", 5.2f) })); + triggers.push_back(new TriggerNode("moonfire on attacker", { NextAction("moonfire on attacker", 5.1f) })); + } + + if (tab == DRUID_TAB_FERAL && bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE)) + { + triggers.push_back(new TriggerNode("clearcasting and medium aoe", { NextAction("swipe (cat)", 25.5f) })); + triggers.push_back(new TriggerNode("medium aoe", { NextAction("swipe (cat)", 25.0f) })); + } } diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h index 2f1c2e787..7a8bcc24b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/GenericDruidStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICDRUIDSTRATEGY_H -#define _PLAYERBOT_GENERICDRUIDSTRATEGY_H +#ifndef PLAYERBOTS_GENERICDRUIDSTRATEGY_H +#define PLAYERBOTS_GENERICDRUIDSTRATEGY_H #include "CombatStrategy.h" @@ -55,4 +56,13 @@ public: std::string const getName() override { return "healer dps"; } }; +class DruidAoeStrategy : public Strategy +{ +public: + DruidAoeStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "aoe"; } +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp deleted file mode 100644 index 5d2c4ce34..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 "HealDruidStrategy.h" - -#include "Playerbots.h" - -class HealDruidStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - HealDruidStrategyActionNodeFactory() { - creators["nourish on party"] = &nourtish_on_party; - } - -private: - static ActionNode* nourtish_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("nourish on party", - /*P*/ {}, - /*A*/ { NextAction("healing touch on party") }, - /*C*/ {}); - } -}; - -HealDruidStrategy::HealDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) -{ - actionNodeFactories.Add(new HealDruidStrategyActionNodeFactory()); -} - -void HealDruidStrategy::InitTriggers(std::vector& triggers) -{ - GenericDruidStrategy::InitTriggers(triggers); - - triggers.push_back(new TriggerNode( - "party member to heal out of spell range", - { NextAction("reach party member to heal", ACTION_CRITICAL_HEAL + 9) })); - - // CRITICAL - triggers.push_back( - new TriggerNode("party member critical health", - { - NextAction("tree form", ACTION_CRITICAL_HEAL + 4.1f), - NextAction("swiftmend on party", ACTION_CRITICAL_HEAL + 4), - NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 3), - NextAction("wild growth on party", ACTION_CRITICAL_HEAL + 2), - NextAction("nourish on party", ACTION_CRITICAL_HEAL + 1), - })); - - triggers.push_back( - new TriggerNode("party member critical health", - { NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4) })); - - triggers.push_back(new TriggerNode( - "group heal setting", - { - NextAction("tree form", ACTION_MEDIUM_HEAL + 2.3f), - NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 2.2f), - NextAction("rejuvenation on not full", ACTION_MEDIUM_HEAL + 2.1f), - })); - - triggers.push_back( - new TriggerNode("medium group heal setting", - { - NextAction("tree form", ACTION_CRITICAL_HEAL + 0.6f), - NextAction("tranquility", ACTION_CRITICAL_HEAL + 0.5f) })); - - // LOW - triggers.push_back( - new TriggerNode("party member low health", - { NextAction("tree form", ACTION_MEDIUM_HEAL + 1.5f), - NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 1.4f), - NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 1.3f), - NextAction("swiftmend on party", ACTION_MEDIUM_HEAL + 1.2), - NextAction("nourish on party", ACTION_MEDIUM_HEAL + 1.1f), - })); - - // MEDIUM - triggers.push_back( - new TriggerNode("party member medium health", - { - NextAction("tree form", ACTION_MEDIUM_HEAL + 0.5f), - NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 0.4f), - NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 0.3f), - NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 0.2f), - NextAction("nourish on party", ACTION_MEDIUM_HEAL + 0.1f) })); - - // almost full - triggers.push_back( - new TriggerNode("party member almost full health", - { NextAction("wild growth on party", ACTION_LIGHT_HEAL + 0.3f), - NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 0.2f), - NextAction("regrowth on party", ACTION_LIGHT_HEAL + 0.1f) })); - - triggers.push_back( - new TriggerNode("medium mana", { NextAction("innervate", ACTION_HIGH + 5) })); - - triggers.push_back(new TriggerNode("enemy too close for spell", - { NextAction("flee", ACTION_MOVE + 9) })); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h deleted file mode 100644 index 36ce40271..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/HealDruidStrategy.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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_HEALDRUIDSTRATEGY_H -#define _PLAYERBOT_HEALDRUIDSTRATEGY_H - -#include "GenericDruidStrategy.h" -#include "Strategy.h" - -class PlayerbotAI; - -class HealDruidStrategy : public GenericDruidStrategy -{ -public: - HealDruidStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "heal"; } - uint32 GetType() const override { return STRATEGY_TYPE_RANGED | STRATEGY_TYPE_HEAL; } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp deleted file mode 100644 index 5dc0f85d9..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 "MeleeDruidStrategy.h" - -#include "Playerbots.h" - -MeleeDruidStrategy::MeleeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {} - -std::vector MeleeDruidStrategy::getDefaultActions() -{ - return { - NextAction("faerie fire", ACTION_DEFAULT + 0.1f), - NextAction("melee", ACTION_DEFAULT) - }; -} - -void MeleeDruidStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode( - "omen of clarity", - { - NextAction("omen of clarity", ACTION_HIGH + 9) - } - ) - ); - - CombatStrategy::InitTriggers(triggers); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h deleted file mode 100644 index 67bbbbfed..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/MeleeDruidStrategy.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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_MELEEDRUIDSTRATEGY_H -#define _PLAYERBOT_MELEEDRUIDSTRATEGY_H - -#include "CombatStrategy.h" - -class MeleeDruidStrategy : public CombatStrategy -{ -public: - MeleeDruidStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "melee"; } - std::vector getDefaultActions() override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp deleted file mode 100644 index c472ce8d8..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp +++ /dev/null @@ -1,307 +0,0 @@ -/* - * 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 "OffhealDruidCatStrategy.h" - - #include "Playerbots.h" - #include "Strategy.h" - - class OffhealDruidCatStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - OffhealDruidCatStrategyActionNodeFactory() - { - creators["cat form"] = &cat_form; - creators["mangle (cat)"] = &mangle_cat; - creators["shred"] = &shred; - creators["rake"] = &rake; - creators["rip"] = &rip; - creators["ferocious bite"] = &ferocious_bite; - creators["savage roar"] = &savage_roar; - creators["faerie fire (feral)"] = &faerie_fire_feral; - creators["healing touch on party"] = &healing_touch_on_party; - creators["regrowth on party"] = ®rowth_on_party; - creators["rejuvenation on party"] = &rejuvenation_on_party; - } - -private: - static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "cat form", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mangle (cat)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* shred([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "shred", - /*P*/ {}, - /*A*/ { NextAction("claw") }, - /*C*/ {} - ); - } - - static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rake", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rip", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "ferocious bite", - /*P*/ {}, - /*A*/ { NextAction("rip") }, - /*C*/ {} - ); - } - - static ActionNode* savage_roar([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "savage roar", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "faerie fire (feral)", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* healing_touch_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "healing touch on party", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ { NextAction("cat form") } - ); - } - - static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "regrowth on party", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ { NextAction("cat form") } - ); - } - - static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "rejuvenation on party", - /*P*/ { NextAction("caster form") }, - /*A*/ {}, - /*C*/ { NextAction("cat form") } - ); - } -}; - -OffhealDruidCatStrategy::OffhealDruidCatStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI) -{ - actionNodeFactories.Add(new OffhealDruidCatStrategyActionNodeFactory()); -} - -std::vector OffhealDruidCatStrategy::getDefaultActions() -{ - return { - NextAction("mangle (cat)", ACTION_DEFAULT + 0.5f), - NextAction("shred", ACTION_DEFAULT + 0.4f), - NextAction("rake", ACTION_DEFAULT + 0.3f), - NextAction("melee", ACTION_DEFAULT), - NextAction("cat form", ACTION_DEFAULT - 0.1f) - }; -} - -void OffhealDruidCatStrategy::InitTriggers(std::vector& triggers) -{ - FeralDruidStrategy::InitTriggers(triggers); - - triggers.push_back( - new TriggerNode( - "cat form", - { - NextAction("cat form", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "savage roar", - { - NextAction("savage roar", ACTION_HIGH + 7) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points available", - { - NextAction("rip", ACTION_HIGH + 6) - } - ) - ); - triggers.push_back( - new TriggerNode( - "ferocious bite time", - { - NextAction("ferocious bite", ACTION_HIGH + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "target with combo points almost dead", - { - NextAction("ferocious bite", ACTION_HIGH + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "mangle (cat)", - { - NextAction("mangle (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "rake", - { - NextAction("rake", ACTION_HIGH + 2) - } - ) - ); - triggers.push_back( - new TriggerNode( - "almost full energy available", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "combo points not full", - { - NextAction("shred", ACTION_DEFAULT + 0.4f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "faerie fire (feral)", - { - NextAction("faerie fire (feral)", ACTION_NORMAL) - } - ) - ); - triggers.push_back( - new TriggerNode( - "enemy out of melee", - { - NextAction("feral charge - cat", ACTION_HIGH + 9), - NextAction("dash", ACTION_HIGH + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "medium aoe", - { - NextAction("swipe (cat)", ACTION_HIGH + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "low energy", - { - NextAction("tiger's fury", ACTION_NORMAL + 1) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member critical health", - { - NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 6), - NextAction("healing touch on party", ACTION_CRITICAL_HEAL + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member low health", - { - NextAction("healing touch on party", ACTION_MEDIUM_HEAL + 5) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member medium health", - { - NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 8) - } - ) - ); - triggers.push_back( - new TriggerNode( - "party member to heal out of spell range", - { - NextAction("reach party member to heal", ACTION_EMERGENCY + 3) - } - ) - ); - triggers.push_back( - new TriggerNode( - "low mana", - { - NextAction("innervate", ACTION_HIGH + 4) - } - ) - ); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h deleted file mode 100644 index 83775ef98..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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_OFFHEALDRUIDCATSTRATEGY_H - #define _PLAYERBOT_OFFHEALDRUIDCATSTRATEGY_H - - #include "FeralDruidStrategy.h" - - class PlayerbotAI; - - class OffhealDruidCatStrategy : public FeralDruidStrategy - { - public: - OffhealDruidCatStrategy(PlayerbotAI* botAI); - - void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "offheal"; } - std::vector getDefaultActions() override; - uint32 GetType() const override - { - return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_HEAL | STRATEGY_TYPE_MELEE; - } - }; - - #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp new file mode 100644 index 000000000..e7bf7e3b7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.cpp @@ -0,0 +1,104 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "RestoDruidStrategy.h" + +#include "Playerbots.h" + +RestoDruidStrategy::RestoDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI) +{ + // No custom ActionNodeFactory needed +} + +void RestoDruidStrategy::InitTriggers(std::vector& triggers) +{ + GenericDruidStrategy::InitTriggers(triggers); + + triggers.push_back(new TriggerNode("no healer dps strategy", + { NextAction("tree form", 5.0f) })); + + triggers.push_back(new TriggerNode( + "party member to heal out of spell range", + { NextAction("reach party member to heal", 39.0f) })); + + triggers.push_back( + new TriggerNode("party member critical health", + { + NextAction("tree form", 34.1f), + NextAction("swiftmend on party", 34.0f), + NextAction("wild growth on party", 33.0f), + NextAction("nourish on party", 32.0f), + NextAction("regrowth on party", 31.0f), + NextAction("healing touch on party", 30.0f), + })); + + triggers.push_back( + new TriggerNode("party member critical health", + { NextAction("nature's swiftness", 58.0f) })); + + triggers.push_back(new TriggerNode( + "nature's swiftness active", + { NextAction("healing touch on party", 55.0f) })); + + triggers.push_back(new TriggerNode("clearcasting", + { NextAction("lifebloom on main tank", 13.0f) })); + + // LOW + triggers.push_back( + new TriggerNode("party member low health", + { + NextAction("tree form", 21.5f), + NextAction("swiftmend on party", 21.4f), + NextAction("wild growth on party", 21.3f), + NextAction("nourish on party", 21.2f), + NextAction("regrowth on party", 21.1f), + NextAction("healing touch on party", 21.0f), + })); + + // MEDIUM + triggers.push_back( + new TriggerNode("party member medium health", + { + NextAction("tree form", 20.5f), + NextAction("swiftmend on party", 20.4f), + NextAction("wild growth on party", 20.3f), + NextAction("nourish on party", 20.2f), + NextAction("regrowth on party", 20.1f), + NextAction("healing touch on party", 20.0f), + })); + + // ALMOST FULL + triggers.push_back( + new TriggerNode("party member almost full health", + { + NextAction("wild growth on party", 10.3f), + NextAction("rejuvenation on party", 10.2f), + NextAction("regrowth on party", 10.1f), + })); + + triggers.push_back( + new TriggerNode("medium mana", { NextAction("innervate", 25.0f) })); + + triggers.push_back(new TriggerNode("enemy too close for spell", + { NextAction("flee", 39.0f) })); +} + +void DruidTranquilityStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode("medium group heal setting", + { NextAction("tree form", 30.6f), NextAction("tranquility", 30.5f) })); +} + +void DruidBlanketStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode( + "wild growth blanket", + { NextAction("tree form", 8.1f), NextAction("wild growth blanket", 8.0f) })); + + triggers.push_back(new TriggerNode( + "rejuvenation blanket", + { NextAction("tree form", 6.1f), NextAction("rejuvenation blanket", 6.0f) })); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h new file mode 100644 index 000000000..e318a8ea7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Druid/Strategy/RestoDruidStrategy.h @@ -0,0 +1,43 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RESTODRUIDSTRATEGY_H +#define PLAYERBOTS_RESTODRUIDSTRATEGY_H + +#include "GenericDruidStrategy.h" +#include "Strategy.h" + +class PlayerbotAI; + +class RestoDruidStrategy : public GenericDruidStrategy +{ +public: + RestoDruidStrategy(PlayerbotAI* botAI); + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "resto"; } + uint32 GetType() const override { return STRATEGY_TYPE_RANGED | STRATEGY_TYPE_HEAL; } +}; + +class DruidBlanketStrategy : public Strategy +{ +public: + DruidBlanketStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "blanketing"; } +}; + +class DruidTranquilityStrategy : public Strategy +{ +public: + DruidTranquilityStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "tranquility"; } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp deleted file mode 100644 index 6cf8552a3..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 "DruidTriggers.h" -#include "Player.h" -#include "Playerbots.h" - -bool MarkOfTheWildOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget()); -} - -bool MarkOfTheWildTrigger::IsActive() -{ - return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget()); -} - -bool ThornsOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget()); -} - -bool EntanglingRootsKiteTrigger::IsActive() -{ - return DebuffTrigger::IsActive() && AI_VALUE(uint8, "attacker count") < 3 && !GetTarget()->GetPower(POWER_MANA); -} - -bool ThornsTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget()); } - -bool BearFormTrigger::IsActive() { return !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr); } - -bool TreeFormTrigger::IsActive() { return !botAI->HasAura(33891, bot); } - -bool CatFormTrigger::IsActive() { return !botAI->HasAura("cat form", bot); } - -const std::set HurricaneChannelCheckTrigger::HURRICANE_SPELL_IDS = { - 16914, // Hurricane Rank 1 - 17401, // Hurricane Rank 2 - 17402, // Hurricane Rank 3 - 27012, // Hurricane Rank 4 - 48467 // Hurricane Rank 5 -}; - -bool HurricaneChannelCheckTrigger::IsActive() -{ - Player* bot = botAI->GetBot(); - - // Check if the bot is channeling a spell - if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) - { - // Only trigger if the spell being channeled is Hurricane - if (HURRICANE_SPELL_IDS.count(spell->m_spellInfo->Id)) - { - uint8 attackerCount = AI_VALUE(uint8, "attacker count"); - return attackerCount < minEnemies; - } - } - - // Not channeling Hurricane - return false; -} diff --git a/modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.h b/modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.h deleted file mode 100644 index 5541cc10e..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Druid/Trigger/DruidTriggers.h +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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_DRUIDTRIGGERS_H -#define _PLAYERBOT_DRUIDTRIGGERS_H - -#include "CureTriggers.h" -#include "GenericTriggers.h" -#include "Player.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "SharedDefines.h" -#include "Trigger.h" -#include - -class PlayerbotAI; - -class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger -{ -public: - MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 2 * 2000) {} - - bool IsActive() override; -}; - -class MarkOfTheWildTrigger : public BuffTrigger -{ -public: - MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 2 * 2000) {} - - bool IsActive() override; -}; - -class ThornsOnPartyTrigger : public BuffOnPartyTrigger -{ -public: - ThornsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "thorns", 2 * 2000) {} - - bool IsActive() override; -}; - -class ThornsOnMainTankTrigger : public BuffOnMainTankTrigger -{ -public: - ThornsOnMainTankTrigger(PlayerbotAI* botAI) : BuffOnMainTankTrigger(botAI, "thorns", false, 2 * 2000) {} -}; - -class ThornsTrigger : public BuffTrigger -{ -public: - ThornsTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "thorns", 2 * 2000) {} - - bool IsActive() override; -}; - -class OmenOfClarityTrigger : public BuffTrigger -{ -public: - OmenOfClarityTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "omen of clarity") {} -}; - -class RakeTrigger : public DebuffTrigger -{ -public: - RakeTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "rake", 1, true) {} -}; - -class InsectSwarmTrigger : public DebuffTrigger -{ -public: - InsectSwarmTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "insect swarm", 1, true) {} -}; - -class MoonfireTrigger : public DebuffTrigger -{ -public: - MoonfireTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "moonfire", 1, true) {} -}; - -class FaerieFireTrigger : public DebuffTrigger -{ -public: - FaerieFireTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "faerie fire", 1, false, 25.0f) {} -}; - -class FaerieFireFeralTrigger : public DebuffTrigger -{ -public: - FaerieFireFeralTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "faerie fire (feral)") {} -}; - -class BashInterruptSpellTrigger : public InterruptSpellTrigger -{ -public: - BashInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "bash") {} -}; - -class TigersFuryTrigger : public BuffTrigger -{ -public: - TigersFuryTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "tiger's fury") {} -}; - -class BerserkTrigger : public BoostTrigger -{ -public: - BerserkTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "berserk") {} -}; - -class SavageRoarTrigger : public BuffTrigger -{ -public: - SavageRoarTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "savage roar") {} -}; - -class NaturesGraspTrigger : public BuffTrigger -{ -public: - NaturesGraspTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "nature's grasp") {} -}; - -class EntanglingRootsTrigger : public HasCcTargetTrigger -{ -public: - EntanglingRootsTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "entangling roots") {} -}; - -class EntanglingRootsKiteTrigger : public DebuffTrigger -{ -public: - EntanglingRootsKiteTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "entangling roots") {} - - bool IsActive() override; -}; - -class HibernateTrigger : public HasCcTargetTrigger -{ -public: - HibernateTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "hibernate") {} -}; - -class CurePoisonTrigger : public NeedCureTrigger -{ -public: - CurePoisonTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class PartyMemberCurePoisonTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCurePoisonTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cure poison", DISPEL_POISON) - { - } -}; - -class BearFormTrigger : public BuffTrigger -{ -public: - BearFormTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "bear form") {} - - bool IsActive() override; -}; - -class TreeFormTrigger : public BuffTrigger -{ -public: - TreeFormTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "tree of life") {} - - bool IsActive() override; -}; - -class CatFormTrigger : public BuffTrigger -{ -public: - CatFormTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "cat form") {} - - bool IsActive() override; -}; - -class EclipseSolarTrigger : public HasAuraTrigger -{ -public: - EclipseSolarTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "eclipse (solar)") {} -}; - -class EclipseLunarTrigger : public HasAuraTrigger -{ -public: - EclipseLunarTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "eclipse (lunar)") {} -}; - -class BashInterruptEnemyHealerSpellTrigger : public InterruptEnemyHealerTrigger -{ -public: - BashInterruptEnemyHealerSpellTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "bash") {} -}; - -class NaturesSwiftnessTrigger : public BuffTrigger -{ -public: - NaturesSwiftnessTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "nature's swiftness") {} -}; - -class DruidPartyMemberRemoveCurseTrigger : public PartyMemberNeedCureTrigger -{ -public: - DruidPartyMemberRemoveCurseTrigger(PlayerbotAI* ai) - : PartyMemberNeedCureTrigger(ai, "druid remove curse", DISPEL_CURSE) - { - } -}; - -class EclipseSolarCooldownTrigger : public SpellCooldownTrigger -{ -public: - EclipseSolarCooldownTrigger(PlayerbotAI* ai) : SpellCooldownTrigger(ai, "eclipse (solar)") {} - bool IsActive() override { return bot->HasSpellCooldown(48517); } -}; - -class EclipseLunarCooldownTrigger : public SpellCooldownTrigger -{ -public: - EclipseLunarCooldownTrigger(PlayerbotAI* ai) : SpellCooldownTrigger(ai, "eclipse (lunar)") {} - bool IsActive() override { return bot->HasSpellCooldown(48518); } -}; - -class MangleCatTrigger : public DebuffTrigger -{ -public: - MangleCatTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "mangle (cat)", 1, false, 0.0f) {} - bool IsActive() override - { - return DebuffTrigger::IsActive() && !botAI->HasAura("mangle (bear)", GetTarget(), false, false, -1, true) - && !botAI->HasAura("trauma", GetTarget(), false, false, -1, true); - } -}; - -class FerociousBiteTimeTrigger : public Trigger -{ -public: - FerociousBiteTimeTrigger(PlayerbotAI* ai) : Trigger(ai, "ferocious bite time") {} - bool IsActive() override - { - Unit* target = AI_VALUE(Unit*, "current target"); - if (!target) - return false; - - uint8 cp = AI_VALUE2(uint8, "combo", "current target"); - if (cp < 5) - return false; - - Aura* roar = botAI->GetAura("savage roar", bot); - bool roarCheck = !roar || roar->GetDuration() > 10000; - if (!roarCheck) - return false; - - Aura* rip = botAI->GetAura("rip", target, true); - bool ripCheck = !rip || rip->GetDuration() > 10000; - if (!ripCheck) - return false; - - return true; - } -}; - -class HurricaneChannelCheckTrigger : public Trigger -{ -public: - HurricaneChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2) - : Trigger(botAI, "hurricane channel check"), minEnemies(minEnemies) - { - } - - bool IsActive() override; - -protected: - uint32 minEnemies; - static const std::set HURRICANE_SPELL_IDS; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Action/HunterActions.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterActions.cpp similarity index 50% rename from modules/mod-playerbots/src/Ai/Class/Hunter/Action/HunterActions.cpp rename to modules/mod-playerbots/src/Ai/Class/Hunter/HunterActions.cpp index 18e5d5905..f1289573e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Action/HunterActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HunterActions.h" @@ -16,31 +17,21 @@ bool CastViperStingAction::isUseful() AI_VALUE2(uint8, "mana", "current target") >= 30; } -bool CastAspectOfTheCheetahAction::isUseful() -{ - return !botAI->HasAnyAuraOf(GetTarget(), "aspect of the cheetah", "aspect of the pack", nullptr); -} - bool CastAspectOfTheHawkAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - if (bot->HasSpell(61846) || bot->HasSpell(61847)) // Aspect of the Dragonhawk spell IDs - return false; - return true; + return target && !botAI->HasSpell("aspect of the dragonhawk"); } bool CastArcaneShotAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - if (bot->HasSpell(53301) || bot->HasSpell(60051) || bot->HasSpell(60052) || bot->HasSpell(60053)) // Explosive Shot spell IDs + if (!target || !botAI->HasSpell("explosive shot")) return false; // Armor Penetration rating check - will not cast Arcane Shot above 435 ArP - int32 armorPenRating = bot->GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1) + bot->GetUInt32Value(CR_ARMOR_PENETRATION); + int32 armorPenRating = + bot->GetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1) + bot->GetUInt32Value(CR_ARMOR_PENETRATION); if (armorPenRating > 435) return false; @@ -50,20 +41,21 @@ bool CastArcaneShotAction::isUseful() bool CastImmolationTrapAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - if (bot->HasSpell(13813) || bot->HasSpell(14316) || bot->HasSpell(14317) || bot->HasSpell(27025) || bot->HasSpell(49066) || bot->HasSpell(49067)) // Explosive Trap spell IDs - return false; - return true; + return target && !botAI->HasSpell("explosive trap"); } -Value* CastFreezingTrap::GetTargetValue() { return context->GetValue("cc target", "freezing trap"); } +Value* CastFreezingTrap::GetTargetValue() +{ + return context->GetValue("cc target", "freezing trap"); +} bool FeedPetAction::Execute(Event /*event*/) { - if (Pet* pet = bot->GetPet()) - if (pet->getPetType() == HUNTER_PET && pet->GetHappinessState() != HAPPY) - pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS))); + if (Pet* pet = bot->GetPet(); pet && pet->getPetType() == HUNTER_PET && + pet->GetHappinessState() != HAPPY) + { + pet->SetPower(POWER_HAPPINESS, pet->GetMaxPower(Powers(POWER_HAPPINESS))); + } return true; } @@ -79,6 +71,7 @@ bool CastAutoShotAction::isUseful() { return false; } + return AI_VALUE(uint32, "active spell") != AI_VALUE2(uint32, "spell id", getName()); } @@ -87,6 +80,7 @@ bool CastDisengageAction::Execute(Event event) Unit* target = AI_VALUE(Unit*, "current target"); if (!target) return false; + // can cast spell check passed in isUseful() bot->SetOrientation(bot->GetAngle(target)); return CastSpellAction::Execute(event); @@ -97,11 +91,20 @@ bool CastDisengageAction::isUseful() return !botAI->HasStrategy("trap weave", BOT_STATE_COMBAT); } -Value* CastScareBeastCcAction::GetTargetValue() { return context->GetValue("cc target", "scare beast"); } +Value* CastScareBeastCcAction::GetTargetValue() +{ + return context->GetValue("cc target", "scare beast"); +} -bool CastScareBeastCcAction::Execute(Event /*event*/) { return botAI->CastSpell("scare beast", GetTarget()); } +bool CastScareBeastCcAction::Execute(Event /*event*/) +{ + return botAI->CastSpell("scare beast", GetTarget()); +} -bool CastWingClipAction::isUseful() { return CastSpellAction::isUseful() && !botAI->HasAura(spell, GetTarget()); } +bool CastWingClipAction::isUseful() +{ + return CastSpellAction::isUseful() && !botAI->HasAura(spell, GetTarget()); +} std::vector CastWingClipAction::getPrerequisites() { diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Action/HunterActions.h b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterActions.h similarity index 50% rename from modules/mod-playerbots/src/Ai/Class/Hunter/Action/HunterActions.h rename to modules/mod-playerbots/src/Ai/Class/Hunter/HunterActions.h index 8e2113525..14c5b3944 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Action/HunterActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HUNTERACTIONS_H -#define _PLAYERBOT_HUNTERACTIONS_H +#ifndef PLAYERBOTS_HUNTERACTIONS_H +#define PLAYERBOTS_HUNTERACTIONS_H #include "AiObject.h" #include "Event.h" @@ -19,52 +20,58 @@ class Unit; class CastTrueshotAuraAction : public CastBuffSpellAction { public: - CastTrueshotAuraAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "trueshot aura") {} + CastTrueshotAuraAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "trueshot aura") {} +}; + +class CastAspectOfTheDragonhawkAction : public CastBuffSpellAction +{ +public: + CastAspectOfTheDragonhawkAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the dragonhawk") {} }; class CastAspectOfTheHawkAction : public CastBuffSpellAction { public: - CastAspectOfTheHawkAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the hawk") {} + CastAspectOfTheHawkAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the hawk") {} bool isUseful() override; }; class CastAspectOfTheMonkeyAction : public CastBuffSpellAction { public: - CastAspectOfTheMonkeyAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the monkey") {} -}; - -class CastAspectOfTheDragonhawkAction : public CastBuffSpellAction -{ -public: - CastAspectOfTheDragonhawkAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the dragonhawk") {} + CastAspectOfTheMonkeyAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the monkey") {} }; class CastAspectOfTheWildAction : public CastBuffSpellAction { public: - CastAspectOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the wild") {} + CastAspectOfTheWildAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the wild") {} }; class CastAspectOfTheCheetahAction : public CastBuffSpellAction { public: - CastAspectOfTheCheetahAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the cheetah") {} - - bool isUseful() override; + CastAspectOfTheCheetahAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the cheetah") {} }; class CastAspectOfThePackAction : public CastBuffSpellAction { public: - CastAspectOfThePackAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the pack") {} + CastAspectOfThePackAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the pack") {} }; class CastAspectOfTheViperAction : public CastBuffSpellAction { public: - CastAspectOfTheViperAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "aspect of the viper") {} + CastAspectOfTheViperAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "aspect of the viper") {} }; // Cooldown Spells @@ -72,26 +79,28 @@ public: class CastRapidFireAction : public CastBuffSpellAction { public: - CastRapidFireAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "rapid fire") {} + CastRapidFireAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "rapid fire") {} }; class CastDeterrenceAction : public CastBuffSpellAction { public: - CastDeterrenceAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "deterrence") {} + CastDeterrenceAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "deterrence") {} }; class CastReadinessAction : public CastBuffSpellAction { public: - CastReadinessAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "readiness") {} + CastReadinessAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "readiness") {} }; class CastDisengageAction : public CastSpellAction { public: CastDisengageAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "disengage") {} - bool Execute(Event event) override; bool isUseful() override; }; @@ -101,14 +110,15 @@ public: class CastScareBeastAction : public CastSpellAction { public: - CastScareBeastAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "scare beast") {} + CastScareBeastAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "scare beast") {} }; class CastScareBeastCcAction : public CastSpellAction { public: - CastScareBeastCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "scare beast on cc") {} - + CastScareBeastCcAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "scare beast on cc") {} Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -116,34 +126,41 @@ public: class CastFreezingTrap : public CastDebuffSpellAction { public: - CastFreezingTrap(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "freezing trap") {} - + CastFreezingTrap(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "freezing trap") {} Value* GetTargetValue() override; }; class CastWyvernStingAction : public CastDebuffSpellAction { public: - CastWyvernStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "wyvern sting", true) {} + CastWyvernStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "wyvern sting", true) {} }; class CastSilencingShotAction : public CastSpellAction { public: - CastSilencingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "silencing shot") {} + CastSilencingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "silencing shot") {} }; class CastConcussiveShotAction : public CastSnareSpellAction { public: - CastConcussiveShotAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "concussive shot") {} + CastConcussiveShotAction(PlayerbotAI* botAI) : + CastSnareSpellAction(botAI, "concussive shot") {} }; class CastIntimidationAction : public CastBuffSpellAction { public: - CastIntimidationAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "intimidation", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastIntimidationAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "intimidation", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; // Threat Spells @@ -151,19 +168,22 @@ public: class CastDistractingShotAction : public CastSpellAction { public: - CastDistractingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "distracting shot") {} + CastDistractingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "distracting shot") {} }; class CastMisdirectionOnMainTankAction : public BuffOnMainTankAction { public: - CastMisdirectionOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "misdirection", true) {} + CastMisdirectionOnMainTankAction(PlayerbotAI* botAI) : + BuffOnMainTankAction(botAI, "misdirection", true) {} }; class CastFeignDeathAction : public CastBuffSpellAction { public: - CastFeignDeathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feign death") {} + CastFeignDeathAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "feign death") {} }; // Pet Spells @@ -172,7 +192,6 @@ class FeedPetAction : public Action { public: FeedPetAction(PlayerbotAI* botAI) : Action(botAI, "feed pet") {} - bool Execute(Event event) override; }; @@ -186,27 +205,39 @@ class CastMendPetAction : public CastAuraSpellAction { public: CastMendPetAction(PlayerbotAI* botAI) : CastAuraSpellAction(botAI, "mend pet") {} - std::string const GetTargetName() override { return "pet target"; } + std::string const GetTargetName() override + { + return "pet target"; + } }; class CastRevivePetAction : public CastBuffSpellAction { public: - CastRevivePetAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "revive pet") {} + CastRevivePetAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "revive pet") {} }; class CastKillCommandAction : public CastBuffSpellAction { public: - CastKillCommandAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "kill command", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastKillCommandAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "kill command", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; class CastBestialWrathAction : public CastBuffSpellAction { public: - CastBestialWrathAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "bestial wrath", false, 5000) {} - std::string const GetTargetName() override { return "pet target"; } + CastBestialWrathAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "bestial wrath", false, 5000) {} + std::string const GetTargetName() override + { + return "pet target"; + } }; // Direct Damage Spells @@ -215,14 +246,18 @@ class CastAutoShotAction : public CastSpellAction { public: CastAutoShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "auto shot") {} - ActionThreatType getThreatType() override { return ActionThreatType::None; } + ActionThreatType getThreatType() override + { + return ActionThreatType::None; + } bool isUseful() override; }; class CastArcaneShotAction : public CastSpellAction { public: - CastArcaneShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "arcane shot") {} + CastArcaneShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "arcane shot") {} bool isUseful() override; }; @@ -235,7 +270,8 @@ public: class CastChimeraShotAction : public CastSpellAction { public: - CastChimeraShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "chimera shot") {} + CastChimeraShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "chimera shot") {} }; class CastSteadyShotAction : public CastSpellAction @@ -255,7 +291,8 @@ public: class CastHuntersMarkAction : public CastDebuffSpellAction { public: - CastHuntersMarkAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "hunter's mark") {} + CastHuntersMarkAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "hunter's mark") {} bool isUseful() override { // Bypass TTL check @@ -266,20 +303,23 @@ public: class CastTranquilizingShotAction : public CastSpellAction { public: - CastTranquilizingShotAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "tranquilizing shot") {} + CastTranquilizingShotAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "tranquilizing shot") {} }; class CastViperStingAction : public CastDebuffSpellAction { public: - CastViperStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "viper sting", true) {} + CastViperStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "viper sting", true) {} bool isUseful() override; }; class CastSerpentStingAction : public CastDebuffSpellAction { public: - CastSerpentStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "serpent sting", true) {} + CastSerpentStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "serpent sting", true) {} bool isUseful() override { // Bypass TTL check @@ -290,7 +330,8 @@ public: class CastScorpidStingAction : public CastDebuffSpellAction { public: - CastScorpidStingAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "scorpid sting", true) {} + CastScorpidStingAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "scorpid sting", true) {} bool isUseful() override { // Bypass TTL check @@ -301,7 +342,8 @@ public: class CastSerpentStingOnAttackerAction : public CastDebuffSpellOnAttackerAction { public: - CastSerpentStingOnAttackerAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "serpent sting", true) {} + CastSerpentStingOnAttackerAction(PlayerbotAI* botAI) + : CastDebuffSpellOnAttackerAction(botAI, "serpent sting", true) {} bool isUseful() override { // Bypass TTL check @@ -312,20 +354,23 @@ public: class CastImmolationTrapAction : public CastSpellAction { public: - CastImmolationTrapAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "immolation trap") {} + CastImmolationTrapAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "immolation trap") {} bool isUseful() override; }; class CastExplosiveTrapAction : public CastSpellAction { public: - CastExplosiveTrapAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "explosive trap") {} + CastExplosiveTrapAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "explosive trap") {} }; -class CastBlackArrow : public CastDebuffSpellAction +class CastBlackArrowAction : public CastDebuffSpellAction { public: - CastBlackArrow(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "black arrow", true) {} + CastBlackArrowAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "black arrow", true) {} bool isUseful() override { if (botAI->HasStrategy("trap weave", BOT_STATE_COMBAT)) @@ -335,77 +380,89 @@ public: } }; -class CastExplosiveShotAction : public CastDebuffSpellAction +class CastExplosiveShotBaseAction : public CastDebuffSpellAction { public: - CastExplosiveShotAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - bool isUseful() override - { - // Bypass TTL check - return CastAuraSpellAction::isUseful(); - } + CastExplosiveShotBaseAction(PlayerbotAI* botAI) + : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} }; // Rank 4 -class CastExplosiveShotRank4Action : public CastDebuffSpellAction +class CastExplosiveShotRank4Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank4Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60053, GetTarget()); } + CastExplosiveShotRank4Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event /*event*/) override + { + return botAI->CastSpell(60053, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60053); } }; // Rank 3 -class CastExplosiveShotRank3Action : public CastDebuffSpellAction +class CastExplosiveShotRank3Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank3Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60052, GetTarget()); } + CastExplosiveShotRank3Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event /*event*/) override + { + return botAI->CastSpell(60052, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60052); } }; // Rank 2 -class CastExplosiveShotRank2Action : public CastDebuffSpellAction +class CastExplosiveShotRank2Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank2Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(60051, GetTarget()); } + CastExplosiveShotRank2Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event /*event*/) override + { + return botAI->CastSpell(60051, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(60051); } }; // Rank 1 -class CastExplosiveShotRank1Action : public CastDebuffSpellAction +class CastExplosiveShotRank1Action : public CastExplosiveShotBaseAction { public: - CastExplosiveShotRank1Action(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "explosive shot", true, 0.0f) {} - - bool Execute(Event event) override { return botAI->CastSpell(53301, GetTarget()); } + CastExplosiveShotRank1Action(PlayerbotAI* botAI) : + CastExplosiveShotBaseAction(botAI) {} + bool Execute(Event /*event*/) override + { + return botAI->CastSpell(53301, GetTarget()); + } bool isUseful() override { Unit* target = GetTarget(); if (!target) return false; + return !target->HasAura(53301); } }; @@ -415,8 +472,8 @@ public: class CastWingClipAction : public CastSpellAction { public: - CastWingClipAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wing clip") {} - + CastWingClipAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "wing clip") {} bool isUseful() override; std::vector getPrerequisites() override; }; @@ -424,13 +481,15 @@ public: class CastRaptorStrikeAction : public CastSpellAction { public: - CastRaptorStrikeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "raptor strike") {} + CastRaptorStrikeAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "raptor strike") {} }; class CastMongooseBiteAction : public CastSpellAction { public: - CastMongooseBiteAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "mongoose bite") {} + CastMongooseBiteAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "mongoose bite") {} }; // AoE Spells @@ -445,7 +504,10 @@ class CastVolleyAction : public CastSpellAction { public: CastVolleyAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "volley") {} - ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } + ActionThreatType getThreatType() override + { + return ActionThreatType::Aoe; + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.cpp index def1e4693..15326dd98 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HunterAiObjectContext.h" @@ -22,7 +23,6 @@ public: HunterStrategyFactoryInternal() { creators["nc"] = &HunterStrategyFactoryInternal::nc; - creators["boost"] = &HunterStrategyFactoryInternal::boost; creators["pet"] = &HunterStrategyFactoryInternal::pet; creators["cc"] = &HunterStrategyFactoryInternal::cc; creators["trap weave"] = &HunterStrategyFactoryInternal::trap_weave; @@ -34,7 +34,6 @@ public: private: static Strategy* nc(PlayerbotAI* botAI) { return new GenericHunterNonCombatStrategy(botAI); } - static Strategy* boost(PlayerbotAI* botAI) { return new HunterBoostStrategy(botAI); } static Strategy* pet(PlayerbotAI* botAI) { return new HunterPetStrategy(botAI); } static Strategy* cc(PlayerbotAI* botAI) { return new HunterCcStrategy(botAI); } static Strategy* trap_weave(PlayerbotAI* botAI) { return new HunterTrapWeaveStrategy(botAI); } @@ -51,14 +50,12 @@ public: { creators["bspeed"] = &HunterBuffStrategyFactoryInternal::bspeed; creators["bdps"] = &HunterBuffStrategyFactoryInternal::bdps; - creators["bmana"] = &HunterBuffStrategyFactoryInternal::bmana; creators["rnature"] = &HunterBuffStrategyFactoryInternal::rnature; } private: static Strategy* bspeed(PlayerbotAI* botAI) { return new HunterBuffSpeedStrategy(botAI); } static Strategy* bdps(PlayerbotAI* botAI) { return new HunterBuffDpsStrategy(botAI); } - static Strategy* bmana(PlayerbotAI* botAI) { return new HunterBuffManaStrategy(botAI); } static Strategy* rnature(PlayerbotAI* botAI) { return new HunterNatureResistanceStrategy(botAI); } }; @@ -67,7 +64,6 @@ class HunterTriggerFactoryInternal : public NamedObjectContext public: HunterTriggerFactoryInternal() { - creators["aspect of the viper"] = &HunterTriggerFactoryInternal::aspect_of_the_viper; creators["black arrow"] = &HunterTriggerFactoryInternal::black_arrow; creators["no stings"] = &HunterTriggerFactoryInternal::NoStings; creators["hunters pet dead"] = &HunterTriggerFactoryInternal::hunters_pet_dead; @@ -75,10 +71,9 @@ public: creators["hunters pet medium health"] = &HunterTriggerFactoryInternal::hunters_pet_medium_health; creators["hunter's mark"] = &HunterTriggerFactoryInternal::hunters_mark; creators["freezing trap"] = &HunterTriggerFactoryInternal::freezing_trap; - creators["aspect of the pack"] = &HunterTriggerFactoryInternal::aspect_of_the_pack; creators["rapid fire"] = &HunterTriggerFactoryInternal::rapid_fire; - creators["aspect of the hawk"] = &HunterTriggerFactoryInternal::aspect_of_the_hawk; - creators["aspect of the monkey"] = &HunterTriggerFactoryInternal::aspect_of_the_monkey; + creators["aspect of the pack"] = &HunterTriggerFactoryInternal::aspect_of_the_pack; + creators["aspect of the dragonhawk"] = &HunterTriggerFactoryInternal::aspect_of_the_dragonhawk; creators["aspect of the wild"] = &HunterTriggerFactoryInternal::aspect_of_the_wild; creators["aspect of the viper"] = &HunterTriggerFactoryInternal::aspect_of_the_viper; creators["trueshot aura"] = &HunterTriggerFactoryInternal::trueshot_aura; @@ -107,10 +102,8 @@ public: private: static Trigger* auto_shot(PlayerbotAI* botAI) { return new AutoShotTrigger(botAI); } static Trigger* scare_beast(PlayerbotAI* botAI) { return new ScareBeastTrigger(botAI); } - static Trigger* concussive_shot_on_snare_target(PlayerbotAI* botAI) - { - return new ConsussiveShotSnareTrigger(botAI); - } + static Trigger* concussive_shot_on_snare_target(PlayerbotAI* botAI) { + return new ConcussiveShotOnSnareTargetTrigger(botAI); } static Trigger* pet_not_happy(PlayerbotAI* botAI) { return new HunterPetNotHappy(botAI); } static Trigger* serpent_sting_on_attacker(PlayerbotAI* botAI) { return new SerpentStingOnAttackerTrigger(botAI); } static Trigger* trueshot_aura(PlayerbotAI* botAI) { return new TrueshotAuraTrigger(botAI); } @@ -125,18 +118,17 @@ private: static Trigger* freezing_trap(PlayerbotAI* botAI) { return new FreezingTrapTrigger(botAI); } static Trigger* aspect_of_the_pack(PlayerbotAI* botAI) { return new HunterAspectOfThePackTrigger(botAI); } static Trigger* rapid_fire(PlayerbotAI* botAI) { return new RapidFireTrigger(botAI); } - static Trigger* aspect_of_the_hawk(PlayerbotAI* botAI) { return new HunterAspectOfTheHawkTrigger(botAI); } - static Trigger* aspect_of_the_monkey(PlayerbotAI* botAI) { return new HunterAspectOfTheMonkeyTrigger(botAI); } + static Trigger* aspect_of_the_dragonhawk(PlayerbotAI* botAI) { return new HunterAspectOfTheDragonhawkTrigger(botAI); } static Trigger* aspect_of_the_wild(PlayerbotAI* botAI) { return new HunterAspectOfTheWildTrigger(botAI); } static Trigger* low_ammo(PlayerbotAI* botAI) { return new HunterLowAmmoTrigger(botAI); } static Trigger* no_ammo(PlayerbotAI* botAI) { return new HunterNoAmmoTrigger(botAI); } static Trigger* has_ammo(PlayerbotAI* botAI) { return new HunterHasAmmoTrigger(botAI); } static Trigger* switch_to_melee(PlayerbotAI* botAI) { return new SwitchToMeleeTrigger(botAI); } static Trigger* switch_to_ranged(PlayerbotAI* botAI) { return new SwitchToRangedTrigger(botAI); } - static Trigger* misdirection_on_main_tank(PlayerbotAI* ai) { return new MisdirectionOnMainTankTrigger(ai); } - static Trigger* remove_enrage(PlayerbotAI* ai) { return new TargetRemoveEnrageTrigger(ai); } - static Trigger* remove_magic(PlayerbotAI* ai) { return new TargetRemoveMagicTrigger(ai); } - static Trigger* immolation_trap_no_cd(PlayerbotAI* ai) { return new ImmolationTrapNoCdTrigger(ai); } + static Trigger* misdirection_on_main_tank(PlayerbotAI* botAI) { return new MisdirectionOnMainTankTrigger(botAI); } + static Trigger* remove_enrage(PlayerbotAI* botAI) { return new TargetRemoveEnrageTrigger(botAI); } + static Trigger* remove_magic(PlayerbotAI* botAI) { return new TargetRemoveMagicTrigger(botAI); } + static Trigger* immolation_trap_no_cd(PlayerbotAI* botAI) { return new ImmolationTrapNoCdTrigger(botAI); } static Trigger* kill_command(PlayerbotAI* botAI) { return new KillCommandTrigger(botAI); } static Trigger* explosive_shot(PlayerbotAI* botAI) { return new ExplosiveShotTrigger(botAI); } static Trigger* lock_and_load(PlayerbotAI* botAI) { return new LockAndLoadTrigger(botAI); } @@ -153,7 +145,6 @@ public: creators["auto shot"] = &HunterAiObjectContextInternal::auto_shot; creators["aimed shot"] = &HunterAiObjectContextInternal::aimed_shot; creators["chimera shot"] = &HunterAiObjectContextInternal::chimera_shot; - creators["explosive shot"] = &HunterAiObjectContextInternal::explosive_shot; creators["arcane shot"] = &HunterAiObjectContextInternal::arcane_shot; creators["concussive shot"] = &HunterAiObjectContextInternal::concussive_shot; creators["distracting shot"] = &HunterAiObjectContextInternal::distracting_shot; @@ -176,6 +167,7 @@ public: creators["deterrence"] = &HunterAiObjectContextInternal::deterrence; creators["readiness"] = &HunterAiObjectContextInternal::readiness; creators["aspect of the hawk"] = &HunterAiObjectContextInternal::aspect_of_the_hawk; + creators["aspect of the dragonhawk"] = &HunterAiObjectContextInternal::aspect_of_the_dragonhawk; creators["aspect of the monkey"] = &HunterAiObjectContextInternal::aspect_of_the_monkey; creators["aspect of the wild"] = &HunterAiObjectContextInternal::aspect_of_the_wild; creators["aspect of the viper"] = &HunterAiObjectContextInternal::aspect_of_the_viper; @@ -191,7 +183,6 @@ public: creators["bestial wrath"] = &HunterAiObjectContextInternal::bestial_wrath; creators["scare beast"] = &HunterAiObjectContextInternal::scare_beast; creators["scare beast on cc"] = &HunterAiObjectContextInternal::scare_beast_on_cc; - creators["aspect of the dragonhawk"] = &HunterAiObjectContextInternal::aspect_of_the_dragonhawk; creators["tranquilizing shot"] = &HunterAiObjectContextInternal::tranquilizing_shot; creators["steady shot"] = &HunterAiObjectContextInternal::steady_shot; creators["kill shot"] = &HunterAiObjectContextInternal::kill_shot; @@ -200,6 +191,7 @@ public: creators["disengage"] = &HunterAiObjectContextInternal::disengage; creators["immolation trap"] = &HunterAiObjectContextInternal::immolation_trap; creators["explosive trap"] = &HunterAiObjectContextInternal::explosive_trap; + creators["explosive shot base"] = &HunterAiObjectContextInternal::explosive_shot_base; creators["explosive shot rank 4"] = &HunterAiObjectContextInternal::explosive_shot_rank_4; creators["explosive shot rank 3"] = &HunterAiObjectContextInternal::explosive_shot_rank_3; creators["explosive shot rank 2"] = &HunterAiObjectContextInternal::explosive_shot_rank_2; @@ -218,7 +210,6 @@ private: static Action* auto_shot(PlayerbotAI* botAI) { return new CastAutoShotAction(botAI); } static Action* aimed_shot(PlayerbotAI* botAI) { return new CastAimedShotAction(botAI); } static Action* chimera_shot(PlayerbotAI* botAI) { return new CastChimeraShotAction(botAI); } - static Action* explosive_shot(PlayerbotAI* botAI) { return new CastExplosiveShotAction(botAI); } static Action* arcane_shot(PlayerbotAI* botAI) { return new CastArcaneShotAction(botAI); } static Action* concussive_shot(PlayerbotAI* botAI) { return new CastConcussiveShotAction(botAI); } static Action* distracting_shot(PlayerbotAI* botAI) { return new CastDistractingShotAction(botAI); } @@ -234,12 +225,13 @@ private: static Action* kill_command(PlayerbotAI* botAI) { return new CastKillCommandAction(botAI); } static Action* revive_pet(PlayerbotAI* botAI) { return new CastRevivePetAction(botAI); } static Action* call_pet(PlayerbotAI* botAI) { return new CastCallPetAction(botAI); } - static Action* black_arrow(PlayerbotAI* botAI) { return new CastBlackArrow(botAI); } + static Action* black_arrow(PlayerbotAI* botAI) { return new CastBlackArrowAction(botAI); } static Action* freezing_trap(PlayerbotAI* botAI) { return new CastFreezingTrap(botAI); } static Action* rapid_fire(PlayerbotAI* botAI) { return new CastRapidFireAction(botAI); } static Action* deterrence(PlayerbotAI* botAI) { return new CastDeterrenceAction(botAI); } static Action* readiness(PlayerbotAI* botAI) { return new CastReadinessAction(botAI); } static Action* aspect_of_the_hawk(PlayerbotAI* botAI) { return new CastAspectOfTheHawkAction(botAI); } + static Action* aspect_of_the_dragonhawk(PlayerbotAI* botAI) { return new CastAspectOfTheDragonhawkAction(botAI); } static Action* aspect_of_the_monkey(PlayerbotAI* botAI) { return new CastAspectOfTheMonkeyAction(botAI); } static Action* aspect_of_the_wild(PlayerbotAI* botAI) { return new CastAspectOfTheWildAction(botAI); } static Action* aspect_of_the_viper(PlayerbotAI* botAI) { return new CastAspectOfTheViperAction(botAI); } @@ -248,20 +240,20 @@ private: static Action* wing_clip(PlayerbotAI* botAI) { return new CastWingClipAction(botAI); } static Action* raptor_strike(PlayerbotAI* botAI) { return new CastRaptorStrikeAction(botAI); } static Action* mongoose_bite(PlayerbotAI* botAI) { return new CastMongooseBiteAction(botAI); } - static Action* aspect_of_the_dragonhawk(PlayerbotAI* ai) { return new CastAspectOfTheDragonhawkAction(ai); } - static Action* tranquilizing_shot(PlayerbotAI* ai) { return new CastTranquilizingShotAction(ai); } - static Action* steady_shot(PlayerbotAI* ai) { return new CastSteadyShotAction(ai); } - static Action* kill_shot(PlayerbotAI* ai) { return new CastKillShotAction(ai); } - static Action* misdirection_on_main_tank(PlayerbotAI* ai) { return new CastMisdirectionOnMainTankAction(ai); } - static Action* silencing_shot(PlayerbotAI* ai) { return new CastSilencingShotAction(ai); } - static Action* disengage(PlayerbotAI* ai) { return new CastDisengageAction(ai); } - static Action* immolation_trap(PlayerbotAI* ai) { return new CastImmolationTrapAction(ai); } - static Action* explosive_trap(PlayerbotAI* ai) { return new CastExplosiveTrapAction(ai); } - static Action* explosive_shot_rank_4(PlayerbotAI* ai) { return new CastExplosiveShotRank4Action(ai); } - static Action* explosive_shot_rank_3(PlayerbotAI* ai) { return new CastExplosiveShotRank3Action(ai); } - static Action* explosive_shot_rank_2(PlayerbotAI* ai) { return new CastExplosiveShotRank2Action(ai); } - static Action* explosive_shot_rank_1(PlayerbotAI* ai) { return new CastExplosiveShotRank1Action(ai); } - static Action* intimidation(PlayerbotAI* ai) { return new CastIntimidationAction(ai); } + static Action* tranquilizing_shot(PlayerbotAI* botAI) { return new CastTranquilizingShotAction(botAI); } + static Action* steady_shot(PlayerbotAI* botAI) { return new CastSteadyShotAction(botAI); } + static Action* kill_shot(PlayerbotAI* botAI) { return new CastKillShotAction(botAI); } + static Action* misdirection_on_main_tank(PlayerbotAI* botAI) { return new CastMisdirectionOnMainTankAction(botAI); } + static Action* silencing_shot(PlayerbotAI* botAI) { return new CastSilencingShotAction(botAI); } + static Action* disengage(PlayerbotAI* botAI) { return new CastDisengageAction(botAI); } + static Action* immolation_trap(PlayerbotAI* botAI) { return new CastImmolationTrapAction(botAI); } + static Action* explosive_trap(PlayerbotAI* botAI) { return new CastExplosiveTrapAction(botAI); } + static Action* explosive_shot_base(PlayerbotAI* botAI) { return new CastExplosiveShotBaseAction(botAI); } + static Action* explosive_shot_rank_4(PlayerbotAI* botAI) { return new CastExplosiveShotRank4Action(botAI); } + static Action* explosive_shot_rank_3(PlayerbotAI* botAI) { return new CastExplosiveShotRank3Action(botAI); } + static Action* explosive_shot_rank_2(PlayerbotAI* botAI) { return new CastExplosiveShotRank2Action(botAI); } + static Action* explosive_shot_rank_1(PlayerbotAI* botAI) { return new CastExplosiveShotRank1Action(botAI); } + static Action* intimidation(PlayerbotAI* botAI) { return new CastIntimidationAction(botAI); } }; SharedNamedObjectContextList HunterAiObjectContext::sharedStrategyContexts; diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.h index 27178104a..13b990afc 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HUNTERAIOBJECTCONTEXT_H -#define _PLAYERBOT_HUNTERAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_HUNTERAIOBJECTCONTEXT_H +#define PLAYERBOTS_HUNTERAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterTriggers.cpp similarity index 63% rename from modules/mod-playerbots/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Hunter/HunterTriggers.cpp index 84ece04b7..c2fc1693e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Trigger/HunterTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HunterTriggers.h" @@ -16,8 +17,7 @@ bool KillCommandTrigger::IsActive() { - Unit* target = GetTarget(); - return !botAI->HasAura("kill command", target); + return !botAI->HasAura("kill command", GetTarget()); } bool BlackArrowTrigger::IsActive() @@ -26,36 +26,46 @@ bool BlackArrowTrigger::IsActive() return false; return DebuffTrigger::IsActive(); - return BuffTrigger::IsActive(); } -bool HunterAspectOfTheHawkTrigger::IsActive() +bool HunterAspectOfTheDragonhawkTrigger::IsActive() { Unit* target = GetTarget(); - return SpellTrigger::IsActive() && !botAI->HasAura("aspect of the hawk", target) && - !botAI->HasAura("aspect of the dragonhawk", target) && - (!AI_VALUE2(bool, "has mana", "self target") || AI_VALUE2(uint8, "mana", "self target") > 70); + if (!target) + return false; + + if (!SpellTrigger::IsActive()) + return false; + + if (botAI->HasAura("aspect of the hawk", target) || + botAI->HasAura("aspect of the dragonhawk", target)) + return false; + + if (botAI->HasAura("aspect of the viper", target)) + return AI_VALUE2(uint8, "mana", "self target") >= 60; + + return true; } bool HunterNoStingsActiveTrigger::IsActive() { Unit* target = AI_VALUE(Unit*, "current target"); - return DebuffTrigger::IsActive() && target && !botAI->HasAura("serpent sting", target, false, true) && - !botAI->HasAura("scorpid sting", target, false, true) && !botAI->HasAura("viper sting", target, false, true); - return BuffTrigger::IsActive(); + return DebuffTrigger::IsActive() && target && + !botAI->HasAura("serpent sting", target, false, true) && + !botAI->HasAura("scorpid sting", target, false, true) && + !botAI->HasAura("viper sting", target, false, true); } bool HuntersPetDeadTrigger::IsActive() { - // Unit* pet = AI_VALUE(Unit*, "pet target"); - // return pet && AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); return AI_VALUE(bool, "pet dead") && !AI_VALUE2(bool, "mounted", "self target"); } bool HuntersPetLowHealthTrigger::IsActive() { Unit* pet = AI_VALUE(Unit*, "pet target"); - return pet && AI_VALUE2(uint8, "health", "pet target") < 40 && !AI_VALUE2(bool, "dead", "pet target") && + return pet && AI_VALUE2(uint8, "health", "pet target") < 40 && + !AI_VALUE2(bool, "dead", "pet target") && !AI_VALUE2(bool, "mounted", "self target"); } @@ -73,9 +83,14 @@ bool HunterPetNotHappy::IsActive() bool HunterAspectOfTheViperTrigger::IsActive() { - return SpellTrigger::IsActive() && !botAI->HasAura(spell, GetTarget()) && + if (botAI->HasStrategy("rnature", BotState::BOT_STATE_COMBAT) || + botAI->HasStrategy("rnature", BotState::BOT_STATE_NON_COMBAT) || + botAI->HasStrategy("bspeed", BotState::BOT_STATE_COMBAT) || + botAI->HasStrategy("bspeed", BotState::BOT_STATE_NON_COMBAT)) + return false; + + return BuffTrigger::IsActive() && AI_VALUE2(uint8, "mana", "self target") < (sPlayerbotAIConfig.lowMana / 2); - ; } bool HunterAspectOfThePackTrigger::IsActive() @@ -85,11 +100,14 @@ bool HunterAspectOfThePackTrigger::IsActive() bool HunterLowAmmoTrigger::IsActive() { - return bot->GetGroup() && (AI_VALUE2(uint32, "item count", "ammo") < 100) && - (AI_VALUE2(uint32, "item count", "ammo") > 0); + uint32 ammoCount = AI_VALUE2(uint32, "item count", "ammo"); + return bot->GetGroup() && ammoCount > 0 && ammoCount < 100; } -bool HunterHasAmmoTrigger::IsActive() { return !AmmoCountTrigger::IsActive(); } +bool HunterHasAmmoTrigger::IsActive() +{ + return !AmmoCountTrigger::IsActive(); +} bool SwitchToRangedTrigger::IsActive() { @@ -107,6 +125,8 @@ bool SwitchToMeleeTrigger::IsActive() ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", "current target"), 8.0f)); } +// Valid targets for "Improved Tracking". +// Optional/Utility targets (uncomment for selfbot). bool NoTrackTrigger::IsActive() { std::vector track_list = { @@ -115,8 +135,13 @@ bool NoTrackTrigger::IsActive() "track dragonkin", "track elementals", "track giants", - "track hidden", - "track humanoids" + "track humanoids", + "track undead", + // "track hidden", + // "find herbs", + // "find minerals", + // "find fish", + // "find treasure", }; for (auto &track: track_list) @@ -124,6 +149,7 @@ bool NoTrackTrigger::IsActive() if (botAI->HasAura(track, bot)) return false; } + return true; } @@ -131,17 +157,17 @@ bool SerpentStingOnAttackerTrigger::IsActive() { if (!DebuffOnAttackerTrigger::IsActive()) return false; + Unit* target = GetTarget(); if (!target) - { return false; - } + return !botAI->HasAura("scorpid sting", target, false, true) && !botAI->HasAura("viper sting", target, false, true); - return BuffTrigger::IsActive(); } -const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = { +const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = +{ 1510, // Volley Rank 1 14294, // Volley Rank 2 14295, // Volley Rank 3 @@ -152,19 +178,12 @@ const std::set VolleyChannelCheckTrigger::VOLLEY_SPELL_IDS = { bool VolleyChannelCheckTrigger::IsActive() { - Player* bot = botAI->GetBot(); - - // Check if the bot is channeling a spell - if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + spell && VOLLEY_SPELL_IDS.count(spell->m_spellInfo->Id)) { - // Only trigger if the spell being channeled is Volley - if (VOLLEY_SPELL_IDS.count(spell->m_spellInfo->Id)) - { - uint8 attackerCount = AI_VALUE(uint8, "attacker count"); - return attackerCount < minEnemies; - } + uint8 attackerCount = AI_VALUE(uint8, "attacker count"); + return attackerCount < minEnemies; } - // Not channeling Volley return false; } diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Trigger/HunterTriggers.h b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterTriggers.h similarity index 82% rename from modules/mod-playerbots/src/Ai/Class/Hunter/Trigger/HunterTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Hunter/HunterTriggers.h index 6bc5f3c44..285335eb7 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Trigger/HunterTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/HunterTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HUNTERTRIGGERS_H -#define _PLAYERBOT_HUNTERTRIGGERS_H +#ifndef PLAYERBOTS_HUNTERTRIGGERS_H +#define PLAYERBOTS_HUNTERTRIGGERS_H #include "CureTriggers.h" #include "GenericTriggers.h" @@ -16,16 +17,10 @@ class PlayerbotAI; // Buff and Out of Combat Triggers -class HunterAspectOfTheMonkeyTrigger : public BuffTrigger +class HunterAspectOfTheDragonhawkTrigger : public BuffTrigger { public: - HunterAspectOfTheMonkeyTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the monkey") {} -}; - -class HunterAspectOfTheHawkTrigger : public BuffTrigger -{ -public: - HunterAspectOfTheHawkTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the hawk") {} + HunterAspectOfTheDragonhawkTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "aspect of the dragonhawk") {} bool IsActive() override; }; @@ -130,10 +125,10 @@ public: FreezingTrapTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "freezing trap") {} }; -class ConsussiveShotSnareTrigger : public SnareTargetTrigger +class ConcussiveShotOnSnareTargetTrigger : public SnareTargetTrigger { public: - ConsussiveShotSnareTrigger(PlayerbotAI* botAI) : SnareTargetTrigger(botAI, "concussive shot") {} + ConcussiveShotOnSnareTargetTrigger(PlayerbotAI* botAI) : SnareTargetTrigger(botAI, "concussive shot") {} }; class ScareBeastTrigger : public HasCcTargetTrigger @@ -212,25 +207,25 @@ public: class MisdirectionOnMainTankTrigger : public BuffOnMainTankTrigger { public: - MisdirectionOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "misdirection", true) {} + MisdirectionOnMainTankTrigger(PlayerbotAI* botAI) : BuffOnMainTankTrigger(botAI, "misdirection", true) {} }; class TargetRemoveEnrageTrigger : public TargetAuraDispelTrigger { public: - TargetRemoveEnrageTrigger(PlayerbotAI* ai) : TargetAuraDispelTrigger(ai, "tranquilizing shot", DISPEL_ENRAGE) {} + TargetRemoveEnrageTrigger(PlayerbotAI* botAI) : TargetAuraDispelTrigger(botAI, "tranquilizing shot", DISPEL_ENRAGE) {} }; class TargetRemoveMagicTrigger : public TargetAuraDispelTrigger { public: - TargetRemoveMagicTrigger(PlayerbotAI* ai) : TargetAuraDispelTrigger(ai, "tranquilizing shot", DISPEL_MAGIC) {} + TargetRemoveMagicTrigger(PlayerbotAI* botAI) : TargetAuraDispelTrigger(botAI, "tranquilizing shot", DISPEL_MAGIC) {} }; class ImmolationTrapNoCdTrigger : public SpellNoCooldownTrigger { public: - ImmolationTrapNoCdTrigger(PlayerbotAI* ai) : SpellNoCooldownTrigger(ai, "immolation trap") {} + ImmolationTrapNoCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "immolation trap") {} }; BEGIN_TRIGGER(HuntersPetDeadTrigger, Trigger) diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp index a2c302d37..207fba812 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.cpp @@ -1,46 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BeastMasteryHunterStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class BeastMasteryHunterStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - BeastMasteryHunterStrategyActionNodeFactory() - { - creators["auto shot"] = &auto_shot; - creators["kill command"] = &kill_command; - creators["kill shot"] = &kill_shot; - creators["viper sting"] = &viper_sting; - creators["serpent sting"] = serpent_sting; - creators["aimed shot"] = &aimed_shot; - creators["arcane shot"] = &arcane_shot; - creators["steady shot"] = &steady_shot; - creators["multi-shot"] = &multi_shot; - creators["volley"] = &volley; - } - -private: - static ActionNode* auto_shot(PlayerbotAI*) { return new ActionNode("auto shot", {}, {}, {}); } - static ActionNode* kill_command(PlayerbotAI*) { return new ActionNode("kill command", {}, {}, {}); } - static ActionNode* kill_shot(PlayerbotAI*) { return new ActionNode("kill shot", {}, {}, {}); } - static ActionNode* viper_sting(PlayerbotAI*) { return new ActionNode("viper sting", {}, {}, {}); } - static ActionNode* serpent_sting(PlayerbotAI*) { return new ActionNode("serpent sting", {}, {}, {}); } - static ActionNode* aimed_shot(PlayerbotAI*) { return new ActionNode("aimed shot", {}, {}, {}); } - static ActionNode* arcane_shot(PlayerbotAI*) { return new ActionNode("arcane shot", {}, {}, {}); } - static ActionNode* steady_shot(PlayerbotAI*) { return new ActionNode("steady shot", {}, {}, {}); } - static ActionNode* multi_shot(PlayerbotAI*) { return new ActionNode("multi shot", {}, {}, {}); } - static ActionNode* volley(PlayerbotAI*) { return new ActionNode("volley", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== BeastMasteryHunterStrategy::BeastMasteryHunterStrategy(PlayerbotAI* botAI) : GenericHunterStrategy(botAI) { - actionNodeFactories.Add(new BeastMasteryHunterStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.h b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.h index 5235aa861..651b3314e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/BeastMasteryHunterStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_BEASTMASTERYHUNTERSTRATEGY_H -#define _PLAYERBOT_BEASTMASTERYHUNTERSTRATEGY_H +#ifndef PLAYERBOTS_BEASTMASTERYHUNTERSTRATEGY_H +#define PLAYERBOTS_BEASTMASTERYHUNTERSTRATEGY_H #include "GenericHunterStrategy.h" #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp index 00c35dc2a..b1c3dc8ef 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.cpp @@ -1,65 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericHunterNonCombatStrategy.h" - #include "Playerbots.h" -class GenericHunterNonCombatStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - GenericHunterNonCombatStrategyActionNodeFactory() - { - creators["rapid fire"] = &rapid_fire; - creators["boost"] = &rapid_fire; - creators["aspect of the pack"] = &aspect_of_the_pack; - } - -private: - static ActionNode* rapid_fire([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("rapid fire", - /*P*/ {}, - /*A*/ { NextAction("readiness")}, - /*C*/ {}); - } - - static ActionNode* aspect_of_the_pack([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("aspect of the pack", - /*P*/ {}, - /*A*/ { NextAction("aspect of the cheetah")}, - /*C*/ {}); - } -}; - GenericHunterNonCombatStrategy::GenericHunterNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) { - actionNodeFactories.Add(new GenericHunterNonCombatStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } void GenericHunterNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("trueshot aura", { NextAction("trueshot aura", 2.0f)})); - triggers.push_back(new TriggerNode("often", { - NextAction("apply stone", 1.0f), - NextAction("apply oil", 1.0f), - })); - triggers.push_back(new TriggerNode("low ammo", { NextAction("say::low ammo", ACTION_NORMAL)})); - triggers.push_back(new TriggerNode("no track", { NextAction("track humanoids", ACTION_NORMAL)})); - triggers.push_back(new TriggerNode("no ammo", { NextAction("equip upgrades packet action", ACTION_HIGH + 1)})); + triggers.push_back(new TriggerNode("trueshot aura", { NextAction("trueshot aura", 2.0f) })); + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f), + NextAction("apply oil", 1.0f) })); + triggers.push_back(new TriggerNode("low ammo", { NextAction("say::low ammo", ACTION_NORMAL) })); + triggers.push_back(new TriggerNode("no track", { NextAction("track humanoids", ACTION_NORMAL) })); + triggers.push_back(new TriggerNode("no ammo", { NextAction("equip upgrades packet action", ACTION_HIGH + 1) })); } void HunterPetStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("no pet", { NextAction("call pet", 60.0f)})); - triggers.push_back(new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f)})); - triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f)})); - triggers.push_back(new TriggerNode("pet not happy", { NextAction("feed pet", 60.0f)})); - triggers.push_back(new TriggerNode("hunters pet medium health", { NextAction("mend pet", 60.0f)})); - triggers.push_back(new TriggerNode("hunters pet dead", { NextAction("revive pet", 60.0f)})); + triggers.push_back(new TriggerNode("no pet", { NextAction("call pet", 60.0f) })); + triggers.push_back(new TriggerNode("has pet", { NextAction("toggle pet spell", 60.0f) })); + triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) })); + triggers.push_back(new TriggerNode("pet not happy", { NextAction("feed pet", 60.0f) })); + triggers.push_back(new TriggerNode("hunters pet medium health", { NextAction("mend pet", 60.0f) })); + triggers.push_back(new TriggerNode("hunters pet dead", { NextAction("revive pet", 60.0f) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.h index 697b1e291..eb05ce1e9 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICHUNTERNONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICHUNTERNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICHUNTERNONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICHUNTERNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp index 0a9e88a54..d11dcd005 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericHunterStrategy.h" @@ -11,11 +12,6 @@ public: GenericHunterStrategyActionNodeFactory() { creators["rapid fire"] = &rapid_fire; - creators["boost"] = &rapid_fire; - creators["aspect of the pack"] = &aspect_of_the_pack; - creators["aspect of the dragonhawk"] = &aspect_of_the_dragonhawk; - creators["feign death"] = &feign_death; - creators["wing clip"] = &wing_clip; creators["mongoose bite"] = &mongoose_bite; creators["raptor strike"] = &raptor_strike; creators["explosive trap"] = &explosive_trap; @@ -29,40 +25,6 @@ private: /*A*/ { NextAction("readiness") }, /*C*/ {}); } - - static ActionNode* aspect_of_the_pack([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("aspect of the pack", - /*P*/ {}, - /*A*/ { NextAction("aspect of the cheetah") }, - /*C*/ {}); - } - - static ActionNode* aspect_of_the_dragonhawk([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("aspect of the dragonhawk", - /*P*/ {}, - /*A*/ { NextAction("aspect of the hawk") }, - /*C*/ {}); - } - - static ActionNode* feign_death([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("feign death", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* wing_clip([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("wing clip", - /*P*/ {}, - // /*A*/ { NextAction("mongoose bite") }, - {}, - /*C*/ {}); - } - static ActionNode* mongoose_bite([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("mongoose bite", @@ -70,7 +32,6 @@ private: /*A*/ { NextAction("raptor strike") }, /*C*/ {}); } - static ActionNode* raptor_strike([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("raptor strike", @@ -78,7 +39,6 @@ private: /*A*/ {}, /*C*/ {}); } - static ActionNode* explosive_trap([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("explosive trap", @@ -102,7 +62,6 @@ void GenericHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("hunter's mark", { NextAction("hunter's mark", 29.5f) })); triggers.push_back(new TriggerNode("rapid fire", { NextAction("rapid fire", 29.0f) })); triggers.push_back(new TriggerNode("aspect of the viper", { NextAction("aspect of the viper", 28.0f) })); - triggers.push_back(new TriggerNode("aspect of the hawk", { NextAction("aspect of the dragonhawk", 27.5f) })); // Aggro/Threat/Defensive Triggers triggers.push_back(new TriggerNode("has aggro", { NextAction("concussive shot", 20.0f) })); @@ -118,14 +77,12 @@ void GenericHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("tranquilizing shot magic", { NextAction("tranquilizing shot", 61.0f) })); // Ranged-based Triggers - triggers.push_back(new TriggerNode("enemy within melee", { - NextAction("explosive trap", 37.0f), - NextAction("mongoose bite", 22.0f), - NextAction("wing clip", 21.0f) })); + triggers.push_back(new TriggerNode("enemy within melee", { NextAction("explosive trap", 37.0f), + NextAction("mongoose bite", 22.0f), + NextAction("wing clip", 21.0f) })); - triggers.push_back(new TriggerNode("enemy too close for auto shot", { - NextAction("disengage", 35.0f), - NextAction("flee", 34.0f) })); + triggers.push_back(new TriggerNode("enemy too close for auto shot", { NextAction("disengage", 35.0f), + NextAction("flee", 34.0f) })); } // ===== AoE Strategy, 2/3+ enemies ===== @@ -138,10 +95,6 @@ void AoEHunterStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("light aoe", { NextAction("multi-shot", 21.0f) })); } -void HunterBoostStrategy::InitTriggers(std::vector& triggers) -{ -} - void HunterCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("scare beast", { NextAction("scare beast on cc", 23.0f) })); diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h index 01aef4cec..3eddcdd1d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/GenericHunterStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICHUNTERSTRATEGY_H -#define _PLAYERBOT_GENERICHUNTERSTRATEGY_H +#ifndef PLAYERBOTS_GENERICHUNTERSTRATEGY_H +#define PLAYERBOTS_GENERICHUNTERSTRATEGY_H #include "CombatStrategy.h" #include "Strategy.h" @@ -30,15 +31,6 @@ public: std::string const getName() override { return "aoe"; } }; -class HunterBoostStrategy : public Strategy -{ -public: - HunterBoostStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - - std::string const getName() override { return "boost"; } - void InitTriggers(std::vector& triggers) override; -}; - class HunterCcStrategy : public Strategy { public: diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp index 3601d7711..3cd920379 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HunterBuffStrategies.h" @@ -10,9 +11,21 @@ class BuffHunterStrategyActionNodeFactory : public NamedObjectFactory { public: - BuffHunterStrategyActionNodeFactory() { creators["aspect of the hawk"] = &aspect_of_the_hawk; } + BuffHunterStrategyActionNodeFactory() + { + creators["aspect of the dragonhawk"] = &aspect_of_the_dragonhawk; + creators["aspect of the hawk"] = &aspect_of_the_hawk; + creators["aspect of the pack"] = &aspect_of_the_pack; + } private: + static ActionNode* aspect_of_the_dragonhawk([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("aspect of the dragonhawk", + /*P*/ {}, + /*A*/ { NextAction("aspect of the hawk") }, + /*C*/ {}); + } static ActionNode* aspect_of_the_hawk([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("aspect of the hawk", @@ -20,9 +33,16 @@ private: /*A*/ { NextAction("aspect of the monkey") }, /*C*/ {}); } + static ActionNode* aspect_of_the_pack([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("aspect of the pack", + /*P*/ {}, + /*A*/ { NextAction("aspect of the cheetah") }, + /*C*/ {}); + } }; -HunterBuffDpsStrategy::HunterBuffDpsStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) +HunterBuffDpsStrategy::HunterBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) { actionNodeFactories.Add(new BuffHunterStrategyActionNodeFactory()); } @@ -30,24 +50,35 @@ HunterBuffDpsStrategy::HunterBuffDpsStrategy(PlayerbotAI* botAI) : NonCombatStra void HunterBuffDpsStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( - new TriggerNode("aspect of the hawk", { NextAction("aspect of the dragonhawk", 20.1f), - NextAction("aspect of the hawk", 20.0f) })); + new TriggerNode( + "aspect of the dragonhawk", + { + NextAction("aspect of the dragonhawk", ACTION_HIGH) + } + ) + ); } void HunterNatureResistanceStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("aspect of the wild", - { NextAction("aspect of the wild", 20.0f) })); + triggers.push_back( + new TriggerNode( + "aspect of the wild", + { + NextAction("aspect of the wild", ACTION_HIGH) + } + ) + ); } void HunterBuffSpeedStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("aspect of the pack", - { NextAction("aspect of the pack", 20.0f) })); -} - -void HunterBuffManaStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back(new TriggerNode("aspect of the viper", - { NextAction("aspect of the viper", 20.0f) })); + triggers.push_back( + new TriggerNode( + "aspect of the pack", + { + NextAction("aspect of the pack", ACTION_HIGH) + } + ) + ); } diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h index 7df8bc1bc..bd51330d7 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/HunterBuffStrategies.h @@ -1,49 +1,41 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HUNTERBUFFSTRATEGIES_H -#define _PLAYERBOT_HUNTERBUFFSTRATEGIES_H +#ifndef PLAYERBOTS_HUNTERBUFFSTRATEGIES_H +#define PLAYERBOTS_HUNTERBUFFSTRATEGIES_H -#include "NonCombatStrategy.h" +#include "Strategy.h" class PlayerbotAI; -class HunterBuffSpeedStrategy : public NonCombatStrategy +class HunterBuffSpeedStrategy : public Strategy { public: - HunterBuffSpeedStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + HunterBuffSpeedStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "bspeed"; } - void InitTriggers(std::vector& triggers) override; }; -class HunterBuffManaStrategy : public NonCombatStrategy -{ -public: - HunterBuffManaStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} - - std::string const getName() override { return "bmana"; } - void InitTriggers(std::vector& triggers) override; -}; - -class HunterBuffDpsStrategy : public NonCombatStrategy +class HunterBuffDpsStrategy : public Strategy { public: HunterBuffDpsStrategy(PlayerbotAI* botAI); - std::string const getName() override { return "bdps"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "bdps"; } }; -class HunterNatureResistanceStrategy : public NonCombatStrategy +class HunterNatureResistanceStrategy : public Strategy { public: - HunterNatureResistanceStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + HunterNatureResistanceStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} - std::string const getName() override { return "rnature"; } void InitTriggers(std::vector& triggers) override; + std::string const getName() override { return "rnature"; } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp index e4e8a9a4f..3028ea0f6 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.cpp @@ -1,50 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MarksmanshipHunterStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class MarksmanshipHunterStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - MarksmanshipHunterStrategyActionNodeFactory() - { - creators["auto shot"] = &auto_shot; - creators["silencing shot"] = &silencing_shot; - creators["kill command"] = &kill_command; - creators["kill shot"] = &kill_shot; - creators["viper sting"] = &viper_sting; - creators["serpent sting"] = serpent_sting; - creators["chimera shot"] = &chimera_shot; - creators["aimed shot"] = &aimed_shot; - creators["arcane shot"] = &arcane_shot; - creators["steady shot"] = &steady_shot; - creators["multi-shot"] = &multi_shot; - creators["volley"] = &volley; - } - -private: - static ActionNode* auto_shot(PlayerbotAI*) { return new ActionNode("auto shot", {}, {}, {}); } - static ActionNode* silencing_shot(PlayerbotAI*) { return new ActionNode("silencing shot", {}, {}, {}); } - static ActionNode* kill_command(PlayerbotAI*) { return new ActionNode("kill command", {}, {}, {}); } - static ActionNode* kill_shot(PlayerbotAI*) { return new ActionNode("kill shot", {}, {}, {}); } - static ActionNode* viper_sting(PlayerbotAI*) { return new ActionNode("viper sting", {}, {}, {}); } - static ActionNode* serpent_sting(PlayerbotAI*) { return new ActionNode("serpent sting", {}, {}, {}); } - static ActionNode* chimera_shot(PlayerbotAI*) { return new ActionNode("chimera shot", {}, {}, {}); } - static ActionNode* aimed_shot(PlayerbotAI*) { return new ActionNode("aimed shot", {}, {}, {}); } - static ActionNode* arcane_shot(PlayerbotAI*) { return new ActionNode("arcane shot", {}, {}, {}); } - static ActionNode* steady_shot(PlayerbotAI*) { return new ActionNode("steady shot", {}, {}, {}); } - static ActionNode* multi_shot(PlayerbotAI*) { return new ActionNode("multi shot", {}, {}, {}); } - static ActionNode* volley(PlayerbotAI*) { return new ActionNode("volley", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== MarksmanshipHunterStrategy::MarksmanshipHunterStrategy(PlayerbotAI* botAI) : GenericHunterStrategy(botAI) { - actionNodeFactories.Add(new MarksmanshipHunterStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.h b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.h index 800d40a8a..96a996f04 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/MarksmanshipHunterStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MARKSMANSHIPHUNTERSTRATEGY_H -#define _PLAYERBOT_MARKSMANSHIPHUNTERSTRATEGY_H +#ifndef PLAYERBOTS_MARKSMANSHIPHUNTERSTRATEGY_H +#define PLAYERBOTS_MARKSMANSHIPHUNTERSTRATEGY_H #include "GenericHunterStrategy.h" #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp index 796891a9b..15e84a29b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "SurvivalHunterStrategy.h" @@ -12,36 +13,35 @@ class SurvivalHunterStrategyActionNodeFactory : public NamedObjectFactory& triggers) } ) ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 3", 27.5f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 2", 27.0f) - } - ) - ); - triggers.push_back( - new TriggerNode( - "lock and load", - { - NextAction("explosive shot rank 1", 26.5f) - } - ) - ); triggers.push_back( new TriggerNode( "kill command", diff --git a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.h b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.h index 2e2b52f13..ffee89672 100644 --- a/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Hunter/Strategy/SurvivalHunterStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SURVIVALHUNTERSTRATEGY_H -#define _PLAYERBOT_SURVIVALHUNTERSTRATEGY_H +#ifndef PLAYERBOTS_SURVIVALHUNTERSTRATEGY_H +#define PLAYERBOTS_SURVIVALHUNTERSTRATEGY_H #include "GenericHunterStrategy.h" #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Action/MageActions.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/MageActions.cpp similarity index 69% rename from modules/mod-playerbots/src/Ai/Class/Mage/Action/MageActions.cpp rename to modules/mod-playerbots/src/Ai/Class/Mage/MageActions.cpp index 5eb63ea86..d6377c276 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Action/MageActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/MageActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MageActions.h" @@ -11,7 +12,21 @@ #include "ServerFacade.h" #include "SharedDefines.h" -Value* CastPolymorphAction::GetTargetValue() { return context->GetValue("cc target", getName()); } +std::vector CastMoltenArmorAction::getAlternatives() +{ + if (!botAI->HasSpell("molten armor")) + return NextAction::merge({ NextAction("mage armor") }, CastBuffSpellAction::getAlternatives()); + + return CastBuffSpellAction::getAlternatives(); +} + +std::vector CastMageArmorAction::getAlternatives() +{ + if (!botAI->HasSpell("mage armor")) + return NextAction::merge({ NextAction("ice armor") }, CastBuffSpellAction::getAlternatives()); + + return CastBuffSpellAction::getAlternatives(); +} bool UseManaSapphireAction::isUseful() { @@ -52,22 +67,23 @@ bool UseManaAgateAction::isUseful() bool CastFrostNovaAction::isUseful() { Unit* target = AI_VALUE(Unit*, "current target"); - if (!target || !target->IsInWorld()) + if (!target || !target->IsInWorld() || target->isFrozen() || + (target->ToCreature() && + target->ToCreature()->HasMechanicTemplateImmunity(1 << (MECHANIC_FREEZE - 1)))) + { return false; + } - if (target->ToCreature() && target->ToCreature()->HasMechanicTemplateImmunity(1 << (MECHANIC_FREEZE - 1))) - return false; - - if (target->isFrozen()) - return false; - - return ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", GetTargetName()), 10.f); + return ServerFacade::instance().IsDistanceLessOrEqualThan( + AI_VALUE2(float, "distance", GetTargetName()), 10.f); } bool CastConeOfColdAction::isUseful() { bool facingTarget = AI_VALUE2(bool, "facing", "current target"); - bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", GetTargetName()), 10.f); + bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan( + AI_VALUE2(float, "distance", GetTargetName()), 10.f); + return facingTarget && targetClose; } @@ -76,6 +92,7 @@ bool CastDragonsBreathAction::isUseful() Unit* target = AI_VALUE(Unit*, "current target"); if (!target) return false; + bool facingTarget = AI_VALUE2(bool, "facing", "current target"); bool targetClose = bot->IsWithinCombatRange(target, 10.0f); return facingTarget && targetClose; @@ -86,6 +103,7 @@ bool CastBlastWaveAction::isUseful() Unit* target = AI_VALUE(Unit*, "current target"); if (!target) return false; + bool targetClose = bot->IsWithinCombatRange(target, 10.0f); return targetClose; } @@ -102,14 +120,11 @@ Unit* CastFocusMagicOnPartyAction::GetTarget() for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || member == bot || !member->IsAlive()) - continue; - - if (member->GetMap() != bot->GetMap() || bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance) - continue; - - if (member->HasAura(54646)) + if (!member || member == bot || !member->IsAlive() || member->GetMap() != bot->GetMap() || + bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance || member->HasAura(54646)) // Focus Magic + { continue; + } if (member->getClass() == CLASS_MAGE) return member; @@ -138,7 +153,7 @@ bool CastBlinkBackAction::Execute(Event event) Unit* target = AI_VALUE(Unit*, "current target"); if (!target) return false; - // can cast spell check passed in isUseful() + bot->SetOrientation(bot->GetAngle(target) + M_PI); return CastSpellAction::Execute(event); } diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Action/MageActions.h b/modules/mod-playerbots/src/Ai/Class/Mage/MageActions.h similarity index 89% rename from modules/mod-playerbots/src/Ai/Class/Mage/Action/MageActions.h rename to modules/mod-playerbots/src/Ai/Class/Mage/MageActions.h index 39d1531a7..9961f7acf 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Action/MageActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/MageActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MAGEACTIONS_H -#define _PLAYERBOT_MAGEACTIONS_H +#ifndef PLAYERBOTS_MAGEACTIONS_H +#define PLAYERBOTS_MAGEACTIONS_H #include "GenericSpellActions.h" #include "SharedDefines.h" @@ -18,12 +19,14 @@ class CastMoltenArmorAction : public CastBuffSpellAction { public: CastMoltenArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "molten armor") {} + std::vector getAlternatives() override; }; class CastMageArmorAction : public CastBuffSpellAction { public: CastMageArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mage armor") {} + std::vector getAlternatives() override; }; class CastIceArmorAction : public CastBuffSpellAction @@ -38,16 +41,16 @@ public: CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {} }; -class CastArcaneIntellectAction : public CastBuffSpellAction +class CastArcaneIntellectAction : public GroupBuffSpellAction { public: - CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {} + CastArcaneIntellectAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "arcane intellect") {} }; -class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction +class CastArcaneIntellectOnPartyAction : public GroupBuffOnPartyAction { public: - CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {} + CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "arcane intellect") {} }; class CastFocusMagicOnPartyAction : public CastSpellAction @@ -60,7 +63,8 @@ public: class CastSummonWaterElementalAction : public CastBuffSpellAction { public: - CastSummonWaterElementalAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "summon water elemental") {} + CastSummonWaterElementalAction(PlayerbotAI* botAI) + : CastBuffSpellAction(botAI, "summon water elemental") {} }; // Boost Actions @@ -215,11 +219,10 @@ public: // CC, Interrupt, and Dispel Actions -class CastPolymorphAction : public CastBuffSpellAction +class CastPolymorphAction : public CastCrowdControlSpellAction { public: - CastPolymorphAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "polymorph") {} - Value* GetTargetValue() override; + CastPolymorphAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "polymorph") {} }; class CastSpellstealAction : public CastSpellAction @@ -237,7 +240,8 @@ public: class CastCounterspellOnEnemyHealerAction : public CastSpellOnEnemyHealerAction { public: - CastCounterspellOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "counterspell") {} + CastCounterspellOnEnemyHealerAction(PlayerbotAI* botAI) + : CastSpellOnEnemyHealerAction(botAI, "counterspell") {} }; class CastFrostNovaAction : public CastSpellAction @@ -276,9 +280,7 @@ class CastRemoveLesserCurseOnPartyAction : public CurePartyMemberAction { public: CastRemoveLesserCurseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "remove lesser curse", DISPEL_CURSE) - { - } + : CurePartyMemberAction(botAI, "remove lesser curse", DISPEL_CURSE) {} }; // Damage and Debuff Actions @@ -332,7 +334,6 @@ public: CastLivingBombAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "living bomb", true) {} bool isUseful() override { - // Bypass TTL check return CastAuraSpellAction::isUseful(); } }; @@ -343,7 +344,6 @@ public: CastLivingBombOnAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "living bomb", true) {} bool isUseful() override { - // Bypass TTL check return CastAuraSpellAction::isUseful(); } }; diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.cpp index 477c0e075..1f2eaf3cd 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MageAiObjectContext.h" @@ -89,6 +90,7 @@ public: creators["arcane intellect"] = &MageTriggerFactoryInternal::arcane_intellect; creators["arcane intellect on party"] = &MageTriggerFactoryInternal::arcane_intellect_on_party; creators["mage armor"] = &MageTriggerFactoryInternal::mage_armor; + creators["molten armor"] = &MageTriggerFactoryInternal::molten_armor; creators["remove curse"] = &MageTriggerFactoryInternal::remove_curse; creators["remove curse on party"] = &MageTriggerFactoryInternal::remove_curse_on_party; creators["counterspell"] = &MageTriggerFactoryInternal::counterspell; @@ -143,6 +145,7 @@ private: static Trigger* arcane_intellect(PlayerbotAI* botAI) { return new ArcaneIntellectTrigger(botAI); } static Trigger* arcane_intellect_on_party(PlayerbotAI* botAI) { return new ArcaneIntellectOnPartyTrigger(botAI); } static Trigger* mage_armor(PlayerbotAI* botAI) { return new MageArmorTrigger(botAI); } + static Trigger* molten_armor(PlayerbotAI* botAI) { return new MoltenArmorTrigger(botAI); } static Trigger* remove_curse(PlayerbotAI* botAI) { return new RemoveCurseTrigger(botAI); } static Trigger* remove_curse_on_party(PlayerbotAI* botAI) { return new PartyMemberRemoveCurseTrigger(botAI); } static Trigger* counterspell(PlayerbotAI* botAI) { return new CounterspellInterruptSpellTrigger(botAI); } diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.h index c2cd2f5e7..e8025c74a 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/MageAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MAGEAIOBJECTCONTEXT_H -#define _PLAYERBOT_MAGEAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_MAGEAIOBJECTCONTEXT_H +#define PLAYERBOTS_MAGEAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Trigger/MageTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/MageTriggers.cpp similarity index 64% rename from modules/mod-playerbots/src/Ai/Class/Mage/Trigger/MageTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Mage/MageTriggers.cpp index e22210798..c7e92d2b9 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Trigger/MageTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/MageTriggers.cpp @@ -1,10 +1,10 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "MageTriggers.h" -#include "MageActions.h" #include "Playerbots.h" #include "Player.h" #include "Spell.h" @@ -23,7 +23,7 @@ bool NoManaGemTrigger::IsActive() 5513, // Mana Jade 5514 // Mana Agate }; - Player* bot = botAI->GetBot(); + for (uint32 gemId : gemIds) { if (bot->GetItemCount(gemId, false) > 0) // false = only in bags @@ -32,11 +32,6 @@ bool NoManaGemTrigger::IsActive() return true; } -bool ArcaneIntellectOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget()); -} - bool ArcaneIntellectTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget()); @@ -45,17 +40,25 @@ bool ArcaneIntellectTrigger::IsActive() bool MageArmorTrigger::IsActive() { Unit* target = GetTarget(); - return !botAI->HasAura("ice armor", target) && !botAI->HasAura("frost armor", target) && - !botAI->HasAura("molten armor", target) && !botAI->HasAura("mage armor", target); + return botAI->HasSpell("mage armor") && !botAI->HasAura("mage armor", target) && + !botAI->HasAura("ice armor", target) && !botAI->HasAura("frost armor", target) && + !botAI->HasAura("molten armor", target); +} + +bool MoltenArmorTrigger::IsActive() +{ + Unit* target = GetTarget(); + return botAI->HasSpell("molten armor") && !botAI->HasAura("molten armor", target) && + !botAI->HasAura("ice armor", target) && !botAI->HasAura("frost armor", target) && + !botAI->HasAura("mage armor", target); } bool FrostNovaOnTargetTrigger::IsActive() { Unit* target = GetTarget(); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } + return botAI->HasAura(spell, target); } @@ -63,15 +66,15 @@ bool FrostbiteOnTargetTrigger::IsActive() { Unit* target = GetTarget(); if (!target || !target->IsAlive() || !target->IsInWorld()) - { return false; - } + return botAI->HasAura(spell, target); } bool NoFocusMagicTrigger::IsActive() { - if (!bot->HasSpell(54646)) + constexpr uint32 SPELL_FOCUS_MAGIC = 54646; + if (!bot->HasSpell(SPELL_FOCUS_MAGIC)) return false; Group* group = bot->GetGroup(); @@ -84,7 +87,7 @@ bool NoFocusMagicTrigger::IsActive() if (!member || member == bot || !member->IsAlive()) continue; - if (member->HasAura(54646, bot->GetGUID())) + if (member->HasAura(SPELL_FOCUS_MAGIC, bot->GetGUID())) return false; } return true; @@ -92,24 +95,17 @@ bool NoFocusMagicTrigger::IsActive() bool DeepFreezeCooldownTrigger::IsActive() { - Player* bot = botAI->GetBot(); - static const uint32 DEEP_FREEZE_SPELL_ID = 44572; - - // If the bot does NOT have Deep Freeze, treat as "on cooldown" - if (!bot->HasSpell(DEEP_FREEZE_SPELL_ID)) - return true; - - // Otherwise, use the default cooldown logic - return SpellCooldownTrigger::IsActive(); + constexpr uint32 SPELL_DEEP_FREEZE = 44572; + return !bot->HasSpell(SPELL_DEEP_FREEZE) || + SpellCooldownTrigger::IsActive(); } -const std::set FlamestrikeNearbyTrigger::FLAMESTRIKE_SPELL_IDS = {2120, 2121, 8422, 8423, 10215, - 10216, 27086, 42925, 42926}; +const std::unordered_set FlamestrikeNearbyTrigger::FLAMESTRIKE_SPELL_IDS = { + 2120, 2121, 8422, 8423, 10215, 10216, 27086, 42925, 42926 +}; bool FlamestrikeNearbyTrigger::IsActive() { - Player* bot = botAI->GetBot(); - for (uint32 spellId : FLAMESTRIKE_SPELL_IDS) { Aura* aura = bot->GetAura(spellId, bot->GetGUID()); @@ -133,7 +129,6 @@ bool ImprovedScorchTrigger::IsActive() if (!target || !target->IsAlive() || !target->IsInWorld()) return false; - // List of all spell IDs for Improved Scorch, Winter's Chill, and Shadow Mastery static const uint32 ImprovedScorchExclusiveDebuffs[] = {// Shadow Mastery 17794, 17797, 17798, 17799, 17800, // Winter's Chill @@ -147,11 +142,10 @@ bool ImprovedScorchTrigger::IsActive() return false; } - // Use default DebuffTrigger logic for the rest (only trigger if debuff is missing or expiring) return DebuffTrigger::IsActive(); } -const std::set BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = { +const std::unordered_set BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = { 10, // Blizzard Rank 1 6141, // Blizzard Rank 2 8427, // Blizzard Rank 3 @@ -165,19 +159,12 @@ const std::set BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = { bool BlizzardChannelCheckTrigger::IsActive() { - Player* bot = botAI->GetBot(); - - // Check if the bot is channeling a spell - if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)) + if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + spell && BLIZZARD_SPELL_IDS.count(spell->m_spellInfo->Id)) { - // Only trigger if the spell being channeled is Blizzard - if (BLIZZARD_SPELL_IDS.count(spell->m_spellInfo->Id)) - { - uint8 attackerCount = AI_VALUE(uint8, "attacker count"); - return attackerCount < minEnemies; - } + uint8 attackerCount = AI_VALUE(uint8, "attacker count"); + return attackerCount < minEnemies; } - // Not channeling Blizzard return false; } diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Trigger/MageTriggers.h b/modules/mod-playerbots/src/Ai/Class/Mage/MageTriggers.h similarity index 84% rename from modules/mod-playerbots/src/Ai/Class/Mage/Trigger/MageTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Mage/MageTriggers.h index e769916f4..679f7934d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Trigger/MageTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/MageTriggers.h @@ -1,36 +1,32 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MAGETRIGGERS_H -#define _PLAYERBOT_MAGETRIGGERS_H +#ifndef PLAYERBOTS_MAGETRIGGERS_H +#define PLAYERBOTS_MAGETRIGGERS_H #include "CureTriggers.h" #include "GenericTriggers.h" #include "SharedDefines.h" #include "Trigger.h" #include "Playerbots.h" -#include "PlayerbotAI.h" -#include #include -class PlayerbotAI; - // Buff and Out of Combat Triggers class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger { public: - ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "arcane intellect", 2 * 2000) {} - - bool IsActive() override; + ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "arcane intellect", 4 * 2000) {} }; class ArcaneIntellectTrigger : public BuffTrigger { public: - ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 2 * 2000) {} + ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 4 * 2000) {} bool IsActive() override; }; @@ -41,6 +37,13 @@ public: bool IsActive() override; }; +class MoltenArmorTrigger : public BuffTrigger +{ +public: + MoltenArmorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "molten armor", 5 * 2000) {} + bool IsActive() override; +}; + class NoFocusMagicTrigger : public Trigger { public: @@ -58,7 +61,6 @@ class NoManaGemTrigger : public Trigger { public: NoManaGemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no mana gem") {} - bool IsActive() override; }; @@ -109,10 +111,8 @@ public: class ArcaneBlast4StacksAndMissileBarrageTrigger : public TwoTriggers { public: - ArcaneBlast4StacksAndMissileBarrageTrigger(PlayerbotAI* ai) - : TwoTriggers(ai, "arcane blast stack", "missile barrage") - { - } + ArcaneBlast4StacksAndMissileBarrageTrigger(PlayerbotAI* botAI) + : TwoTriggers(botAI, "arcane blast stack", "missile barrage") {} }; class CombustionTrigger : public BoostTrigger @@ -138,7 +138,7 @@ public: class ColdSnapTrigger : public TwoTriggers { public: - ColdSnapTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "icy veins on cd", "deep freeze on cd") {} + ColdSnapTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "icy veins on cd", "deep freeze on cd") {} }; class MirrorImageTrigger : public BoostTrigger @@ -181,9 +181,8 @@ public: class PartyMemberRemoveCurseTrigger : public PartyMemberNeedCureTrigger { public: - PartyMemberRemoveCurseTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "remove curse", DISPEL_CURSE) - { - } + PartyMemberRemoveCurseTrigger(PlayerbotAI* botAI) + : PartyMemberNeedCureTrigger(botAI, "remove curse", DISPEL_CURSE) {} }; class SpellstealTrigger : public TargetAuraDispelTrigger @@ -216,7 +215,7 @@ public: class LivingBombOnAttackersTrigger : public DebuffOnAttackerTrigger { public: - LivingBombOnAttackersTrigger(PlayerbotAI* ai) : DebuffOnAttackerTrigger(ai, "living bomb", true) {} + LivingBombOnAttackersTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "living bomb", true) {} bool IsActive() override { return BuffTrigger::IsActive(); } }; @@ -282,13 +281,13 @@ public: protected: float radius; - static const std::set FLAMESTRIKE_SPELL_IDS; + static const std::unordered_set FLAMESTRIKE_SPELL_IDS; }; class FlamestrikeBlizzardTrigger : public TwoTriggers { public: - FlamestrikeBlizzardTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "flamestrike nearby", "medium aoe") {} + FlamestrikeBlizzardTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "flamestrike nearby", "medium aoe") {} }; class BlizzardChannelCheckTrigger : public Trigger @@ -301,7 +300,7 @@ public: protected: uint32 minEnemies; - static const std::set BLIZZARD_SPELL_IDS; + static const std::unordered_set BLIZZARD_SPELL_IDS; }; class BlastWaveOffCdTrigger : public SpellNoCooldownTrigger @@ -313,7 +312,8 @@ public: class BlastWaveOffCdTriggerAndMediumAoeTrigger : public TwoTriggers { public: - BlastWaveOffCdTriggerAndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "blast wave off cd", "medium aoe") {} + BlastWaveOffCdTriggerAndMediumAoeTrigger(PlayerbotAI* botAI) + : TwoTriggers(botAI, "blast wave off cd", "medium aoe") {} }; class NoFirestarterStrategyTrigger : public Trigger diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.cpp index 4707231db..4bf62bc06 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ArcaneMageStrategy.h" @@ -13,22 +14,18 @@ public: ArcaneMageStrategyActionNodeFactory() { creators["arcane blast"] = &arcane_blast; - creators["arcane barrage"] = &arcane_barrage; - creators["arcane missiles"] = &arcane_missiles; - creators["fire blast"] = &fire_blast; - creators["frostbolt"] = &frostbolt; - creators["arcane power"] = &arcane_power; - creators["icy veins"] = &icy_veins; } private: - static ActionNode* arcane_blast(PlayerbotAI*) { return new ActionNode("arcane blast", {}, {}, {}); } - static ActionNode* arcane_barrage(PlayerbotAI*) { return new ActionNode("arcane barrage", {}, {}, {}); } - static ActionNode* arcane_missiles(PlayerbotAI*) { return new ActionNode("arcane missiles", {}, {}, {}); } - static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); } - static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", {}, {}, {}); } - static ActionNode* arcane_power(PlayerbotAI*) { return new ActionNode("arcane power", {}, {}, {}); } - static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", {}, {}, {}); } + // Arcane Barrage is the alternate for Arcane Blast (cast while moving, or + // when Arcane Blast is unavailable - e.g. not yet learned at low levels). + static ActionNode* arcane_blast([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("arcane blast", + /*P*/ {}, + /*A*/ { NextAction("arcane barrage") }, + /*C*/ {}); + } }; // ===== Single Target Strategy ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.h b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.h index e654a655b..c5693a215 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/ArcaneMageStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ARCANEMAGESTRATEGY_H -#define _PLAYERBOT_ARCANEMAGESTRATEGY_H +#ifndef PLAYERBOTS_ARCANEMAGESTRATEGY_H +#define PLAYERBOTS_ARCANEMAGESTRATEGY_H #include "GenericMageStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.cpp index 4914c72df..9e83ee3e6 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.cpp @@ -1,41 +1,16 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FireMageStrategy.h" #include "Playerbots.h" #include "Strategy.h" -// ===== Action Node Factory ===== -class FireMageStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - FireMageStrategyActionNodeFactory() - { - creators["fireball"] = &fireball; - creators["frostbolt"] = &frostbolt; - creators["fire blast"] = &fire_blast; - creators["pyroblast"] = &pyroblast; - creators["scorch"] = &scorch; - creators["living bomb"] = &living_bomb; - creators["combustion"] = &combustion; - } - -private: - static ActionNode* fireball(PlayerbotAI*) { return new ActionNode("fireball", {}, {}, {}); } - static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", {}, {}, {}); } - static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); } - static ActionNode* pyroblast(PlayerbotAI*) { return new ActionNode("pyroblast", {}, {}, {}); } - static ActionNode* scorch(PlayerbotAI*) { return new ActionNode("scorch", {}, {}, {}); } - static ActionNode* living_bomb(PlayerbotAI*) { return new ActionNode("living bomb", {}, {}, {}); } - static ActionNode* combustion(PlayerbotAI*) { return new ActionNode("combustion", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== FireMageStrategy::FireMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) { - actionNodeFactories.Add(new FireMageStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.h b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.h index 03d50641b..d3017867c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FireMageStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FIREMAGESTRATEGY_H -#define _PLAYERBOT_FIREMAGESTRATEGY_H +#ifndef PLAYERBOTS_FIREMAGESTRATEGY_H +#define PLAYERBOTS_FIREMAGESTRATEGY_H #include "GenericMageStrategy.h" @@ -28,4 +29,5 @@ public: void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "firestarter"; } }; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.cpp index 4448a4349..a9f268f1b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.cpp @@ -1,40 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FrostFireMageStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class FrostFireMageStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - FrostFireMageStrategyActionNodeFactory() - { - creators["frostfire bolt"] = &frostfire_bolt; - creators["fire blast"] = &fire_blast; - creators["pyroblast"] = &pyroblast; - creators["combustion"] = &combustion; - creators["icy veins"] = &icy_veins; - creators["scorch"] = &scorch; - creators["living bomb"] = &living_bomb; - } - -private: - static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", {}, {}, {}); } - static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); } - static ActionNode* pyroblast(PlayerbotAI*) { return new ActionNode("pyroblast", {}, {}, {}); } - static ActionNode* combustion(PlayerbotAI*) { return new ActionNode("combustion", {}, {}, {}); } - static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", {}, {}, {}); } - static ActionNode* scorch(PlayerbotAI*) { return new ActionNode("scorch", {}, {}, {}); } - static ActionNode* living_bomb(PlayerbotAI*) { return new ActionNode("living bomb", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== FrostFireMageStrategy::FrostFireMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) { - actionNodeFactories.Add(new FrostFireMageStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.h b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.h index 00bbf03a5..c086292aa 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostFireMageStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FROSTFIREMAGESTRATEGY_H -#define _PLAYERBOT_FROSTFIREMAGESTRATEGY_H +#ifndef PLAYERBOTS_FROSTFIREMAGESTRATEGY_H +#define PLAYERBOTS_FROSTFIREMAGESTRATEGY_H #include "GenericMageStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp index 34ed81dba..d23e7dd87 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.cpp @@ -1,47 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FrostMageStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class FrostMageStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - FrostMageStrategyActionNodeFactory() - { - creators["cold snap"] = &cold_snap; - creators["ice barrier"] = &ice_barrier; - creators["summon water elemental"] = &summon_water_elemental; - creators["deep freeze"] = &deep_freeze; - creators["icy veins"] = &icy_veins; - creators["frostbolt"] = &frostbolt; - creators["ice lance"] = &ice_lance; - creators["fire blast"] = &fire_blast; - creators["fireball"] = &fireball; - creators["frostfire bolt"] = &frostfire_bolt; - } - -private: - static ActionNode* cold_snap(PlayerbotAI*) { return new ActionNode("cold snap", {}, {}, {}); } - static ActionNode* ice_barrier(PlayerbotAI*) { return new ActionNode("ice barrier", {}, {}, {}); } - static ActionNode* summon_water_elemental(PlayerbotAI*) { return new ActionNode("summon water elemental", {}, {}, {}); } - static ActionNode* deep_freeze(PlayerbotAI*) { return new ActionNode("deep freeze", {}, {}, {}); } - static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", {}, {}, {}); } - static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", {}, {}, {}); } - static ActionNode* ice_lance(PlayerbotAI*) { return new ActionNode("ice lance", {}, {}, {}); } - static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); } - static ActionNode* fireball(PlayerbotAI*) { return new ActionNode("fireball", {}, {}, {}); } - static ActionNode* frostfire_bolt(PlayerbotAI*) { return new ActionNode("frostfire bolt", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== FrostMageStrategy::FrostMageStrategy(PlayerbotAI* botAI) : GenericMageStrategy(botAI) { - actionNodeFactories.Add(new FrostMageStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.h b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.h index 9b027fed8..be0c13f19 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/FrostMageStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FROSTMAGESTRATEGY_H -#define _PLAYERBOT_FROSTMAGESTRATEGY_H +#ifndef PLAYERBOTS_FROSTMAGESTRATEGY_H +#define PLAYERBOTS_FROSTMAGESTRATEGY_H #include "GenericMageStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.cpp index eab98ea5a..574302fba 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericMageNonCombatStrategy.h" @@ -12,28 +13,10 @@ class GenericMageNonCombatStrategyActionNodeFactory : public NamedObjectFactory< public: GenericMageNonCombatStrategyActionNodeFactory() { - creators["molten armor"] = &molten_armor; - creators["mage armor"] = &mage_armor; creators["ice armor"] = &ice_armor; } private: - static ActionNode* molten_armor([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("molten armor", - /*P*/ {}, - /*A*/ { NextAction("mage armor") }, - /*C*/ {}); - } - - static ActionNode* mage_armor([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("mage armor", - /*P*/ {}, - /*A*/ { NextAction("ice armor") }, - /*C*/ {}); - } - static ActionNode* ice_armor([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("ice armor", @@ -65,7 +48,7 @@ void MageBuffManaStrategy::InitTriggers(std::vector& triggers) void MageBuffDpsStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("mage armor", { NextAction("molten armor", 19.0f) })); + triggers.push_back(new TriggerNode("molten armor", { NextAction("molten armor", 19.0f) })); } void MageBuffStrategy::InitTriggers(std::vector& triggers) diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.h index d31261f66..910c52d6a 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICMAGENONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICMAGENONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICMAGENONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICMAGENONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp index 75a63efa4..fd68e46b8 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericMageStrategy.h" @@ -8,6 +9,18 @@ #include "Playerbots.h" #include "RangedCombatStrategy.h" +namespace +{ +constexpr uint32 SPELL_CONJURE_MANA_SAPPHIRE = 42985; +constexpr uint32 SPELL_CONJURE_MANA_EMERALD = 27101; +constexpr uint32 SPELL_CONJURE_MANA_RUBY = 10054; +constexpr uint32 SPELL_CONJURE_MANA_CITRINE = 10053; +constexpr uint32 SPELL_CONJURE_MANA_JADE = 3552; +constexpr uint32 SPELL_CONJURE_MANA_AGATE = 759; +constexpr uint32 SPELL_FROSTFIRE_BOLT = 44614; +constexpr uint32 SPELL_ICE_SHARDS = 15047; +} + class GenericMageStrategyActionNodeFactory : public NamedObjectFactory { public: @@ -15,16 +28,7 @@ public: { creators["frostbolt"] = &frostbolt; creators["frostfire bolt"] = &frostfire_bolt; - creators["ice lance"] = &ice_lance; - creators["fire blast"] = &fire_blast; creators["scorch"] = &scorch; - creators["frost nova"] = &frost_nova; - creators["cone of cold"] = &cone_of_cold; - creators["icy veins"] = &icy_veins; - creators["combustion"] = &combustion; - creators["evocation"] = &evocation; - creators["dragon's breath"] = &dragons_breath; - creators["blast wave"] = &blast_wave; creators["remove curse"] = &remove_curse; creators["remove curse on party"] = &remove_curse_on_party; creators["fireball"] = &fireball; @@ -47,22 +51,6 @@ private: /*C*/ {}); } - static ActionNode* ice_lance([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("ice lance", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* fire_blast([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("fire blast", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* scorch([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("scorch", @@ -71,62 +59,6 @@ private: /*C*/ {}); } - static ActionNode* frost_nova([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("frost nova", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* cone_of_cold([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("cone of cold", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* icy_veins([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("icy veins", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* combustion([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("combustion", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* evocation([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("evocation", - /*P*/ {}, - /*A*/ { NextAction("mana potion") }, - /*C*/ {}); - } - - static ActionNode* dragons_breath([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("dragon's breath", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - - static ActionNode* blast_wave([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("blast wave", - /*P*/ {}, - /*A*/ {}, - /*C*/ {}); - } - static ActionNode* remove_curse([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("remove curse", @@ -174,20 +106,19 @@ void GenericMageStrategy::InitTriggers(std::vector& triggers) // Mana Threshold Triggers Player* bot = botAI->GetBot(); - if (bot->HasSpell(42985)) // Mana Sapphire + if (bot->HasSpell(SPELL_CONJURE_MANA_SAPPHIRE)) triggers.push_back(new TriggerNode("high mana", { NextAction("use mana sapphire", 90.0f) })); - else if (bot->HasSpell(27101)) // Mana Emerald + else if (bot->HasSpell(SPELL_CONJURE_MANA_EMERALD)) triggers.push_back(new TriggerNode("high mana", { NextAction("use mana emerald", 90.0f) })); - else if (bot->HasSpell(10054)) // Mana Ruby + else if (bot->HasSpell(SPELL_CONJURE_MANA_RUBY)) triggers.push_back(new TriggerNode("high mana", { NextAction("use mana ruby", 90.0f) })); - else if (bot->HasSpell(10053)) // Mana Citrine + else if (bot->HasSpell(SPELL_CONJURE_MANA_CITRINE)) triggers.push_back(new TriggerNode("high mana", { NextAction("use mana citrine", 90.0f) })); - else if (bot->HasSpell(3552)) // Mana Jade + else if (bot->HasSpell(SPELL_CONJURE_MANA_JADE)) triggers.push_back(new TriggerNode("high mana", { NextAction("use mana jade", 90.0f) })); - else if (bot->HasSpell(759)) // Mana Agate + else if (bot->HasSpell(SPELL_CONJURE_MANA_AGATE)) triggers.push_back(new TriggerNode("high mana", { NextAction("use mana agate", 90.0f) })); - triggers.push_back(new TriggerNode("medium mana", { NextAction("mana potion", 90.0f) })); triggers.push_back(new TriggerNode("low mana", { NextAction("evocation", 90.0f) })); // Counterspell / Spellsteal Triggers @@ -206,15 +137,15 @@ void MageBoostStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Arcane + if (tab == MAGE_TAB_ARCANE) { triggers.push_back(new TriggerNode("arcane power", { NextAction("arcane power", 29.0f) })); triggers.push_back(new TriggerNode("icy veins", { NextAction("icy veins", 28.5f) })); triggers.push_back(new TriggerNode("mirror image", { NextAction("mirror image", 28.0f) })); } - else if (tab == 1) + else if (tab == MAGE_TAB_FIRE) { - if (bot->HasSpell(44614) /*Frostfire Bolt*/ && bot->HasAura(15047) /*Ice Shards*/) + if (bot->HasSpell(SPELL_FROSTFIRE_BOLT) && bot->HasAura(SPELL_ICE_SHARDS)) { // Frostfire triggers.push_back(new TriggerNode("combustion", { NextAction("combustion", 18.0f) })); triggers.push_back(new TriggerNode("icy veins", { NextAction("icy veins", 17.5f) })); @@ -226,7 +157,7 @@ void MageBoostStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("mirror image", { NextAction("mirror image", 17.5f) })); } } - else if (tab == 2) // Frost + else if (tab == MAGE_TAB_FROST) // Frost { triggers.push_back(new TriggerNode("cold snap", { NextAction("cold snap", 28.0f) })); triggers.push_back(new TriggerNode("icy veins", { NextAction("icy veins", 27.5f) })); @@ -237,6 +168,14 @@ void MageBoostStrategy::InitTriggers(std::vector& triggers) void MageCcStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("polymorph", { NextAction("polymorph", 30.0f) })); + + Player* bot = botAI->GetBot(); + int tab = AiFactory::GetPlayerSpecTab(bot); + if (tab == MAGE_TAB_FIRE) + { + triggers.push_back(new TriggerNode("enemy too close for spell", {NextAction("dragon's breath", ACTION_INTERRUPT + 1)})); + triggers.push_back(new TriggerNode("enemy is close", {NextAction("blast wave", ACTION_INTERRUPT)})); + } } void MageAoeStrategy::InitTriggers(std::vector& triggers) @@ -246,15 +185,14 @@ void MageAoeStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Arcane + if (tab == MAGE_TAB_ARCANE) { triggers.push_back(new TriggerNode("flamestrike active and medium aoe", { NextAction("blizzard", 24.0f) })); triggers.push_back(new TriggerNode("medium aoe", { NextAction("flamestrike", 23.0f), NextAction("blizzard", 22.0f) })); - triggers.push_back(new TriggerNode("light aoe", { NextAction("arcane explosion", 21.0f) })); } - else if (tab == 1) // Fire and Frostfire + else if (tab == MAGE_TAB_FIRE) { triggers.push_back( new TriggerNode("medium aoe", { @@ -267,7 +205,7 @@ void MageAoeStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("firestarter", { NextAction("flamestrike", 40.0f) })); triggers.push_back(new TriggerNode("living bomb on attackers", { NextAction("living bomb on attackers", 21.0f) })); } - else if (tab == 2) // Frost + else if (tab == MAGE_TAB_FROST) { triggers.push_back(new TriggerNode("flamestrike active and medium aoe", { NextAction("blizzard", 24.0f) })); triggers.push_back(new TriggerNode("medium aoe", { diff --git a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.h b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.h index 939147949..543547167 100644 --- a/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Mage/Strategy/GenericMageStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICMAGESTRATEGY_H -#define _PLAYERBOT_GENERICMAGESTRATEGY_H +#ifndef PLAYERBOTS_GENERICMAGESTRATEGY_H +#define PLAYERBOTS_GENERICMAGESTRATEGY_H #include "CombatStrategy.h" #include "RangedCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.cpp deleted file mode 100644 index 726f83546..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ /dev/null @@ -1,502 +0,0 @@ -/* - * 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 "PaladinActions.h" - -#include "AiFactory.h" -#include "Event.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "SharedDefines.h" -#include "../../../../../src/server/scripts/Spells/spell_generic.cpp" -#include "GenericBuffUtils.h" -#include "Group.h" -#include "ObjectAccessor.h" - -using ai::buff::MakeAuraQualifierForBuff; -using ai::buff::UpgradeToGroupIfAppropriate; - -// Helper : detect tank role on the target (player bot or not) return true if spec is tank or if the bot have tank strategies (bear/tank/tank face). -static inline bool IsTankRole(Player* p) -{ - if (!p) return false; - if (p->HasTankSpec()) - return true; - if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p)) - { - if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) || - otherAI->HasStrategy("tank", BOT_STATE_COMBAT) || - otherAI->HasStrategy("tank face", BOT_STATE_NON_COMBAT) || - otherAI->HasStrategy("tank face", BOT_STATE_COMBAT) || - otherAI->HasStrategy("bear", BOT_STATE_NON_COMBAT) || - otherAI->HasStrategy("bear", BOT_STATE_COMBAT)) - return true; - } - return false; -} - -// Added for solo paladin patch : determine if he's the only paladin on party -static inline bool IsOnlyPaladinInGroup(Player* bot) -{ - if (!bot) return false; - Group* g = bot->GetGroup(); - if (!g) return true; // solo - uint32 pals = 0u; - for (GroupReference* r = g->GetFirstMember(); r; r = r->next()) - { - Player* p = r->GetSource(); - if (!p || !p->IsInWorld()) continue; - if (p->getClass() == CLASS_PALADIN) ++pals; - } - return pals == 1u; -} - -static inline bool GroupHasTankOfClass(Group* g, uint8 classId) -{ - return GroupHasTankOfClass(g, static_cast(classId)); -} - -inline std::string const GetActualBlessingOfMight(Unit* target) -{ - if (!target->ToPlayer()) - { - return "blessing of might"; - } - - int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); - switch (target->getClass()) - { - case CLASS_MAGE: - case CLASS_PRIEST: - case CLASS_WARLOCK: - return "blessing of wisdom"; - break; - case CLASS_SHAMAN: - if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION) - { - return "blessing of wisdom"; - } - break; - case CLASS_DRUID: - if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE) - { - return "blessing of wisdom"; - } - break; - case CLASS_PALADIN: - if (tab == PALADIN_TAB_HOLY) - { - return "blessing of wisdom"; - } - break; - } - - return "blessing of might"; -} - -inline std::string const GetActualBlessingOfWisdom(Unit* target) -{ - if (!target->ToPlayer()) - { - return "blessing of might"; - } - int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); - switch (target->getClass()) - { - case CLASS_WARRIOR: - case CLASS_ROGUE: - case CLASS_DEATH_KNIGHT: - case CLASS_HUNTER: - return "blessing of might"; - break; - case CLASS_SHAMAN: - if (tab == SHAMAN_TAB_ENHANCEMENT) - { - return "blessing of might"; - } - break; - case CLASS_DRUID: - if (tab == DRUID_TAB_FERAL) - { - return "blessing of might"; - } - break; - case CLASS_PALADIN: - if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION) - { - return "blessing of might"; - } - break; - } - - return "blessing of wisdom"; -} - -inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot) -{ - if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) - return ""; - - Player* tp = target->ToPlayer(); - if (!tp) - return ""; - - if (auto* ai = GET_PLAYERBOT_AI(bot)) - { - if (Unit* mt = ai->GetAiObjectContext()->GetValue("main tank")->Get()) - { - if (mt == target) - return "blessing of sanctuary"; - } - } - - if (tp->HasTankSpec()) - return "blessing of sanctuary"; - - return ""; -} - -Value* CastBlessingOnPartyAction::GetTargetValue() -{ - - return context->GetValue("party member without aura", MakeAuraQualifierForBuff(spell)); -} - -bool CastBlessingOfMightAction::Execute(Event /*event*/) -{ - Unit* target = GetTarget(); - if (!target) - return false; - - std::string castName = GetActualBlessingOfMight(target); - auto RP = ai::chat::MakeGroupAnnouncer(bot); - - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); - return botAI->CastSpell(castName, target); -} - -Value* CastBlessingOfMightOnPartyAction::GetTargetValue() -{ - return context->GetValue( - "party member without aura", - "blessing of might,greater blessing of might,blessing of wisdom,greater blessing of wisdom,blessing of sanctuary,greater blessing of sanctuary" - ); -} - -bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/) -{ - Unit* target = GetTarget(); - if (!target) - return false; - - std::string castName = GetActualBlessingOfMight(target); - auto RP = ai::chat::MakeGroupAnnouncer(bot); - - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); - return botAI->CastSpell(castName, target); -} - -bool CastBlessingOfWisdomAction::Execute(Event /*event*/) -{ - Unit* target = GetTarget(); - if (!target) - return false; - - std::string castName = GetActualBlessingOfWisdom(target); - auto RP = ai::chat::MakeGroupAnnouncer(bot); - - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); - return botAI->CastSpell(castName, target); -} - -Value* CastBlessingOfWisdomOnPartyAction::GetTargetValue() -{ - return context->GetValue( - "party member without aura", - "blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,blessing of sanctuary,greater blessing of sanctuary" - ); -} - -bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/) -{ - Unit* target = GetTarget(); - if (!target) - return false; - - Player* targetPlayer = target->ToPlayer(); - - if (Group* g = bot->GetGroup()) - if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) - return false; - - if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) && - targetPlayer && IsTankRole(targetPlayer)) - { - LOG_DEBUG("playerbots", "[Wisdom/bmana] Skip tank {} (Kings only)", target->GetName()); - return false; - } - - std::string castName = GetActualBlessingOfWisdom(target); - if (castName.empty()) - return false; - - auto RP = ai::chat::MakeGroupAnnouncer(bot); - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); - return botAI->CastSpell(castName, target); -} - -Value* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue() -{ - return context->GetValue( - "party member without aura", - "blessing of sanctuary,greater blessing of sanctuary" - ); -} - -bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) -{ - if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY)) - return false; - - Unit* target = GetTarget(); - if (!target) - { - // Fallback: GetTarget() can be null if no one needs a buff. - // Keep a valid pointer for the checks/logs that follow. - target = bot; - } - - Player* targetPlayer = target ? target->ToPlayer() : nullptr; - - // Small helpers to check relevant auras - const auto HasKingsAura = [&](Unit* u) -> bool { - return botAI->HasAura("blessing of kings", u) || botAI->HasAura("greater blessing of kings", u); - }; - const auto HasSanctAura = [&](Unit* u) -> bool { - return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u); - }; - - if (Group* g = bot->GetGroup()) - { - if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) - { - LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring"); - target = bot; - targetPlayer = bot->ToPlayer(); - } - } - - if (Player* self = bot->ToPlayer()) - { - bool selfHasSanct = HasSanctAura(self); - bool needSelf = IsTankRole(self) && !selfHasSanct; - - LOG_DEBUG("playerbots", "[Sanct] {} isTank={} selfHasSanct={} needSelf={}", - bot->GetName(), IsTankRole(self), selfHasSanct, needSelf); - - if (needSelf) - { - target = self; - targetPlayer = self; - } - } - - // Try to re-target a valid tank in group if needed - bool targetOk = false; - if (targetPlayer) - { - bool hasSanct = HasSanctAura(targetPlayer); - targetOk = IsTankRole(targetPlayer) && !hasSanct; - } - - if (!targetOk) - { - if (Group* g = bot->GetGroup()) - { - for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next()) - { - Player* p = gref->GetSource(); - if (!p) continue; - if (!p->IsInWorld() || !p->IsAlive()) continue; - if (!IsTankRole(p)) continue; - - bool hasSanct = HasSanctAura(p); - if (!hasSanct) - { - target = p; // prioritize this tank - targetPlayer = p; - targetOk = true; - break; - } - } - } - } - - { - bool hasKings = HasKingsAura(target); - bool hasSanct = HasSanctAura(target); - bool knowSanct = bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY); - LOG_DEBUG("playerbots", "[Sanct] Final target={} hasKings={} hasSanct={} knowSanct={}", - target->GetName(), hasKings, hasSanct, knowSanct); - } - - std::string castName = GetActualBlessingOfSanctuary(target, bot); - // If internal logic didn't recognize the tank (e.g., bear druid), force single-target Sanctuary - if (castName.empty()) - { - if (targetPlayer) - { - if (IsTankRole(targetPlayer)) - castName = "blessing of sanctuary"; // force single-target - else - return false; - } - else - return false; - } - if (targetPlayer && !IsTankRole(targetPlayer)) - { - auto RP = ai::chat::MakeGroupAnnouncer(bot); - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); - } - else - { - castName = "blessing of sanctuary"; - } - - bool ok = botAI->CastSpell(castName, target); - LOG_DEBUG("playerbots", "[Sanct] Cast {} on {} result={}", castName, target->GetName(), ok); - return ok; -} - -Value* CastBlessingOfKingsOnPartyAction::GetTargetValue() -{ - return context->GetValue( - "party member without aura", - "blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary" - ); -} - -bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/) -{ - Unit* target = GetTarget(); - if (!target) - return false; - - Group* g = bot->GetGroup(); - if (!g) - return false; - - // Added for patch solo paladin, never buff itself to not remove his sanctuary buff - if (botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT) && IsOnlyPaladinInGroup(bot)) - { - if (target->GetGUID() == bot->GetGUID()) - { - LOG_DEBUG("playerbots", "[Kings/bstats-solo] Skip self to keep Sanctuary on {}", bot->GetName()); - return false; - } - } - // End solo paladin patch - - Player* targetPlayer = target->ToPlayer(); - if (targetPlayer && !g->IsMember(targetPlayer->GetGUID())) - return false; - - const bool hasBmana = botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT); - const bool hasBstats = botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT); - - if (hasBmana) - { - if (!targetPlayer || !IsTankRole(targetPlayer)) - { - LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName()); - return false; - } - } - - if (targetPlayer) - { - const bool isTank = IsTankRole(targetPlayer); - const bool hasSanctFromMe = - target->HasAura(SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) || - target->HasAura(SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID()); - const bool hasSanctAny = - botAI->HasAura("blessing of sanctuary", target) || - botAI->HasAura("greater blessing of sanctuary", target); - - if (isTank && hasSanctFromMe) - { - LOG_DEBUG("playerbots", "[Kings] Skip: {} has my Sanctuary and is a tank", target->GetName()); - return false; - } - - if (hasBstats && isTank && hasSanctAny) - { - LOG_DEBUG("playerbots", "[Kings] Skip (bstats): {} already has Sanctuary and is a tank", target->GetName()); - return false; - } - } - - std::string castName = "blessing of kings"; - - bool allowGreater = true; - - if (hasBmana) - allowGreater = false; - - if (allowGreater && hasBstats && targetPlayer) - { - switch (targetPlayer->getClass()) - { - case CLASS_WARRIOR: - case CLASS_PALADIN: - case CLASS_DRUID: - case CLASS_DEATH_KNIGHT: - allowGreater = false; - break; - default: - break; - } - } - - if (allowGreater) - { - auto RP = ai::chat::MakeGroupAnnouncer(bot); - castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP); - } - - return botAI->CastSpell(castName, target); -} - -bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); } - -Value* CastTurnUndeadAction::GetTargetValue() { return context->GetValue("cc target", getName()); } - -Unit* CastRighteousDefenseAction::GetTarget() -{ - Unit* current_target = AI_VALUE(Unit*, "current target"); - if (!current_target) - { - return NULL; - } - return current_target->GetVictim(); -} - -bool CastDivineSacrificeAction::isUseful() -{ - return GetTarget() && (GetTarget() != nullptr) && CastSpellAction::isUseful() && - !botAI->HasAura("divine guardian", GetTarget(), false, false, -1, true); -} - -bool CastCancelDivineSacrificeAction::Execute(Event /*event*/) -{ - botAI->RemoveAura("divine sacrifice"); - return true; -} - -bool CastCancelDivineSacrificeAction::isUseful() -{ - return botAI->HasAura("divine sacrifice", GetTarget(), false, true, -1, true); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.cpp new file mode 100644 index 000000000..357db7587 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.cpp @@ -0,0 +1,606 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PaladinActions.h" + +#include "AiFactory.h" +#include "Event.h" +#include "GenericBuffUtils.h" +#include "PaladinGreaterBlessingAction.h" +#include "PaladinHelper.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +static bool IsBlessingTargetCandidate(Player* bot, Player* player) +{ + if (!player || !player->IsAlive() || player->GetMapId() != bot->GetMapId()) + return false; + + if (player->IsGameMaster()) + return false; + + return bot->GetDistance(player) < sPlayerbotAIConfig.spellDistance * 2 && + bot->IsWithinLOS(player->GetPositionX(), player->GetPositionY(), + player->GetPositionZ()); +} + +static bool HasBlessingAura( + PlayerbotAI* botAI, Unit* target, std::initializer_list auraNames) +{ + for (char const* auraName : auraNames) + { + if (botAI->HasAura(auraName, target)) + return true; + } + + return false; +} + +static bool IsGreaterBlessingMode(Player* bot) +{ + return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()); +} + +template +static Unit* FindBlessingTarget( + Player* bot, PlayerbotAI* botAI, Predicate&& predicate) +{ + std::vector masters; + std::vector healers; + std::vector tanks; + std::vector others; + + Player* master = botAI->GetMaster(); + auto addPlayer = [&](Player* player) + { + if (!IsBlessingTargetCandidate(bot, player)) + return; + + if (player == master) + masters.push_back(player); + else if (botAI->IsHeal(player)) + healers.push_back(player); + else if (botAI->IsTank(player)) + tanks.push_back(player); + else + others.push_back(player); + }; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + addPlayer(ref->GetSource()); + } + else + { + addPlayer(bot); + } + + std::vector*> orderedLists = { + &masters, &healers, &tanks, &others }; + for (std::vector* players : orderedLists) + { + for (Player* player : *players) + { + if (predicate(player)) + return player; + } + } + + return nullptr; +} + +static inline bool IsTankRole(Player* player) +{ + if (!player) + return false; + + if (player->HasTankSpec()) + return true; + + if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(player)) + { + if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) || + otherAI->HasStrategy("tank", BOT_STATE_COMBAT) || + otherAI->HasStrategy("tank face", BOT_STATE_NON_COMBAT) || + otherAI->HasStrategy("tank face", BOT_STATE_COMBAT) || + otherAI->HasStrategy("bear", BOT_STATE_NON_COMBAT) || + otherAI->HasStrategy("bear", BOT_STATE_COMBAT)) + return true; + } + + return false; +} + +static inline bool IsOnlyPaladinInGroup(Player* bot) +{ + if (!bot) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return true; + + uint32 paladins = 0u; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* player = ref->GetSource(); + if (!player || !player->IsInWorld()) continue; + if (player->getClass() == CLASS_PALADIN) ++paladins; + } + + return paladins == 1u; +} + +inline std::string const GetActualBlessingOfMight(Unit* target) +{ + if (!target->ToPlayer()) + return "blessing of might"; + + uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); + switch (target->getClass()) + { + case CLASS_MAGE: + case CLASS_PRIEST: + case CLASS_WARLOCK: + return "blessing of wisdom"; + break; + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION) + return "blessing of wisdom"; + break; + case CLASS_DRUID: + if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE) + return "blessing of wisdom"; + break; + case CLASS_PALADIN: + if (tab == PALADIN_TAB_HOLY) + return "blessing of wisdom"; + break; + } + + return "blessing of might"; +} + +inline std::string const GetActualBlessingOfWisdom(Unit* target) +{ + if (!target->ToPlayer()) + return "blessing of might"; + + uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer()); + switch (target->getClass()) + { + case CLASS_WARRIOR: + case CLASS_ROGUE: + case CLASS_DEATH_KNIGHT: + case CLASS_HUNTER: + return "blessing of might"; + break; + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ENHANCEMENT) + return "blessing of might"; + break; + case CLASS_DRUID: + if (tab == DRUID_TAB_FERAL) + return "blessing of might"; + break; + case CLASS_PALADIN: + if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION) + return "blessing of might"; + break; + } + + return "blessing of wisdom"; +} + +inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot) +{ + if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + return ""; + + Player* targetPlayer = target->ToPlayer(); + if (!targetPlayer) + return ""; + + if (auto* botAI = GET_PLAYERBOT_AI(bot)) + { + if (Unit* mainTank = + botAI->GetAiObjectContext()->GetValue("main tank")->Get()) + { + if (mainTank == target) + return "blessing of sanctuary"; + } + } + + if (targetPlayer->HasTankSpec()) + return "blessing of sanctuary"; + + return ""; +} + +Unit* CastBlessingOfMightOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + return !HasBlessingAura(botAI, player, + { "blessing of might", "greater blessing of might", + "blessing of wisdom", "greater blessing of wisdom", + "blessing of sanctuary", "greater blessing of sanctuary" }); + }); +} + +bool CastBlessingOfMightAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + if (!target) + return false; + + std::string castName = GetActualBlessingOfMight(target); + return botAI->CastSpell(castName, target); +} + +Value* CastBlessingOfMightOnPartyAction::GetTargetValue() +{ + return context->GetValue( + "party member without aura", + "blessing of might,greater blessing of might,blessing of wisdom," + "greater blessing of wisdom,blessing of sanctuary," + "greater blessing of sanctuary" + ); +} + +bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/) +{ + if (IsGreaterBlessingMode(bot)) + return false; + + Unit* target = GetTarget(); + if (!target) + return false; + + std::string castName = GetActualBlessingOfMight(target); + return botAI->CastSpell(castName, target); +} + +bool CastBlessingOfWisdomAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + if (!target) + return false; + + std::string castName = GetActualBlessingOfWisdom(target); + return botAI->CastSpell(castName, target); +} + +Unit* CastBlessingOfWisdomOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && IsTankRole(player)) + return false; + + return !HasBlessingAura(botAI, player, + { "blessing of might", "greater blessing of might", + "blessing of wisdom", "greater blessing of wisdom", + "blessing of sanctuary", "greater blessing of sanctuary" }); + }); +} + +Value* CastBlessingOfWisdomOnPartyAction::GetTargetValue() +{ + return context->GetValue( + "party member without aura", + "blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might," + "blessing of sanctuary,greater blessing of sanctuary" + ); +} + +bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/) +{ + if (IsGreaterBlessingMode(bot)) + return false; + + Unit* target = GetTarget(); + if (!target) + return false; + + Player* targetPlayer = target->ToPlayer(); + + if (Group* group = bot->GetGroup()) + if (targetPlayer && !group->IsMember(targetPlayer->GetGUID())) + return false; + + if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && + targetPlayer && IsTankRole(targetPlayer)) + return false; + + std::string castName = GetActualBlessingOfWisdom(target); + if (castName.empty()) + return false; + + return botAI->CastSpell(castName, target); +} + +Value* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue() +{ + return context->GetValue( + "party member without aura", + "blessing of sanctuary,greater blessing of sanctuary" + ); +} + +bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/) +{ + if (IsGreaterBlessingMode(bot)) + return false; + + if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + return false; + + Unit* target = GetTarget(); + if (!target) + target = bot; + + Player* targetPlayer = target ? target->ToPlayer() : nullptr; + + const auto HasSanctAura = [&](Unit* unit) -> bool { + return botAI->HasAura("blessing of sanctuary", unit) || + botAI->HasAura("greater blessing of sanctuary", unit); + }; + + if (Group* group = bot->GetGroup()) + { + if (targetPlayer && !group->IsMember(targetPlayer->GetGUID())) + { + target = bot; + targetPlayer = bot->ToPlayer(); + } + } + + if (Player* self = bot->ToPlayer()) + { + bool selfHasSanct = HasSanctAura(self); + bool needSelf = IsTankRole(self) && !selfHasSanct; + + if (needSelf) + { + target = self; + targetPlayer = self; + } + } + + bool targetOk = false; + if (targetPlayer) + { + bool hasSanct = HasSanctAura(targetPlayer); + targetOk = IsTankRole(targetPlayer) && !hasSanct; + } + + if (!targetOk) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* player = ref->GetSource(); + if (!player) continue; + if (!player->IsInWorld() || !player->IsAlive()) continue; + if (!IsTankRole(player)) continue; + + bool hasSanct = HasSanctAura(player); + if (!hasSanct) + { + target = player; + targetPlayer = player; + targetOk = true; + break; + } + } + } + } + + if (GetActualBlessingOfSanctuary(target, bot).empty()) + { + if (targetPlayer) + { + if (IsTankRole(targetPlayer)) + return botAI->CastSpell("blessing of sanctuary", target); + else + return false; + } + else + return false; + } + + return botAI->CastSpell("blessing of sanctuary", target); +} + +Unit* CastBlessingOfSanctuaryOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + return nullptr; + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + return IsTankRole(player) && + !HasBlessingAura(botAI, player, + { "blessing of sanctuary", "greater blessing of sanctuary" }); + }); +} + +Value* CastBlessingOfKingsOnPartyAction::GetTargetValue() +{ + return context->GetValue( + "party member without aura", + "blessing of kings,greater blessing of kings," + "blessing of sanctuary,greater blessing of sanctuary" + ); +} + +Unit* CastBlessingOfKingsOnPartyAction::GetTarget() +{ + if (IsGreaterBlessingMode(bot)) + return nullptr; + + const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT); + const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT); + const bool onlyPaladinInGroup = IsOnlyPaladinInGroup(bot); + + return FindBlessingTarget(bot, botAI, [&](Player* player) + { + const bool isTank = IsTankRole(player); + const bool hasKingsOrSanct = HasBlessingAura(botAI, player, + { "blessing of kings", "greater blessing of kings", + "blessing of sanctuary", "greater blessing of sanctuary" }); + + if (hasKingsOrSanct) + return false; + + if (hasBwisdom) + return isTank; + + if (hasBkings) + { + if (isTank) + return false; + + if (onlyPaladinInGroup && player == bot) + return false; + } + + return true; + }); +} + +bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/) +{ + if (IsGreaterBlessingMode(bot)) + return false; + + Unit* target = GetTarget(); + if (!target) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT) && + IsOnlyPaladinInGroup(bot)) + { + if (target->GetGUID() == bot->GetGUID()) + return false; + } + + Player* targetPlayer = target->ToPlayer(); + if (targetPlayer && !group->IsMember(targetPlayer->GetGUID())) + return false; + + const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT); + const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT); + + if (hasBwisdom && (!targetPlayer || !IsTankRole(targetPlayer))) + return false; + + if (targetPlayer) + { + const bool isTank = IsTankRole(targetPlayer); + const bool hasSanctFromMe = + target->HasAura(ai::paladin::SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) || + target->HasAura(ai::paladin::SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID()); + const bool hasSanctAny = + botAI->HasAura("blessing of sanctuary", target) || + botAI->HasAura("greater blessing of sanctuary", target); + + if (isTank && hasSanctFromMe) + return false; + + if (hasBkings && isTank && hasSanctAny) + return false; + } + + return botAI->CastSpell("blessing of kings", target); +} + +bool CastSealSpellAction::isUseful() +{ + return AI_VALUE2(bool, "combat", "self target"); +} + +Value* CastTurnUndeadAction::GetTargetValue() +{ + return context->GetValue("cc target", getName()); +} + +Unit* CastHandOfFreedomOnPartyAction::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = + selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return CastBuffSpellAction::GetTarget(); +} + +Value* CastHandOfFreedomOnPartyAction::GetTargetValue() +{ + return context->GetValue("party member snared target"); +} + +bool CastHandOfFreedomOnPartyAction::isUseful() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + return CastBuffSpellAction::isUseful() && + !ai::paladin::HasAnyPaladinHandFromCaster(target, bot); +} + +Unit* CastRighteousDefenseAction::GetTarget() +{ + Unit* current_target = AI_VALUE(Unit*, "current target"); + if (!current_target) + return nullptr; + + return current_target->GetVictim(); +} + +bool CastDivineSacrificeAction::isUseful() +{ + return GetTarget() && (GetTarget() != nullptr) && CastSpellAction::isUseful() && + !botAI->HasAura("divine guardian", GetTarget(), false, false, -1, true); +} + +bool CastCancelDivineSacrificeAction::Execute(Event /*event*/) +{ + botAI->RemoveAura("divine sacrifice"); + return true; +} + +bool CastCancelDivineSacrificeAction::isUseful() +{ + return botAI->HasAura("divine sacrifice", GetTarget(), false, true, -1, true); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.h similarity index 86% rename from modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.h rename to modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.h index 3bacc8846..88fd1283c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinActions.h @@ -1,17 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PALADINACTIONS_H -#define _PLAYERBOT_PALADINACTIONS_H +#ifndef PLAYERBOTS_PALADINACTIONS_H +#define PLAYERBOTS_PALADINACTIONS_H #include "AiObject.h" #include "GenericSpellActions.h" -#include "SharedDefines.h" - -class PlayerbotAI; -class Unit; // seals BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness"); @@ -88,25 +85,13 @@ public: bool Execute(Event event) override; }; -class CastBlessingOnPartyAction : public BuffOnPartyAction -{ -public: - CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) : BuffOnPartyAction(botAI, name), name(name) - { - } - - Value* GetTargetValue() override; - -private: - std::string name; -}; - class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction { public: CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {} std::string const getName() override { return "blessing of might on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -125,6 +110,7 @@ public: CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {} std::string const getName() override { return "blessing of wisdom on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -135,12 +121,13 @@ public: CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {} }; -class CastBlessingOfKingsOnPartyAction : public CastBlessingOnPartyAction +class CastBlessingOfKingsOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : CastBlessingOnPartyAction(botAI, "blessing of kings") {} + CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of kings") {} std::string const getName() override { return "blessing of kings on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; // added for Sanctuary priority bool Execute(Event event) override; // added for 2 paladins logic }; @@ -154,11 +141,10 @@ public: class CastBlessingOfSanctuaryOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") - { - } + CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {} std::string const getName() override { return "blessing of sanctuary on party"; } + Unit* GetTarget() override; Value* GetTargetValue() override; bool Execute(Event event) override; }; @@ -173,18 +159,14 @@ class CastHolyShockOnPartyAction : public HealPartyMemberAction { public: CastHolyShockOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) - { - } + : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) {} }; class CastHolyLightOnPartyAction : public HealPartyMemberAction { public: CastHolyLightOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) - { - } + : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) {} }; class CastFlashOfLightAction : public CastHealingSpellAction @@ -197,9 +179,7 @@ class CastFlashOfLightOnPartyAction : public HealPartyMemberAction { public: CastFlashOfLightOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) - { - } + : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) {} }; class CastLayOnHandsAction : public CastHealingSpellAction @@ -357,9 +337,7 @@ class CastHammerOfJusticeOnEnemyHealerAction : public CastSpellOnEnemyHealerActi { public: CastHammerOfJusticeOnEnemyHealerAction(PlayerbotAI* botAI) - : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") - { - } + : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") {} }; class CastHammerOfJusticeSnareAction : public CastSnareSpellAction @@ -368,6 +346,12 @@ public: CastHammerOfJusticeSnareAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "hammer of justice") {} }; +class CastSenseUndeadAction : public CastBuffSpellAction +{ +public: + CastSenseUndeadAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "sense undead") {} +}; + class CastTurnUndeadAction : public CastBuffSpellAction { public: @@ -376,30 +360,43 @@ public: Value* GetTargetValue() override; }; +class CastHandOfFreedomOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport +{ +public: + CastHandOfFreedomOnPartyAction(PlayerbotAI* botAI) + : CastBuffSpellAction(botAI, "hand of freedom"), PartyMemberActionNameSupport("hand of freedom") {} + + Unit* GetTarget() override; + Value* GetTargetValue() override; + std::string const GetTargetName() override { return "party member snared target"; } + std::string const getName() override { return PartyMemberActionNameSupport::getName(); } + bool isUseful() override; +}; + PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection"); class CastDivinePleaAction : public CastBuffSpellAction { public: - CastDivinePleaAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "divine plea") {} + CastDivinePleaAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "divine plea") {} }; class ShieldOfRighteousnessAction : public CastMeleeSpellAction { public: - ShieldOfRighteousnessAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "shield of righteousness") {} + ShieldOfRighteousnessAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shield of righteousness") {} }; class CastBeaconOfLightOnMainTankAction : public BuffOnMainTankAction { public: - CastBeaconOfLightOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "beacon of light", true) {} + CastBeaconOfLightOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "beacon of light", true) {} }; class CastSacredShieldOnMainTankAction : public BuffOnMainTankAction { public: - CastSacredShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "sacred shield", false) {} + CastSacredShieldOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "sacred shield", false) {} }; class CastAvengingWrathAction : public CastBuffSpellAction @@ -428,4 +425,5 @@ public: bool Execute(Event event) override; bool isUseful() override; }; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.cpp new file mode 100644 index 000000000..dc01b0dbd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.cpp @@ -0,0 +1,1202 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PaladinGreaterBlessingAction.h" + +#include "AiObjectContext.h" +#include "AiFactory.h" +#include "Event.h" +#include "GenericBuffUtils.h" +#include "PaladinHelper.h" +#include "Playerbots.h" +#include "SharedDefines.h" +#include "SpellAuraEffects.h" +#include "Value.h" + +#include +#include + +namespace ai::gbless +{ +namespace +{ + constexpr uint32 GREATER_BLESSING_ASSIGNMENT_CACHE_MS = 4 * 1000; + constexpr uint32 GREATER_BLESSING_PENDING_ASSIGNMENT_CACHE_MS = 500; + constexpr uint8 MAX_BLESSING_SLOTS = 4; + constexpr uint8 MAX_CLASS_ID = 12; + + constexpr size_t BaseBlessingCategoryCount = MAX_BLESSING_SLOTS; + + enum PaladinBlessingCapability : uint8 + { + PALADIN_BLESSING_CAPABILITY_NONE = 0, + PALADIN_BLESSING_CAPABILITY_IMPROVED_WISDOM = 1 << 0, + PALADIN_BLESSING_CAPABILITY_IMPROVED_MIGHT = 1 << 1, + PALADIN_BLESSING_CAPABILITY_SANCTUARY = 1 << 2 + }; + + constexpr size_t BaseBlessingIndex(BaseBlessingCategory category) + { + return static_cast(static_cast(category) - static_cast(BASE_MIGHT)); + } + + bool UsesRoleBucket(uint8 classId) + { + switch (classId) + { + case CLASS_WARRIOR: + case CLASS_DEATH_KNIGHT: + case CLASS_SHAMAN: + case CLASS_PALADIN: + case CLASS_DRUID: + return true; + default: + return false; + } + } + + bool IsSameBucket( + CachedBlessingBucketAssignment const& left, + CachedBlessingBucketAssignment const& right) + { + return left.classId == right.classId && + left.byRole == right.byRole && + (!left.byRole || left.role == right.role); + } + + uint8 GetPaladinBlessingCapabilities(Player* player) + { + if (!player) + return PALADIN_BLESSING_CAPABILITY_NONE; + + uint8 capabilities = PALADIN_BLESSING_CAPABILITY_NONE; + if (player->HasAura(SPELL_IMPROVED_MIGHT_R1) || + player->HasAura(SPELL_IMPROVED_MIGHT_R2)) + { + capabilities |= PALADIN_BLESSING_CAPABILITY_IMPROVED_MIGHT; + } + if (player->HasAura(SPELL_IMPROVED_WISDOM_R1) || + player->HasAura(SPELL_IMPROVED_WISDOM_R2)) + { + capabilities |= PALADIN_BLESSING_CAPABILITY_IMPROVED_WISDOM; + } + if (player->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY)) + capabilities |= PALADIN_BLESSING_CAPABILITY_SANCTUARY; + + return capabilities; + } + + int TalentScore(Player* player) + { + uint8 const capabilities = GetPaladinBlessingCapabilities(player); + int score = 0; + + if (capabilities & PALADIN_BLESSING_CAPABILITY_IMPROVED_MIGHT) + score += 2; + + if (capabilities & PALADIN_BLESSING_CAPABILITY_IMPROVED_WISDOM) + score += 1; + + return score; + } + + int TalentMatchScore(Player* player, BaseBlessingCategory category) + { + if (!player) + return std::numeric_limits::min() / 4; + + uint8 const capabilities = GetPaladinBlessingCapabilities(player); + + if (category == BASE_SANCTUARY) + { + if (!(capabilities & PALADIN_BLESSING_CAPABILITY_SANCTUARY)) + return std::numeric_limits::min() / 4; + + return 2; + } + + if (category == BASE_MIGHT && + (capabilities & PALADIN_BLESSING_CAPABILITY_IMPROVED_MIGHT)) + { + return 1; + } + if (category == BASE_WISDOM && + (capabilities & PALADIN_BLESSING_CAPABILITY_IMPROVED_WISDOM)) + { + return 1; + } + + return 0; + } + + void SelectActivePaladinPool( + std::vector& botPaladins) + { + std::sort(botPaladins.begin(), botPaladins.end(), + [](Player* left, Player* right) + { + return left->GetGUID() < right->GetGUID(); + }); + + std::vector selectedPaladins; + selectedPaladins.reserve(botPaladins.size()); + std::vector selected(botPaladins.size(), false); + + auto const selectFirstWithCapability = [&](uint8 capability) + { + for (size_t index = 0; index < botPaladins.size(); ++index) + { + if (selected[index]) + continue; + + if (!(GetPaladinBlessingCapabilities(botPaladins[index]) & capability)) + continue; + + selected[index] = true; + selectedPaladins.push_back(botPaladins[index]); + return; + } + }; + + selectFirstWithCapability(PALADIN_BLESSING_CAPABILITY_SANCTUARY); + selectFirstWithCapability(PALADIN_BLESSING_CAPABILITY_IMPROVED_MIGHT); + selectFirstWithCapability(PALADIN_BLESSING_CAPABILITY_IMPROVED_WISDOM); + + for (size_t index = 0; index < botPaladins.size(); ++index) + { + if (selected[index]) + continue; + + selectedPaladins.push_back(botPaladins[index]); + } + + botPaladins = std::move(selectedPaladins); + } + + struct DesiredBlessingSet + { + std::array ordered = {}; + std::array wants = {}; + uint8 count = 0; + }; + + struct PresentBucket + { + uint8 classId = 0; + RoleProfile role = ROLE_CASTER; + bool byRole = false; + uint8 memberCount = 0; + DesiredBlessingSet desired; + }; + + DesiredBlessingSet BuildDesiredBlessingSet( + RoleProfile role, uint8 paladinCount, bool anySanctuaryAvailable) + { + DesiredBlessingSet desired; + + auto const& priority = BASE_BLESSING_PRIORITIES[role]; + uint8 requestedCount = std::min(paladinCount, MAX_BLESSING_SLOTS); + + for (uint8 index = 0; + index < MAX_BLESSING_SLOTS && desired.count < requestedCount; + ++index) + { + BaseBlessingCategory category = priority.priorities[index]; + if (category == BASE_NONE) + continue; + + if (category == BASE_SANCTUARY && !anySanctuaryAvailable) + category = BASE_KINGS; + + if (category == BASE_NONE || desired.wants[BaseBlessingIndex(category)]) + continue; + + desired.ordered[desired.count++] = category; + desired.wants[BaseBlessingIndex(category)] = true; + } + + return desired; + } + + std::vector OrderedCommonBases( + std::vector const& classBuckets, + std::array const& commonBases) + { + std::vector ordered; + + for (PresentBucket const* bucket : classBuckets) + { + for (uint8 index = 0; index < bucket->desired.count; ++index) + { + BaseBlessingCategory category = bucket->desired.ordered[index]; + if (!commonBases[BaseBlessingIndex(category)]) + continue; + + if (std::find(ordered.begin(), ordered.end(), category) == ordered.end()) + ordered.push_back(category); + } + } + + return ordered; + } + + bool ComputeBestOwners( + std::vector const& categories, + std::vector const& botPaladins, + std::vector const& candidatePaladins, + std::vector& outOwners, + int* outScore = nullptr) + { + outOwners.clear(); + if (categories.empty()) + { + if (outScore) + *outScore = 0; + return true; + } + + std::vector currentOwners(categories.size(), -1); + std::vector bestOwners(categories.size(), -1); + std::vector used(candidatePaladins.size(), false); + int bestScore = std::numeric_limits::min(); + int bestTalentCost = std::numeric_limits::max(); + bool found = false; + + auto search = [&](auto&& self, size_t position, int score, int talentCost) -> void + { + if (position >= categories.size()) + { + if (!found || score > bestScore || + (score == bestScore && talentCost < bestTalentCost) || + (score == bestScore && + talentCost == bestTalentCost && + std::lexicographical_compare(currentOwners.begin(), currentOwners.end(), + bestOwners.begin(), bestOwners.end()))) + { + found = true; + bestScore = score; + bestTalentCost = talentCost; + bestOwners = currentOwners; + } + return; + } + + for (size_t candidateIndex = 0; + candidateIndex < candidatePaladins.size(); ++candidateIndex) + { + if (used[candidateIndex]) + continue; + + int paladinIndex = candidatePaladins[candidateIndex]; + int matchScore = TalentMatchScore(botPaladins[paladinIndex], categories[position]); + if (matchScore <= std::numeric_limits::min() / 8) + continue; + + used[candidateIndex] = true; + currentOwners[position] = paladinIndex; + self(self, position + 1, score + matchScore, + talentCost + TalentScore(botPaladins[paladinIndex])); + currentOwners[position] = -1; + used[candidateIndex] = false; + } + }; + + search(search, 0, 0, 0); + + if (!found) + return false; + + outOwners = std::move(bestOwners); + if (outScore) + *outScore = bestScore; + return true; + } + + struct ClassPlanPreference + { + std::array slotCoverage = {}; + std::array matchedSlotCoverage = {}; + uint8 commonCount = 0; + int ownerScore = std::numeric_limits::min(); + int commonTalentCost = std::numeric_limits::max(); + uint8 selectedMask = 0; + std::vector classWideOwners; + bool valid = false; + }; + + uint8 OrderedIndexForCategory(PresentBucket const* bucket, BaseBlessingCategory category) + { + for (uint8 index = 0; index < bucket->desired.count; ++index) + { + if (bucket->desired.ordered[index] == category) + return index; + } + + return MAX_BLESSING_SLOTS; + } + + bool IsBetterClassPlanPreference( + ClassPlanPreference const& candidate, ClassPlanPreference const& best) + { + if (!best.valid) + return true; + + for (uint8 index = 0; index < MAX_BLESSING_SLOTS; ++index) + { + if (candidate.slotCoverage[index] != best.slotCoverage[index]) + return candidate.slotCoverage[index] > best.slotCoverage[index]; + } + + for (uint8 index = 0; index < MAX_BLESSING_SLOTS; ++index) + { + if (candidate.matchedSlotCoverage[index] != best.matchedSlotCoverage[index]) + return candidate.matchedSlotCoverage[index] > best.matchedSlotCoverage[index]; + } + + if (candidate.commonCount != best.commonCount) + return candidate.commonCount > best.commonCount; + + if (candidate.ownerScore != best.ownerScore) + return candidate.ownerScore > best.ownerScore; + + if (candidate.commonTalentCost != best.commonTalentCost) + return candidate.commonTalentCost < best.commonTalentCost; + + if (std::lexicographical_compare(candidate.classWideOwners.begin(), + candidate.classWideOwners.end(), + best.classWideOwners.begin(), + best.classWideOwners.end())) + return true; + + return candidate.selectedMask < best.selectedMask; + } + + bool ComputeOwnersForClassPlan( + std::vector const& classWideBases, + std::vector> const& exclusiveBasesByBucket, + std::vector const& botPaladins, + std::vector const& allPaladins, + std::vector& outClassWideOwners, + std::vector>& outExclusiveOwnersByBucket, + int& outOwnerScore, + int& outCommonTalentCost) + { + outClassWideOwners.clear(); + outExclusiveOwnersByBucket.clear(); + outOwnerScore = std::numeric_limits::min(); + outCommonTalentCost = std::numeric_limits::max(); + + std::vector currentClassWideOwners(classWideBases.size(), -1); + std::vector bestClassWideOwners(classWideBases.size(), -1); + std::vector used(allPaladins.size(), false); + std::vector> bestExclusiveOwnersByBucket(exclusiveBasesByBucket.size()); + bool found = false; + + auto search = [&](auto&& self, size_t position, int commonScore, int commonTalentCost) -> void + { + if (position >= classWideBases.size()) + { + std::vector availablePaladins; + availablePaladins.reserve(allPaladins.size()); + for (size_t candidateIndex = 0; candidateIndex < allPaladins.size(); ++candidateIndex) + { + if (!used[candidateIndex]) + availablePaladins.push_back(allPaladins[candidateIndex]); + } + + int totalOwnerScore = commonScore; + std::vector> exclusiveOwnersByBucket(exclusiveBasesByBucket.size()); + for (size_t bucketIndex = 0; bucketIndex < exclusiveBasesByBucket.size(); ++bucketIndex) + { + int exclusiveScore = 0; + if (!ComputeBestOwners( + exclusiveBasesByBucket[bucketIndex], botPaladins, availablePaladins, + exclusiveOwnersByBucket[bucketIndex], &exclusiveScore)) + { + return; + } + + totalOwnerScore += exclusiveScore; + } + + if (!found || totalOwnerScore > outOwnerScore || + (totalOwnerScore == outOwnerScore && commonTalentCost < outCommonTalentCost) || + (totalOwnerScore == outOwnerScore && + commonTalentCost == outCommonTalentCost && + std::lexicographical_compare(currentClassWideOwners.begin(), currentClassWideOwners.end(), + bestClassWideOwners.begin(), bestClassWideOwners.end()))) + { + found = true; + outOwnerScore = totalOwnerScore; + outCommonTalentCost = commonTalentCost; + bestClassWideOwners = currentClassWideOwners; + bestExclusiveOwnersByBucket = std::move(exclusiveOwnersByBucket); + } + + return; + } + + for (size_t candidateIndex = 0; candidateIndex < allPaladins.size(); ++candidateIndex) + { + if (used[candidateIndex]) + continue; + + int const paladinIndex = allPaladins[candidateIndex]; + int const matchScore = TalentMatchScore(botPaladins[paladinIndex], classWideBases[position]); + if (matchScore <= std::numeric_limits::min() / 8) + continue; + + used[candidateIndex] = true; + currentClassWideOwners[position] = paladinIndex; + self(self, position + 1, commonScore + matchScore, + commonTalentCost + TalentScore(botPaladins[paladinIndex])); + currentClassWideOwners[position] = -1; + used[candidateIndex] = false; + } + }; + + search(search, 0, 0, 0); + + if (!found) + return false; + + outClassWideOwners = std::move(bestClassWideOwners); + outExclusiveOwnersByBucket = std::move(bestExclusiveOwnersByBucket); + return true; + } + + bool ComputeBestClassAssignments( + std::vector const& classBuckets, + std::vector const& botPaladins, + std::vector const& allPaladins, + std::vector& outClassWideOwners, + std::vector>& outExclusiveOwnersByBucket, + std::vector& outClassWideBases, + std::vector>& outExclusiveBasesByBucket) + { + outClassWideOwners.clear(); + outExclusiveOwnersByBucket.clear(); + outClassWideBases.clear(); + outExclusiveBasesByBucket.clear(); + + uint8 const categoryMaskLimit = + static_cast(1u << (static_cast(BASE_SANCTUARY) + 1u)); + uint8 commonUnionMask = 0; + ClassPlanPreference bestPreference; + std::vector bestClassWideOwners; + std::vector> bestExclusiveOwnersByBucket; + std::vector bestClassWideBases; + std::vector> bestExclusiveBasesByBucket; + + for (uint8 baseValue = BASE_MIGHT; baseValue <= BASE_SANCTUARY; ++baseValue) + { + BaseBlessingCategory category = static_cast(baseValue); + if (std::all_of(classBuckets.begin(), classBuckets.end(), + [&](PresentBucket const* bucket) + { + return bucket->desired.wants[BaseBlessingIndex(category)]; + })) + { + commonUnionMask |= static_cast(1u << static_cast(category)); + } + } + + for (uint8 promotedMask = 0; promotedMask < categoryMaskLimit; ++promotedMask) + { + if ((promotedMask & commonUnionMask) != promotedMask) + continue; + + ClassPlanPreference candidatePreference; + candidatePreference.selectedMask = promotedMask; + + std::vector> exclusiveBasesByBucket(classBuckets.size()); + + for (size_t bucketIndex = 0; bucketIndex < classBuckets.size(); ++bucketIndex) + { + PresentBucket const* bucket = classBuckets[bucketIndex]; + for (uint8 index = 0; index < bucket->desired.count; ++index) + { + BaseBlessingCategory category = bucket->desired.ordered[index]; + candidatePreference.slotCoverage[index] += bucket->memberCount; + + if (!(promotedMask & static_cast(1u << static_cast(category)))) + exclusiveBasesByBucket[bucketIndex].push_back(category); + } + } + + std::array promotedCommonBases = {}; + for (uint8 baseValue = BASE_MIGHT; baseValue <= BASE_SANCTUARY; ++baseValue) + { + BaseBlessingCategory category = static_cast(baseValue); + promotedCommonBases[BaseBlessingIndex(category)] = + (promotedMask & static_cast(1u << static_cast(category))) != 0; + } + + std::vector classWideBases = + OrderedCommonBases(classBuckets, promotedCommonBases); + + std::vector classWideOwners; + std::vector> exclusiveOwnersByBucket; + int ownerScore = 0; + int commonTalentCost = 0; + if (!ComputeOwnersForClassPlan( + classWideBases, exclusiveBasesByBucket, botPaladins, allPaladins, + classWideOwners, exclusiveOwnersByBucket, ownerScore, commonTalentCost)) + { + continue; + } + + candidatePreference.commonCount = static_cast(classWideBases.size()); + candidatePreference.ownerScore = ownerScore; + candidatePreference.commonTalentCost = commonTalentCost; + candidatePreference.classWideOwners = classWideOwners; + + for (size_t index = 0; index < classWideBases.size(); ++index) + { + Player* owner = botPaladins[classWideOwners[index]]; + BaseBlessingCategory category = classWideBases[index]; + if (TalentMatchScore(owner, category) <= 0) + continue; + + for (PresentBucket const* bucket : classBuckets) + { + uint8 orderedIndex = OrderedIndexForCategory(bucket, category); + if (orderedIndex >= MAX_BLESSING_SLOTS) + continue; + + candidatePreference.matchedSlotCoverage[orderedIndex] += bucket->memberCount; + } + } + + for (size_t bucketIndex = 0; bucketIndex < classBuckets.size(); ++bucketIndex) + { + PresentBucket const* bucket = classBuckets[bucketIndex]; + auto const& exclusiveBases = exclusiveBasesByBucket[bucketIndex]; + auto const& exclusiveOwners = exclusiveOwnersByBucket[bucketIndex]; + + for (size_t index = 0; index < exclusiveBases.size(); ++index) + { + Player* owner = botPaladins[exclusiveOwners[index]]; + BaseBlessingCategory category = exclusiveBases[index]; + if (TalentMatchScore(owner, category) <= 0) + continue; + + uint8 orderedIndex = OrderedIndexForCategory(bucket, category); + if (orderedIndex >= MAX_BLESSING_SLOTS) + continue; + + candidatePreference.matchedSlotCoverage[orderedIndex] += bucket->memberCount; + } + } + + candidatePreference.valid = true; + + if (!IsBetterClassPlanPreference(candidatePreference, bestPreference)) + continue; + + bestPreference = candidatePreference; + bestClassWideOwners = std::move(classWideOwners); + bestExclusiveOwnersByBucket = std::move(exclusiveOwnersByBucket); + bestClassWideBases = std::move(classWideBases); + bestExclusiveBasesByBucket = std::move(exclusiveBasesByBucket); + } + + if (!bestPreference.valid) + return false; + + outClassWideOwners = std::move(bestClassWideOwners); + outExclusiveOwnersByBucket = std::move(bestExclusiveOwnersByBucket); + outClassWideBases = std::move(bestClassWideBases); + outExclusiveBasesByBucket = std::move(bestExclusiveBasesByBucket); + return true; + } + + void AddUniqueAssignment( + std::vector& assignments, + CachedBlessingBucketAssignment const& assignment) + { + auto existing = std::find_if(assignments.begin(), assignments.end(), + [&](CachedBlessingBucketAssignment const& cachedAssignment) + { + return cachedAssignment.blessing == assignment.blessing && + IsSameBucket(cachedAssignment, assignment); + }); + + if (existing == assignments.end()) + assignments.push_back(assignment); + } + + bool ComputeGreaterBlessingAssignments( + PlayerbotAI* botAI, std::vector& outAssignments) + { + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + if (!IsEligibleGroupForAutoBlessings(group)) + return false; + + std::vector botPaladins; + struct RaidMember + { + Player* player; + RoleProfile role; + }; + std::vector raidMembers; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* player = ref->GetSource(); + if (!player || !player->IsInWorld()) + continue; + + if (player->getClass() == CLASS_PALADIN && GET_PLAYERBOT_AI(player)) + botPaladins.push_back(player); + + // Important: keep dead members in the assignment model so revive does + // not temporarily delete an entire blessing bucket from the cache. + raidMembers.push_back({player, ResolveRoleProfile(player)}); + } + + if (botPaladins.empty()) + return false; + + SelectActivePaladinPool(botPaladins); + + uint8 activePaladinCount = + std::min(static_cast(botPaladins.size()), MAX_BLESSING_SLOTS); + + bool anySanctuaryAvailable = false; + for (uint8 paladinIndex = 0; paladinIndex < activePaladinCount; ++paladinIndex) + { + if (GetPaladinBlessingCapabilities(botPaladins[paladinIndex]) & + PALADIN_BLESSING_CAPABILITY_SANCTUARY) + { + anySanctuaryAvailable = true; + break; + } + } + + int mySlot = -1; + for (size_t i = 0; i < botPaladins.size(); ++i) + { + if (botPaladins[i]->GetGUID() == bot->GetGUID()) + { + mySlot = static_cast(i); + break; + } + } + if (mySlot < 0 || mySlot >= activePaladinCount) + return false; + + std::vector buckets; + buckets.reserve(raidMembers.size()); + + for (auto const& member : raidMembers) + { + uint8 classId = member.player->getClass(); + if (classId >= MAX_CLASS_ID) + continue; + + PresentBucket bucket; + bucket.classId = classId; + bucket.role = member.role; + bucket.byRole = UsesRoleBucket(classId); + bucket.memberCount = 1; + bucket.desired = BuildDesiredBlessingSet( + member.role, activePaladinCount, anySanctuaryAvailable); + + if (!bucket.byRole) + bucket.role = ROLE_CASTER; + + if (!bucket.desired.count) + continue; + + auto existing = std::find_if(buckets.begin(), buckets.end(), + [&](PresentBucket const& other) + { + return other.classId == bucket.classId && + other.byRole == bucket.byRole && + (!bucket.byRole || other.role == bucket.role); + }); + + if (existing == buckets.end()) + buckets.push_back(bucket); + else + ++existing->memberCount; + } + + if (buckets.empty()) + return false; + + std::vector allPaladins; + allPaladins.reserve(activePaladinCount); + for (uint8 paladinIndex = 0; paladinIndex < activePaladinCount; ++paladinIndex) + allPaladins.push_back(paladinIndex); + + outAssignments.clear(); + + for (uint8 classId = 0; classId < MAX_CLASS_ID; ++classId) + { + std::vector classBuckets; + for (PresentBucket const& bucket : buckets) + { + if (bucket.classId == classId) + classBuckets.push_back(&bucket); + } + + if (classBuckets.empty()) + continue; + + std::vector classWideOwners; + std::vector> exclusiveOwnersByBucket; + std::vector classWideBases; + std::vector> exclusiveBasesByBucket; + if (!ComputeBestClassAssignments( + classBuckets, botPaladins, allPaladins, + classWideOwners, exclusiveOwnersByBucket, classWideBases, + exclusiveBasesByBucket)) + { + continue; + } + + for (size_t index = 0; index < classWideBases.size(); ++index) + { + if (classWideOwners[index] != mySlot) + continue; + + CachedBlessingBucketAssignment assignment; + assignment.classId = classId; + assignment.role = classBuckets.front()->role; + assignment.byRole = false; + assignment.blessing = ToGreaterVariant(classWideBases[index]); + AddUniqueAssignment(outAssignments, assignment); + } + + for (size_t bucketIndex = 0; bucketIndex < classBuckets.size(); ++bucketIndex) + { + PresentBucket const* bucket = classBuckets[bucketIndex]; + auto const& exclusiveBases = exclusiveBasesByBucket[bucketIndex]; + auto const& exclusiveOwners = exclusiveOwnersByBucket[bucketIndex]; + + for (size_t index = 0; index < exclusiveBases.size(); ++index) + { + if (exclusiveOwners[index] != mySlot) + continue; + + CachedBlessingBucketAssignment assignment; + assignment.classId = bucket->classId; + assignment.role = bucket->role; + assignment.byRole = true; + assignment.blessing = ToSingleVariant(exclusiveBases[index]); + AddUniqueAssignment(outAssignments, assignment); + } + } + } + + return !outAssignments.empty(); + } + + class GreaterBlessingAssignmentsValue : public CalculatedValue + { + public: + GreaterBlessingAssignmentsValue(PlayerbotAI* botAI) + : CalculatedValue( + botAI, "greater blessing assignments", GREATER_BLESSING_ASSIGNMENT_CACHE_MS) {} + + protected: + CachedBlessingAssignments Calculate() override + { + CachedBlessingAssignments cached; + + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + cached.groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + std::vector assignments; + if (!ComputeGreaterBlessingAssignments(botAI, assignments)) + return cached; + + cached.valid = true; + cached.assignments = std::move(assignments); + + return cached; + } + }; + +} + +UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI) +{ + return new GreaterBlessingAssignmentsValue(botAI); +} + +bool IsEligibleGroupForAutoBlessings(Group const* group) +{ + if (!group) + return false; + + switch (sPlayerbotAIConfig.autoGreaterBlessings) + { + case AutoPartyBuffMode::RAID_ONLY: + return group->isRaidGroup(); + case AutoPartyBuffMode::GROUP_OR_RAID: + return true; + case AutoPartyBuffMode::DISABLED: + default: + return false; + } +} + +static bool HasMyExactBlessing(PlayerbotAI* botAI, Unit* target, BlessingType type) +{ + std::string name = BlessingSpellName(type); + if (name.empty()) + return false; + + return botAI->HasAura(name.c_str(), target, false, true); +} + +static int32 GetAuraStrength(Aura const* aura, AuraType auraType) +{ + if (!aura) + return 0; + + int32 amount = 0; + for (uint8 effect = 0; effect < MAX_SPELL_EFFECTS; ++effect) + { + AuraEffect* auraEffect = aura->GetEffect(effect); + if (!auraEffect || auraEffect->GetAuraType() != auraType) + continue; + + amount = std::max(amount, auraEffect->GetAmount()); + } + + return amount; +} + +static int32 GetExistingBlessingStrength( + PlayerbotAI* botAI, Unit* target, BaseBlessingCategory category) +{ + if (category != BASE_MIGHT && category != BASE_WISDOM) + return 0; + + AuraType auraType = + category == BASE_MIGHT ? SPELL_AURA_MOD_ATTACK_POWER : SPELL_AURA_MOD_POWER_REGEN; + int32 strongestAmount = 0; + + for (BlessingType type : {ToSingleVariant(category), ToGreaterVariant(category)}) + { + Aura* aura = botAI->GetAura(BlessingSpellName(type), target); + strongestAmount = std::max(strongestAmount, GetAuraStrength(aura, auraType)); + } + + return strongestAmount; +} + +static bool HasSameFamilyBlessing( + PlayerbotAI* botAI, Unit* target, BaseBlessingCategory category) +{ + for (BlessingType type : {ToSingleVariant(category), ToGreaterVariant(category)}) + { + if (botAI->HasAura(BlessingSpellName(type), target)) + return true; + } + + return false; +} + +static int32 GetBlessingCastStrength(Player* caster, BlessingType type, uint32 spellId) +{ + if (!caster || !spellId) + return 0; + + BaseBlessingCategory category = BaseBlessingOf(type); + if (category != BASE_MIGHT && category != BASE_WISDOM) + return 0; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return 0; + + AuraType auraType = + category == BASE_MIGHT ? SPELL_AURA_MOD_ATTACK_POWER : SPELL_AURA_MOD_POWER_REGEN; + int32 amount = 0; + for (uint8 effect = 0; effect < MAX_SPELL_EFFECTS; ++effect) + { + if (spellInfo->Effects[effect].ApplyAuraName != auraType) + continue; + + amount = std::max(amount, spellInfo->Effects[effect].BasePoints + 1); + } + + if (amount <= 0) + return 0; + + switch (category) + { + case BASE_MIGHT: + if (caster->HasAura(SPELL_IMPROVED_MIGHT_R2)) + return amount * 125 / 100; + if (caster->HasAura(SPELL_IMPROVED_MIGHT_R1)) + return amount * 112 / 100; + break; + case BASE_WISDOM: + if (caster->HasAura(SPELL_IMPROVED_WISDOM_R2)) + return amount * 120 / 100; + if (caster->HasAura(SPELL_IMPROVED_WISDOM_R1)) + return amount * 110 / 100; + break; + default: + break; + } + + return amount; +} + +static bool HasEquivalentOrStrongerSameFamilyBlessing( + PlayerbotAI* botAI, Unit* target, BlessingType castType, uint32 spellId) +{ + BaseBlessingCategory category = BaseBlessingOf(castType); + if (category != BASE_MIGHT && category != BASE_WISDOM) + return HasSameFamilyBlessing(botAI, target, category); + + int32 castStrength = GetBlessingCastStrength(botAI->GetBot(), castType, spellId); + if (castStrength <= 0) + return false; + + return GetExistingBlessingStrength(botAI, target, category) >= castStrength; +} + +static bool MatchesBucket(Player* player, CachedBlessingBucketAssignment const& assignment) +{ + if (!player || player->getClass() != assignment.classId) + return false; + + return !assignment.byRole || ResolveRoleProfile(player) == assignment.role; +} + +static Player* FindMissingBlessingTarget( + PlayerbotAI* botAI, CachedBlessingBucketAssignment const& assignment, + BlessingType castType, uint32 spellId, std::string const& spellName) +{ + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* player = ref->GetSource(); + if (!player || !player->IsInWorld() || !player->IsAlive()) + continue; + + if (!(player->IsInSameGroupWith(bot) || player->IsInSameRaidWith(bot))) + continue; + + if (!MatchesBucket(player, assignment) || + HasMyExactBlessing(botAI, player, castType) || + HasEquivalentOrStrongerSameFamilyBlessing(botAI, player, castType, spellId) || + !botAI->CanCastSpell(spellName, player)) + { + continue; + } + + return player; + } + + return nullptr; +} + +static bool GetCachedAssignments( + AiObjectContext* context, Group* group, + std::vector& outAssignments) +{ + uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + Value* cacheValue = + context->GetValue("greater blessing assignments"); + if (!cacheValue) + return false; + + CachedBlessingAssignments cachedAssignments = cacheValue->Get(); + if (cachedAssignments.groupKey != groupKey) + { + cacheValue->Reset(); + cachedAssignments = cacheValue->Get(); + } + + if (!cachedAssignments.valid || cachedAssignments.groupKey != groupKey) + return false; + + outAssignments = cachedAssignments.assignments; + return true; +} + +static bool FindPendingAssignmentFromAssignments( + PlayerbotAI* botAI, + std::vector const& assignments, + GreaterBlessingPlayerAssignment& outAssignment, + std::string& outSpellName) +{ + Player* bot = botAI->GetBot(); + AiObjectContext* aiContext = botAI->GetAiObjectContext(); + if (!aiContext) + return false; + + for (auto const& assigned : assignments) + { + if (assigned.blessing == BLESSING_NONE) + continue; + + BlessingType castType = assigned.blessing; + std::string spellName = BlessingSpellName(castType); + if (spellName.empty()) + continue; + + if (IsGreaterVariant(castType)) + { + uint32 spellId = aiContext->GetValue("spell id", spellName)->Get(); + if (spellId && ai::buff::HasRequiredReagents(bot, spellId)) + { + ai::buff::ClearMissingBuffReagentNotice(botAI, spellName); + } + else + { + castType = ToSingleVariant(castType); + spellName = BlessingSpellName(castType); + if (spellName.empty()) + continue; + } + } + + uint32 spellId = aiContext->GetValue("spell id", spellName)->Get(); + if (!spellId) + continue; + + Player* target = FindMissingBlessingTarget( + botAI, assigned, castType, spellId, spellName); + if (!target) + continue; + + outAssignment = {target, assigned.blessing}; + outSpellName = spellName; + return true; + } + + return false; +} + +class GreaterBlessingPendingAssignmentValue : public CalculatedValue +{ +public: + GreaterBlessingPendingAssignmentValue(PlayerbotAI* botAI) + : CalculatedValue( + botAI, "greater blessing pending assignment", + GREATER_BLESSING_PENDING_ASSIGNMENT_CACHE_MS) {} + +protected: + CachedPendingBlessingAssignment Calculate() override + { + CachedPendingBlessingAssignment cached; + + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + cached.groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + std::vector assignments; + if (!GetCachedAssignments(this->context, group, assignments)) + return cached; + + if (!FindPendingAssignmentFromAssignments( + botAI, assignments, cached.assignment, cached.spellName)) + { + return cached; + } + + cached.valid = true; + return cached; + } +}; + +UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI) +{ + return new GreaterBlessingPendingAssignmentValue(botAI); +} + +} + +CastGreaterBlessingAssignmentAction::CastGreaterBlessingAssignmentAction( + PlayerbotAI* botAI) : Action(botAI, "cast greater blessing assignment") {} + +bool CastGreaterBlessingAssignmentAction::isUseful() +{ + return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()); +} + +bool CastGreaterBlessingAssignmentAction::HasPendingAssignment() +{ + ai::gbless::GreaterBlessingPlayerAssignment assignment; + std::string spellName; + + return FindPendingAssignment(assignment, spellName); +} + +bool CastGreaterBlessingAssignmentAction::Execute(Event /*event*/) +{ + ai::gbless::GreaterBlessingPlayerAssignment assignment; + std::string spellName; + if (!FindPendingAssignment(assignment, spellName)) + return false; + + AiObjectContext* aiContext = botAI->GetAiObjectContext(); + if (!aiContext) + return false; + + std::string missingReagentGroupName; + if (ai::gbless::IsGreaterVariant(assignment.blessing)) + { + std::string const assignedSpellName = ai::gbless::BlessingSpellName(assignment.blessing); + uint32 const assignedSpellId = aiContext->GetValue("spell id", assignedSpellName)->Get(); + if (assignedSpellId && spellName != assignedSpellName && !ai::buff::HasRequiredReagents(bot, assignedSpellId)) + missingReagentGroupName = assignedSpellName; + } + + if (!botAI->HasSpell(spellName)) + return false; + + if (!botAI->CastSpell(spellName, assignment.player)) + return false; + + if (!missingReagentGroupName.empty()) + ai::buff::TryAnnounceMissingBuffReagents(botAI, spellName, missingReagentGroupName); + + return true; +} + +bool CastGreaterBlessingAssignmentAction::FindPendingAssignment( + ai::gbless::GreaterBlessingPlayerAssignment& outAssignment, std::string& outSpellName) +{ + Group* group = bot->GetGroup(); + uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + Value* pendingValue = + context->GetValue("greater blessing pending assignment"); + if (!pendingValue) + return false; + + ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get(); + if (pendingAssignment.groupKey != groupKey) + { + pendingValue->Reset(); + pendingAssignment = pendingValue->Get(); + } + + if (!pendingAssignment.valid || pendingAssignment.groupKey != groupKey) + return false; + + outAssignment = pendingAssignment.assignment; + outSpellName = pendingAssignment.spellName; + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.h new file mode 100644 index 000000000..7100f9eb7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Actions/PaladinGreaterBlessingAction.h @@ -0,0 +1,268 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PALADINGREATERBLESSINGACTION_H +#define PLAYERBOTS_PALADINGREATERBLESSINGACTION_H + +#include +#include +#include + +#include "Action.h" +#include "AiFactory.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +class UntypedValue; + +namespace ai::gbless +{ + enum RoleProfile : uint8 + { + ROLE_CASTER = 0, + ROLE_PHYSICAL_DPS = 1, + ROLE_HYBRID_DPS = 2, + ROLE_DRUID_TANK = 3, + ROLE_WARRIOR_DK_TANK = 4, + ROLE_PALADIN_TANK = 5, + + ROLE_PROFILE_COUNT = 6 + }; + + enum BlessingType : uint8 + { + BLESSING_NONE = 0, + BLESSING_MIGHT_SINGLE = 1, + BLESSING_MIGHT_GREATER = 2, + BLESSING_WISDOM_SINGLE = 3, + BLESSING_WISDOM_GREATER = 4, + BLESSING_KINGS_SINGLE = 5, + BLESSING_KINGS_GREATER = 6, + BLESSING_SANCTUARY_SINGLE = 7, + BLESSING_SANCTUARY_GREATER = 8 + }; + + enum BaseBlessingCategory : uint8 + { + BASE_NONE = 0, + BASE_MIGHT = 1, + BASE_WISDOM = 2, + BASE_KINGS = 3, + BASE_SANCTUARY = 4 + }; + + inline constexpr BaseBlessingCategory BaseBlessingOf(BlessingType type) + { + switch (type) + { + case BLESSING_MIGHT_SINGLE: + case BLESSING_MIGHT_GREATER: return BASE_MIGHT; + case BLESSING_WISDOM_SINGLE: + case BLESSING_WISDOM_GREATER: return BASE_WISDOM; + case BLESSING_KINGS_SINGLE: + case BLESSING_KINGS_GREATER: return BASE_KINGS; + case BLESSING_SANCTUARY_SINGLE: + case BLESSING_SANCTUARY_GREATER: return BASE_SANCTUARY; + default: return BASE_NONE; + } + } + + inline constexpr bool IsSingleVariant(BlessingType type) + { + return type == BLESSING_MIGHT_SINGLE || type == BLESSING_WISDOM_SINGLE || + type == BLESSING_KINGS_SINGLE || type == BLESSING_SANCTUARY_SINGLE; + } + + inline constexpr bool IsGreaterVariant(BlessingType type) + { + return type == BLESSING_MIGHT_GREATER || type == BLESSING_WISDOM_GREATER || + type == BLESSING_KINGS_GREATER || type == BLESSING_SANCTUARY_GREATER; + } + + inline constexpr BlessingType ToSingleVariant(BaseBlessingCategory category) + { + switch (category) + { + case BASE_MIGHT: return BLESSING_MIGHT_SINGLE; + case BASE_WISDOM: return BLESSING_WISDOM_SINGLE; + case BASE_KINGS: return BLESSING_KINGS_SINGLE; + case BASE_SANCTUARY: return BLESSING_SANCTUARY_SINGLE; + default: return BLESSING_NONE; + } + } + + inline constexpr BlessingType ToSingleVariant(BlessingType type) + { + return ToSingleVariant(BaseBlessingOf(type)); + } + + inline constexpr BlessingType ToGreaterVariant(BaseBlessingCategory category) + { + switch (category) + { + case BASE_MIGHT: return BLESSING_MIGHT_GREATER; + case BASE_WISDOM: return BLESSING_WISDOM_GREATER; + case BASE_KINGS: return BLESSING_KINGS_GREATER; + case BASE_SANCTUARY: return BLESSING_SANCTUARY_GREATER; + default: return BLESSING_NONE; + } + } + + inline constexpr BlessingType ToGreaterVariant(BlessingType type) + { + return ToGreaterVariant(BaseBlessingOf(type)); + } + + inline std::string BlessingSpellName(BlessingType type) + { + switch (type) + { + case BLESSING_MIGHT_SINGLE: return "blessing of might"; + case BLESSING_MIGHT_GREATER: return "greater blessing of might"; + case BLESSING_WISDOM_SINGLE: return "blessing of wisdom"; + case BLESSING_WISDOM_GREATER: return "greater blessing of wisdom"; + case BLESSING_KINGS_SINGLE: return "blessing of kings"; + case BLESSING_KINGS_GREATER: return "greater blessing of kings"; + case BLESSING_SANCTUARY_SINGLE: return "blessing of sanctuary"; + case BLESSING_SANCTUARY_GREATER: return "greater blessing of sanctuary"; + default: return ""; + } + } + + struct BaseBlessingPriorityEntry + { + BaseBlessingCategory priorities[4]; + }; + + inline constexpr BaseBlessingPriorityEntry BASE_BLESSING_PRIORITIES[ROLE_PROFILE_COUNT] = + { + // All casters + {{ BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY, BASE_MIGHT }}, + // Physical DPS (no mana) + {{ BASE_MIGHT, BASE_KINGS, BASE_SANCTUARY, BASE_NONE }}, + // Hybrid DPS + {{ BASE_MIGHT, BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY }}, + // Druid tanks + {{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_WISDOM, }}, + // Warrior and DK tanks + {{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_NONE }}, + // Paladin tanks + {{ BASE_SANCTUARY, BASE_MIGHT, BASE_WISDOM, BASE_KINGS }}, + }; + + constexpr uint32 SPELL_IMPROVED_MIGHT_R1 = 20042; + constexpr uint32 SPELL_IMPROVED_MIGHT_R2 = 20045; + constexpr uint32 SPELL_IMPROVED_WISDOM_R1 = 20244; + constexpr uint32 SPELL_IMPROVED_WISDOM_R2 = 20245; + + inline RoleProfile ResolveRoleProfile(Player* player) + { + if (!player) + return ROLE_CASTER; + + uint8 cls = player->getClass(); + int tab = AiFactory::GetPlayerSpecTab(player); + bool isTank = PlayerbotAI::IsTank(player); + + switch (cls) + { + case CLASS_WARRIOR: + if (isTank) + return ROLE_WARRIOR_DK_TANK; + return ROLE_PHYSICAL_DPS; + + case CLASS_DEATH_KNIGHT: + if (isTank) + return ROLE_WARRIOR_DK_TANK; + return ROLE_PHYSICAL_DPS; + + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ENHANCEMENT) + return ROLE_HYBRID_DPS; + return ROLE_CASTER; + + case CLASS_PALADIN: + if (isTank) + return ROLE_PALADIN_TANK; + if (tab == PALADIN_TAB_HOLY) + return ROLE_CASTER; + return ROLE_HYBRID_DPS; + + case CLASS_DRUID: + if (tab == DRUID_TAB_FERAL) + return isTank ? ROLE_DRUID_TANK : ROLE_HYBRID_DPS; + return ROLE_CASTER; + + case CLASS_ROGUE: + return ROLE_PHYSICAL_DPS; + + case CLASS_HUNTER: + return ROLE_HYBRID_DPS; + + case CLASS_MAGE: + return ROLE_CASTER; + + case CLASS_WARLOCK: + return ROLE_CASTER; + + case CLASS_PRIEST: + return ROLE_CASTER; + + default: + return ROLE_CASTER; + } + } + + struct GreaterBlessingPlayerAssignment + { + Player* player = nullptr; + BlessingType blessing = BLESSING_NONE; + }; + + struct CachedBlessingBucketAssignment + { + uint8 classId = 0; + RoleProfile role = ROLE_CASTER; + bool byRole = false; + BlessingType blessing = BLESSING_NONE; + }; + + struct CachedBlessingAssignments + { + uint32 groupKey = 0; + bool valid = false; + std::vector assignments; + }; + + struct CachedPendingBlessingAssignment + { + uint32 groupKey = 0; + bool valid = false; + GreaterBlessingPlayerAssignment assignment; + std::string spellName; + }; + + UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI); + UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI); + bool IsEligibleGroupForAutoBlessings(Group const* group); +} + +class CastGreaterBlessingAssignmentAction : public Action +{ +public: + CastGreaterBlessingAssignmentAction(PlayerbotAI* botAI); + + bool Execute(Event event) override; + bool isUseful() override; + bool HasPendingAssignment(); + +private: + bool FindPendingAssignment( + ai::gbless::GreaterBlessingPlayerAssignment& outAssignment, + std::string& outSpellName); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 45f676ec9..999fd456b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -1,17 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PaladinAiObjectContext.h" #include "DpsPaladinStrategy.h" #include "GenericPaladinNonCombatStrategy.h" +#include "PaladinGreaterBlessingAction.h" #include "HealPaladinStrategy.h" #include "NamedObjectContext.h" #include "OffhealRetPaladinStrategy.h" #include "PaladinActions.h" #include "PaladinBuffStrategies.h" +#include "PaladinPullStrategy.h" #include "PaladinTriggers.h" #include "Playerbots.h" #include "TankPaladinStrategy.h" @@ -22,6 +25,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 +35,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); } @@ -67,17 +72,17 @@ class PaladinBuffStrategyFactoryInternal : public NamedObjectContext public: PaladinBuffStrategyFactoryInternal() : NamedObjectContext(false, true) { - creators["bhealth"] = &PaladinBuffStrategyFactoryInternal::bhealth; - creators["bmana"] = &PaladinBuffStrategyFactoryInternal::bmana; - creators["bdps"] = &PaladinBuffStrategyFactoryInternal::bdps; - creators["bstats"] = &PaladinBuffStrategyFactoryInternal::bstats; + creators["bsanc"] = &PaladinBuffStrategyFactoryInternal::bsanc; + creators["bwisdom"] = &PaladinBuffStrategyFactoryInternal::bwisdom; + creators["bmight"] = &PaladinBuffStrategyFactoryInternal::bmight; + creators["bkings"] = &PaladinBuffStrategyFactoryInternal::bkings; } private: - static Strategy* bhealth(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); } - static Strategy* bmana(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); } - static Strategy* bdps(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); } - static Strategy* bstats(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); } + static Strategy* bsanc(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); } + static Strategy* bwisdom(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); } + static Strategy* bmight(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); } + static Strategy* bkings(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); } }; class PaladinCombatStrategyFactoryInternal : public NamedObjectContext @@ -132,7 +137,9 @@ public: &PaladinTriggerFactoryInternal::hammer_of_justice_on_enemy_target; creators["hammer of justice on snare target"] = &PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target; + creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead; creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor; + creators["divine shield low health"] = &PaladinTriggerFactoryInternal::divine_shield_low_health; creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead; creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield; creators["consecration"] = &PaladinTriggerFactoryInternal::consecration; @@ -141,6 +148,7 @@ public: creators["repentance interrupt"] = &PaladinTriggerFactoryInternal::repentance_interrupt; creators["beacon of light on main tank"] = &PaladinTriggerFactoryInternal::beacon_of_light_on_main_tank; creators["sacred shield on main tank"] = &PaladinTriggerFactoryInternal::sacred_shield_on_main_tank; + creators["hand of freedom on party"] = &PaladinTriggerFactoryInternal::hand_of_freedom_on_party; creators["blessing of kings on party"] = &PaladinTriggerFactoryInternal::blessing_of_kings_on_party; creators["blessing of wisdom on party"] = &PaladinTriggerFactoryInternal::blessing_of_wisdom_on_party; @@ -148,11 +156,14 @@ public: creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party; creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath; + creators["greater blessing needed"] = &PaladinTriggerFactoryInternal::greater_blessing_needed; } private: + static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); } static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); } static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); } + static Trigger* divine_shield_low_health(PlayerbotAI* botAI) { return new DivineShieldLowHealthTrigger(botAI); } static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); } static Trigger* righteous_fury(PlayerbotAI* botAI) { return new RighteousFuryTrigger(botAI); } static Trigger* judgement(PlayerbotAI* botAI) { return new JudgementTrigger(botAI); } @@ -203,8 +214,9 @@ private: static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); } static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); } static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); } - static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); } - static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); } + static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new BeaconOfLightOnMainTankTrigger(botAI); } + static Trigger* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new SacredShieldOnMainTankTrigger(botAI); } + static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); } static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); } static Trigger* blessing_of_wisdom_on_party(PlayerbotAI* botAI) @@ -218,6 +230,10 @@ private: } static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); } + static Trigger* greater_blessing_needed(PlayerbotAI* botAI) + { + return new GreaterBlessingNeededTrigger(botAI); + } }; class PaladinAiObjectContextInternal : public NamedObjectContext @@ -288,6 +304,7 @@ public: creators["hammer of justice on snare target"] = &PaladinAiObjectContextInternal::hammer_of_justice_on_snare_target; creators["divine favor"] = &PaladinAiObjectContextInternal::divine_favor; + creators["sense undead"] = &PaladinAiObjectContextInternal::sense_undead; creators["turn undead"] = &PaladinAiObjectContextInternal::turn_undead; creators["blessing of protection on party"] = &PaladinAiObjectContextInternal::blessing_of_protection_on_party; creators["righteous defense"] = &PaladinAiObjectContextInternal::righteous_defense; @@ -305,6 +322,9 @@ public: creators["divine illumination"] = &PaladinAiObjectContextInternal::divine_illumination; creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice; creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice; + creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party; + creators["cast greater blessing assignment"] = + &PaladinAiObjectContextInternal::cast_greater_blessing_assignment; } private: @@ -312,6 +332,7 @@ private: { return new CastBlessingOfProtectionProtectAction(botAI); } + static Action* sense_undead(PlayerbotAI* botAI) { return new CastSenseUndeadAction(botAI); } static Action* turn_undead(PlayerbotAI* botAI) { return new CastTurnUndeadAction(botAI); } static Action* divine_favor(PlayerbotAI* botAI) { return new CastDivineFavorAction(botAI); } static Action* righteous_fury(PlayerbotAI* botAI) { return new CastRighteousFuryAction(botAI); } @@ -402,14 +423,41 @@ private: static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); } static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); } static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); } - static Action* divine_plea(PlayerbotAI* ai) { return new CastDivinePleaAction(ai); } - static Action* shield_of_righteousness(PlayerbotAI* ai) { return new ShieldOfRighteousnessAction(ai); } - static Action* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new CastBeaconOfLightOnMainTankAction(ai); } - static Action* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new CastSacredShieldOnMainTankAction(ai); } - static Action* avenging_wrath(PlayerbotAI* ai) { return new CastAvengingWrathAction(ai); } - static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); } - static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); } - static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); } + static Action* divine_plea(PlayerbotAI* botAI) { return new CastDivinePleaAction(botAI); } + static Action* shield_of_righteousness(PlayerbotAI* botAI) { return new ShieldOfRighteousnessAction(botAI); } + static Action* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new CastBeaconOfLightOnMainTankAction(botAI); } + static Action* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new CastSacredShieldOnMainTankAction(botAI); } + static Action* avenging_wrath(PlayerbotAI* botAI) { return new CastAvengingWrathAction(botAI); } + static Action* divine_illumination(PlayerbotAI* botAI) { return new CastDivineIlluminationAction(botAI); } + static Action* divine_sacrifice(PlayerbotAI* botAI) { return new CastDivineSacrificeAction(botAI); } + static Action* cancel_divine_sacrifice(PlayerbotAI* botAI) { return new CastCancelDivineSacrificeAction(botAI); } + static Action* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new CastHandOfFreedomOnPartyAction(botAI); } + static Action* cast_greater_blessing_assignment(PlayerbotAI* botAI) + { + return new CastGreaterBlessingAssignmentAction(botAI); + } +}; + +class PaladinValueContextInternal : public NamedObjectContext +{ +public: + PaladinValueContextInternal() + { + creators["greater blessing assignments"] = &PaladinValueContextInternal::greater_blessing_assignments; + creators["greater blessing pending assignment"] = + &PaladinValueContextInternal::greater_blessing_pending_assignment; + } + +private: + static UntypedValue* greater_blessing_assignments(PlayerbotAI* botAI) + { + return ai::gbless::greater_blessing_assignments_value(botAI); + } + + static UntypedValue* greater_blessing_pending_assignment(PlayerbotAI* botAI) + { + return ai::gbless::greater_blessing_pending_assignment_value(botAI); + } }; SharedNamedObjectContextList PaladinAiObjectContext::sharedStrategyContexts; @@ -454,4 +502,5 @@ void PaladinAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContext void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) { AiObjectContext::BuildSharedValueContexts(valueContexts); + valueContexts.Add(new PaladinValueContextInternal()); } diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.h index 4ca5f94be..0eb5136c8 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PALADINAIOBJECTCONTEXT_H -#define _PLAYERBOT_PALADINAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_PALADINAIOBJECTCONTEXT_H +#define PLAYERBOTS_PALADINAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinHelper.h b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinHelper.h new file mode 100644 index 000000000..a04fb52f6 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinHelper.h @@ -0,0 +1,46 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PALADINHELPER_H +#define PLAYERBOTS_PALADINHELPER_H + +#include + +#include "Unit.h" + +class Player; + +namespace ai::paladin +{ +static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022; +static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038; +static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044; +static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940; +static constexpr uint32 SPELL_BLESSING_OF_SANCTUARY = 20911; +static constexpr uint32 SPELL_GREATER_BLESSING_OF_SANCTUARY = 25899; + +inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list spellIds) +{ + if (!target || !caster) + return false; + + for (uint32 spellId : spellIds) + { + if (target->HasAura(spellId, caster->GetGUID())) + return true; + } + + return false; +} + +inline bool HasAnyPaladinHandFromCaster(Unit* target, Player* caster) +{ + return HasHandFromCaster(target, caster, + { SPELL_HAND_OF_PROTECTION, SPELL_HAND_OF_SALVATION, SPELL_HAND_OF_FREEDOM, SPELL_HAND_OF_SACRIFICE }); +} +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.cpp new file mode 100644 index 000000000..e4fc97a11 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.cpp @@ -0,0 +1,107 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PaladinTriggers.h" + +#include "GenericBuffUtils.h" +#include "PaladinGreaterBlessingAction.h" +#include "PaladinActions.h" +#include "PaladinHelper.h" +#include "Playerbots.h" + +bool SealTrigger::IsActive() +{ + Unit* target = GetTarget(); + return !botAI->HasAura("seal of justice", target) && !botAI->HasAura("seal of command", target) && + !botAI->HasAura("seal of vengeance", target) && !botAI->HasAura("seal of corruption", target) && + !botAI->HasAura("seal of righteousness", target) && !botAI->HasAura("seal of light", target) && + (!botAI->HasAura("seal of wisdom", target) || AI_VALUE2(uint8, "mana", "self target") > 70); +} + +bool CrusaderAuraTrigger::IsActive() +{ + Unit* target = GetTarget(); + return AI_VALUE2(bool, "mounted", "self target") && !botAI->HasAura("crusader aura", target); +} + +bool BlessingTrigger::IsActive() +{ + Unit* target = GetTarget(); + return SpellTrigger::IsActive() && + !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", + "blessing of kings", "blessing of sanctuary", nullptr); +} + +bool DivineShieldLowHealthTrigger::IsActive() +{ + return botAI->HasAura("divine shield", bot) && AI_VALUE2(uint8, "health", "self target") < 80; +} + +Unit* HandOfFreedomOnPartyTrigger::GetTarget() +{ + bool const selfImpaired = botAI->IsMovementImpaired(bot); + bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot); + + if (!bot->GetGroup()) + { + if (selfImpaired && !hasSelfHand) + return bot; + + return nullptr; + } + + if (selfImpaired && !hasSelfHand) + return bot; + + return Trigger::GetTarget(); +} + +bool HandOfFreedomOnPartyTrigger::IsActive() +{ + Unit* target = GetTarget(); + if (!target) + return false; + + if (target != bot && + bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) + return false; + + if (!botAI->CanCastSpell("hand of freedom", target)) + return false; + + return !ai::paladin::HasAnyPaladinHandFromCaster(target, bot) && botAI->IsMovementImpaired(target); +} + +bool NotSensingUndeadTrigger::IsActive() +{ + return !botAI->HasAura("sense undead", bot); +} + +bool GreaterBlessingNeededTrigger::IsActive() +{ + if (!ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup())) + return false; + + if (ai::buff::ShouldDeferGreaterBlessingAssignmentForRecentLogin(bot)) + return false; + + Group* group = bot->GetGroup(); + uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0; + + Value* pendingValue = + context->GetValue("greater blessing pending assignment"); + if (!pendingValue) + return false; + + ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get(); + if (pendingAssignment.groupKey != groupKey) + { + pendingValue->Reset(); + pendingAssignment = pendingValue->Get(); + } + + return pendingAssignment.valid && pendingAssignment.groupKey == groupKey; +} diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.h similarity index 64% rename from modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.h index 7352dbc81..6933caebe 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/PaladinTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PALADINTRIGGERS_H -#define _PLAYERBOT_PALADINTRIGGERS_H +#ifndef PLAYERBOTS_PALADINTRIGGERS_H +#define PLAYERBOTS_PALADINTRIGGERS_H #include "CureTriggers.h" #include "GenericTriggers.h" @@ -13,32 +14,6 @@ class PlayerbotAI; -inline std::string const GetActualBlessingOfMight(Unit* target) -{ - switch (target->getClass()) - { - case CLASS_MAGE: - case CLASS_PRIEST: - case CLASS_WARLOCK: - return "blessing of wisdom"; - } - - return "blessing of might"; -} - -inline std::string const GetActualBlessingOfWisdom(Unit* target) -{ - switch (target->getClass()) - { - case CLASS_WARRIOR: - case CLASS_ROGUE: - case CLASS_DEATH_KNIGHT: - return "blessing of might"; - } - - return "blessing of wisdom"; -} - BUFF_TRIGGER(HolyShieldTrigger, "holy shield"); BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury"); @@ -77,9 +52,7 @@ class BlessingOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingTrigger : public BuffTrigger @@ -93,7 +66,8 @@ public: class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger { public: - HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "hammer of justice") {} + HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) + : InterruptSpellTrigger(botAI, "hammer of justice") {} }; class HammerOfJusticeSnareTrigger : public SnareTargetTrigger @@ -144,9 +118,7 @@ class CleanseCurePartyMemberDiseaseTrigger : public PartyMemberNeedCureTrigger { public: CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) {} }; class CleanseCurePoisonTrigger : public NeedCureTrigger @@ -159,9 +131,7 @@ class CleanseCurePartyMemberPoisonTrigger : public PartyMemberNeedCureTrigger { public: CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) {} }; class CleanseCureMagicTrigger : public NeedCureTrigger @@ -173,15 +143,15 @@ public: class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger { public: - CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) - { - } + CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) {} }; class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger { public: - HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} + HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) + : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} }; class DivineFavorTrigger : public BuffTrigger @@ -190,6 +160,22 @@ public: DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} }; +class DivineShieldLowHealthTrigger : public Trigger +{ +public: + DivineShieldLowHealthTrigger(PlayerbotAI* botAI) : Trigger(botAI, "divine shield low health") {} + + bool IsActive() override; +}; + +class NotSensingUndeadTrigger : public BuffTrigger +{ +public: + NotSensingUndeadTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "not sensing undead") {} + + bool IsActive() override; +}; + class TurnUndeadTrigger : public HasCcTargetTrigger { public: @@ -201,51 +187,80 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield"); class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger { public: - BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "beacon of light", true) {} + BeaconOfLightOnMainTankTrigger(PlayerbotAI* botAI) + : BuffOnMainTankTrigger(botAI, "beacon of light", true) {} }; class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger { public: - SacredShieldOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "sacred shield", false) {} + SacredShieldOnMainTankTrigger(PlayerbotAI* botAI) + : BuffOnMainTankTrigger(botAI, "sacred shield", false) {} }; -class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfKingsOnPartyTrigger : public BlessingOnPartyTrigger { public: - BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} + BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) + : BlessingOnPartyTrigger(botAI) + { + spell = "blessing of kings"; + } }; -class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfWisdomOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) + : BlessingOnPartyTrigger(botAI) { + spell = "blessing of might,blessing of wisdom"; } }; -class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfMightOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) + : BlessingOnPartyTrigger(botAI) { + spell = "blessing of might,blessing of wisdom"; } }; -class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger +class BlessingOfSanctuaryOnPartyTrigger : public BlessingOnPartyTrigger { public: BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) + : BlessingOnPartyTrigger(botAI) { + spell = "blessing of sanctuary"; } }; +class HandOfFreedomOnPartyTrigger : public Trigger +{ +public: + HandOfFreedomOnPartyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "hand of freedom on party", 1) {} + + Unit* GetTarget() override; + std::string const GetTargetName() override { return "party member snared target"; } + bool IsActive() override; +}; + class AvengingWrathTrigger : public BoostTrigger { public: AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} }; + +class GreaterBlessingNeededTrigger : public Trigger +{ +public: + GreaterBlessingNeededTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "greater blessing needed", 4) {} + + bool IsActive() override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp index 185fb72d7..f5af8c57c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DpsPaladinStrategy.h" @@ -15,11 +16,7 @@ public: { creators["sanctity aura"] = &sanctity_aura; creators["retribution aura"] = &retribution_aura; - creators["seal of corruption"] = &seal_of_corruption; - creators["seal of vengeance"] = &seal_of_vengeance; - creators["seal of command"] = &seal_of_command; creators["blessing of might"] = &blessing_of_might; - creators["crusader strike"] = &crusader_strike; creators["repentance"] = &repentance; creators["repentance on enemy healer"] = &repentance_on_enemy_healer; creators["repentance on snare target"] = &repentance_on_snare_target; @@ -27,36 +24,6 @@ public: } private: - static ActionNode* seal_of_corruption([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "seal of corruption", - /*P*/ {}, - /*A*/ { NextAction("seal of vengeance") }, - /*C*/ {} - ); - } - - static ActionNode* seal_of_vengeance([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "seal of vengeance", - /*P*/ {}, - /*A*/ { NextAction("seal of command") }, - /*C*/ {} - ); - } - - static ActionNode* seal_of_command([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "seal of command", - /*P*/ {}, - /*A*/ { NextAction("seal of righteousness") }, - /*C*/ {} - ); - } - static ActionNode* blessing_of_might([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -67,16 +34,6 @@ private: ); } - static ActionNode* crusader_strike([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "crusader strike", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - static ActionNode* repentance([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.h index 9611946a3..1716e1dff 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DPSPALADINSTRATEGY_H -#define _PLAYERBOT_DPSPALADINSTRATEGY_H +#ifndef PLAYERBOTS_DPSPALADINSTRATEGY_H +#define PLAYERBOTS_DPSPALADINSTRATEGY_H #include "GenericPaladinStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp index 7f9919e06..e6a58a33b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericPaladinNonCombatStrategy.h" @@ -19,14 +20,18 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector& tr NonCombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) })); - triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", 25.0f) })); - triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", 27.0f) })); - triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", 28.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 5.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 6.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 7.0f) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 8.0f) })); + triggers.push_back(new TriggerNode("not sensing undead", { NextAction("sense undead", ACTION_IDLE + 1.0f) })); int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot()); - if (specTab == 0 || specTab == 1) // Holy or Protection - triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); - if (specTab == 2) // Retribution - triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + if (specTab == PALADIN_TAB_HOLY) + triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) })); + if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION) + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) })); + + triggers.push_back(new TriggerNode("greater blessing needed", + { NextAction("cast greater blessing assignment", ACTION_NORMAL) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.h index c4e21a561..cefac0882 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICPALADINNONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICPALADINNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICPALADINNONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICPALADINNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp index 49143f9ae..b5911c85d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericPaladinStrategy.h" @@ -16,27 +17,23 @@ void GenericPaladinStrategy::InitTriggers(std::vector& triggers) { CombatStrategy::InitTriggers(triggers); - triggers.push_back(new TriggerNode("critical health", { NextAction("divine shield", - ACTION_HIGH + 5) })); - triggers.push_back( - new TriggerNode("hammer of justice interrupt", - { NextAction("hammer of justice", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "hammer of justice on enemy healer", + triggers.push_back(new TriggerNode("hammer of justice interrupt", + { NextAction("hammer of justice", ACTION_INTERRUPT) })); + triggers.push_back(new TriggerNode("hammer of justice on enemy healer", { NextAction("hammer of justice on enemy healer", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "hammer of justice on snare target", + triggers.push_back(new TriggerNode("hammer of justice on snare target", { NextAction("hammer of justice on snare target", ACTION_INTERRUPT) })); - triggers.push_back(new TriggerNode( - "critical health", { NextAction("lay on hands", ACTION_EMERGENCY) })); - triggers.push_back( - new TriggerNode("party member critical health", - { NextAction("lay on hands on party", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode( - "protect party member", - { NextAction("blessing of protection on party", ACTION_EMERGENCY + 2) })); - triggers.push_back( - new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); + triggers.push_back(new TriggerNode("critical health", { NextAction("divine shield", ACTION_EMERGENCY) })); + triggers.push_back(new TriggerNode("critical health", { NextAction("lay on hands", ACTION_EMERGENCY + 1) })); + triggers.push_back(new TriggerNode("party member critical health", + { NextAction("lay on hands on party", ACTION_EMERGENCY + 2) })); + triggers.push_back(new TriggerNode("divine shield low health", + { NextAction("flash of light", ACTION_EMERGENCY + 3), NextAction("holy light", ACTION_EMERGENCY + 2)})); + triggers.push_back(new TriggerNode("protect party member", + { NextAction("blessing of protection on party", ACTION_EMERGENCY + 3) })); + triggers.push_back(new TriggerNode("high mana", { NextAction("divine plea", ACTION_HIGH) })); + triggers.push_back(new TriggerNode("hand of freedom on party", + { NextAction("hand of freedom on party", ACTION_HIGH + 4) })); } void PaladinCureStrategy::InitTriggers(std::vector& triggers) @@ -58,9 +55,8 @@ void PaladinCureStrategy::InitTriggers(std::vector& triggers) { NextAction("cleanse magic on party", ACTION_DISPEL + 1) })); } -void PaladinBoostStrategy::InitTriggers(std::vector& triggers) +void PaladinBoostStrategy::InitTriggers(std::vector& /*triggers*/) { - // triggers.push_back(new TriggerNode("divine favor", { NextAction("divine favor", // ACTION_HIGH + 1) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.h index 2a3ec516c..010d4210a 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICPALADINSTRATEGY_H -#define _PLAYERBOT_GENERICPALADINSTRATEGY_H +#ifndef PLAYERBOTS_GENERICPALADINSTRATEGY_H +#define PLAYERBOTS_GENERICPALADINSTRATEGY_H #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h index f1d0d342e..7096a1408 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICPALADINSTRATEGYACTIONNODEFACTORY_H -#define _PLAYERBOT_GENERICPALADINSTRATEGYACTIONNODEFACTORY_H +#ifndef PLAYERBOTS_GENERICPALADINSTRATEGYACTIONNODEFACTORY_H +#define PLAYERBOTS_GENERICPALADINSTRATEGYACTIONNODEFACTORY_H #include "Action.h" #include "NamedObjectContext.h" @@ -16,32 +17,21 @@ class GenericPaladinStrategyActionNodeFactory : public NamedObjectFactory, 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HealPaladinStrategy.h" @@ -8,13 +9,9 @@ #include "Playerbots.h" #include "Strategy.h" -class HealPaladinStrategyActionNodeFactory : public NamedObjectFactory -{ -}; - HealPaladinStrategy::HealPaladinStrategy(PlayerbotAI* botAI) : GenericPaladinStrategy(botAI) { - actionNodeFactories.Add(new HealPaladinStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } std::vector HealPaladinStrategy::getDefaultActions() @@ -30,7 +27,7 @@ void HealPaladinStrategy::InitTriggers(std::vector& triggers) new TriggerNode( "seal", { - NextAction("seal of wisdom", ACTION_HIGH) + NextAction("seal of wisdom", ACTION_HIGH), } ) ); diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.h index f7f802236..43da2f446 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HEALPALADINSTRATEGY_H -#define _PLAYERBOT_HEALPALADINSTRATEGY_H +#ifndef PLAYERBOTS_HEALPALADINSTRATEGY_H +#define PLAYERBOTS_HEALPALADINSTRATEGY_H #include "GenericPaladinStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.cpp index b25fc36b3..593f94563 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "OffhealRetPaladinStrategy.h" @@ -18,8 +19,6 @@ public: creators["seal of vengeance"] = &seal_of_vengeance; creators["seal of command"] = &seal_of_command; creators["blessing of might"] = &blessing_of_might; - creators["crusader strike"] = &crusader_strike; - creators["divine plea"] = &divine_plea; } private: @@ -72,26 +71,6 @@ private: /*C*/ {} ); } - - static ActionNode* crusader_strike([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "crusader strike", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* divine_plea([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "divine plea", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } }; OffhealRetPaladinStrategy::OffhealRetPaladinStrategy(PlayerbotAI* botAI) : GenericPaladinStrategy(botAI) diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.h index eeb9672ed..63ea4cb15 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/OffhealRetPaladinStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ - #ifndef _PLAYERBOT_OFFHEALRETPALADINSTRATEGY_H -#define _PLAYERBOT_OFFHEALRETPALADINSTRATEGY_H + #ifndef PLAYERBOTS_OFFHEALRETPALADINSTRATEGY_H +#define PLAYERBOTS_OFFHEALRETPALADINSTRATEGY_H #include "GenericPaladinStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.cpp index 884c2f277..fa7aa48c6 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PaladinBuffStrategies.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h index 25f8886a3..aa7a8189c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinBuffStrategies.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PALADINBUFFSTRATEGIES_H -#define _PLAYERBOT_PALADINBUFFSTRATEGIES_H +#ifndef PLAYERBOTS_PALADINBUFFSTRATEGIES_H +#define PLAYERBOTS_PALADINBUFFSTRATEGIES_H #include "Strategy.h" @@ -16,7 +17,7 @@ public: PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bmana"; } + std::string const getName() override { return "bwisdom"; } }; class PaladinBuffHealthStrategy : public Strategy @@ -25,7 +26,7 @@ public: PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bhealth"; } + std::string const getName() override { return "bsanc"; } }; class PaladinBuffDpsStrategy : public Strategy @@ -34,7 +35,7 @@ public: PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bdps"; } + std::string const getName() override { return "bmight"; } }; class PaladinBuffArmorStrategy : public Strategy @@ -88,7 +89,7 @@ public: PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} void InitTriggers(std::vector& triggers) override; - std::string const getName() override { return "bstats"; } + std::string const getName() override { return "bkings"; } }; class PaladinShadowResistanceStrategy : public Strategy diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp new file mode 100644 index 000000000..9d5c56e19 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.cpp @@ -0,0 +1,36 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PaladinPullStrategy.h" + +#include "PlayerbotAI.h" +#include "Playerbots.h" + +std::string PaladinPullStrategy::GetPullActionName() const +{ + Unit* target = GetTarget(); + if (!target || + (!botAI->HasStrategy("tank", BOT_STATE_COMBAT) && !botAI->HasStrategy("tank", BOT_STATE_NON_COMBAT))) + { + return PullStrategy::GetPullActionName(); + } + + if (botAI->CanCastSpell("avenger's shield", target)) + return "avenger's shield"; + + if (botAI->CanCastSpell("hand of reckoning", 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/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h new file mode 100644 index 000000000..6511b2952 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/PaladinPullStrategy.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PALADINPULLSTRATEGY_H +#define PLAYERBOTS_PALADINPULLSTRATEGY_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/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp index b76116e04..af068e57f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TankPaladinStrategy.h" @@ -95,13 +96,14 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) } ) ); - triggers.push_back(new TriggerNode( - "light aoe", - { - NextAction("avenger's shield", ACTION_HIGH + 5) - } - ) -); + triggers.push_back( + new TriggerNode( + "light aoe", + { + NextAction("avenger's shield", ACTION_HIGH + 5) + } + ) + ); triggers.push_back( new TriggerNode( "medium aoe", @@ -122,13 +124,6 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode( "medium health", - { NextAction("holy shield", ACTION_HIGH + 4) - } - ) - ); - triggers.push_back( - new TriggerNode( - "low health", { NextAction("holy shield", ACTION_HIGH + 4) } @@ -136,20 +131,12 @@ void TankPaladinStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "critical health", + "avenging wrath", { - NextAction("holy shield", ACTION_HIGH + 4) + NextAction("avenging wrath", ACTION_HIGH + 2) } ) ); - triggers.push_back( - new TriggerNode( - "avenging wrath", - { - NextAction("avenging wrath", ACTION_HIGH + 2) - } - ) -); triggers.push_back( new TriggerNode( "target critical health", diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.h b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.h index b4a0e0ca0..e7f961474 100644 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Paladin/Strategy/TankPaladinStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TANKPALADINSTRATEGY_H -#define _PLAYERBOT_TANKPALADINSTRATEGY_H +#ifndef PLAYERBOTS_TANKPALADINSTRATEGY_H +#define PLAYERBOTS_TANKPALADINSTRATEGY_H #include "GenericPaladinStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp deleted file mode 100644 index 6c333bc7e..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 "PaladinTriggers.h" - -#include "PaladinActions.h" -#include "PlayerbotAIConfig.h" -#include "Playerbots.h" - -bool SealTrigger::IsActive() -{ - Unit* target = GetTarget(); - return !botAI->HasAura("seal of justice", target) && !botAI->HasAura("seal of command", target) && - !botAI->HasAura("seal of vengeance", target) && !botAI->HasAura("seal of corruption", target) && - !botAI->HasAura("seal of righteousness", target) && !botAI->HasAura("seal of light", target) && - (!botAI->HasAura("seal of wisdom", target) || AI_VALUE2(uint8, "mana", "self target") > 70); -} - -bool CrusaderAuraTrigger::IsActive() -{ - Unit* target = GetTarget(); - return AI_VALUE2(bool, "mounted", "self target") && !botAI->HasAura("crusader aura", target); -} - -bool BlessingTrigger::IsActive() -{ - Unit* target = GetTarget(); - return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", - "blessing of kings", "blessing of sanctuary", nullptr); -} diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Action/PriestActions.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/PriestActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Class/Priest/Action/PriestActions.cpp rename to modules/mod-playerbots/src/Ai/Class/Priest/PriestActions.cpp index 1bfd7d2a8..702045de0 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Action/PriestActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/PriestActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PriestActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Action/PriestActions.h b/modules/mod-playerbots/src/Ai/Class/Priest/PriestActions.h similarity index 81% rename from modules/mod-playerbots/src/Ai/Class/Priest/Action/PriestActions.h rename to modules/mod-playerbots/src/Ai/Class/Priest/PriestActions.h index 4e94f27cc..671091489 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Action/PriestActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/PriestActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PRIESTACTIONS_H -#define _PLAYERBOT_PRIESTACTIONS_H +#ifndef PLAYERBOTS_PRIESTACTIONS_H +#define PLAYERBOTS_PRIESTACTIONS_H #include "GenericSpellActions.h" #include "PlayerbotAIConfig.h" @@ -13,9 +14,19 @@ class PlayerbotAI; // disc -BUFF_ACTION(CastPowerWordFortitudeAction, "power word: fortitude"); -BUFF_PARTY_ACTION(CastPowerWordFortitudeOnPartyAction, "power word: fortitude"); -BUFF_PARTY_ACTION(CastPrayerOfFortitudeOnPartyAction, "prayer of fortitude"); +class CastPowerWordFortitudeAction : public GroupBuffSpellAction +{ +public: + CastPowerWordFortitudeAction(PlayerbotAI* botAI) + : GroupBuffSpellAction(botAI, "power word: fortitude") {} +}; + +class CastPowerWordFortitudeOnPartyAction : public GroupBuffOnPartyAction +{ +public: + CastPowerWordFortitudeOnPartyAction(PlayerbotAI* botAI) + : GroupBuffOnPartyAction(botAI, "power word: fortitude") {} +}; BUFF_ACTION(CastPowerWordShieldAction, "power word: shield"); BUFF_ACTION(CastInnerFireAction, "inner fire"); @@ -26,9 +37,19 @@ CC_ACTION(CastShackleUndeadAction, "shackle undead"); SPELL_ACTION_U(CastManaBurnAction, "mana burn", AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20); BUFF_ACTION(CastLevitateAction, "levitate"); -BUFF_ACTION(CastDivineSpiritAction, "divine spirit"); -BUFF_PARTY_ACTION(CastDivineSpiritOnPartyAction, "divine spirit"); -BUFF_PARTY_ACTION(CastPrayerOfSpiritOnPartyAction, "prayer of spirit"); +class CastDivineSpiritAction : public GroupBuffSpellAction +{ +public: + CastDivineSpiritAction(PlayerbotAI* botAI) + : GroupBuffSpellAction(botAI, "divine spirit") {} +}; + +class CastDivineSpiritOnPartyAction : public GroupBuffOnPartyAction +{ +public: + CastDivineSpiritOnPartyAction(PlayerbotAI* botAI) + : GroupBuffOnPartyAction(botAI, "divine spirit") {} +}; // disc 2.4.3 SPELL_ACTION(CastMassDispelAction, "mass dispel"); @@ -97,19 +118,29 @@ public: SPELL_ACTION(CastShadowWordDeathAction, "shadow word: death"); // shadow -DEBUFF_CHECKISOWNER_ACTION(CastPowerWordPainAction, "shadow word: pain"); -DEBUFF_ENEMY_ACTION(CastPowerWordPainOnAttackerAction, "shadow word: pain"); +DEBUFF_CHECKISOWNER_ACTION(CastShadowWordPainAction, "shadow word: pain"); +DEBUFF_ENEMY_ACTION(CastShadowWordPainOnAttackerAction, "shadow word: pain"); SPELL_ACTION(CastMindBlastAction, "mind blast"); SPELL_ACTION(CastPsychicScreamAction, "psychic scream"); DEBUFF_ACTION(CastMindSootheAction, "mind soothe"); BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup()); -BUFF_ACTION(CastShadowProtectionAction, "shadow protection"); -BUFF_PARTY_ACTION(CastShadowProtectionOnPartyAction, "shadow protection"); -BUFF_PARTY_ACTION(CastPrayerOfShadowProtectionAction, "prayer of shadow protection"); +class CastShadowProtectionAction : public GroupBuffSpellAction +{ +public: + CastShadowProtectionAction(PlayerbotAI* botAI) + : GroupBuffSpellAction(botAI, "shadow protection") {} +}; + +class CastShadowProtectionOnPartyAction : public GroupBuffOnPartyAction +{ +public: + CastShadowProtectionOnPartyAction(PlayerbotAI* botAI) + : GroupBuffOnPartyAction(botAI, "shadow protection") {} +}; // shadow talents SPELL_ACTION(CastMindFlayAction, "mind flay"); -DEBUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace"); +BUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace"); BUFF_ACTION(CastShadowformAction, "shadowform"); SPELL_ACTION(CastSilenceAction, "silence"); ENEMY_HEALER_ACTION(CastSilenceOnEnemyHealerAction, "silence"); diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.cpp index 7ffac96cc..73e149026 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PriestAiObjectContext.h" @@ -92,8 +93,6 @@ public: creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection; creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party; creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead; - creators["prayer of fortitude on party"] = &PriestTriggerFactoryInternal::prayer_of_fortitude_on_party; - creators["prayer of spirit on party"] = &PriestTriggerFactoryInternal::prayer_of_spirit_on_party; creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire; creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness; creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness; @@ -114,10 +113,10 @@ private: static Trigger* vampiric_touch(PlayerbotAI* botAI) { return new VampiricTouchTrigger(botAI); } static Trigger* vampiric_touch_on_attacker(PlayerbotAI* botAI) { return new VampiricTouchOnAttackerTrigger(botAI); } static Trigger* devouring_plague(PlayerbotAI* botAI) { return new DevouringPlagueTrigger(botAI); } - static Trigger* shadow_word_pain(PlayerbotAI* botAI) { return new PowerWordPainTrigger(botAI); } + static Trigger* shadow_word_pain(PlayerbotAI* botAI) { return new ShadowWordPainTrigger(botAI); } static Trigger* shadow_word_pain_on_attacker(PlayerbotAI* botAI) { - return new PowerWordPainOnAttackerTrigger(botAI); + return new ShadowWordPainOnAttackerTrigger(botAI); } static Trigger* dispel_magic(PlayerbotAI* botAI) { return new DispelMagicTrigger(botAI); } static Trigger* dispel_magic_party_member(PlayerbotAI* botAI) { return new DispelMagicPartyMemberTrigger(botAI); } @@ -136,8 +135,6 @@ private: static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); } static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); } static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); } - static Trigger* prayer_of_fortitude_on_party(PlayerbotAI* botAI) { return new PrayerOfFortitudeTrigger(botAI); } - static Trigger* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new PrayerOfSpiritTrigger(botAI); } static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); } static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); } static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); } @@ -207,8 +204,6 @@ public: creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection; creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party; creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead; - creators["prayer of fortitude on party"] = &PriestAiObjectContextInternal::prayer_of_fortitude_on_party; - creators["prayer of spirit on party"] = &PriestAiObjectContextInternal::prayer_of_spirit_on_party; creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party; creators["silence"] = &PriestAiObjectContextInternal::silence; creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer; @@ -261,10 +256,10 @@ private: static Action* psychic_scream(PlayerbotAI* botAI) { return new CastPsychicScreamAction(botAI); } static Action* circle_of_healing(PlayerbotAI* botAI) { return new CastCircleOfHealingAction(botAI); } static Action* resurrection(PlayerbotAI* botAI) { return new CastResurrectionAction(botAI); } - static Action* shadow_word_pain(PlayerbotAI* botAI) { return new CastPowerWordPainAction(botAI); } + static Action* shadow_word_pain(PlayerbotAI* botAI) { return new CastShadowWordPainAction(botAI); } static Action* shadow_word_pain_on_attacker(PlayerbotAI* botAI) { - return new CastPowerWordPainOnAttackerAction(botAI); + return new CastShadowWordPainOnAttackerAction(botAI); } static Action* devouring_plague(PlayerbotAI* botAI) { return new CastDevouringPlagueAction(botAI); } static Action* mind_flay(PlayerbotAI* botAI) { return new CastMindFlayAction(botAI); } @@ -311,11 +306,6 @@ private: static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); } static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); } static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); } - static Action* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new CastPrayerOfSpiritOnPartyAction(botAI); } - static Action* prayer_of_fortitude_on_party(PlayerbotAI* botAI) - { - return new CastPrayerOfFortitudeOnPartyAction(botAI); - } static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); } static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); } static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); } diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.h index 83e3449cb..fc57cad9c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/PriestAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PRIESTAIOBJECTCONTEXT_H -#define _PLAYERBOT_PRIESTAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_PRIESTAIOBJECTCONTEXT_H +#define PLAYERBOTS_PRIESTAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/PriestTriggers.cpp similarity index 53% rename from modules/mod-playerbots/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Priest/PriestTriggers.cpp index 7f61ba6ee..d9cdf0a14 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Trigger/PriestTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/PriestTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PriestTriggers.h" @@ -8,10 +9,9 @@ #include "Player.h" #include "Playerbots.h" -bool PowerWordFortitudeOnPartyTrigger::IsActive() +bool ShadowProtectionTrigger::IsActive() { - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("power word : fortitude", GetTarget()) && - !botAI->HasAura("prayer of fortitude", GetTarget()); + return BuffTrigger::IsActive() && !botAI->HasAura("prayer of shadow protection", GetTarget()); } bool PowerWordFortitudeTrigger::IsActive() @@ -20,43 +20,12 @@ bool PowerWordFortitudeTrigger::IsActive() !botAI->HasAura("prayer of fortitude", GetTarget()); } -bool DivineSpiritOnPartyTrigger::IsActive() -{ - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) && - !botAI->HasAura("prayer of spirit", GetTarget()); -} - bool DivineSpiritTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) && !botAI->HasAura("prayer of spirit", GetTarget()); } -bool PrayerOfFortitudeTrigger::IsActive() -{ - Unit* target = GetTarget(); - if (!target || !target->IsPlayer()) - return false; - - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of fortitude", GetTarget()) && - botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) && - botAI->GetBuffedCount((Player*)GetTarget(), "prayer of fortitude") < 4 && - !botAI->GetBuffedCount((Player*)GetTarget(), "power word: fortitude"); -} - -bool PrayerOfSpiritTrigger::IsActive() -{ - Unit* target = GetTarget(); - if (!target || !target->IsPlayer()) - return false; - - return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of spirit", GetTarget()) && - botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) && - // botAI->GetManaPercent() > 50 && - botAI->GetBuffedCount((Player*)GetTarget(), "prayer of spirit") < 4 && - !botAI->GetBuffedCount((Player*)GetTarget(), "divine spirit"); -} - bool InnerFireTrigger::IsActive() { Unit* target = GetTarget(); diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Trigger/PriestTriggers.h b/modules/mod-playerbots/src/Ai/Class/Priest/PriestTriggers.h similarity index 61% rename from modules/mod-playerbots/src/Ai/Class/Priest/Trigger/PriestTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Priest/PriestTriggers.h index 5ae91985d..13d35e496 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Trigger/PriestTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/PriestTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PRIESTTRIGGERS_H -#define _PLAYERBOT_PRIESTTRIGGERS_H +#ifndef PLAYERBOTS_PRIESTTRIGGERS_H +#define PLAYERBOTS_PRIESTTRIGGERS_H #include "CureTriggers.h" #include "SharedDefines.h" @@ -14,8 +15,8 @@ class PlayerbotAI; DEBUFF_CHECKISOWNER_TRIGGER(HolyFireTrigger, "holy fire"); -DEBUFF_CHECKISOWNER_TRIGGER(PowerWordPainTrigger, "shadow word: pain"); -DEBUFF_ENEMY_TRIGGER(PowerWordPainOnAttackerTrigger, "shadow word: pain"); +DEBUFF_CHECKISOWNER_TRIGGER(ShadowWordPainTrigger, "shadow word: pain"); +DEBUFF_ENEMY_TRIGGER(ShadowWordPainOnAttackerTrigger, "shadow word: pain"); DEBUFF_CHECKISOWNER_TRIGGER(VampiricTouchTrigger, "vampiric touch"); DEBUFF_ENEMY_TRIGGER(VampiricTouchOnAttackerTrigger, "vampiric touch on attacker"); BUFF_TRIGGER(VampiricEmbraceTrigger, "vampiric embrace"); @@ -27,8 +28,6 @@ BUFF_TRIGGER_A(InnerFireTrigger, "inner fire"); BUFF_TRIGGER_A(ShadowformTrigger, "shadowform"); BOOST_TRIGGER(PowerInfusionTrigger, "power infusion"); BUFF_TRIGGER(InnerFocusTrigger, "inner focus"); -BUFF_TRIGGER(ShadowProtectionTrigger, "shadow protection"); -BUFF_PARTY_TRIGGER(ShadowProtectionOnPartyTrigger, "shadow protection"); CC_TRIGGER(ShackleUndeadTrigger, "shackle undead"); INTERRUPT_TRIGGER(SilenceTrigger, "silence"); INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence"); @@ -44,20 +43,34 @@ SNARE_TRIGGER(ChastiseTrigger, "chastise"); BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend"); +class ShadowProtectionTrigger : public BuffTrigger +{ +public: + ShadowProtectionTrigger(PlayerbotAI* botAI) + : BuffTrigger(botAI, "shadow protection", 4 * 2000) {} + + bool IsActive() override; +}; + +class ShadowProtectionOnPartyTrigger : public BuffOnPartyTrigger +{ +public: + ShadowProtectionOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "shadow protection", 4 * 2000) {} +}; + class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger { public: - PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) - { - } - - bool IsActive() override; + PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) {} }; class PowerWordFortitudeTrigger : public BuffTrigger { public: - PowerWordFortitudeTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {} + PowerWordFortitudeTrigger(PlayerbotAI* botAI) + : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {} bool IsActive() override; }; @@ -65,31 +78,15 @@ public: class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger { public: - DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {} - - bool IsActive() override; + DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {} }; class DivineSpiritTrigger : public BuffTrigger { public: - DivineSpiritTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine spirit", 4 * 2000) {} - - bool IsActive() override; -}; - -class PrayerOfFortitudeTrigger : public BuffOnPartyTrigger -{ -public: - PrayerOfFortitudeTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of fortitude", 3 * 2000) {} - - bool IsActive() override; -}; - -class PrayerOfSpiritTrigger : public BuffOnPartyTrigger -{ -public: - PrayerOfSpiritTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of spirit", 2 * 2000) {} + DivineSpiritTrigger(PlayerbotAI* botAI) + : BuffTrigger(botAI, "divine spirit", 4 * 2000) {} bool IsActive() override; }; @@ -106,9 +103,7 @@ class MindSearChannelCheckTrigger : public Trigger { public: MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2) - : Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) - { - } + : Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) {} bool IsActive() override; diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.cpp index 60da13433..74791d5b0 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericPriestStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.h index 35fd7db13..622a49d37 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICPRIESTSTRATEGY_H -#define _PLAYERBOT_GENERICPRIESTSTRATEGY_H +#ifndef PLAYERBOTS_GENERICPRIESTSTRATEGY_H +#define PLAYERBOTS_GENERICPRIESTSTRATEGY_H #include "CombatStrategy.h" #include "RangedCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategyActionNodeFactory.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategyActionNodeFactory.h index ab6a0c2ba..19ab33405 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategyActionNodeFactory.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/GenericPriestStrategyActionNodeFactory.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICPRIESTSTRATEGYACTIONNODEFACTORY_H -#define _PLAYERBOT_GENERICPRIESTSTRATEGYACTIONNODEFACTORY_H +#ifndef PLAYERBOTS_GENERICPRIESTSTRATEGYACTIONNODEFACTORY_H +#define PLAYERBOTS_GENERICPRIESTSTRATEGYACTIONNODEFACTORY_H #include "Action.h" #include "NamedObjectContext.h" @@ -21,7 +22,6 @@ public: creators["divine spirit"] = &divine_spirit; creators["divine spirit on party"] = &divine_spirit_on_party; creators["power word: shield"] = &power_word_shield; - // creators["power word: shield on party"] = &power_word_shield_on_party; creators["renew"] = &renew; creators["renew on party"] = &renew_on_party; creators["greater heal"] = &greater_heal; @@ -33,8 +33,6 @@ public: creators["flash heal"] = &flash_heal; creators["flash heal on party"] = &flash_heal_on_party; creators["psychic scream"] = &psychic_scream; - // creators["fade"] = &fade; - creators["shadowfiend"] = &shadowfiend; } private: @@ -210,16 +208,6 @@ private: /*C*/ {} ); } - - static ActionNode* shadowfiend([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "shadowfiend", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } }; class CurePriestStrategyActionNodeFactory : public NamedObjectFactory diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.cpp index 35f764e4d..8fcf80658 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HealPriestStrategy.h" @@ -86,6 +87,7 @@ void HealPriestStrategy::InitTriggers(std::vector& triggers) new TriggerNode( "party member almost full health", { + NextAction("power word: shield on party", ACTION_LIGHT_HEAL + 3), NextAction("prayer of mending on party", ACTION_LIGHT_HEAL + 2), NextAction("renew on party", ACTION_LIGHT_HEAL + 1) } diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.h index 49fd764e2..c747ed2b1 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HealPriestStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HEALPRIESTSTRATEGY_H -#define _PLAYERBOT_HEALPRIESTSTRATEGY_H +#ifndef PLAYERBOTS_HEALPRIESTSTRATEGY_H +#define PLAYERBOTS_HEALPRIESTSTRATEGY_H #include "GenericPriestStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.cpp index 399f21c61..75c27b821 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "HolyPriestStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.h index 5ae94c383..1fb138d01 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/HolyPriestStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HOLYPRIESTSTRATEGY_H -#define _PLAYERBOT_HOLYPRIESTSTRATEGY_H +#ifndef PLAYERBOTS_HOLYPRIESTSTRATEGY_H +#define PLAYERBOTS_HOLYPRIESTSTRATEGY_H #include "GenericPriestStrategyActionNodeFactory.h" #include "HealPriestStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp index 6447d4519..201f76d98 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PriestNonCombatStrategy.h" @@ -19,6 +20,8 @@ void PriestNonCombatStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("inner fire",{ NextAction("inner fire", 10.0f) })); + triggers.push_back( + new TriggerNode("vampiric embrace", { NextAction("vampiric embrace", 16.0f) })); triggers.push_back(new TriggerNode( "party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11), NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) })); @@ -54,12 +57,6 @@ void PriestBuffStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); - triggers.push_back( - new TriggerNode("prayer of fortitude on party", - { NextAction("prayer of fortitude on party", 12.0f) })); - triggers.push_back( - new TriggerNode("prayer of spirit on party", - { NextAction("prayer of spirit on party", 14.0f) })); triggers.push_back( new TriggerNode("power word: fortitude on party", { NextAction("power word: fortitude on party", 11.0f) })); diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.h index 465d05a59..b9fba9506 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PRIESTNONCOMBATSTRATEGY_H -#define _PLAYERBOT_PRIESTNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_PRIESTNONCOMBATSTRATEGY_H +#define PLAYERBOTS_PRIESTNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h index c7d4aba82..599bcaf65 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/PriestNonCombatStrategyActionNodeFactory.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PRIESTNONCOMBATSTRATEGYACTIONNODEFACTORY_H -#define _PLAYERBOT_PRIESTNONCOMBATSTRATEGYACTIONNODEFACTORY_H +#ifndef PLAYERBOTS_PRIESTNONCOMBATSTRATEGYACTIONNODEFACTORY_H +#define PLAYERBOTS_PRIESTNONCOMBATSTRATEGYACTIONNODEFACTORY_H #include "Action.h" #include "NamedObjectContext.h" @@ -30,8 +31,6 @@ public: creators["flash heal"] = &flash_heal; creators["flash heal on party"] = &flash_heal_on_party; creators["circle of healing on party"] = &circle_of_healing; - creators["prayer of fortitude on party"] = &prayer_of_fortitude_on_party; - creators["prayer of spirit on party"] = &prayer_of_spirit_on_party; } private: @@ -134,20 +133,6 @@ private: /*A*/ {}, /*C*/ {}); } - static ActionNode* prayer_of_fortitude_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("prayer of fortitude on party", - /*P*/ { NextAction("remove shadowform") }, - /*A*/ { NextAction("power word: fortitude on party") }, - /*C*/ {}); - } - static ActionNode* prayer_of_spirit_on_party([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("prayer of spirit on party", - /*P*/ { NextAction("remove shadowform") }, - /*A*/ { NextAction("divine spirit on party") }, - /*C*/ {}); - } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.cpp index 1308f7225..5c9f96f46 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ShadowPriestStrategy.h" @@ -51,14 +52,6 @@ void ShadowPriestStrategy::InitTriggers(std::vector& triggers) } ) ); - triggers.push_back( - new TriggerNode( - "vampiric embrace", - { - NextAction("vampiric embrace", 16.0f) - } - ) - ); triggers.push_back( new TriggerNode( "silence", diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.h index 6bf681b76..5e83f7400 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHADOWPRIESTSTRATEGY_H -#define _PLAYERBOT_SHADOWPRIESTSTRATEGY_H +#ifndef PLAYERBOTS_SHADOWPRIESTSTRATEGY_H +#define PLAYERBOTS_SHADOWPRIESTSTRATEGY_H #include "GenericPriestStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategyActionNodeFactory.h b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategyActionNodeFactory.h index 4efcc6f6a..9f0ad6d0c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategyActionNodeFactory.h +++ b/modules/mod-playerbots/src/Ai/Class/Priest/Strategy/ShadowPriestStrategyActionNodeFactory.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHADOWPRIESTSTRATEGYACTIONNODEFACTORY_H -#define _PLAYERBOT_SHADOWPRIESTSTRATEGYACTIONNODEFACTORY_H +#ifndef PLAYERBOTS_SHADOWPRIESTSTRATEGYACTIONNODEFACTORY_H +#define PLAYERBOTS_SHADOWPRIESTSTRATEGYACTIONNODEFACTORY_H #include "Action.h" #include "NamedObjectContext.h" @@ -17,7 +18,6 @@ public: ShadowPriestStrategyActionNodeFactory() { creators["mind blast"] = &mind_blast; - creators["dispersion"] = &dispersion; creators["mind flay"] = &mind_flay; creators["smite"] = &smite; } @@ -47,13 +47,6 @@ private: /*C*/ {}); } - static ActionNode* dispersion([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("dispersion", - /*P*/ {}, - /*A*/ { NextAction("mana potion") }, - /*C*/ {}); - } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.cpp index b554a8253..973d37413 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RogueActions.h" @@ -11,6 +12,14 @@ #include "PlayerbotAIConfig.h" #include "Playerbots.h" +namespace +{ +constexpr uint32 BG_WS_SPELL_WARSONG_FLAG = 23333; +constexpr uint32 BG_WS_SPELL_SILVERWING_FLAG = 23335; +constexpr uint32 BG_EY_NETHERSTORM_FLAG_SPELL = 34976; +constexpr uint32 SPELL_MASTER_POISONER_RANK_3 = 58410; +} + bool CastStealthAction::isUseful() { Unit* target = AI_VALUE(Unit*, "current target"); @@ -22,7 +31,8 @@ bool CastStealthAction::isUseful() bool CastStealthAction::isPossible() { // do not use with WSG flag or EYE flag - return !botAI->HasAura(23333, bot) && !botAI->HasAura(23335, bot) && !botAI->HasAura(34976, bot); + return !bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) && !bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG) && + !bot->HasAura(BG_EY_NETHERSTORM_FLAG_SPELL); } bool UnstealthAction::Execute(Event /*event*/) @@ -50,7 +60,8 @@ bool CheckStealthAction::Execute(Event /*event*/) bool CastVanishAction::isUseful() { // do not use with WSG flag or EYE flag - return !botAI->HasAura(23333, bot) && !botAI->HasAura(23335, bot) && !botAI->HasAura(34976, bot); + return !bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) && !bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG) && + !bot->HasAura(BG_EY_NETHERSTORM_FLAG_SPELL); } bool CastEnvenomAction::isUseful() @@ -61,7 +72,7 @@ bool CastEnvenomAction::isUseful() bool CastEnvenomAction::isPossible() { // alternate to eviscerate if talents unlearned - return botAI->HasAura(58410, bot) /* Master Poisoner */; + return bot->HasAura(SPELL_MASTER_POISONER_RANK_3); } bool CastTricksOfTheTradeOnMainTankAction::isUseful() diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.h index dd0ad4735..ea326651e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ROGUEACTIONS_H -#define _PLAYERBOT_ROGUEACTIONS_H +#ifndef PLAYERBOTS_ROGUEACTIONS_H +#define PLAYERBOTS_ROGUEACTIONS_H #include "GenericSpellActions.h" #include "UseItemAction.h" @@ -78,6 +79,12 @@ public: CastFeintAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feint") {} }; +class CastColdBloodAction : public CastBuffSpellAction +{ +public: + CastColdBloodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold blood") {} +}; + class CastDismantleAction : public CastSpellAction { public: diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.cpp index d99fa4687..941ccdf0f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RogueComboActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.h index be840ecc7..595b73f8c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueComboActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ROGUECOMBOACTIONS_H -#define _PLAYERBOT_ROGUECOMBOACTIONS_H +#ifndef PLAYERBOTS_ROGUECOMBOACTIONS_H +#define PLAYERBOTS_ROGUECOMBOACTIONS_H #include "GenericSpellActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueFinishingActions.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueFinishingActions.h index 333e45896..b0b790fcd 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueFinishingActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueFinishingActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ROGUEFINISHINGACTIONS_H -#define _PLAYERBOT_ROGUEFINISHINGACTIONS_H +#ifndef PLAYERBOTS_ROGUEFINISHINGACTIONS_H +#define PLAYERBOTS_ROGUEFINISHINGACTIONS_H #include "GenericSpellActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.cpp index 3e4a832af..54b0f911e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RogueOpeningActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.h index 8d7c64919..94d71ea67 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Action/RogueOpeningActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ROGUEOPENINGACTIONS_H -#define _PLAYERBOT_ROGUEOPENINGACTIONS_H +#ifndef PLAYERBOTS_ROGUEOPENINGACTIONS_H +#define PLAYERBOTS_ROGUEOPENINGACTIONS_H #include "GenericSpellActions.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.cpp index 8586d93d1..c244cdc7c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RogueAiObjectContext.h" @@ -143,6 +144,7 @@ public: creators["use instant poison on off hand"] = &RogueAiObjectContextInternal::use_instant_poison_off_hand; creators["fan of knives"] = &RogueAiObjectContextInternal::fan_of_knives; creators["killing spree"] = &RogueAiObjectContextInternal::killing_spree; + creators["cold blood"] = &RogueAiObjectContextInternal::cold_blood; } private: @@ -184,6 +186,7 @@ private: static Action* use_instant_poison_off_hand(PlayerbotAI* ai) { return new UseInstantPoisonOffHandAction(ai); } static Action* fan_of_knives(PlayerbotAI* ai) { return new FanOfKnivesAction(ai); } static Action* killing_spree(PlayerbotAI* ai) { return new CastKillingSpreeAction(ai); } + static Action* cold_blood(PlayerbotAI* ai) { return new CastColdBloodAction(ai); } }; SharedNamedObjectContextList RogueAiObjectContext::sharedStrategyContexts; diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.h index 3628f8737..8c7fe1b01 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ROGUEAIOBJECTCONTEXT_H -#define _PLAYERBOT_ROGUEAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_ROGUEAIOBJECTCONTEXT_H +#define PLAYERBOTS_ROGUEAIOBJECTCONTEXT_H #include "AiObjectContext.h" #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Trigger/RogueTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueTriggers.cpp similarity index 83% rename from modules/mod-playerbots/src/Ai/Class/Rogue/Trigger/RogueTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Rogue/RogueTriggers.cpp index c33dbb7fe..09316384d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Trigger/RogueTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RogueTriggers.h" @@ -9,6 +10,12 @@ #include "Playerbots.h" #include "ServerFacade.h" +namespace +{ +constexpr uint32 SPELL_STEALTH = 1784; +constexpr uint32 SPELL_SPRINT_RANK_1 = 2983; +} + // bool AdrenalineRushTrigger::isPossible() // { // return !botAI->HasAura("stealth", bot); @@ -29,7 +36,7 @@ bool UnstealthTrigger::IsActive() bool StealthTrigger::IsActive() { - if (botAI->HasAura("stealth", bot) || bot->IsInCombat() || bot->HasSpellCooldown(1784)) + if (bot->HasAura(SPELL_STEALTH) || bot->IsInCombat() || bot->HasSpellCooldown(SPELL_STEALTH)) return false; float distance = 30.f; @@ -63,13 +70,13 @@ bool StealthTrigger::IsActive() return target && ServerFacade::instance().GetDistance2d(bot, target) < distance; } -bool SapTrigger::IsPossible() { return bot->GetLevel() > 10 && bot->HasSpell(6770) && !bot->IsInCombat(); } +bool SapTrigger::IsPossible() { return bot->GetLevel() > 10 && botAI->HasSpell("sap") && !bot->IsInCombat(); } -bool SprintTrigger::IsPossible() { return bot->HasSpell(2983); } +bool SprintTrigger::IsPossible() { return bot->HasSpell(SPELL_SPRINT_RANK_1); } bool SprintTrigger::IsActive() { - if (bot->HasSpellCooldown(2983)) + if (bot->HasSpellCooldown(SPELL_SPRINT_RANK_1)) return false; float distance = botAI->GetMaster() ? 45.0f : 35.0f; @@ -105,7 +112,7 @@ bool SprintTrigger::IsActive() bool ExposeArmorTrigger::IsActive() { - Unit* target = AI_VALUE(Unit*, "current target"); // Get the bot's current target + Unit* target = AI_VALUE(Unit*, "current target"); return DebuffTrigger::IsActive() && !botAI->HasAura("sunder armor", target, false, false, -1, true) && AI_VALUE2(uint8, "combo", "current target") <= 3; } diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Trigger/RogueTriggers.h b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueTriggers.h similarity index 90% rename from modules/mod-playerbots/src/Ai/Class/Rogue/Trigger/RogueTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Rogue/RogueTriggers.h index d9a184238..c5cb57848 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Trigger/RogueTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/RogueTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ROGUETRIGGERS_H -#define _PLAYERBOT_ROGUETRIGGERS_H +#ifndef PLAYERBOTS_ROGUETRIGGERS_H +#define PLAYERBOTS_ROGUETRIGGERS_H #include "GenericTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp index b8563893c..46f997e89 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp @@ -1,3 +1,8 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ #include "AssassinationRogueStrategy.h" @@ -29,7 +34,7 @@ private: return new ActionNode( "envenom", /*P*/ {}, - /*A*/ { NextAction("rupture") }, + /*A*/ { NextAction("eviscerate") }, /*C*/ {} ); } @@ -108,10 +113,10 @@ void AssassinationRogueStrategy::InitTriggers(std::vector& trigger triggers.push_back( new TriggerNode( - "combo points 3 available", + "combo points 4 available", { - NextAction("envenom", ACTION_HIGH + 5), - NextAction("eviscerate", ACTION_HIGH + 3) + NextAction("cold blood", ACTION_HIGH + 6), + NextAction("envenom", ACTION_HIGH + 5) } ) ); @@ -120,8 +125,7 @@ void AssassinationRogueStrategy::InitTriggers(std::vector& trigger new TriggerNode( "target with combo points almost dead", { - NextAction("envenom", ACTION_HIGH + 4), - NextAction("eviscerate", ACTION_HIGH + 2) + NextAction("envenom", ACTION_HIGH + 4) } ) ); diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.h index 3a6ac209d..e4478e52e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.h @@ -1,6 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#ifndef _PLAYERBOT_ASSASSINATIONROGUESTRATEGY_H -#define _PLAYERBOT_ASSASSINATIONROGUESTRATEGY_H +#ifndef PLAYERBOTS_ASSASSINATIONROGUESTRATEGY_H +#define PLAYERBOTS_ASSASSINATIONROGUESTRATEGY_H #include "MeleeCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp index 22c6a6f83..07bde401d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DpsRogueStrategy.h" @@ -12,36 +13,13 @@ class DpsRogueStrategyActionNodeFactory : public NamedObjectFactory public: DpsRogueStrategyActionNodeFactory() { - creators["mutilate"] = &mutilate; creators["sinister strike"] = &sinister_strike; creators["kick"] = &kick; - creators["kidney shot"] = &kidney_shot; creators["backstab"] = &backstab; - creators["melee"] = &melee; creators["rupture"] = &rupture; } private: - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ {}, - /*A*/ { - NextAction("mutilate") }, - /*C*/ {} - ); - } - static ActionNode* mutilate([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mutilate", - /*P*/ {}, - /*A*/ { - NextAction("sinister strike") }, - /*C*/ {} - ); - } static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -62,22 +40,13 @@ private: /*C*/ {} ); } - static ActionNode* kidney_shot([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "kidney shot", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } static ActionNode* backstab([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( "backstab", /*P*/ {}, /*A*/ { - NextAction("mutilate") }, + NextAction("sinister strike") }, /*C*/ {} ); } @@ -140,7 +109,7 @@ void DpsRogueStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rupture", ACTION_HIGH + 1), NextAction("eviscerate", ACTION_HIGH) @@ -257,9 +226,6 @@ public: StealthedRogueStrategyActionNodeFactory() { creators["ambush"] = &ambush; - creators["cheap shot"] = &cheap_shot; - creators["garrote"] = &garrote; - creators["sap"] = &sap; creators["sinister strike"] = &sinister_strike; } @@ -274,36 +240,6 @@ private: ); } - static ActionNode* cheap_shot([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "cheap shot", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* garrote([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "garrote", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* sap([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "sap", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -335,7 +271,7 @@ void StealthedRogueStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("eviscerate", ACTION_HIGH) } diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.h index ae418b6c9..71780a9b2 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DPSROGUESTRATEGY_H -#define _PLAYERBOT_DPSROGUESTRATEGY_H +#ifndef PLAYERBOTS_DPSROGUESTRATEGY_H +#define PLAYERBOTS_DPSROGUESTRATEGY_H #include "CombatStrategy.h" #include "MeleeCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.cpp index 88838f2dd..252683204 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericRogueNonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.h index 8bc4e3c5d..5ec38b12d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Rogue/Strategy/GenericRogueNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICROGUENONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICROGUENONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICROGUENONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICROGUENONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.h b/modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.h deleted file mode 100644 index 69f29f049..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.h +++ /dev/null @@ -1,667 +0,0 @@ -/* - * 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_SHAMANACTIONS_H -#define _PLAYERBOT_SHAMANACTIONS_H - -#include "GenericSpellActions.h" -#include "Playerbots.h" -#include "SharedDefines.h" -#include "TotemsShamanStrategy.h" - -class PlayerbotAI; - -// Buff and Out of Combat Actions - -class CastWaterShieldAction : public CastBuffSpellAction -{ -public: - CastWaterShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water shield") {} -}; - -class CastLightningShieldAction : public CastBuffSpellAction -{ -public: - CastLightningShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "lightning shield") {} -}; - -class CastEarthlivingWeaponAction : public CastEnchantItemAction -{ -public: - CastEarthlivingWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "earthliving weapon") {} -}; - -class CastRockbiterWeaponAction : public CastEnchantItemAction -{ -public: - CastRockbiterWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "rockbiter weapon") {} -}; - -class CastFlametongueWeaponAction : public CastEnchantItemAction -{ -public: - CastFlametongueWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "flametongue weapon") {} -}; - -class CastFrostbrandWeaponAction : public CastEnchantItemAction -{ -public: - CastFrostbrandWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "frostbrand weapon") {} -}; - -class CastWindfuryWeaponAction : public CastEnchantItemAction -{ -public: - CastWindfuryWeaponAction(PlayerbotAI* botAI) : CastEnchantItemAction(botAI, "windfury weapon") {} -}; - -class CastAncestralSpiritAction : public ResurrectPartyMemberAction -{ -public: - CastAncestralSpiritAction(PlayerbotAI* botAI) : ResurrectPartyMemberAction(botAI, "ancestral spirit") {} -}; - -class CastWaterBreathingAction : public CastBuffSpellAction -{ -public: - CastWaterBreathingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water breathing") {} -}; - -class CastWaterWalkingAction : public CastBuffSpellAction -{ -public: - CastWaterWalkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "water walking") {} -}; - -class CastWaterBreathingOnPartyAction : public BuffOnPartyAction -{ -public: - CastWaterBreathingOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "water breathing") {} -}; - -class CastWaterWalkingOnPartyAction : public BuffOnPartyAction -{ -public: - CastWaterWalkingOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "water walking") {} -}; - -// Boost Actions - -class CastHeroismAction : public CastBuffSpellAction -{ -public: - CastHeroismAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "heroism") {} -}; - -class CastBloodlustAction : public CastBuffSpellAction -{ -public: - CastBloodlustAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "bloodlust") {} -}; - -class CastElementalMasteryAction : public CastBuffSpellAction -{ -public: - CastElementalMasteryAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "elemental mastery") {} -}; - -class CastShamanisticRageAction : public CastBuffSpellAction -{ -public: - CastShamanisticRageAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "shamanistic rage") {} -}; - -class CastFeralSpiritAction : public CastSpellAction -{ -public: - CastFeralSpiritAction(PlayerbotAI* ai) : CastSpellAction(ai, "feral spirit") {} -}; - -class CastSpiritWalkAction : public Action -{ -public: - CastSpiritWalkAction(PlayerbotAI* botAI) : Action(botAI, "spirit walk") {} - - bool Execute(Event event) override; -}; - -// CC, Interrupt, and Dispel Actions - -class CastWindShearAction : public CastSpellAction -{ -public: - CastWindShearAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wind shear") {} -}; - -class CastWindShearOnEnemyHealerAction : public CastSpellOnEnemyHealerAction -{ -public: - CastWindShearOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "wind shear") {} -}; - -class CastPurgeAction : public CastSpellAction -{ -public: - CastPurgeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "purge") {} -}; - -class CastCleanseSpiritAction : public CastCureSpellAction -{ -public: - CastCleanseSpiritAction(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cleanse spirit") {} -}; - -class CastCleanseSpiritPoisonOnPartyAction : public CurePartyMemberAction -{ -public: - CastCleanseSpiritPoisonOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_POISON) - { - } - - std::string const getName() override { return "cleanse spirit poison on party"; } -}; - -class CastCleanseSpiritCurseOnPartyAction : public CurePartyMemberAction -{ -public: - CastCleanseSpiritCurseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_CURSE) - { - } - - std::string const getName() override { return "cleanse spirit curse on party"; } -}; - -class CastCleanseSpiritDiseaseOnPartyAction : public CurePartyMemberAction -{ -public: - CastCleanseSpiritDiseaseOnPartyAction(PlayerbotAI* botAI) - : CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_DISEASE) - { - } - - std::string const getName() override { return "cleanse spirit disease on party"; } -}; - -class CastCurePoisonActionSham : public CastCureSpellAction -{ -public: - CastCurePoisonActionSham(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cure poison") {} -}; - -class CastCurePoisonOnPartyActionSham : public CurePartyMemberAction -{ -public: - CastCurePoisonOnPartyActionSham(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "cure poison", DISPEL_POISON) {} - - std::string const getName() override { return "cure poison on party"; } -}; - -class CastCureDiseaseActionSham : public CastCureSpellAction -{ -public: - CastCureDiseaseActionSham(PlayerbotAI* botAI) : CastCureSpellAction(botAI, "cure disease") {} -}; - -class CastCureDiseaseOnPartyActionSham : public CurePartyMemberAction -{ -public: - CastCureDiseaseOnPartyActionSham(PlayerbotAI* botAI) : CurePartyMemberAction(botAI, "cure disease", DISPEL_DISEASE) - { - } - - std::string const getName() override { return "cure disease on party"; } -}; - -// Damage and Debuff Actions - -class CastFireNovaAction : public CastMeleeSpellAction -{ -public: - CastFireNovaAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "fire nova") {} - bool isUseful() override; -}; - -class CastStormstrikeAction : public CastMeleeSpellAction -{ -public: - CastStormstrikeAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "stormstrike") {} -}; - -class CastLavaLashAction : public CastMeleeSpellAction -{ -public: - CastLavaLashAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "lava lash") {} -}; - -class CastFlameShockAction : public CastDebuffSpellAction -{ -public: - CastFlameShockAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} - bool isUseful() override - { - // Bypass TTL check - return CastAuraSpellAction::isUseful(); - } -}; - -class CastEarthShockAction : public CastSpellAction -{ -public: - CastEarthShockAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "earth shock") {} -}; - -class CastFrostShockAction : public CastSnareSpellAction -{ -public: - CastFrostShockAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "frost shock") {} -}; - -class CastChainLightningAction : public CastSpellAction -{ -public: - CastChainLightningAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "chain lightning") {} - ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } -}; - -class CastLightningBoltAction : public CastSpellAction -{ -public: - CastLightningBoltAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "lightning bolt") {} -}; - -class CastThunderstormAction : public CastSpellAction -{ -public: - CastThunderstormAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "thunderstorm") {} -}; - -class CastLavaBurstAction : public CastSpellAction -{ -public: - CastLavaBurstAction(PlayerbotAI* ai) : CastSpellAction(ai, "lava burst") {} - bool isUseful() override; -}; - -// Healing Actions - -class CastLesserHealingWaveAction : public CastHealingSpellAction -{ -public: - CastLesserHealingWaveAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "lesser healing wave") {} -}; - -class CastLesserHealingWaveOnPartyAction : public HealPartyMemberAction -{ -public: - CastLesserHealingWaveOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "lesser healing wave", 25.0f, HealingManaEfficiency::LOW) - { - } -}; - -class CastHealingWaveAction : public CastHealingSpellAction -{ -public: - CastHealingWaveAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "healing wave") {} -}; - -class CastHealingWaveOnPartyAction : public HealPartyMemberAction -{ -public: - CastHealingWaveOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "healing wave", 50.0f, HealingManaEfficiency::MEDIUM) - { - } -}; - -class CastChainHealAction : public HealPartyMemberAction -{ -public: - CastChainHealAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) - { - } -}; - -class CastRiptideAction : public CastHealingSpellAction -{ -public: - CastRiptideAction(PlayerbotAI* botAI) : CastHealingSpellAction(botAI, "riptide") {} -}; - -class CastRiptideOnPartyAction : public HealPartyMemberAction -{ -public: - CastRiptideOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "riptide", 15.0f, HealingManaEfficiency::VERY_HIGH) - { - } -}; - -class CastEarthShieldAction : public CastBuffSpellAction -{ -public: - CastEarthShieldAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "earth shield") {} -}; - -class CastEarthShieldOnPartyAction : public BuffOnPartyAction -{ -public: - CastEarthShieldOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "earth shield") {} -}; - -class CastEarthShieldOnMainTankAction : public BuffOnMainTankAction -{ -public: - CastEarthShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "earth shield", false) {} -}; - -// Totem Spells - -class CastTotemAction : public CastBuffSpellAction -{ -public: - CastTotemAction(PlayerbotAI* botAI, std::string const spell, std::string const buffName = "") - : CastBuffSpellAction(botAI, spell) - { - buff = (buffName == "") ? spell : buffName; - } - - bool isUseful() override; - -protected: - std::string buff; -}; - -class CastCallOfTheElementsAction : public CastSpellAction -{ -public: - CastCallOfTheElementsAction(PlayerbotAI* ai) : CastSpellAction(ai, "call of the elements") {} -}; - -class CastTotemicRecallAction : public CastSpellAction -{ -public: - CastTotemicRecallAction(PlayerbotAI* ai) : CastSpellAction(ai, "totemic recall") {} -}; - -class CastStrengthOfEarthTotemAction : public CastTotemAction -{ -public: - CastStrengthOfEarthTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "strength of earth totem", "strength of earth") {} -}; - -class CastStoneskinTotemAction : public CastTotemAction -{ -public: - CastStoneskinTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "stoneskin totem", "stoneskin") {} -}; - -class CastTremorTotemAction : public CastTotemAction -{ -public: - CastTremorTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "tremor totem", "") {} -}; - -class CastEarthbindTotemAction : public CastTotemAction -{ -public: - CastEarthbindTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "earthbind totem", "") {} -}; - -class CastStoneclawTotemAction : public CastTotemAction -{ -public: - CastStoneclawTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "stoneclaw totem", "") {} - bool isUseful() override; -}; - -class CastSearingTotemAction : public CastTotemAction -{ -public: - CastSearingTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "searing totem", "") {} -}; - -class CastMagmaTotemAction : public CastTotemAction -{ -public: - CastMagmaTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "magma totem", "") {} - std::string const GetTargetName() override { return "self target"; } - bool isUseful() override; -}; - -class CastFlametongueTotemAction : public CastTotemAction -{ -public: - CastFlametongueTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "flametongue totem", "flametongue totem") {} -}; - -class CastTotemOfWrathAction : public CastTotemAction -{ -public: - CastTotemOfWrathAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "totem of wrath", "totem of wrath") {} -}; - -class CastFrostResistanceTotemAction : public CastTotemAction -{ -public: - CastFrostResistanceTotemAction(PlayerbotAI* botAI) - : CastTotemAction(botAI, "frost resistance totem", "frost resistance") {} -}; - -class CastFireElementalTotemAction : public CastTotemAction -{ -public: - CastFireElementalTotemAction(PlayerbotAI* ai) : CastTotemAction(ai, "fire elemental totem", "") {} - virtual std::string const GetTargetName() override { return "self target"; } - virtual bool isUseful() override { return CastTotemAction::isUseful(); } -}; - -class CastFireElementalTotemMeleeAction : public CastTotemAction -{ -public: - CastFireElementalTotemMeleeAction(PlayerbotAI* ai) : CastTotemAction(ai, "fire elemental totem", "") {} - virtual std::string const GetTargetName() override { return "self target"; } - virtual bool isUseful() override - { - Unit* target = AI_VALUE(Unit*, "current target"); - if (!target || !bot->IsWithinMeleeRange(target)) - return false; - return CastTotemAction::isUseful(); - } -}; - -class CastHealingStreamTotemAction : public CastTotemAction -{ -public: - CastHealingStreamTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "healing stream totem", "") {} -}; - -class CastManaSpringTotemAction : public CastTotemAction -{ -public: - CastManaSpringTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "mana spring totem", "mana spring") {} -}; - -class CastCleansingTotemAction : public CastTotemAction -{ -public: - CastCleansingTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "cleansing totem", "") {} - virtual bool isUseful(); -}; - -class CastManaTideTotemAction : public CastTotemAction -{ -public: - CastManaTideTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "mana tide totem", "") {} - std::string const GetTargetName() override { return "self target"; } -}; - -class CastFireResistanceTotemAction : public CastTotemAction -{ -public: - CastFireResistanceTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "fire resistance totem", "fire resistance") {} -}; - -class CastWrathOfAirTotemAction : public CastTotemAction -{ -public: - CastWrathOfAirTotemAction(PlayerbotAI* ai) : CastTotemAction(ai, "wrath of air totem", "wrath of air totem") {} -}; - -class CastWindfuryTotemAction : public CastTotemAction -{ -public: - CastWindfuryTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "windfury totem", "windfury totem") {} -}; - -class CastNatureResistanceTotemAction : public CastTotemAction -{ -public: - CastNatureResistanceTotemAction(PlayerbotAI* botAI) : CastTotemAction(botAI, "nature resistance totem", "nature resistance") {} -}; - -// Set Strategy Assigned Totems - -class SetTotemAction : public Action -{ -public: - // Template constructor: infers N (size of the id array) at compile time - template - SetTotemAction(PlayerbotAI* botAI, std::string const& totemName, const uint32 (&ids)[N], int actionButtonId) - : Action(botAI, "set " + totemName) - , totemSpellIds(ids) - , totemSpellIdsCount(N) - , actionButtonId(actionButtonId) - {} - - bool Execute(Event event) override; - uint32 const* totemSpellIds; - size_t totemSpellIdsCount; - int actionButtonId; -}; - -class SetStrengthOfEarthTotemAction : public SetTotemAction -{ -public: - SetStrengthOfEarthTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} -}; - -class SetStoneskinTotemAction : public SetTotemAction -{ -public: - SetStoneskinTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} -}; - -class SetTremorTotemAction : public SetTotemAction -{ -public: - SetTremorTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} -}; - -class SetEarthbindTotemAction : public SetTotemAction -{ -public: - SetEarthbindTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} -}; - -class SetSearingTotemAction : public SetTotemAction -{ -public: - SetSearingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} -}; - -class SetMagmaTotemAction : public SetTotemAction -{ -public: - SetMagmaTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} -}; - -class SetFlametongueTotemAction : public SetTotemAction -{ -public: - SetFlametongueTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} -}; - -class SetTotemOfWrathAction : public SetTotemAction -{ -public: - SetTotemOfWrathAction(PlayerbotAI* ai) - : SetTotemAction(ai, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} -}; - -class SetFrostResistanceTotemAction : public SetTotemAction -{ -public: - SetFrostResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} -}; - -class SetHealingStreamTotemAction : public SetTotemAction -{ -public: - SetHealingStreamTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} -}; - -class SetManaSpringTotemAction : public SetTotemAction -{ -public: - SetManaSpringTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} -}; - -class SetCleansingTotemAction : public SetTotemAction -{ -public: - SetCleansingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} -}; - -class SetFireResistanceTotemAction : public SetTotemAction -{ -public: - SetFireResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} -}; - -class SetWrathOfAirTotemAction : public SetTotemAction -{ -public: - SetWrathOfAirTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} -}; - -class SetWindfuryTotemAction : public SetTotemAction -{ -public: - SetWindfuryTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} -}; - -class SetNatureResistanceTotemAction : public SetTotemAction -{ -public: - SetNatureResistanceTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} -}; - -class SetGroundingTotemAction : public SetTotemAction -{ -public: - SetGroundingTotemAction(PlayerbotAI* ai) - : SetTotemAction(ai, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.cpp similarity index 78% rename from modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.cpp rename to modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.cpp index 3bb0ae86f..b4f839b2d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Action/ShamanActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ShamanActions.h" @@ -57,7 +58,8 @@ bool CastLavaBurstAction::isUseful() if (!target) return false; - static const uint32 FLAME_SHOCK_SPELL_IDS[] = {8050, 8052, 8053, 10447, 10448, 29228, 25457, 49232, 49233}; + static const uint32 FLAME_SHOCK_SPELL_IDS[] = + {8050, 8052, 8053, 10447, 10448, 29228, 25457, 49232, 49233}; ObjectGuid botGuid = bot->GetGUID(); for (uint32 spellId : FLAME_SHOCK_SPELL_IDS) @@ -65,6 +67,7 @@ bool CastLavaBurstAction::isUseful() if (target->HasAura(spellId, botGuid)) return true; } + return false; } @@ -77,15 +80,13 @@ bool CastSpiritWalkAction::Execute(Event /*event*/) for (Unit* unit : bot->m_Controlled) { - if (unit->GetEntry() == SPIRIT_WOLF) + if (unit->GetEntry() == SPIRIT_WOLF && unit->HasSpell(SPIRIT_WALK_SPELL)) { - if (unit->HasSpell(SPIRIT_WALK_SPELL)) - { - unit->CastSpell(unit, SPIRIT_WALK_SPELL, false); - return true; - } + unit->CastSpell(unit, SPIRIT_WALK_SPELL, false); + return true; } } + return false; } @@ -105,18 +106,15 @@ bool SetTotemAction::Execute(Event /*event*/) } if (!totemSpell) + return false; + + if (const ActionButton* button = bot->GetActionButton(actionButtonId); + button && button->GetType() == ACTION_BUTTON_SPELL && + button->GetAction() == totemSpell) { return false; } - if (const ActionButton* button = bot->GetActionButton(actionButtonId)) - { - if (button->GetType() == ACTION_BUTTON_SPELL && button->GetAction() == totemSpell) - { - return false; - } - } - bot->addActionButton(actionButtonId, totemSpell, ACTION_BUTTON_SPELL); return true; } diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.h b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.h new file mode 100644 index 000000000..314fd9e66 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanActions.h @@ -0,0 +1,712 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SHAMANACTIONS_H +#define PLAYERBOTS_SHAMANACTIONS_H + +#include "GenericSpellActions.h" +#include "Playerbots.h" +#include "SharedDefines.h" +#include "TotemsShamanStrategy.h" + +class PlayerbotAI; + +// Buff and Out of Combat Actions + +class CastWaterShieldAction : public CastBuffSpellAction +{ +public: + CastWaterShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water shield") {} +}; + +class CastLightningShieldAction : public CastBuffSpellAction +{ +public: + CastLightningShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "lightning shield") {} +}; + +class CastRockbiterWeaponMainHandAction : public CastEnchantItemMainHandAction +{ +public: + CastRockbiterWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "rockbiter weapon") {} +}; + +class CastFlametongueWeaponMainHandAction : public CastEnchantItemMainHandAction +{ +public: + CastFlametongueWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "flametongue weapon") {} +}; + +class CastFlametongueWeaponOffHandAction : public CastEnchantItemOffHandAction +{ +public: + CastFlametongueWeaponOffHandAction(PlayerbotAI* botAI) : + CastEnchantItemOffHandAction(botAI, "flametongue weapon") {} +}; + +/* class CastFrostbrandWeaponOffHandAction : public CastEnchantItemOffHandAction +{ +public: + CastFrostbrandWeaponOffHandAction(PlayerbotAI* botAI) : + CastEnchantItemOffHandAction(botAI, "frostbrand weapon") {} +}; */ + +class CastEarthlivingWeaponMainHandAction : public CastEnchantItemMainHandAction +{ +public: + CastEarthlivingWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "earthliving weapon") {} +}; + +class CastWindfuryWeaponMainHandAction : public CastEnchantItemMainHandAction +{ +public: + CastWindfuryWeaponMainHandAction(PlayerbotAI* botAI) : + CastEnchantItemMainHandAction(botAI, "windfury weapon") {} +}; + +class CastAncestralSpiritAction : public ResurrectPartyMemberAction +{ +public: + CastAncestralSpiritAction(PlayerbotAI* botAI) : + ResurrectPartyMemberAction(botAI, "ancestral spirit") {} +}; + +class CastWaterBreathingAction : public CastBuffSpellAction +{ +public: + CastWaterBreathingAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water breathing") {} +}; + +class CastWaterWalkingAction : public CastBuffSpellAction +{ +public: + CastWaterWalkingAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "water walking") {} +}; + +class CastWaterBreathingOnPartyAction : public BuffOnPartyAction +{ +public: + CastWaterBreathingOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "water breathing") {} +}; + +class CastWaterWalkingOnPartyAction : public BuffOnPartyAction +{ +public: + CastWaterWalkingOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "water walking") {} +}; + +// Boost Actions + +class CastHeroismAction : public CastBuffSpellAction +{ +public: + CastHeroismAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "heroism") {} +}; + +class CastBloodlustAction : public CastBuffSpellAction +{ +public: + CastBloodlustAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "bloodlust") {} +}; + +class CastElementalMasteryAction : public CastBuffSpellAction +{ +public: + CastElementalMasteryAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "elemental mastery") {} +}; + +class CastShamanisticRageAction : public CastBuffSpellAction +{ +public: + CastShamanisticRageAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "shamanistic rage") {} +}; + +class CastFeralSpiritAction : public CastSpellAction +{ +public: + CastFeralSpiritAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "feral spirit") {} +}; + +class CastSpiritWalkAction : public Action +{ +public: + CastSpiritWalkAction(PlayerbotAI* botAI) : Action(botAI, "spirit walk") {} + + bool Execute(Event event) override; +}; + +// CC, Interrupt, and Dispel Actions + +class CastWindShearAction : public CastSpellAction +{ +public: + CastWindShearAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wind shear") {} +}; + +class CastWindShearOnEnemyHealerAction : public CastSpellOnEnemyHealerAction +{ +public: + CastWindShearOnEnemyHealerAction(PlayerbotAI* botAI) : + CastSpellOnEnemyHealerAction(botAI, "wind shear") {} +}; + +class CastPurgeAction : public CastSpellAction +{ +public: + CastPurgeAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "purge") {} +}; + +class CastCleanseSpiritAction : public CastCureSpellAction +{ +public: + CastCleanseSpiritAction(PlayerbotAI* botAI) : + CastCureSpellAction(botAI, "cleanse spirit") {} +}; + +class CastCleanseSpiritPoisonOnPartyAction : public CurePartyMemberAction +{ +public: + CastCleanseSpiritPoisonOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_POISON) {} + + std::string const getName() override { return "cleanse spirit poison on party"; } +}; + +class CastCleanseSpiritCurseOnPartyAction : public CurePartyMemberAction +{ +public: + CastCleanseSpiritCurseOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_CURSE) {} + + std::string const getName() override { return "cleanse spirit curse on party"; } +}; + +class CastCleanseSpiritDiseaseOnPartyAction : public CurePartyMemberAction +{ +public: + CastCleanseSpiritDiseaseOnPartyAction(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cleanse spirit", DISPEL_DISEASE) {} + + std::string const getName() override { return "cleanse spirit disease on party"; } +}; + +class CastCureToxinsActionSham : public CastCureSpellAction +{ +public: + CastCureToxinsActionSham(PlayerbotAI* botAI) : + CastCureSpellAction(botAI, "cure toxins") {} +}; + +class CastCureToxinsPoisonOnPartyActionSham : public CurePartyMemberAction +{ +public: + CastCureToxinsPoisonOnPartyActionSham(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cure toxins", DISPEL_POISON) {} + + std::string const getName() override { return "cure toxins poison on party"; } +}; + +class CastCureToxinsDiseaseOnPartyActionSham : public CurePartyMemberAction +{ +public: + CastCureToxinsDiseaseOnPartyActionSham(PlayerbotAI* botAI) : + CurePartyMemberAction(botAI, "cure toxins", DISPEL_DISEASE) {} + + std::string const getName() override { return "cure toxins disease on party"; } +}; + +// Damage and Debuff Actions + +class CastFireNovaAction : public CastMeleeSpellAction +{ +public: + CastFireNovaAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "fire nova") {} + + bool isUseful() override; +}; + +class CastStormstrikeAction : public CastMeleeSpellAction +{ +public: + CastStormstrikeAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "stormstrike") {} +}; + +class CastLavaLashAction : public CastMeleeSpellAction +{ +public: + CastLavaLashAction(PlayerbotAI* botAI) : + CastMeleeSpellAction(botAI, "lava lash") {} +}; + +class CastFlameShockAction : public CastDebuffSpellAction +{ +public: + CastFlameShockAction(PlayerbotAI* botAI) : + CastDebuffSpellAction(botAI, "flame shock", true, 6.0f) {} + + bool isUseful() override { return CastAuraSpellAction::isUseful(); } +}; + +class CastEarthShockAction : public CastSpellAction +{ +public: + CastEarthShockAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "earth shock") {} +}; + +class CastFrostShockAction : public CastSnareSpellAction +{ +public: + CastFrostShockAction(PlayerbotAI* botAI) : + CastSnareSpellAction(botAI, "frost shock") {} +}; + +class CastChainLightningAction : public CastSpellAction +{ +public: + CastChainLightningAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "chain lightning") {} + + ActionThreatType getThreatType() override { return ActionThreatType::Aoe; } +}; + +class CastLightningBoltAction : public CastSpellAction +{ +public: + CastLightningBoltAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "lightning bolt") {} +}; + +class CastThunderstormAction : public CastSpellAction +{ +public: + CastThunderstormAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "thunderstorm") {} +}; + +class CastLavaBurstAction : public CastSpellAction +{ +public: + CastLavaBurstAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "lava burst") {} + bool isUseful() override; +}; + +// Healing Actions + +class CastLesserHealingWaveAction : public CastHealingSpellAction +{ +public: + CastLesserHealingWaveAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "lesser healing wave") {} +}; + +class CastLesserHealingWaveOnPartyAction : public HealPartyMemberAction +{ +public: + CastLesserHealingWaveOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "lesser healing wave", 25.0f, HealingManaEfficiency::LOW) {} +}; + +class CastHealingWaveAction : public CastHealingSpellAction +{ +public: + CastHealingWaveAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "healing wave") {} +}; + +class CastHealingWaveOnPartyAction : public HealPartyMemberAction +{ +public: + CastHealingWaveOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "healing wave", 50.0f, HealingManaEfficiency::MEDIUM) {} +}; + +class CastChainHealAction : public HealPartyMemberAction +{ +public: + CastChainHealAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "chain heal", 15.0f, HealingManaEfficiency::HIGH) {} +}; + +class CastRiptideAction : public CastHealingSpellAction +{ +public: + CastRiptideAction(PlayerbotAI* botAI) : + CastHealingSpellAction(botAI, "riptide") {} +}; + +class CastRiptideOnPartyAction : public HealPartyMemberAction +{ +public: + CastRiptideOnPartyAction(PlayerbotAI* botAI) : + HealPartyMemberAction(botAI, "riptide", 15.0f, HealingManaEfficiency::VERY_HIGH) {} +}; + +class CastEarthShieldAction : public CastBuffSpellAction +{ +public: + CastEarthShieldAction(PlayerbotAI* botAI) : + CastBuffSpellAction(botAI, "earth shield") {} +}; + +class CastEarthShieldOnPartyAction : public BuffOnPartyAction +{ +public: + CastEarthShieldOnPartyAction(PlayerbotAI* botAI) : + BuffOnPartyAction(botAI, "earth shield") {} +}; + +class CastEarthShieldOnMainTankAction : public BuffOnMainTankAction +{ +public: + CastEarthShieldOnMainTankAction(PlayerbotAI* botAI) : + BuffOnMainTankAction(botAI, "earth shield", false) {} +}; + +// Totem Spells + +class CastTotemAction : public CastBuffSpellAction +{ +public: + CastTotemAction( + PlayerbotAI* botAI, std::string const spell, + std::string const buffName = "") : CastBuffSpellAction(botAI, spell) + { + buff = (buffName == "") ? spell : buffName; + } + + bool isUseful() override; + +protected: + std::string buff; +}; + +class CastCallOfTheElementsAction : public CastSpellAction +{ +public: + CastCallOfTheElementsAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "call of the elements") {} +}; + +class CastTotemicRecallAction : public CastSpellAction +{ +public: + CastTotemicRecallAction(PlayerbotAI* botAI) : + CastSpellAction(botAI, "totemic recall") {} +}; + +class CastStrengthOfEarthTotemAction : public CastTotemAction +{ +public: + CastStrengthOfEarthTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "strength of earth totem", "strength of earth") {} +}; + +class CastStoneskinTotemAction : public CastTotemAction +{ +public: + CastStoneskinTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "stoneskin totem", "stoneskin") {} +}; + +class CastTremorTotemAction : public CastTotemAction +{ +public: + CastTremorTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "tremor totem", "") {} +}; + +class CastEarthbindTotemAction : public CastTotemAction +{ +public: + CastEarthbindTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "earthbind totem", "") {} +}; + +class CastStoneclawTotemAction : public CastTotemAction +{ +public: + CastStoneclawTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "stoneclaw totem", "") {} + bool isUseful() override; +}; + +class CastSearingTotemAction : public CastTotemAction +{ +public: + CastSearingTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "searing totem", "") {} +}; + +class CastMagmaTotemAction : public CastTotemAction +{ +public: + CastMagmaTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "magma totem", "") {} + + std::string const GetTargetName() override { return "self target"; } + bool isUseful() override; +}; + +class CastFlametongueTotemAction : public CastTotemAction +{ +public: + CastFlametongueTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "flametongue totem", "flametongue totem") {} +}; + +class CastTotemOfWrathAction : public CastTotemAction +{ +public: + CastTotemOfWrathAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "totem of wrath", "totem of wrath") {} +}; + +class CastFrostResistanceTotemAction : public CastTotemAction +{ +public: + CastFrostResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "frost resistance totem", "frost resistance") {} +}; + +class CastFireElementalTotemAction : public CastTotemAction +{ +public: + CastFireElementalTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire elemental totem", "") {} + + virtual std::string const GetTargetName() override { return "self target"; } + virtual bool isUseful() override { return CastTotemAction::isUseful(); } +}; + +class CastFireElementalTotemMeleeAction : public CastTotemAction +{ +public: + CastFireElementalTotemMeleeAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire elemental totem", "") {} + + virtual std::string const GetTargetName() override { return "self target"; } + virtual bool isUseful() override + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || !bot->IsWithinMeleeRange(target)) + return false; + return CastTotemAction::isUseful(); + } +}; + +class CastHealingStreamTotemAction : public CastTotemAction +{ +public: + CastHealingStreamTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "healing stream totem", "") {} +}; + +class CastManaSpringTotemAction : public CastTotemAction +{ +public: + CastManaSpringTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "mana spring totem", "mana spring") {} +}; + +class CastCleansingTotemAction : public CastTotemAction +{ +public: + CastCleansingTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "cleansing totem", "") {} + virtual bool isUseful(); +}; + +class CastManaTideTotemAction : public CastTotemAction +{ +public: + CastManaTideTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "mana tide totem", "") {} + + std::string const GetTargetName() override { return "self target"; } +}; + +class CastFireResistanceTotemAction : public CastTotemAction +{ +public: + CastFireResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "fire resistance totem", "fire resistance") {} +}; + +class CastWrathOfAirTotemAction : public CastTotemAction +{ +public: + CastWrathOfAirTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "wrath of air totem", "wrath of air totem") {} +}; + +class CastWindfuryTotemAction : public CastTotemAction +{ +public: + CastWindfuryTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "windfury totem", "windfury totem") {} +}; + +class CastNatureResistanceTotemAction : public CastTotemAction +{ +public: + CastNatureResistanceTotemAction(PlayerbotAI* botAI) : + CastTotemAction(botAI, "nature resistance totem", "nature resistance") {} +}; + +// Set Strategy Assigned Totems + +class SetTotemAction : public Action +{ +public: + // Template constructor: infers N (size of the id array) at compile time + template + SetTotemAction(PlayerbotAI* botAI, std::string const& totemName, const uint32 (&ids)[N], int actionButtonId) : + Action(botAI, "set " + totemName), totemSpellIds(ids), totemSpellIdsCount(N), actionButtonId(actionButtonId) {} + + bool Execute(Event event) override; + uint32 const* totemSpellIds; + size_t totemSpellIdsCount; + int actionButtonId; +}; + +class SetStrengthOfEarthTotemAction : public SetTotemAction +{ +public: + SetStrengthOfEarthTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} +}; + +class SetStoneskinTotemAction : public SetTotemAction +{ +public: + SetStoneskinTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} +}; + +class SetTremorTotemAction : public SetTotemAction +{ +public: + SetTremorTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} +}; + +class SetEarthbindTotemAction : public SetTotemAction +{ +public: + SetEarthbindTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} +}; + +class SetSearingTotemAction : public SetTotemAction +{ +public: + SetSearingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} +}; + +class SetMagmaTotemAction : public SetTotemAction +{ +public: + SetMagmaTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} +}; + +class SetFlametongueTotemAction : public SetTotemAction +{ +public: + SetFlametongueTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} +}; + +class SetTotemOfWrathAction : public SetTotemAction +{ +public: + SetTotemOfWrathAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} +}; + +class SetFrostResistanceTotemAction : public SetTotemAction +{ +public: + SetFrostResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} +}; + +class SetHealingStreamTotemAction : public SetTotemAction +{ +public: + SetHealingStreamTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} +}; + +class SetManaSpringTotemAction : public SetTotemAction +{ +public: + SetManaSpringTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} +}; + +class SetCleansingTotemAction : public SetTotemAction +{ +public: + SetCleansingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} +}; + +class SetFireResistanceTotemAction : public SetTotemAction +{ +public: + SetFireResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} +}; + +class SetWrathOfAirTotemAction : public SetTotemAction +{ +public: + SetWrathOfAirTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} +}; + +class SetWindfuryTotemAction : public SetTotemAction +{ +public: + SetWindfuryTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} +}; + +class SetNatureResistanceTotemAction : public SetTotemAction +{ +public: + SetNatureResistanceTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} +}; + +class SetGroundingTotemAction : public SetTotemAction +{ +public: + SetGroundingTotemAction(PlayerbotAI* botAI) : + SetTotemAction(botAI, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp index 08ee3638d..b0955dc21 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ShamanAiObjectContext.h" @@ -63,14 +64,14 @@ public: ShamanEarthTotemStrategyFactoryInternal() : NamedObjectContext(false, true) { creators["strength of earth"] = &ShamanEarthTotemStrategyFactoryInternal::strength_of_earth_totem; - creators["stoneskin"] = &ShamanEarthTotemStrategyFactoryInternal::stoneclaw_totem; + creators["stoneskin"] = &ShamanEarthTotemStrategyFactoryInternal::stoneskin_totem; creators["tremor"] = &ShamanEarthTotemStrategyFactoryInternal::earth_totem; creators["earthbind"] = &ShamanEarthTotemStrategyFactoryInternal::earthbind_totem; } private: static Strategy* strength_of_earth_totem(PlayerbotAI* botAI) { return new StrengthOfEarthTotemStrategy(botAI); } - static Strategy* stoneclaw_totem(PlayerbotAI* botAI) { return new StoneclawTotemStrategy(botAI); } + static Strategy* stoneskin_totem(PlayerbotAI* botAI) { return new StoneskinTotemStrategy(botAI); } static Strategy* earth_totem(PlayerbotAI* botAI) { return new EarthTotemStrategy(botAI); } static Strategy* earthbind_totem(PlayerbotAI* botAI) { return new EarthbindTotemStrategy(botAI); } }; @@ -158,10 +159,6 @@ public: creators["bloodlust"] = &ShamanATriggerFactoryInternal::bloodlust; creators["elemental mastery"] = &ShamanATriggerFactoryInternal::elemental_mastery; creators["wind shear on enemy healer"] = &ShamanATriggerFactoryInternal::wind_shear_on_enemy_healer; - creators["cure poison"] = &ShamanATriggerFactoryInternal::cure_poison; - creators["party member cure poison"] = &ShamanATriggerFactoryInternal::party_member_cure_poison; - creators["cure disease"] = &ShamanATriggerFactoryInternal::cure_disease; - creators["party member cure disease"] = &ShamanATriggerFactoryInternal::party_member_cure_disease; creators["earth shield on main tank"] = &ShamanATriggerFactoryInternal::earth_shield_on_main_tank; creators["maelstrom weapon 3"] = &ShamanATriggerFactoryInternal::maelstrom_weapon_3; creators["maelstrom weapon 4"] = &ShamanATriggerFactoryInternal::maelstrom_weapon_4; @@ -225,42 +222,38 @@ private: static Trigger* shock(PlayerbotAI* botAI) { return new ShockTrigger(botAI); } static Trigger* frost_shock_snare(PlayerbotAI* botAI) { return new FrostShockSnareTrigger(botAI); } static Trigger* wind_shear_on_enemy_healer(PlayerbotAI* botAI) { return new WindShearInterruptEnemyHealerSpellTrigger(botAI); } - static Trigger* cure_poison(PlayerbotAI* botAI) { return new CurePoisonTrigger(botAI); } - static Trigger* party_member_cure_poison(PlayerbotAI* botAI) { return new PartyMemberCurePoisonTrigger(botAI); } - static Trigger* cure_disease(PlayerbotAI* botAI) { return new CureDiseaseTrigger(botAI); } - static Trigger* party_member_cure_disease(PlayerbotAI* botAI) { return new PartyMemberCureDiseaseTrigger(botAI); } - static Trigger* earth_shield_on_main_tank(PlayerbotAI* ai) { return new EarthShieldOnMainTankTrigger(ai); } - static Trigger* flame_shock(PlayerbotAI* ai) { return new FlameShockTrigger(ai); } + static Trigger* earth_shield_on_main_tank(PlayerbotAI* botAI) { return new EarthShieldOnMainTankTrigger(botAI); } + static Trigger* flame_shock(PlayerbotAI* botAI) { return new FlameShockTrigger(botAI); } static Trigger* fire_elemental_totem(PlayerbotAI* botAI) { return new FireElementalTotemTrigger(botAI); } static Trigger* earth_shock_execute(PlayerbotAI* botAI) { return new EarthShockExecuteTrigger(botAI); } - static Trigger* spirit_walk_ready(PlayerbotAI* ai) { return new SpiritWalkTrigger(ai); } - static Trigger* chain_lightning_no_cd(PlayerbotAI* ai) { return new ChainLightningNoCdTrigger(ai); } - static Trigger* call_of_the_elements_and_enemy_within_melee(PlayerbotAI* ai) { return new CallOfTheElementsAndEnemyWithinMeleeTrigger(ai); } - static Trigger* maelstrom_weapon_5_and_medium_aoe(PlayerbotAI* ai) { return new MaelstromWeapon5AndMediumAoeTrigger(ai); } - static Trigger* maelstrom_weapon_4_and_medium_aoe(PlayerbotAI* ai) { return new MaelstromWeapon4AndMediumAoeTrigger(ai); } - static Trigger* call_of_the_elements(PlayerbotAI* ai) { return new CallOfTheElementsTrigger(ai); } - static Trigger* totemic_recall(PlayerbotAI* ai) { return new TotemicRecallTrigger(ai); } - static Trigger* no_earth_totem(PlayerbotAI* ai) { return new NoEarthTotemTrigger(ai); } - static Trigger* no_fire_totem(PlayerbotAI* ai) { return new NoFireTotemTrigger(ai); } - static Trigger* no_water_totem(PlayerbotAI* ai) { return new NoWaterTotemTrigger(ai); } - static Trigger* no_air_totem(PlayerbotAI* ai) { return new NoAirTotemTrigger(ai); } - static Trigger* set_strength_of_earth_totem(PlayerbotAI* ai) { return new SetStrengthOfEarthTotemTrigger(ai); } - static Trigger* set_stoneskin_totem(PlayerbotAI* ai) { return new SetStoneskinTotemTrigger(ai); } - static Trigger* set_tremor_totem(PlayerbotAI* ai) { return new SetTremorTotemTrigger(ai); } - static Trigger* set_earthbind_totem(PlayerbotAI* ai) { return new SetEarthbindTotemTrigger(ai); } - static Trigger* set_searing_totem(PlayerbotAI* ai) { return new SetSearingTotemTrigger(ai); } - static Trigger* set_magma_totem(PlayerbotAI* ai) { return new SetMagmaTotemTrigger(ai); } - static Trigger* set_flametongue_totem(PlayerbotAI* ai) { return new SetFlametongueTotemTrigger(ai); } - static Trigger* set_totem_of_wrath(PlayerbotAI* ai) { return new SetTotemOfWrathTrigger(ai); } - static Trigger* set_frost_resistance_totem(PlayerbotAI* ai) { return new SetFrostResistanceTotemTrigger(ai); } - static Trigger* set_healing_stream_totem(PlayerbotAI* ai) { return new SetHealingStreamTotemTrigger(ai); } - static Trigger* set_mana_spring_totem(PlayerbotAI* ai) { return new SetManaSpringTotemTrigger(ai); } - static Trigger* set_cleansing_totem(PlayerbotAI* ai) { return new SetCleansingTotemTrigger(ai); } - static Trigger* set_fire_resistance_totem(PlayerbotAI* ai) { return new SetFireResistanceTotemTrigger(ai); } - static Trigger* set_wrath_of_air_totem(PlayerbotAI* ai) { return new SetWrathOfAirTotemTrigger(ai); } - static Trigger* set_windfury_totem(PlayerbotAI* ai) { return new SetWindfuryTotemTrigger(ai); } - static Trigger* set_nature_resistance_totem(PlayerbotAI* ai) { return new SetNatureResistanceTotemTrigger(ai); } - static Trigger* set_grounding_totem(PlayerbotAI* ai) { return new SetGroundingTotemTrigger(ai); } + static Trigger* spirit_walk_ready(PlayerbotAI* botAI) { return new SpiritWalkTrigger(botAI); } + static Trigger* chain_lightning_no_cd(PlayerbotAI* botAI) { return new ChainLightningNoCdTrigger(botAI); } + static Trigger* call_of_the_elements_and_enemy_within_melee(PlayerbotAI* botAI) { return new CallOfTheElementsAndEnemyWithinMeleeTrigger(botAI); } + static Trigger* maelstrom_weapon_5_and_medium_aoe(PlayerbotAI* botAI) { return new MaelstromWeapon5AndMediumAoeTrigger(botAI); } + static Trigger* maelstrom_weapon_4_and_medium_aoe(PlayerbotAI* botAI) { return new MaelstromWeapon4AndMediumAoeTrigger(botAI); } + static Trigger* call_of_the_elements(PlayerbotAI* botAI) { return new CallOfTheElementsTrigger(botAI); } + static Trigger* totemic_recall(PlayerbotAI* botAI) { return new TotemicRecallTrigger(botAI); } + static Trigger* no_earth_totem(PlayerbotAI* botAI) { return new NoEarthTotemTrigger(botAI); } + static Trigger* no_fire_totem(PlayerbotAI* botAI) { return new NoFireTotemTrigger(botAI); } + static Trigger* no_water_totem(PlayerbotAI* botAI) { return new NoWaterTotemTrigger(botAI); } + static Trigger* no_air_totem(PlayerbotAI* botAI) { return new NoAirTotemTrigger(botAI); } + static Trigger* set_strength_of_earth_totem(PlayerbotAI* botAI) { return new SetStrengthOfEarthTotemTrigger(botAI); } + static Trigger* set_stoneskin_totem(PlayerbotAI* botAI) { return new SetStoneskinTotemTrigger(botAI); } + static Trigger* set_tremor_totem(PlayerbotAI* botAI) { return new SetTremorTotemTrigger(botAI); } + static Trigger* set_earthbind_totem(PlayerbotAI* botAI) { return new SetEarthbindTotemTrigger(botAI); } + static Trigger* set_searing_totem(PlayerbotAI* botAI) { return new SetSearingTotemTrigger(botAI); } + static Trigger* set_magma_totem(PlayerbotAI* botAI) { return new SetMagmaTotemTrigger(botAI); } + static Trigger* set_flametongue_totem(PlayerbotAI* botAI) { return new SetFlametongueTotemTrigger(botAI); } + static Trigger* set_totem_of_wrath(PlayerbotAI* botAI) { return new SetTotemOfWrathTrigger(botAI); } + static Trigger* set_frost_resistance_totem(PlayerbotAI* botAI) { return new SetFrostResistanceTotemTrigger(botAI); } + static Trigger* set_healing_stream_totem(PlayerbotAI* botAI) { return new SetHealingStreamTotemTrigger(botAI); } + static Trigger* set_mana_spring_totem(PlayerbotAI* botAI) { return new SetManaSpringTotemTrigger(botAI); } + static Trigger* set_cleansing_totem(PlayerbotAI* botAI) { return new SetCleansingTotemTrigger(botAI); } + static Trigger* set_fire_resistance_totem(PlayerbotAI* botAI) { return new SetFireResistanceTotemTrigger(botAI); } + static Trigger* set_wrath_of_air_totem(PlayerbotAI* botAI) { return new SetWrathOfAirTotemTrigger(botAI); } + static Trigger* set_windfury_totem(PlayerbotAI* botAI) { return new SetWindfuryTotemTrigger(botAI); } + static Trigger* set_nature_resistance_totem(PlayerbotAI* botAI) { return new SetNatureResistanceTotemTrigger(botAI); } + static Trigger* set_grounding_totem(PlayerbotAI* botAI) { return new SetGroundingTotemTrigger(botAI); } }; class ShamanAiObjectContextInternal : public NamedObjectContext @@ -272,11 +265,12 @@ public: creators["lightning shield"] = &ShamanAiObjectContextInternal::lightning_shield; creators["wind shear"] = &ShamanAiObjectContextInternal::wind_shear; creators["wind shear on enemy healer"] = &ShamanAiObjectContextInternal::wind_shear_on_enemy_healer; - creators["rockbiter weapon"] = &ShamanAiObjectContextInternal::rockbiter_weapon; - creators["flametongue weapon"] = &ShamanAiObjectContextInternal::flametongue_weapon; - creators["frostbrand weapon"] = &ShamanAiObjectContextInternal::frostbrand_weapon; - creators["windfury weapon"] = &ShamanAiObjectContextInternal::windfury_weapon; - creators["earthliving weapon"] = &ShamanAiObjectContextInternal::earthliving_weapon; + creators["rockbiter weapon main hand"] = &ShamanAiObjectContextInternal::rockbiter_weapon_main_hand; + creators["flametongue weapon main hand"] = &ShamanAiObjectContextInternal::flametongue_weapon_main_hand; + creators["flametongue weapon off hand"] = &ShamanAiObjectContextInternal::flametongue_weapon_off_hand; + // creators["frostbrand weapon off hand"] = &ShamanAiObjectContextInternal::frostbrand_weapon_off_hand; + creators["windfury weapon main hand"] = &ShamanAiObjectContextInternal::windfury_weapon_main_hand; + creators["earthliving weapon main hand"] = &ShamanAiObjectContextInternal::earthliving_weapon_main_hand; creators["purge"] = &ShamanAiObjectContextInternal::purge; creators["healing wave"] = &ShamanAiObjectContextInternal::healing_wave; creators["lesser healing wave"] = &ShamanAiObjectContextInternal::lesser_healing_wave; @@ -308,10 +302,9 @@ public: creators["heroism"] = &ShamanAiObjectContextInternal::heroism; creators["bloodlust"] = &ShamanAiObjectContextInternal::bloodlust; creators["elemental mastery"] = &ShamanAiObjectContextInternal::elemental_mastery; - creators["cure disease"] = &ShamanAiObjectContextInternal::cure_disease; - creators["cure disease on party"] = &ShamanAiObjectContextInternal::cure_disease_on_party; - creators["cure poison"] = &ShamanAiObjectContextInternal::cure_poison; - creators["cure poison on party"] = &ShamanAiObjectContextInternal::cure_poison_on_party; + creators["cure toxins"] = &ShamanAiObjectContextInternal::cure_toxins; + creators["cure toxins poison on party"] = &ShamanAiObjectContextInternal::cure_toxins_poison_on_party; + creators["cure toxins disease on party"] = &ShamanAiObjectContextInternal::cure_toxins_disease_on_party; creators["lava burst"] = &ShamanAiObjectContextInternal::lava_burst; creators["earth shield on main tank"] = &ShamanAiObjectContextInternal::earth_shield_on_main_tank; creators["shamanistic rage"] = &ShamanAiObjectContextInternal::shamanistic_rage; @@ -368,10 +361,10 @@ private: static Action* frost_shock(PlayerbotAI* botAI) { return new CastFrostShockAction(botAI); } static Action* earth_shock(PlayerbotAI* botAI) { return new CastEarthShockAction(botAI); } static Action* flame_shock(PlayerbotAI* botAI) { return new CastFlameShockAction(botAI); } + static Action* cleanse_spirit(PlayerbotAI* botAI) { return new CastCleanseSpiritAction(botAI); } static Action* cleanse_spirit_poison_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritPoisonOnPartyAction(botAI); } static Action* cleanse_spirit_disease_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritDiseaseOnPartyAction(botAI); } static Action* cleanse_spirit_curse_on_party(PlayerbotAI* botAI) { return new CastCleanseSpiritCurseOnPartyAction(botAI); } - static Action* cleanse_spirit(PlayerbotAI* botAI) { return new CastCleanseSpiritAction(botAI); } static Action* water_walking(PlayerbotAI* botAI) { return new CastWaterWalkingAction(botAI); } static Action* water_breathing(PlayerbotAI* botAI) { return new CastWaterBreathingAction(botAI); } static Action* water_walking_on_party(PlayerbotAI* botAI) { return new CastWaterWalkingOnPartyAction(botAI); } @@ -380,11 +373,12 @@ private: static Action* lightning_shield(PlayerbotAI* botAI) { return new CastLightningShieldAction(botAI); } static Action* fire_nova(PlayerbotAI* botAI) { return new CastFireNovaAction(botAI); } static Action* wind_shear(PlayerbotAI* botAI) { return new CastWindShearAction(botAI); } - static Action* rockbiter_weapon(PlayerbotAI* botAI) { return new CastRockbiterWeaponAction(botAI); } - static Action* flametongue_weapon(PlayerbotAI* botAI) { return new CastFlametongueWeaponAction(botAI); } - static Action* frostbrand_weapon(PlayerbotAI* botAI) { return new CastFrostbrandWeaponAction(botAI); } - static Action* windfury_weapon(PlayerbotAI* botAI) { return new CastWindfuryWeaponAction(botAI); } - static Action* earthliving_weapon(PlayerbotAI* botAI) { return new CastEarthlivingWeaponAction(botAI); } + static Action* rockbiter_weapon_main_hand(PlayerbotAI* botAI) { return new CastRockbiterWeaponMainHandAction(botAI); } + static Action* flametongue_weapon_main_hand(PlayerbotAI* botAI) { return new CastFlametongueWeaponMainHandAction(botAI); } + static Action* flametongue_weapon_off_hand(PlayerbotAI* botAI) { return new CastFlametongueWeaponOffHandAction(botAI); } + // static Action* frostbrand_weapon_off_hand(PlayerbotAI* botAI) { return new CastFrostbrandWeaponOffHandAction(botAI); } + static Action* earthliving_weapon_main_hand(PlayerbotAI* botAI) { return new CastEarthlivingWeaponMainHandAction(botAI); } + static Action* windfury_weapon_main_hand(PlayerbotAI* botAI) { return new CastWindfuryWeaponMainHandAction(botAI); } static Action* purge(PlayerbotAI* botAI) { return new CastPurgeAction(botAI); } static Action* healing_wave(PlayerbotAI* botAI) { return new CastHealingWaveAction(botAI); } static Action* lesser_healing_wave(PlayerbotAI* botAI) { return new CastLesserHealingWaveAction(botAI); } @@ -399,54 +393,53 @@ private: static Action* lava_lash(PlayerbotAI* botAI) { return new CastLavaLashAction(botAI); } static Action* ancestral_spirit(PlayerbotAI* botAI) { return new CastAncestralSpiritAction(botAI); } static Action* wind_shear_on_enemy_healer(PlayerbotAI* botAI) { return new CastWindShearOnEnemyHealerAction(botAI); } - static Action* cure_poison(PlayerbotAI* botAI) { return new CastCurePoisonActionSham(botAI); } - static Action* cure_poison_on_party(PlayerbotAI* botAI) { return new CastCurePoisonOnPartyActionSham(botAI); } - static Action* cure_disease(PlayerbotAI* botAI) { return new CastCureDiseaseActionSham(botAI); } - static Action* cure_disease_on_party(PlayerbotAI* botAI) { return new CastCureDiseaseOnPartyActionSham(botAI); } - static Action* lava_burst(PlayerbotAI* ai) { return new CastLavaBurstAction(ai); } - static Action* earth_shield_on_main_tank(PlayerbotAI* ai) { return new CastEarthShieldOnMainTankAction(ai); } - static Action* shamanistic_rage(PlayerbotAI* ai) { return new CastShamanisticRageAction(ai); } - static Action* feral_spirit(PlayerbotAI* ai) { return new CastFeralSpiritAction(ai); } - static Action* spirit_walk(PlayerbotAI* ai) { return new CastSpiritWalkAction(ai); } - static Action* call_of_the_elements(PlayerbotAI* ai) { return new CastCallOfTheElementsAction(ai); } - static Action* totemic_recall(PlayerbotAI* ai) { return new CastTotemicRecallAction(ai); } - static Action* strength_of_earth_totem(PlayerbotAI* ai) { return new CastStrengthOfEarthTotemAction(ai); } - static Action* stoneskin_totem(PlayerbotAI* ai) { return new CastStoneskinTotemAction(ai); } - static Action* tremor_totem(PlayerbotAI* ai) { return new CastTremorTotemAction(ai); } - static Action* earthbind_totem(PlayerbotAI* ai) { return new CastEarthbindTotemAction(ai); } - static Action* stoneclaw_totem(PlayerbotAI* ai) { return new CastStoneclawTotemAction(ai); } - static Action* searing_totem(PlayerbotAI* ai) { return new CastSearingTotemAction(ai); } - static Action* magma_totem(PlayerbotAI* ai) { return new CastMagmaTotemAction(ai); } - static Action* flametongue_totem(PlayerbotAI* ai) { return new CastFlametongueTotemAction(ai); } - static Action* totem_of_wrath(PlayerbotAI* ai) { return new CastTotemOfWrathAction(ai); } - static Action* frost_resistance_totem(PlayerbotAI* ai) { return new CastFrostResistanceTotemAction(ai); } - static Action* fire_elemental_totem(PlayerbotAI* ai) { return new CastFireElementalTotemAction(ai); } - static Action* fire_elemental_totem_melee(PlayerbotAI* ai) { return new CastFireElementalTotemMeleeAction(ai); } - static Action* healing_stream_totem(PlayerbotAI* ai) { return new CastHealingStreamTotemAction(ai); } - static Action* mana_spring_totem(PlayerbotAI* ai) { return new CastManaSpringTotemAction(ai); } - static Action* cleansing_totem(PlayerbotAI* ai) { return new CastCleansingTotemAction(ai); } - static Action* mana_tide_totem(PlayerbotAI* ai) { return new CastManaTideTotemAction(ai); } - static Action* fire_resistance_totem(PlayerbotAI* ai) { return new CastFireResistanceTotemAction(ai); } - static Action* wrath_of_air_totem(PlayerbotAI* ai) { return new CastWrathOfAirTotemAction(ai); } - static Action* windfury_totem(PlayerbotAI* ai) { return new CastWindfuryTotemAction(ai); } - static Action* nature_resistance_totem(PlayerbotAI* ai) { return new CastNatureResistanceTotemAction(ai); } - static Action* set_strength_of_earth_totem(PlayerbotAI* ai) { return new SetStrengthOfEarthTotemAction(ai); } - static Action* set_stoneskin_totem(PlayerbotAI* ai) { return new SetStoneskinTotemAction(ai); } - static Action* set_tremor_totem(PlayerbotAI* ai) { return new SetTremorTotemAction(ai); } - static Action* set_earthbind_totem(PlayerbotAI* ai) { return new SetEarthbindTotemAction(ai); } - static Action* set_searing_totem(PlayerbotAI* ai) { return new SetSearingTotemAction(ai); } - static Action* set_magma_totem(PlayerbotAI* ai) { return new SetMagmaTotemAction(ai); } - static Action* set_flametongue_totem(PlayerbotAI* ai) { return new SetFlametongueTotemAction(ai); } - static Action* set_totem_of_wrath(PlayerbotAI* ai) { return new SetTotemOfWrathAction(ai); } - static Action* set_frost_resistance_totem(PlayerbotAI* ai) { return new SetFrostResistanceTotemAction(ai); } - static Action* set_healing_stream_totem(PlayerbotAI* ai) { return new SetHealingStreamTotemAction(ai); } - static Action* set_mana_spring_totem(PlayerbotAI* ai) { return new SetManaSpringTotemAction(ai); } - static Action* set_cleansing_totem(PlayerbotAI* ai) { return new SetCleansingTotemAction(ai); } - static Action* set_fire_resistance_totem(PlayerbotAI* ai) { return new SetFireResistanceTotemAction(ai); } - static Action* set_wrath_of_air_totem(PlayerbotAI* ai) { return new SetWrathOfAirTotemAction(ai); } - static Action* set_windfury_totem(PlayerbotAI* ai) { return new SetWindfuryTotemAction(ai); } - static Action* set_nature_resistance_totem(PlayerbotAI* ai) { return new SetNatureResistanceTotemAction(ai); } - static Action* set_grounding_totem(PlayerbotAI* ai) { return new SetGroundingTotemAction(ai); } + static Action* cure_toxins(PlayerbotAI* botAI) { return new CastCureToxinsActionSham(botAI); } + static Action* cure_toxins_poison_on_party(PlayerbotAI* botAI) { return new CastCureToxinsPoisonOnPartyActionSham(botAI); } + static Action* cure_toxins_disease_on_party(PlayerbotAI* botAI) { return new CastCureToxinsDiseaseOnPartyActionSham(botAI); } + static Action* lava_burst(PlayerbotAI* botAI) { return new CastLavaBurstAction(botAI); } + static Action* earth_shield_on_main_tank(PlayerbotAI* botAI) { return new CastEarthShieldOnMainTankAction(botAI); } + static Action* shamanistic_rage(PlayerbotAI* botAI) { return new CastShamanisticRageAction(botAI); } + static Action* feral_spirit(PlayerbotAI* botAI) { return new CastFeralSpiritAction(botAI); } + static Action* spirit_walk(PlayerbotAI* botAI) { return new CastSpiritWalkAction(botAI); } + static Action* call_of_the_elements(PlayerbotAI* botAI) { return new CastCallOfTheElementsAction(botAI); } + static Action* totemic_recall(PlayerbotAI* botAI) { return new CastTotemicRecallAction(botAI); } + static Action* strength_of_earth_totem(PlayerbotAI* botAI) { return new CastStrengthOfEarthTotemAction(botAI); } + static Action* stoneskin_totem(PlayerbotAI* botAI) { return new CastStoneskinTotemAction(botAI); } + static Action* tremor_totem(PlayerbotAI* botAI) { return new CastTremorTotemAction(botAI); } + static Action* earthbind_totem(PlayerbotAI* botAI) { return new CastEarthbindTotemAction(botAI); } + static Action* stoneclaw_totem(PlayerbotAI* botAI) { return new CastStoneclawTotemAction(botAI); } + static Action* searing_totem(PlayerbotAI* botAI) { return new CastSearingTotemAction(botAI); } + static Action* magma_totem(PlayerbotAI* botAI) { return new CastMagmaTotemAction(botAI); } + static Action* flametongue_totem(PlayerbotAI* botAI) { return new CastFlametongueTotemAction(botAI); } + static Action* totem_of_wrath(PlayerbotAI* botAI) { return new CastTotemOfWrathAction(botAI); } + static Action* frost_resistance_totem(PlayerbotAI* botAI) { return new CastFrostResistanceTotemAction(botAI); } + static Action* fire_elemental_totem(PlayerbotAI* botAI) { return new CastFireElementalTotemAction(botAI); } + static Action* fire_elemental_totem_melee(PlayerbotAI* botAI) { return new CastFireElementalTotemMeleeAction(botAI); } + static Action* healing_stream_totem(PlayerbotAI* botAI) { return new CastHealingStreamTotemAction(botAI); } + static Action* mana_spring_totem(PlayerbotAI* botAI) { return new CastManaSpringTotemAction(botAI); } + static Action* cleansing_totem(PlayerbotAI* botAI) { return new CastCleansingTotemAction(botAI); } + static Action* mana_tide_totem(PlayerbotAI* botAI) { return new CastManaTideTotemAction(botAI); } + static Action* fire_resistance_totem(PlayerbotAI* botAI) { return new CastFireResistanceTotemAction(botAI); } + static Action* wrath_of_air_totem(PlayerbotAI* botAI) { return new CastWrathOfAirTotemAction(botAI); } + static Action* windfury_totem(PlayerbotAI* botAI) { return new CastWindfuryTotemAction(botAI); } + static Action* nature_resistance_totem(PlayerbotAI* botAI) { return new CastNatureResistanceTotemAction(botAI); } + static Action* set_strength_of_earth_totem(PlayerbotAI* botAI) { return new SetStrengthOfEarthTotemAction(botAI); } + static Action* set_stoneskin_totem(PlayerbotAI* botAI) { return new SetStoneskinTotemAction(botAI); } + static Action* set_tremor_totem(PlayerbotAI* botAI) { return new SetTremorTotemAction(botAI); } + static Action* set_earthbind_totem(PlayerbotAI* botAI) { return new SetEarthbindTotemAction(botAI); } + static Action* set_searing_totem(PlayerbotAI* botAI) { return new SetSearingTotemAction(botAI); } + static Action* set_magma_totem(PlayerbotAI* botAI) { return new SetMagmaTotemAction(botAI); } + static Action* set_flametongue_totem(PlayerbotAI* botAI) { return new SetFlametongueTotemAction(botAI); } + static Action* set_totem_of_wrath(PlayerbotAI* botAI) { return new SetTotemOfWrathAction(botAI); } + static Action* set_frost_resistance_totem(PlayerbotAI* botAI) { return new SetFrostResistanceTotemAction(botAI); } + static Action* set_healing_stream_totem(PlayerbotAI* botAI) { return new SetHealingStreamTotemAction(botAI); } + static Action* set_mana_spring_totem(PlayerbotAI* botAI) { return new SetManaSpringTotemAction(botAI); } + static Action* set_cleansing_totem(PlayerbotAI* botAI) { return new SetCleansingTotemAction(botAI); } + static Action* set_fire_resistance_totem(PlayerbotAI* botAI) { return new SetFireResistanceTotemAction(botAI); } + static Action* set_wrath_of_air_totem(PlayerbotAI* botAI) { return new SetWrathOfAirTotemAction(botAI); } + static Action* set_windfury_totem(PlayerbotAI* botAI) { return new SetWindfuryTotemAction(botAI); } + static Action* set_nature_resistance_totem(PlayerbotAI* botAI) { return new SetNatureResistanceTotemAction(botAI); } + static Action* set_grounding_totem(PlayerbotAI* botAI) { return new SetGroundingTotemAction(botAI); } }; SharedNamedObjectContextList ShamanAiObjectContext::sharedStrategyContexts; diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.h index cb07ba6e6..33b83da8f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHAMANAIOBJECTCONTEXT_H -#define _PLAYERBOT_SHAMANAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_SHAMANAIOBJECTCONTEXT_H +#define PLAYERBOTS_SHAMANAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanTriggers.cpp similarity index 95% rename from modules/mod-playerbots/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Shaman/ShamanTriggers.cpp index 753edb83f..a7e78defa 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Trigger/ShamanTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ShamanTriggers.h" @@ -261,13 +262,13 @@ bool TotemicRecallTrigger::IsActive() } // Find the active totem strategy for this slot, and return the highest-rank spellId the bot knows for it -static uint32 GetRequiredTotemSpellId(PlayerbotAI* ai, const char* strategies[], +static uint32 GetRequiredTotemSpellId(PlayerbotAI* botAI, const char* strategies[], const uint32* spellList[], const size_t spellCounts[], size_t numStrategies) { - Player* bot = ai->GetBot(); + Player* bot = botAI->GetBot(); for (size_t i = 0; i < numStrategies; ++i) { - if (ai->HasStrategy(strategies[i], BOT_STATE_COMBAT)) + if (botAI->HasStrategy(strategies[i], BOT_STATE_COMBAT)) { // Find the highest-rank spell the bot knows for (size_t j = 0; j < spellCounts[i]; ++j) @@ -283,20 +284,6 @@ static uint32 GetRequiredTotemSpellId(PlayerbotAI* ai, const char* strategies[], return 0; // No relevant strategy active, or bot doesn't know any rank } -// Get the spellId of the currently summoned totem in the slot -static uint32 GetSummonedTotemSpellId(Player* bot, uint8 slot) -{ - ObjectGuid guid = bot->m_SummonSlot[slot]; - if (guid.IsEmpty()) - return 0; - - Creature* totem = bot->GetMap()->GetCreature(guid); - if (!totem) - return 0; - - return totem->GetUInt32Value(UNIT_CREATED_BY_SPELL); -} - bool NoEarthTotemTrigger::IsActive() { // Check if the bot has Stoneskin Totem (required level 4) and prevents the trigger firing if it doesn't diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanTriggers.h similarity index 61% rename from modules/mod-playerbots/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Shaman/ShamanTriggers.h index 65d5081c8..2ef153e76 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Trigger/ShamanTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/ShamanTriggers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHAMANTRIGGERS_H -#define _PLAYERBOT_SHAMANTRIGGERS_H +#ifndef PLAYERBOTS_SHAMANTRIGGERS_H +#define PLAYERBOTS_SHAMANTRIGGERS_H #include "CureTriggers.h" #include "GenericTriggers.h" @@ -41,14 +42,14 @@ const uint32 SPELL_CALL_OF_THE_ELEMENTS = 66842; class MainHandWeaponNoImbueTrigger : public BuffTrigger { public: - MainHandWeaponNoImbueTrigger(PlayerbotAI* ai) : BuffTrigger(ai, "main hand", 1) {} + MainHandWeaponNoImbueTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "main hand", 1) {} virtual bool IsActive(); }; class OffHandWeaponNoImbueTrigger : public BuffTrigger { public: - OffHandWeaponNoImbueTrigger(PlayerbotAI* ai) : BuffTrigger(ai, "off hand", 1) {} + OffHandWeaponNoImbueTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "off hand", 1) {} virtual bool IsActive(); }; @@ -121,7 +122,7 @@ public: class SpiritWalkTrigger : public Trigger { public: - SpiritWalkTrigger(PlayerbotAI* ai) : Trigger(ai, "spirit walk ready") {} + SpiritWalkTrigger(PlayerbotAI* botAI) : Trigger(botAI, "spirit walk ready") {} bool IsActive() override; @@ -165,9 +166,7 @@ class PartyMemberCleanseSpiritPoisonTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritPoisonTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_POISON) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_POISON) {} }; class CleanseSpiritCurseTrigger : public NeedCureTrigger @@ -180,9 +179,7 @@ class PartyMemberCleanseSpiritCurseTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritCurseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_CURSE) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_CURSE) {} }; class CleanseSpiritDiseaseTrigger : public NeedCureTrigger @@ -195,34 +192,7 @@ class PartyMemberCleanseSpiritDiseaseTrigger : public PartyMemberNeedCureTrigger { public: PartyMemberCleanseSpiritDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_DISEASE) - { - } -}; - -class CurePoisonTrigger : public NeedCureTrigger -{ -public: - CurePoisonTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class PartyMemberCurePoisonTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCurePoisonTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cure poison", DISPEL_POISON) {} -}; - -class CureDiseaseTrigger : public NeedCureTrigger -{ -public: - CureDiseaseTrigger(PlayerbotAI* botAI) : NeedCureTrigger(botAI, "cure disease", DISPEL_DISEASE) {} -}; - -class PartyMemberCureDiseaseTrigger : public PartyMemberNeedCureTrigger -{ -public: - PartyMemberCureDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cure disease", DISPEL_DISEASE) {} + : PartyMemberNeedCureTrigger(botAI, "cleanse spirit", DISPEL_DISEASE) {} }; // Damage and Debuff Triggers @@ -250,7 +220,7 @@ public: class FlameShockTrigger : public DebuffTrigger { public: - FlameShockTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "flame shock", 1, true, 6.0f) {} + FlameShockTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "flame shock", 1, true, 6.0f) {} bool IsActive() override { return BuffTrigger::IsActive(); } }; @@ -265,19 +235,19 @@ public: class MaelstromWeapon5AndMediumAoeTrigger : public TwoTriggers { public: - MaelstromWeapon5AndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "maelstrom weapon 5", "medium aoe") {} + MaelstromWeapon5AndMediumAoeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "maelstrom weapon 5", "medium aoe") {} }; class MaelstromWeapon4AndMediumAoeTrigger : public TwoTriggers { public: - MaelstromWeapon4AndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "maelstrom weapon 4", "medium aoe") {} + MaelstromWeapon4AndMediumAoeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "maelstrom weapon 4", "medium aoe") {} }; class ChainLightningNoCdTrigger : public SpellNoCooldownTrigger { public: - ChainLightningNoCdTrigger(PlayerbotAI* ai) : SpellNoCooldownTrigger(ai, "chain lightning") {} + ChainLightningNoCdTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "chain lightning") {} }; // Healing Triggers @@ -307,49 +277,49 @@ protected: class CallOfTheElementsTrigger : public Trigger { public: - CallOfTheElementsTrigger(PlayerbotAI* ai) : Trigger(ai, "call of the elements") {} + CallOfTheElementsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "call of the elements") {} bool IsActive() override; }; class TotemicRecallTrigger : public Trigger { public: - TotemicRecallTrigger(PlayerbotAI* ai) : Trigger(ai, "totemic recall") {} + TotemicRecallTrigger(PlayerbotAI* botAI) : Trigger(botAI, "totemic recall") {} bool IsActive() override; }; class NoEarthTotemTrigger : public Trigger { public: - NoEarthTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no earth totem") {} + NoEarthTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no earth totem") {} bool IsActive() override; }; class NoFireTotemTrigger : public Trigger { public: - NoFireTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no fire totem") {} + NoFireTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no fire totem") {} bool IsActive() override; }; class NoWaterTotemTrigger : public Trigger { public: - NoWaterTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no water totem") {} + NoWaterTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no water totem") {} bool IsActive() override; }; class NoAirTotemTrigger : public Trigger { public: - NoAirTotemTrigger(PlayerbotAI* ai) : Trigger(ai, "no air totem") {} + NoAirTotemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no air totem") {} bool IsActive() override; }; class CallOfTheElementsAndEnemyWithinMeleeTrigger : public TwoTriggers { public: - CallOfTheElementsAndEnemyWithinMeleeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "call of the elements", "enemy within melee") {} + CallOfTheElementsAndEnemyWithinMeleeTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "call of the elements", "enemy within melee") {} }; // Set Strategy Assigned Totems @@ -359,10 +329,8 @@ class SetTotemTrigger : public Trigger public: // Template constructor: infers N (size of the id array) at compile time template - SetTotemTrigger(PlayerbotAI* ai, std::string const& spellName, uint32 requiredSpellId, - const uint32 (&ids)[N], int actionButtonId) - : Trigger(ai, "set " + spellName) - , requiredSpellId(requiredSpellId) + SetTotemTrigger(PlayerbotAI* botAI, std::string const& spellName, const uint32 (&ids)[N], int actionButtonId) + : Trigger(botAI, "set " + spellName) , totemSpellIds(ids) , totemSpellIdsCount(N) , actionButtonId(actionButtonId) @@ -370,7 +338,6 @@ public: bool IsActive() override; private: - uint32 requiredSpellId; uint32 const* totemSpellIds; size_t totemSpellIdsCount; int actionButtonId; @@ -379,120 +346,120 @@ private: class SetStrengthOfEarthTotemTrigger : public SetTotemTrigger { public: - SetStrengthOfEarthTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "strength of earth totem", SPELL_STRENGTH_OF_EARTH_TOTEM_RANK_1, STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStrengthOfEarthTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "strength of earth totem", STRENGTH_OF_EARTH_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetStoneskinTotemTrigger : public SetTotemTrigger { public: - SetStoneskinTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "stoneskin totem", SPELL_STONESKIN_TOTEM_RANK_1, STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetStoneskinTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "stoneskin totem", STONESKIN_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetTremorTotemTrigger : public SetTotemTrigger { public: - SetTremorTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "tremor totem", SPELL_TREMOR_TOTEM_RANK_1, TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetTremorTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "tremor totem", TREMOR_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetEarthbindTotemTrigger : public SetTotemTrigger { public: - SetEarthbindTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "earthbind totem", SPELL_EARTHBIND_TOTEM_RANK_1, EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} + SetEarthbindTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "earthbind totem", EARTHBIND_TOTEM, TOTEM_BAR_SLOT_EARTH) {} }; class SetSearingTotemTrigger : public SetTotemTrigger { public: - SetSearingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "searing totem", SPELL_SEARING_TOTEM_RANK_1, SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetSearingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "searing totem", SEARING_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetMagmaTotemTrigger : public SetTotemTrigger { public: - SetMagmaTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "magma totem", SPELL_MAGMA_TOTEM_RANK_1, MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetMagmaTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "magma totem", MAGMA_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetFlametongueTotemTrigger : public SetTotemTrigger { public: - SetFlametongueTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "flametongue totem", SPELL_FLAMETONGUE_TOTEM_RANK_1, FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFlametongueTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "flametongue totem", FLAMETONGUE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetTotemOfWrathTrigger : public SetTotemTrigger { public: - SetTotemOfWrathTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "totem of wrath", SPELL_TOTEM_OF_WRATH_RANK_1, TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} + SetTotemOfWrathTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "totem of wrath", TOTEM_OF_WRATH, TOTEM_BAR_SLOT_FIRE) {} }; class SetFrostResistanceTotemTrigger : public SetTotemTrigger { public: - SetFrostResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "frost resistance totem", SPELL_FROST_RESISTANCE_TOTEM_RANK_1, FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} + SetFrostResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "frost resistance totem", FROST_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_FIRE) {} }; class SetHealingStreamTotemTrigger : public SetTotemTrigger { public: - SetHealingStreamTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "healing stream totem", SPELL_HEALING_STREAM_TOTEM_RANK_1, HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetHealingStreamTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "healing stream totem", HEALING_STREAM_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetManaSpringTotemTrigger : public SetTotemTrigger { public: - SetManaSpringTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "mana spring totem", SPELL_MANA_SPRING_TOTEM_RANK_1, MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetManaSpringTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "mana spring totem", MANA_SPRING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetCleansingTotemTrigger : public SetTotemTrigger { public: - SetCleansingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "cleansing totem", SPELL_CLEANSING_TOTEM_RANK_1, CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetCleansingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "cleansing totem", CLEANSING_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetFireResistanceTotemTrigger : public SetTotemTrigger { public: - SetFireResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "fire resistance totem", SPELL_FIRE_RESISTANCE_TOTEM_RANK_1, FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} + SetFireResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "fire resistance totem", FIRE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_WATER) {} }; class SetWrathOfAirTotemTrigger : public SetTotemTrigger { public: - SetWrathOfAirTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "wrath of air totem", SPELL_WRATH_OF_AIR_TOTEM_RANK_1, WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWrathOfAirTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "wrath of air totem", WRATH_OF_AIR_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetWindfuryTotemTrigger : public SetTotemTrigger { public: - SetWindfuryTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "windfury totem", SPELL_WINDFURY_TOTEM_RANK_1, WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetWindfuryTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "windfury totem", WINDFURY_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetNatureResistanceTotemTrigger : public SetTotemTrigger { public: - SetNatureResistanceTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "nature resistance totem", SPELL_NATURE_RESISTANCE_TOTEM_RANK_1, NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetNatureResistanceTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "nature resistance totem", NATURE_RESISTANCE_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; class SetGroundingTotemTrigger : public SetTotemTrigger { public: - SetGroundingTotemTrigger(PlayerbotAI* ai) - : SetTotemTrigger(ai, "grounding totem", SPELL_GROUNDING_TOTEM_RANK_1, GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} + SetGroundingTotemTrigger(PlayerbotAI* botAI) + : SetTotemTrigger(botAI, "grounding totem", GROUNDING_TOTEM, TOTEM_BAR_SLOT_AIR) {} }; #endif diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp index c1295687c..efb332c22 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.cpp @@ -1,45 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ElementalShamanStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class ElementalShamanStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - ElementalShamanStrategyActionNodeFactory() - { - creators["flame shock"] = &flame_shock; - creators["earth shock"] = &earth_shock; - creators["lava burst"] = &lava_burst; - creators["lightning bolt"] = &lightning_bolt; - creators["call of the elements"] = &call_of_the_elements; - creators["elemental mastery"] = &elemental_mastery; - creators["stoneclaw totem"] = &stoneclaw_totem; - creators["water shield"] = &water_shield; - creators["thunderstorm"] = &thunderstorm; - } - -private: - static ActionNode* flame_shock(PlayerbotAI*) { return new ActionNode("flame shock", {}, {}, {}); } - static ActionNode* earth_shock(PlayerbotAI*) { return new ActionNode("earth shock", {}, {}, {}); } - static ActionNode* lava_burst(PlayerbotAI*) { return new ActionNode("lava burst", {}, {}, {}); } - static ActionNode* lightning_bolt(PlayerbotAI*) { return new ActionNode("lightning bolt", {}, {}, {}); } - static ActionNode* call_of_the_elements(PlayerbotAI*) { return new ActionNode("call of the elements", {}, {}, {}); } - static ActionNode* elemental_mastery(PlayerbotAI*) { return new ActionNode("elemental mastery", {}, {}, {}); } - static ActionNode* stoneclaw_totem(PlayerbotAI*) { return new ActionNode("stoneclaw totem", {}, {}, {}); } - static ActionNode* water_shield(PlayerbotAI*) { return new ActionNode("water shield", {}, {}, {}); } - static ActionNode* thunderstorm(PlayerbotAI*) { return new ActionNode("thunderstorm", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== ElementalShamanStrategy::ElementalShamanStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) { - actionNodeFactories.Add(new ElementalShamanStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.h b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.h index 3c5848563..74fa66b95 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ElementalShamanStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ELEMENTALSHAMANSTRATEGY_H -#define _PLAYERBOT_ELEMENTALSHAMANSTRATEGY_H +#ifndef PLAYERBOTS_ELEMENTALSHAMANSTRATEGY_H +#define PLAYERBOTS_ELEMENTALSHAMANSTRATEGY_H #include "GenericShamanStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp index 4b7fb7159..9a4b8ed9f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/EnhancementShamanStrategy.cpp @@ -1,10 +1,10 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "EnhancementShamanStrategy.h" - #include "Playerbots.h" // ===== Action Node Factory ===== @@ -13,19 +13,10 @@ class EnhancementShamanStrategyActionNodeFactory : public NamedObjectFactory, 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ENHANCEMENTSHAMANSTRATEGY_H -#define _PLAYERBOT_ENHANCEMENTSHAMANSTRATEGY_H +#ifndef PLAYERBOTS_ENHANCEMENTSHAMANSTRATEGY_H +#define PLAYERBOTS_ENHANCEMENTSHAMANSTRATEGY_H #include "GenericShamanStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.cpp index fd47d023d..52664c159 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericShamanStrategy.h" @@ -16,17 +17,13 @@ public: creators["totem of wrath"] = &totem_of_wrath; creators["flametongue totem"] = &flametongue_totem; creators["magma totem"] = &magma_totem; - creators["searing totem"] = &searing_totem; creators["strength of earth totem"] = &strength_of_earth_totem; - creators["stoneskin totem"] = &stoneskin_totem; creators["cleansing totem"] = &cleansing_totem; - creators["mana spring totem"] = &mana_spring_totem; - creators["healing stream totem"] = &healing_stream_totem; creators["wrath of air totem"] = &wrath_of_air_totem; creators["windfury totem"] = &windfury_totem; - creators["grounding totem"] = &grounding_totem; - creators["wind shear"] = &wind_shear; - creators["purge"] = &purge; + creators["cleanse spirit"] = &cleanse_spirit; + creators["cleanse spirit poison on party"] = &cleanse_spirit_poison_on_party; + creators["cleanse spirit disease on party"] = &cleanse_spirit_disease_on_party; } private: @@ -58,7 +55,6 @@ private: /*A*/ { NextAction("searing totem") }, /*C*/ {}); } - static ActionNode* searing_totem(PlayerbotAI*) { return new ActionNode("searing totem", {}, {}, {}); } static ActionNode* strength_of_earth_totem([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("strength of earth totem", @@ -66,7 +62,6 @@ private: /*A*/ { NextAction("stoneskin totem") }, /*C*/ {}); } - static ActionNode* stoneskin_totem(PlayerbotAI*) { return new ActionNode("stoneskin totem", {}, {}, {}); } static ActionNode* cleansing_totem([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("cleansing totem", @@ -74,8 +69,6 @@ private: /*A*/ { NextAction("mana spring totem") }, /*C*/ {}); } - static ActionNode* mana_spring_totem(PlayerbotAI*) { return new ActionNode("mana spring totem", {}, {}, {}); } - static ActionNode* healing_stream_totem(PlayerbotAI*) { return new ActionNode("healing stream totem", {}, {}, {}); } static ActionNode* wrath_of_air_totem([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode("wrath of air totem", @@ -90,9 +83,27 @@ private: /*A*/ { NextAction("grounding totem") }, /*C*/ {}); } - static ActionNode* grounding_totem(PlayerbotAI*) { return new ActionNode("grounding totem", {}, {}, {}); } - static ActionNode* wind_shear(PlayerbotAI*) { return new ActionNode("wind shear", {}, {}, {}); } - static ActionNode* purge(PlayerbotAI*) { return new ActionNode("purge", {}, {}, {}); } + static ActionNode* cleanse_spirit([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("cleanse spirit", + /*P*/ {}, + /*A*/ { NextAction("cure toxins") }, + /*C*/ {}); + } + static ActionNode* cleanse_spirit_poison_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("cleanse spirit poison on party", + /*P*/ {}, + /*A*/ { NextAction("cure toxins poison on party") }, + /*C*/ {}); + } + static ActionNode* cleanse_spirit_disease_on_party([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode("cleanse spirit disease on party", + /*P*/ {}, + /*A*/ { NextAction("cure toxins disease on party") }, + /*C*/ {}); + } }; GenericShamanStrategy::GenericShamanStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) @@ -107,18 +118,13 @@ void GenericShamanStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("wind shear", { NextAction("wind shear", 23.0f), })); triggers.push_back(new TriggerNode("wind shear on enemy healer", { NextAction("wind shear on enemy healer", 23.0f), })); triggers.push_back(new TriggerNode("purge", { NextAction("purge", ACTION_DISPEL), })); - triggers.push_back(new TriggerNode("medium mana", { NextAction("mana potion", ACTION_DISPEL), })); triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 65.0f), })); } void ShamanCureStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("cure poison", { NextAction("cure poison", 21.0f), })); - triggers.push_back(new TriggerNode("party member cure poison", { NextAction("cure poison on party", 21.0f), })); triggers.push_back(new TriggerNode("cleanse spirit poison", { NextAction("cleanse spirit", 24.0f), })); triggers.push_back(new TriggerNode("party member cleanse spirit poison", { NextAction("cleanse spirit poison on party", 23.0f), })); - triggers.push_back(new TriggerNode("cure disease", { NextAction("cure disease", 31.0f), })); - triggers.push_back(new TriggerNode("party member cure disease", { NextAction("cure disease on party", 30.0f), })); triggers.push_back(new TriggerNode("cleanse spirit disease", { NextAction("cleanse spirit", 24.0f), })); triggers.push_back(new TriggerNode("party member cleanse spirit disease", { NextAction("cleanse spirit disease on party", 23.0f), })); triggers.push_back(new TriggerNode("cleanse spirit curse", { NextAction("cleanse spirit", 24.0f), })); @@ -133,11 +139,11 @@ void ShamanBoostStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { triggers.push_back(new TriggerNode("fire elemental totem", { NextAction("fire elemental totem", 23.0f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { triggers.push_back(new TriggerNode("fire elemental totem", { NextAction("fire elemental totem melee", 24.0f), })); } @@ -149,23 +155,17 @@ void ShamanAoeStrategy::InitTriggers(std::vector& triggers) Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { triggers.push_back(new TriggerNode("medium aoe",{ NextAction("fire nova", 23.0f), })); triggers.push_back(new TriggerNode("chain lightning no cd", { NextAction("chain lightning", 5.6f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { - triggers.push_back(new TriggerNode("medium aoe",{ - NextAction("magma totem", 24.0f), - NextAction("fire nova", 23.0f), })); - + triggers.push_back(new TriggerNode("medium aoe",{ NextAction("fire nova", 23.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 5 and medium aoe", { NextAction("chain lightning", 22.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 4 and medium aoe", { NextAction("chain lightning", 21.0f), })); triggers.push_back(new TriggerNode("enemy within melee", { NextAction("fire nova", 5.1f), })); } - else if (tab == 2) // Restoration - { - // Handled by "Healer DPS" Strategy - } + // Resto AoE handled by "Healer DPS" Strategy } diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.h b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.h index 953b09309..56ec9ee6e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/GenericShamanStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICSHAMANSTRATEGY_H -#define _PLAYERBOT_GENERICSHAMANSTRATEGY_H +#ifndef PLAYERBOTS_GENERICSHAMANSTRATEGY_H +#define PLAYERBOTS_GENERICSHAMANSTRATEGY_H #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp index 698531a85..b0cf73492 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.cpp @@ -1,67 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RestoShamanStrategy.h" - #include "Playerbots.h" -// ===== Action Node Factory ===== -class RestoShamanStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - RestoShamanStrategyActionNodeFactory() - { - creators["mana tide totem"] = &mana_tide_totem; - creators["call of the elements"] = &call_of_the_elements; - creators["stoneclaw totem"] = &stoneclaw_totem; - creators["riptide on party"] = &riptide_on_party; - creators["chain heal on party"] = &chain_heal_on_party; - creators["healing wave on party"] = &healing_wave_on_party; - creators["lesser healing wave on party"] = &lesser_healing_wave_on_party; - creators["earth shield on main tank"] = &earth_shield_on_main_tank; - creators["cleanse spirit poison on party"] = &cleanse_spirit_poison_on_party; - creators["cleanse spirit disease on party"] = &cleanse_spirit_disease_on_party; - creators["cleanse spirit curse on party"] = &cleanse_spirit_curse_on_party; - creators["cleansing totem"] = &cleansing_totem; - creators["water shield"] = &water_shield; - creators["flame shock"] = &flame_shock; - creators["lava burst"] = &lava_burst; - creators["lightning bolt"] = &lightning_bolt; - creators["chain lightning"] = &chain_lightning; - } - -private: - static ActionNode* mana_tide_totem([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode("mana tide totem", - /*P*/ {}, - /*A*/ { NextAction("mana potion") }, - /*C*/ {}); - } - static ActionNode* call_of_the_elements(PlayerbotAI*) { return new ActionNode("call of the elements", {}, {}, {}); } - static ActionNode* stoneclaw_totem(PlayerbotAI*) { return new ActionNode("stoneclaw totem", {}, {}, {}); } - static ActionNode* riptide_on_party(PlayerbotAI*) { return new ActionNode("riptide on party", {}, {}, {}); } - static ActionNode* chain_heal_on_party(PlayerbotAI*) { return new ActionNode("chain heal on party", {}, {}, {}); } - static ActionNode* healing_wave_on_party(PlayerbotAI*) { return new ActionNode("healing wave on party", {}, {}, {}); } - static ActionNode* lesser_healing_wave_on_party(PlayerbotAI*) { return new ActionNode("lesser healing wave on party", {}, {}, {}); } - static ActionNode* earth_shield_on_main_tank(PlayerbotAI*) { return new ActionNode("earth shield on main tank", {}, {}, {}); } - static ActionNode* cleanse_spirit_poison_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit poison on party", {}, {}, {}); } - static ActionNode* cleanse_spirit_disease_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit disease on party", {}, {}, {}); } - static ActionNode* cleanse_spirit_curse_on_party(PlayerbotAI*) { return new ActionNode("cleanse spirit curse on party", {}, {}, {}); } - static ActionNode* cleansing_totem(PlayerbotAI*) { return new ActionNode("cleansing totem", {}, {}, {}); } - static ActionNode* water_shield(PlayerbotAI*) { return new ActionNode("water shield", {}, {}, {}); } - static ActionNode* flame_shock(PlayerbotAI*) { return new ActionNode("flame shock", {}, {}, {}); } - static ActionNode* lava_burst(PlayerbotAI*) { return new ActionNode("lava burst", {}, {}, {}); } - static ActionNode* lightning_bolt(PlayerbotAI*) { return new ActionNode("lightning bolt", {}, {}, {}); } - static ActionNode* chain_lightning(PlayerbotAI*) { return new ActionNode("chain lightning", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== RestoShamanStrategy::RestoShamanStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) { - actionNodeFactories.Add(new RestoShamanStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Trigger Initialization === @@ -75,28 +23,23 @@ void RestoShamanStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("medium mana", { NextAction("mana tide totem", ACTION_HIGH + 5) })); // Healing Triggers - triggers.push_back(new TriggerNode("group heal setting", { - NextAction("riptide on party", 27.0f), - NextAction("chain heal on party", 26.0f) })); + triggers.push_back(new TriggerNode("group heal setting", { NextAction("riptide on party", 27.0f), + NextAction("chain heal on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member critical health", { - NextAction("riptide on party", 25.0f), - NextAction("healing wave on party", 24.0f), - NextAction("lesser healing wave on party", 23.0f) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("riptide on party", 25.0f), + NextAction("healing wave on party", 24.0f), + NextAction("lesser healing wave on party", 23.0f) })); - triggers.push_back(new TriggerNode("party member low health", { - NextAction("riptide on party", 19.0f), - NextAction("healing wave on party", 18.0f), - NextAction("lesser healing wave on party", 17.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("riptide on party", 19.0f), + NextAction("healing wave on party", 18.0f), + NextAction("lesser healing wave on party", 17.0f) })); - triggers.push_back(new TriggerNode("party member medium health", { - NextAction("riptide on party", 16.0f), - NextAction("healing wave on party", 15.0f), - NextAction("lesser healing wave on party", 14.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("riptide on party", 16.0f), + NextAction("healing wave on party", 15.0f), + NextAction("lesser healing wave on party", 14.0f) })); - triggers.push_back(new TriggerNode("party member almost full health", { - NextAction("riptide on party", 12.0f), - NextAction("lesser healing wave on party", 11.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("riptide on party", 12.0f), + NextAction("lesser healing wave on party", 11.0f) })); triggers.push_back(new TriggerNode("earth shield on main tank", { NextAction("earth shield on main tank", ACTION_HIGH + 7) })); @@ -113,12 +56,9 @@ void RestoShamanStrategy::InitTriggers(std::vector& triggers) void ShamanHealerDpsStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("healer should attack", - { NextAction("flame shock", ACTION_DEFAULT + 0.2f), - NextAction("lava burst", ACTION_DEFAULT + 0.1f), - NextAction("lightning bolt", ACTION_DEFAULT) })); + triggers.push_back(new TriggerNode("healer should attack", { NextAction("flame shock", ACTION_DEFAULT + 0.2f), + NextAction("lava burst", ACTION_DEFAULT + 0.1f), + NextAction("lightning bolt", ACTION_DEFAULT) })); - triggers.push_back( - new TriggerNode("medium aoe and healer should attack", - { NextAction("chain lightning", ACTION_DEFAULT + 0.3f) })); + triggers.push_back( new TriggerNode("medium aoe and healer should attack", { NextAction("chain lightning", ACTION_DEFAULT + 0.3f) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.h b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.h index dc26d2ea6..05a3deca3 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/RestoShamanStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RESTOSHAMANSTRATEGY_H -#define _PLAYERBOT_RESTOSHAMANSTRATEGY_H +#ifndef PLAYERBOTS_RESTOSHAMANSTRATEGY_H +#define PLAYERBOTS_RESTOSHAMANSTRATEGY_H #include "GenericShamanStrategy.h" #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp index c72000539..e6ca8d2de 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ShamanNonCombatStrategy.h" @@ -13,45 +14,65 @@ class ShamanNonCombatStrategyActionNodeFactory : public NamedObjectFactory& triggers) NonCombatStrategy::InitTriggers(triggers); // Totemic Recall - triggers.push_back(new TriggerNode("totemic recall", { NextAction("totemic recall", 60.0f), })); + triggers.push_back(new TriggerNode("totemic recall", { NextAction("totemic recall", 60.0f) })); // Healing/Resurrect Triggers - triggers.push_back(new TriggerNode("party member dead", { NextAction("ancestral spirit", ACTION_CRITICAL_HEAL + 10), })); - triggers.push_back(new TriggerNode("party member critical health", { - NextAction("riptide on party", 31.0f), - NextAction("healing wave on party", 30.0f) })); - triggers.push_back(new TriggerNode("party member low health",{ - NextAction("riptide on party", 29.0f), - NextAction("healing wave on party", 28.0f) })); - triggers.push_back(new TriggerNode("party member medium health",{ - NextAction("riptide on party", 27.0f), - NextAction("healing wave on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member almost full health",{ - NextAction("riptide on party", 25.0f), - NextAction("lesser healing wave on party", 24.0f) })); - triggers.push_back(new TriggerNode("group heal setting",{ NextAction("chain heal on party", 27.0f) })); + triggers.push_back(new TriggerNode("party member dead", { NextAction("ancestral spirit", ACTION_CRITICAL_HEAL + 10) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("riptide on party", 31.0f), + NextAction("healing wave on party", 30.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("riptide on party", 29.0f), + NextAction("healing wave on party", 28.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("riptide on party", 27.0f), + NextAction("healing wave on party", 26.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("riptide on party", 25.0f), + NextAction("lesser healing wave on party", 24.0f) })); + triggers.push_back(new TriggerNode("group heal setting", { NextAction("chain heal on party", 27.0f) })); // Cure Triggers - triggers.push_back(new TriggerNode("cure poison", { NextAction("cure poison", 21.0f), })); - triggers.push_back(new TriggerNode("party member cure poison", { NextAction("cure poison on party", 21.0f), })); - triggers.push_back(new TriggerNode("cure disease", { NextAction("cure disease", 31.0f), })); - triggers.push_back(new TriggerNode("party member cure disease", { NextAction("cure disease on party", 30.0f), })); + triggers.push_back(new TriggerNode("cleanse spirit poison", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit poison", { NextAction("cleanse spirit poison on party", 23.0f) })); + triggers.push_back(new TriggerNode("cleanse spirit disease", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit disease", { NextAction("cleanse spirit disease on party", 23.0f) })); + triggers.push_back(new TriggerNode("cleanse spirit curse", { NextAction("cleanse spirit", 24.0f) })); + triggers.push_back(new TriggerNode("party member cleanse spirit curse", { NextAction("cleanse spirit curse on party", 23.0f) })); // Out of Combat Buff Triggers Player* bot = botAI->GetBot(); int tab = AiFactory::GetPlayerSpecTab(bot); - if (tab == 0) // Elemental + if (tab == SHAMAN_TAB_ELEMENTAL) { - triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("flametongue weapon", 22.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("flametongue weapon main hand", 22.0f), })); triggers.push_back(new TriggerNode("water shield", { NextAction("water shield", 21.0f), })); } - else if (tab == 1) // Enhancement + else if (tab == SHAMAN_TAB_ENHANCEMENT) { - triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("windfury weapon", 22.0f), })); - triggers.push_back(new TriggerNode("off hand weapon no imbue", { NextAction("flametongue weapon", 21.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("windfury weapon main hand", 22.0f), })); + triggers.push_back(new TriggerNode("off hand weapon no imbue", { NextAction("flametongue weapon off hand", 21.0f), })); triggers.push_back(new TriggerNode("lightning shield", { NextAction("lightning shield", 20.0f), })); } - else if (tab == 2) // Restoration + else if (tab == SHAMAN_TAB_RESTORATION) { - triggers.push_back(new TriggerNode("main hand weapon no imbue",{ NextAction("earthliving weapon", 22.0f), })); + triggers.push_back(new TriggerNode("main hand weapon no imbue", { NextAction("earthliving weapon main hand", 22.0f), })); triggers.push_back(new TriggerNode("water shield", { NextAction("water shield", 20.0f), })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.h index 9fc34e8b0..372346154 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/ShamanNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SHAMANNONCOMBATSTRATEGY_H -#define _PLAYERBOT_SHAMANNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_SHAMANNONCOMBATSTRATEGY_H +#define PLAYERBOTS_SHAMANNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp index d00cc5e6c..8066d57b8 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.cpp @@ -1,11 +1,23 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TotemsShamanStrategy.h" #include "Playerbots.h" +namespace +{ +constexpr uint32 SPELL_TOTEM_OF_WRATH = 30706; +constexpr uint32 SPELL_FLAMETONGUE_TOTEM = 8227; +constexpr uint32 SPELL_CLEANSING_TOTEM = 8170; +constexpr uint32 SPELL_MANA_SPRING_TOTEM = 5675; +constexpr uint32 SPELL_WRATH_OF_AIR_TOTEM = 3738; +constexpr uint32 SPELL_GROUNDING_TOTEM = 8177; +constexpr uint32 SPELL_WINDFURY_TOTEM = 8512; +} + // These combat strategies are used to set the corresponding totems on the bar, and cast the totem when it's missing. // There are special cases for Totem of Wrath, Windfury Totem, Wrath of Air totem, and Cleansing totem - these totems // aren't learned at level 30, and have fallbacks in order to prevent the trigger from continuously firing. @@ -19,8 +31,8 @@ void StrengthOfEarthTotemStrategy::InitTriggers(std::vector& trigg triggers.push_back(new TriggerNode("no earth totem", { NextAction("strength of earth totem", 55.0f) })); } -StoneclawTotemStrategy::StoneclawTotemStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) {} -void StoneclawTotemStrategy::InitTriggers(std::vector& triggers) +StoneskinTotemStrategy::StoneskinTotemStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) {} +void StoneskinTotemStrategy::InitTriggers(std::vector& triggers) { GenericShamanStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("set stoneskin totem", { NextAction("set stoneskin totem", 60.0f) })); @@ -74,14 +86,10 @@ void TotemOfWrathStrategy::InitTriggers(std::vector& triggers) GenericShamanStrategy::InitTriggers(triggers); // If the bot hasn't learned Totem of Wrath yet, set Flametongue Totem instead. Player* bot = botAI->GetBot(); - if (bot->HasSpell(30706)) - { + if (bot->HasSpell(SPELL_TOTEM_OF_WRATH)) triggers.push_back(new TriggerNode("set totem of wrath", { NextAction("set totem of wrath", 60.0f) })); - } - else if (bot->HasSpell(8227)) - { + else if (bot->HasSpell(SPELL_FLAMETONGUE_TOTEM)) triggers.push_back(new TriggerNode("set flametongue totem", { NextAction("set flametongue totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no fire totem", { NextAction("totem of wrath", 55.0f) })); } @@ -116,14 +124,10 @@ void CleansingTotemStrategy::InitTriggers(std::vector& triggers) GenericShamanStrategy::InitTriggers(triggers); // If the bot hasn't learned Cleansing Totem yet, set Mana Spring Totem instead. Player* bot = botAI->GetBot(); - if (bot->HasSpell(8170)) - { + if (bot->HasSpell(SPELL_CLEANSING_TOTEM)) triggers.push_back(new TriggerNode("set cleansing totem", { NextAction("set cleansing totem", 60.0f) })); - } - else if (bot->HasSpell(5675)) - { + else if (bot->HasSpell(SPELL_MANA_SPRING_TOTEM)) triggers.push_back(new TriggerNode("set mana spring totem", { NextAction("set mana spring totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no water totem", { NextAction("cleansing totem", 55.0f) })); } @@ -142,16 +146,11 @@ void WrathOfAirTotemStrategy::InitTriggers(std::vector& triggers) GenericShamanStrategy::InitTriggers(triggers); // If the bot hasn't learned Wrath of Air Totem yet, set Grounding Totem instead. Player* bot = botAI->GetBot(); - if (bot->HasSpell(3738)) - { + if (bot->HasSpell(SPELL_WRATH_OF_AIR_TOTEM)) triggers.push_back(new TriggerNode("set wrath of air totem", { NextAction("set wrath of air totem", 60.0f) })); - } - else if (bot->HasSpell(8177)) - { + else if (bot->HasSpell(SPELL_GROUNDING_TOTEM)) triggers.push_back(new TriggerNode("set grounding totem", { NextAction("set grounding totem", 60.0f) })); - } - triggers.push_back( - new TriggerNode("no air totem", { NextAction("wrath of air totem", 55.0f) })); + triggers.push_back( new TriggerNode("no air totem", { NextAction("wrath of air totem", 55.0f) })); } WindfuryTotemStrategy::WindfuryTotemStrategy(PlayerbotAI* botAI) : GenericShamanStrategy(botAI) {} @@ -160,14 +159,10 @@ void WindfuryTotemStrategy::InitTriggers(std::vector& triggers) GenericShamanStrategy::InitTriggers(triggers); // If the bot hasn't learned Windfury Totem yet, set Grounding Totem instead. Player* bot = botAI->GetBot(); - if (bot->HasSpell(8512)) - { + if (bot->HasSpell(SPELL_WINDFURY_TOTEM)) triggers.push_back(new TriggerNode("set windfury totem", { NextAction("set windfury totem", 60.0f) })); - } - else if (bot->HasSpell(8177)) - { + else if (bot->HasSpell(SPELL_GROUNDING_TOTEM)) triggers.push_back(new TriggerNode("set grounding totem", { NextAction("set grounding totem", 60.0f) })); - } triggers.push_back(new TriggerNode("no air totem", { NextAction("windfury totem", 55.0f) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.h b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.h index 60488740c..9c73b36a2 100644 --- a/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Shaman/Strategy/TotemsShamanStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TOTEMSSHAMANSTRATEGY_H -#define _PLAYERBOT_TOTEMSSHAMANSTRATEGY_H +#ifndef PLAYERBOTS_TOTEMSSHAMANSTRATEGY_H +#define PLAYERBOTS_TOTEMSSHAMANSTRATEGY_H #include "GenericShamanStrategy.h" #include @@ -229,10 +230,10 @@ public: std::string const getName() override { return "strength of earth"; } }; -class StoneclawTotemStrategy : public GenericShamanStrategy +class StoneskinTotemStrategy : public GenericShamanStrategy { public: - StoneclawTotemStrategy(PlayerbotAI* botAI); + StoneskinTotemStrategy(PlayerbotAI* botAI); void InitTriggers(std::vector& triggers) override; std::string const getName() override { return "stoneskin"; } }; diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.cpp index 78da04d99..f425c4403 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.cpp @@ -1,50 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AfflictionWarlockStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class AfflictionWarlockStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - AfflictionWarlockStrategyActionNodeFactory() - { - creators["corruption"] = &corruption; - creators["corruption on attacker"] = &corruption; - creators["unstable affliction"] = &unstable_affliction; - creators["unstable affliction on attacker"] = &unstable_affliction; - creators["haunt"] = &haunt; - creators["shadow bolt"] = &shadow_bolt; - creators["drain soul"] = &drain_soul; - creators["life tap"] = &life_tap; - creators["shadowflame"] = &shadowflame; - creators["seed of corruption on attacker"] = &seed_of_corruption; - creators["seed of corruption"] = &seed_of_corruption; - creators["rain of fire"] = &rain_of_fire; - } - -private: - static ActionNode* corruption(PlayerbotAI*) { return new ActionNode("corruption", {}, {}, {}); } - static ActionNode* corruption_on_attacker(PlayerbotAI*) { return new ActionNode("corruption on attacker", {}, {}, {}); } - static ActionNode* unstable_affliction(PlayerbotAI*) { return new ActionNode("unstable affliction", {}, {}, {}); } - static ActionNode* unstable_affliction_on_attacker(PlayerbotAI*) { return new ActionNode("unstable affliction on attacker", {}, {}, {}); } - static ActionNode* haunt(PlayerbotAI*) { return new ActionNode("haunt", {}, {}, {}); } - static ActionNode* shadow_bolt(PlayerbotAI*) { return new ActionNode("shadow bolt", {}, {}, {}); } - static ActionNode* drain_soul(PlayerbotAI*) { return new ActionNode("drain soul", {}, {}, {}); } - static ActionNode* life_tap(PlayerbotAI*) { return new ActionNode("life tap", {}, {}, {}); } - static ActionNode* shadowflame(PlayerbotAI*) { return new ActionNode("shadowflame", {}, {}, {}); } - static ActionNode* seed_of_corruption_on_attacker(PlayerbotAI*) { return new ActionNode("seed of corruption on attacker", {}, {}, {}); } - static ActionNode* seed_of_corruption(PlayerbotAI*) { return new ActionNode("seed of corruption", {}, {}, {}); } - static ActionNode* rain_of_fire(PlayerbotAI*) { return new ActionNode("rain of fire", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== AfflictionWarlockStrategy::AfflictionWarlockStrategy(PlayerbotAI* botAI) : GenericWarlockStrategy(botAI) { - actionNodeFactories.Add(new AfflictionWarlockStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.h index 503715bd4..cf20f4974 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/AfflictionWarlockStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AFFLICTIONWARLOCKSTRATEGY_H -#define _PLAYERBOT_AFFLICTIONWARLOCKSTRATEGY_H +#ifndef PLAYERBOTS_AFFLICTIONWARLOCKSTRATEGY_H +#define PLAYERBOTS_AFFLICTIONWARLOCKSTRATEGY_H #include "GenericWarlockStrategy.h" #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.cpp index 6bb4e9de5..a3299589d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.cpp @@ -1,58 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DemonologyWarlockStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class DemonologyWarlockStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - DemonologyWarlockStrategyActionNodeFactory() - { - creators["metamorphosis"] = &metamorphosis; - creators["demonic empowerment"] = &demonic_empowerment; - creators["corruption"] = &corruption; - creators["corruption on attacker"] = &corruption_on_attacker; - creators["immolate"] = &immolate; - creators["immolate on attacker"] = &immolate_on_attacker; - creators["incinerate"] = &incinerate; - creators["soul fire"] = &soul_fire; - creators["shadow bolt"] = &shadow_bolt; - creators["life tap"] = &life_tap; - creators["immolation aura"] = &immolation_aura; - creators["shadowflame"] = &shadowflame; - creators["seed of corruption on attacker"] = &seed_of_corruption_on_attacker; - creators["seed of corruption"] = &seed_of_corruption; - creators["rain of fire"] = &rain_of_fire; - creators["demon charge"] = &demon_charge; - } - -private: - static ActionNode* metamorphosis(PlayerbotAI*) { return new ActionNode("metamorphosis", {}, {}, {}); } - static ActionNode* demonic_empowerment(PlayerbotAI*) { return new ActionNode("demonic empowerment", {}, {}, {}); } - static ActionNode* corruption(PlayerbotAI*) { return new ActionNode("corruption", {}, {}, {}); } - static ActionNode* corruption_on_attacker(PlayerbotAI*) { return new ActionNode("corruption on attacker", {}, {}, {}); } - static ActionNode* immolate(PlayerbotAI*) { return new ActionNode("immolate", {}, {}, {}); } - static ActionNode* immolate_on_attacker(PlayerbotAI*) { return new ActionNode("immolate on attacker", {}, {}, {}); } - static ActionNode* incinerate(PlayerbotAI*) { return new ActionNode("incinerate", {}, {}, {}); } - static ActionNode* soul_fire(PlayerbotAI*) { return new ActionNode("soul fire", {}, {}, {}); } - static ActionNode* shadow_bolt(PlayerbotAI*) { return new ActionNode("shadow bolt", {}, {}, {}); } - static ActionNode* life_tap(PlayerbotAI*) { return new ActionNode("life tap", {}, {}, {}); } - static ActionNode* immolation_aura(PlayerbotAI*) { return new ActionNode("immolation aura", {}, {}, {}); } - static ActionNode* shadowflame(PlayerbotAI*) { return new ActionNode("shadowflame", {}, {}, {}); } - static ActionNode* seed_of_corruption_on_attacker(PlayerbotAI*) { return new ActionNode("seed of corruption on attacker", {}, {}, {}); } - static ActionNode* seed_of_corruption(PlayerbotAI*) { return new ActionNode("seed of corruption", {}, {}, {}); } - static ActionNode* rain_of_fire(PlayerbotAI*) { return new ActionNode("rain of fire", {}, {}, {}); } - static ActionNode* demon_charge(PlayerbotAI*) { return new ActionNode("demon charge", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== DemonologyWarlockStrategy::DemonologyWarlockStrategy(PlayerbotAI* botAI) : GenericWarlockStrategy(botAI) { - actionNodeFactories.Add(new DemonologyWarlockStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.h index 5a84175a5..625935766 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DemonologyWarlockStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DEMONOLOGYWARLOCKSTRATEGY_H -#define _PLAYERBOT_DEMONOLOGYWARLOCKSTRATEGY_H +#ifndef PLAYERBOTS_DEMONOLOGYWARLOCKSTRATEGY_H +#define PLAYERBOTS_DEMONOLOGYWARLOCKSTRATEGY_H #include "GenericWarlockStrategy.h" #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.cpp index af44de01c..15b231bfa 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.cpp @@ -1,54 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "DestructionWarlockStrategy.h" #include "Playerbots.h" -// ===== Action Node Factory ===== -class DestructionWarlockStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - DestructionWarlockStrategyActionNodeFactory() - { - creators["immolate"] = &immolate; - creators["conflagrate"] = &conflagrate; - creators["chaos bolt"] = &chaos_bolt; - creators["incinerate"] = &incinerate; - creators["corruption"] = &corruption; - creators["corruption on attacker"] = &corruption_on_attacker; - creators["shadow bolt"] = &shadow_bolt; - creators["shadowburn"] = &shadowburn; - creators["life tap"] = &life_tap; - creators["shadowfury"] = &shadowfury; - creators["shadowflame"] = &shadowflame; - creators["seed of corruption"] = &seed_of_corruption; - creators["seed of corruption on attacker"] = &seed_of_corruption; - creators["rain of fire"] = &rain_of_fire; - } - -private: - static ActionNode* immolate(PlayerbotAI*) { return new ActionNode("immolate", {}, {}, {}); } - static ActionNode* conflagrate(PlayerbotAI*) { return new ActionNode("conflagrate", {}, {}, {}); } - static ActionNode* chaos_bolt(PlayerbotAI*) { return new ActionNode("chaos bolt", {}, {}, {}); } - static ActionNode* incinerate(PlayerbotAI*) { return new ActionNode("incinerate", {}, {}, {}); } - static ActionNode* corruption(PlayerbotAI*) { return new ActionNode("corruption", {}, {}, {}); } - static ActionNode* corruption_on_attacker(PlayerbotAI*) { return new ActionNode("corruption on attacker", {}, {}, {}); } - static ActionNode* shadow_bolt(PlayerbotAI*) { return new ActionNode("shadow bolt", {}, {}, {}); } - static ActionNode* shadowburn(PlayerbotAI*) { return new ActionNode("shadowburn", {}, {}, {}); } - static ActionNode* life_tap(PlayerbotAI*) { return new ActionNode("life tap", {}, {}, {}); } - static ActionNode* shadowfury(PlayerbotAI*) { return new ActionNode("shadowfury", {}, {}, {}); } - static ActionNode* shadowflame(PlayerbotAI*) { return new ActionNode("shadowflame", {}, {}, {}); } - static ActionNode* seed_of_corruption(PlayerbotAI*) { return new ActionNode("seed of corruption", {}, {}, {}); } - static ActionNode* seed_of_corruption_on_attacker(PlayerbotAI*) { return new ActionNode("seed of corruption on attacker", {}, {}, {}); } - static ActionNode* rain_of_fire(PlayerbotAI*) { return new ActionNode("rain of fire", {}, {}, {}); } -}; - -// ===== Single Target Strategy ===== DestructionWarlockStrategy::DestructionWarlockStrategy(PlayerbotAI* botAI) : GenericWarlockStrategy(botAI) { - actionNodeFactories.Add(new DestructionWarlockStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } // ===== Default Actions ===== diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.h index 68fe67bb3..800dbd470 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/DestructionWarlockStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_DESTRUCTIONWARLOCKSTRATEGY_H -#define _PLAYERBOT_DESTRUCTIONWARLOCKSTRATEGY_H +#ifndef PLAYERBOTS_DESTRUCTIONWARLOCKSTRATEGY_H +#define PLAYERBOTS_DESTRUCTIONWARLOCKSTRATEGY_H #include "GenericWarlockStrategy.h" #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp index d22abe3ca..7698e520d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericWarlockNonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.h index 0da7973a0..744a40944 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICWARLOCKNONCOMBATSTRATEGY_H -#define _PLAYERBOT_GENERICWARLOCKNONCOMBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICWARLOCKNONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICWARLOCKNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp index 9c85f7861..25d3f23f0 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.cpp @@ -1,33 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericWarlockStrategy.h" -class GenericWarlockStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - GenericWarlockStrategyActionNodeFactory() - { - creators["banish on cc"] = &banish_on_cc; - creators["fear on cc"] = &fear_on_cc; - creators["spell lock"] = &spell_lock; - creators["devour magic purge"] = &devour_magic_purge; - creators["devour magic cleanse"] = &devour_magic_cleanse; - } - -private: - static ActionNode* banish_on_cc(PlayerbotAI*) { return new ActionNode("banish on cc", {}, {}, {}); } - static ActionNode* fear_on_cc(PlayerbotAI*) { return new ActionNode("fear on cc", {}, {}, {}); } - static ActionNode* spell_lock(PlayerbotAI*) { return new ActionNode("spell lock", {}, {}, {}); } - static ActionNode* devour_magic_purge(PlayerbotAI*) { return new ActionNode("devour magic purge", {}, {}, {}); } - static ActionNode* devour_magic_cleanse(PlayerbotAI*) { return new ActionNode("devour magic cleanse", {}, {}, {}); } -}; - GenericWarlockStrategy::GenericWarlockStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) { - actionNodeFactories.Add(new GenericWarlockStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } std::vector GenericWarlockStrategy::getDefaultActions() @@ -124,12 +105,12 @@ void AoEWarlockStrategy::InitTriggers(std::vector& triggers) ); } -void WarlockBoostStrategy::InitTriggers(std::vector& triggers) +void WarlockBoostStrategy::InitTriggers(std::vector& /*triggers*/) { // Placeholder for future boost triggers } -void WarlockPetStrategy::InitTriggers(std::vector& triggers) +void WarlockPetStrategy::InitTriggers(std::vector& /*triggers*/) { // Placeholder for future pet triggers } diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.h index a44a03de4..e7a795232 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/GenericWarlockStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICWARLOCKSTRATEGY_H -#define _PLAYERBOT_GENERICWARLOCKSTRATEGY_H +#ifndef PLAYERBOTS_GENERICWARLOCKSTRATEGY_H +#define PLAYERBOTS_GENERICWARLOCKSTRATEGY_H #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp index 5c922af90..9b3b3ae9f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.cpp @@ -1,46 +1,24 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TankWarlockStrategy.h" -// Combat strategy for a Warlock Tank, for certain bosses like Twin Emperors -// Priority is set to spam Searing Pain and use Shadow Ward on CD -// Disabled by default -// To enable, type "co +tank" -// To disable, type "co -tank" - -// ===== Action Node Factory ===== -class TankWarlockStrategyActionNodeFactory : public NamedObjectFactory -{ -public: - TankWarlockStrategyActionNodeFactory() - { - creators["shadow ward"] = &shadow_ward; - creators["searing pain"] = &searing_pain; - } - -private: - static ActionNode* shadow_ward(PlayerbotAI*) { return new ActionNode("shadow ward", {}, {}, {}); } - static ActionNode* searing_pain(PlayerbotAI*) { return new ActionNode("searing pain", {}, {}, {}); } -}; - -// ===== Warlock Tank Combat Strategy ===== TankWarlockStrategy::TankWarlockStrategy(PlayerbotAI* botAI) : GenericWarlockStrategy(botAI) { - actionNodeFactories.Add(new TankWarlockStrategyActionNodeFactory()); + // No custom ActionNodeFactory needed } std::vector TankWarlockStrategy::getDefaultActions() { - // Shadow Ward is the highest priority, Searing Pain next. return { NextAction("shadow ward", 27.5f), NextAction("searing pain", 27.0f) }; } -void TankWarlockStrategy::InitTriggers(std::vector& triggers) +void TankWarlockStrategy::InitTriggers(std::vector& /*triggers*/) { } diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.h index f20ffe894..f65d9bac7 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/Strategy/TankWarlockStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TANKWARLOCKSTRATEGY_H -#define _PLAYERBOT_TANKWARLOCKSTRATEGY_H +#ifndef PLAYERBOTS_TANKWARLOCKSTRATEGY_H +#define PLAYERBOTS_TANKWARLOCKSTRATEGY_H #include "GenericWarlockStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Action/WarlockActions.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockActions.cpp similarity index 96% rename from modules/mod-playerbots/src/Ai/Class/Warlock/Action/WarlockActions.cpp rename to modules/mod-playerbots/src/Ai/Class/Warlock/WarlockActions.cpp index 93798b702..d9424fbd8 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Action/WarlockActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockActions.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WarlockActions.h" @@ -27,6 +28,9 @@ bool CastDrainSoulAction::isUseful() { return AI_VALUE2(uint32, "item count", "s // Checks if the bot's health is above a certain threshold, and if so, allows casting Life Tap bool CastLifeTapAction::isUseful() { return AI_VALUE2(uint8, "health", "self target") > sPlayerbotAIConfig.lowHealth; } +Value* CastBanishOnCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } +Value* CastFearOnCcAction::GetTargetValue() { return context->GetValue("rti cc target"); } + // Checks if the target marked with the moon icon can be banished bool CastBanishOnCcAction::isPossible() { @@ -74,11 +78,7 @@ bool CastShadowflameAction::isUseful() bool CastRainOfFireAction::isUseful() { Unit* target = GetTarget(); - if (!target) - return false; - if (bot->HasSpell(27243) || bot->HasSpell(47835) || bot->HasSpell(47836)) // Seed of Corruption spell IDs - return false; - return true; + return target && !botAI->HasSpell("seed of corruption"); } // Checks if the enemies are close enough to use Hellfire diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Action/WarlockActions.h b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockActions.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Class/Warlock/Action/WarlockActions.h rename to modules/mod-playerbots/src/Ai/Class/Warlock/WarlockActions.h index 09f346370..6c6c0d51e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Action/WarlockActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WARLOCKACTIONS_H -#define _PLAYERBOT_WARLOCKACTIONS_H +#ifndef PLAYERBOTS_WARLOCKACTIONS_H +#define PLAYERBOTS_WARLOCKACTIONS_H #include "GenericSpellActions.h" #include "UseItemAction.h" @@ -170,6 +171,7 @@ class CastBanishOnCcAction : public CastCrowdControlSpellAction { public: CastBanishOnCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "banish") {} + Value* GetTargetValue() override; bool isPossible() override; }; @@ -177,6 +179,7 @@ class CastFearOnCcAction : public CastCrowdControlSpellAction { public: CastFearOnCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "fear") {} + Value* GetTargetValue() override; bool isPossible() override; }; diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp index 27b9166c6..541aed07e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WarlockAiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.h index 29b3792d9..81460f43d 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WARLOCKAIOBJECTCONTEXT_H -#define _PLAYERBOT_WARLOCKAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_WARLOCKAIOBJECTCONTEXT_H +#define PLAYERBOTS_WARLOCKAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockTriggers.cpp similarity index 90% rename from modules/mod-playerbots/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp rename to modules/mod-playerbots/src/Ai/Class/Warlock/WarlockTriggers.cpp index 1df531e7f..bbe5754b9 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Trigger/WarlockTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockTriggers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WarlockTriggers.h" @@ -50,22 +51,6 @@ bool TooManySoulShardsTrigger::IsActive() { return GetSoulShardCount(botAI->GetB bool OutOfSoulstoneTrigger::IsActive() { return GetSoulstoneCount(botAI->GetBot()) == 0; } -// Checks if the target marked with the moon icon can be banished -bool BanishTrigger::IsActive() -{ - Unit* ccTarget = context->GetValue("cc target", "banish")->Get(); - Unit* moonTarget = context->GetValue("rti cc target")->Get(); - return ccTarget && moonTarget && ccTarget == moonTarget && HasCcTargetTrigger::IsActive(); -} - -// Checks if the target marked with the moon icon can be feared -bool FearTrigger::IsActive() -{ - Unit* ccTarget = context->GetValue("cc target", "fear")->Get(); - Unit* moonTarget = context->GetValue("rti cc target")->Get(); - return ccTarget && moonTarget && ccTarget == moonTarget && HasCcTargetTrigger::IsActive(); -} - bool DemonArmorTrigger::IsActive() { Unit* target = GetTarget(); @@ -120,10 +105,8 @@ bool LifeTapTrigger::IsActive() // Checks if the Life Tap Glyph buff is active bool LifeTapGlyphBuffTrigger::IsActive() { - if (!botAI->HasAura(63320, bot)) - return false; - - return BuffTrigger::IsActive(); + constexpr uint32 SPELL_LIFE_TAP_GLYPH = 63320; + return bot->HasAura(SPELL_LIFE_TAP_GLYPH) && BuffTrigger::IsActive(); } // Checks if the target has a conflicting debuff that is equal to Curse of the Elements diff --git a/modules/mod-playerbots/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockTriggers.h similarity index 94% rename from modules/mod-playerbots/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Warlock/WarlockTriggers.h index d66fe537e..fd48026d8 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warlock/Trigger/WarlockTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Warlock/WarlockTriggers.h @@ -1,13 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WARLOCKTRIGGERS_H -#define _PLAYERBOT_WARLOCKTRIGGERS_H +#ifndef PLAYERBOTS_WARLOCKTRIGGERS_H +#define PLAYERBOTS_WARLOCKTRIGGERS_H #include "GenericTriggers.h" #include "PlayerbotAI.h" +#include "RtiTriggers.h" #include "Playerbots.h" #include "CureTriggers.h" #include "Trigger.h" @@ -137,18 +139,16 @@ public: // CC and Pet Triggers -class BanishTrigger : public HasCcTargetTrigger +class BanishTrigger : public RtiCcTrigger { public: - BanishTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "banish") {} - bool IsActive() override; + BanishTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "banish") {} }; -class FearTrigger : public HasCcTargetTrigger +class FearTrigger : public RtiCcTrigger { public: - FearTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "fear") {} - bool IsActive() override; + FearTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "fear") {} }; class SpellLockInterruptSpellTrigger : public InterruptSpellTrigger diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp index 6d6e0b655..70b87117b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ArmsWarriorStrategy.h" @@ -15,13 +16,10 @@ public: creators["piercing howl"] = &piercing_howl; creators["mocking blow"] = &mocking_blow; creators["heroic strike"] = &heroic_strike; - creators["enraged regeneration"] = &enraged_regeneration; - creators["retaliation"] = &retaliation; - creators["shattering throw"] = &shattering_throw; } private: - static ActionNode* charge(PlayerbotAI* botAI) + static ActionNode* charge(PlayerbotAI* /*botAI*/) { return new ActionNode( "charge", @@ -31,7 +29,7 @@ private: ); } - static ActionNode* death_wish(PlayerbotAI* botAI) + static ActionNode* death_wish(PlayerbotAI* /*botAI*/) { return new ActionNode( "death wish", @@ -41,7 +39,7 @@ private: ); } - static ActionNode* piercing_howl(PlayerbotAI* botAI) + static ActionNode* piercing_howl(PlayerbotAI* /*botAI*/) { return new ActionNode( "piercing howl", @@ -51,7 +49,7 @@ private: ); } - static ActionNode* mocking_blow(PlayerbotAI* botAI) + static ActionNode* mocking_blow(PlayerbotAI* /*botAI*/) { return new ActionNode( "mocking blow", @@ -61,7 +59,7 @@ private: ); } - static ActionNode* heroic_strike(PlayerbotAI* botAI) + static ActionNode* heroic_strike(PlayerbotAI* /*botAI*/) { return new ActionNode( "heroic strike", @@ -70,36 +68,6 @@ private: /*C*/ {} ); } - - static ActionNode* enraged_regeneration(PlayerbotAI* botAI) - { - return new ActionNode( - "enraged regeneration", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* retaliation(PlayerbotAI* botAI) - { - return new ActionNode( - "retaliation", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* shattering_throw(PlayerbotAI* botAI) - { - return new ActionNode( - "shattering throw", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } }; ArmsWarriorStrategy::ArmsWarriorStrategy(PlayerbotAI* botAI) : GenericWarriorStrategy(botAI) @@ -112,6 +80,7 @@ std::vector ArmsWarriorStrategy::getDefaultActions() return { NextAction("bladestorm", ACTION_DEFAULT + 0.2f), NextAction("mortal strike", ACTION_DEFAULT + 0.1f), + NextAction("sunder armor", ACTION_DEFAULT + 0.05f), NextAction("melee", ACTION_DEFAULT) }; } diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.h index 126d16210..94689439c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/ArmsWarriorStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ARMSWARRIORSTRATEGY_H -#define _PLAYERBOT_ARMSWARRIORSTRATEGY_H +#ifndef PLAYERBOTS_ARMSWARRIORSTRATEGY_H +#define PLAYERBOTS_ARMSWARRIORSTRATEGY_H #include "GenericWarriorStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp index 67e5cb461..f6f853e3c 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FuryWarriorStrategy.h" @@ -14,11 +15,10 @@ public: creators["intercept"] = &intercept; creators["piercing howl"] = &piercing_howl; creators["pummel"] = &pummel; - creators["enraged regeneration"] = &enraged_regeneration; } private: - static ActionNode* charge(PlayerbotAI* botAI) + static ActionNode* charge(PlayerbotAI* /*botAI*/) { return new ActionNode( "charge", @@ -28,7 +28,7 @@ private: ); } - static ActionNode* intercept(PlayerbotAI* botAI) + static ActionNode* intercept(PlayerbotAI* /*botAI*/) { return new ActionNode( "intercept", @@ -38,7 +38,7 @@ private: ); } - static ActionNode* piercing_howl(PlayerbotAI* botAI) + static ActionNode* piercing_howl(PlayerbotAI* /*botAI*/) { return new ActionNode( "piercing howl", @@ -48,7 +48,7 @@ private: ); } - static ActionNode* pummel(PlayerbotAI* botAI) + static ActionNode* pummel(PlayerbotAI* /*botAI*/) { return new ActionNode( "pummel", @@ -57,16 +57,6 @@ private: /*C*/ {} ); } - - static ActionNode* enraged_regeneration(PlayerbotAI* botAI) - { - return new ActionNode( - "enraged regeneration", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } }; FuryWarriorStrategy::FuryWarriorStrategy(PlayerbotAI* botAI) : GenericWarriorStrategy(botAI) diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.h index 79413960b..353c88b2f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/FuryWarriorStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FURYWARRIORSTRATEGY_H -#define _PLAYERBOT_FURYWARRIORSTRATEGY_H +#ifndef PLAYERBOTS_FURYWARRIORSTRATEGY_H +#define PLAYERBOTS_FURYWARRIORSTRATEGY_H #include "GenericWarriorStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp index 091aed2b8..3b48bfc14 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.cpp @@ -1,15 +1,40 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericWarriorNonCombatStrategy.h" #include "Playerbots.h" +class GenericWarriorNonCombatStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + GenericWarriorNonCombatStrategyActionNodeFactory() { creators["berserker rage"] = &berserker_rage; } + +private: + static ActionNode* berserker_rage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "berserker rage", + /*P*/ { NextAction("berserker stance") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + +GenericWarriorNonCombatStrategy::GenericWarriorNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) +{ + actionNodeFactories.Add(new GenericWarriorNonCombatStrategyActionNodeFactory()); +} + void GenericWarriorNonCombatStrategy::InitTriggers(std::vector& triggers) { NonCombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + triggers.push_back(new TriggerNode( + "fear sleep sap", { NextAction("berserker rage", ACTION_EMERGENCY + 1) })); } diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h index 276432583..be69f2e0e 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorNonCombatStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICWARRIORNONCOBATSTRATEGY_H -#define _PLAYERBOT_GENERICWARRIORNONCOBATSTRATEGY_H +#ifndef PLAYERBOTS_GENERICWARRIORNONCOMBATSTRATEGY_H +#define PLAYERBOTS_GENERICWARRIORNONCOMBATSTRATEGY_H #include "NonCombatStrategy.h" @@ -13,7 +14,7 @@ class PlayerbotAI; class GenericWarriorNonCombatStrategy : public NonCombatStrategy { public: - GenericWarriorNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI) {} + GenericWarriorNonCombatStrategy(PlayerbotAI* botAI); std::string const getName() override { return "nc"; } void InitTriggers(std::vector& triggers) override; diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp index 178984de9..a61f6a199 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.cpp @@ -1,15 +1,33 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GenericWarriorStrategy.h" #include "Playerbots.h" +class GenericWarriorStrategyActionNodeFactory : public NamedObjectFactory +{ +public: + GenericWarriorStrategyActionNodeFactory() { creators["berserker rage"] = &berserker_rage; } + +private: + static ActionNode* berserker_rage([[maybe_unused]] PlayerbotAI* botAI) + { + return new ActionNode( + "berserker rage", + /*P*/ { NextAction("berserker stance") }, + /*A*/ {}, + /*C*/ {} + ); + } +}; + GenericWarriorStrategy::GenericWarriorStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) { - + actionNodeFactories.Add(new GenericWarriorStrategyActionNodeFactory()); } void GenericWarriorStrategy::InitTriggers(std::vector& triggers) @@ -17,6 +35,8 @@ void GenericWarriorStrategy::InitTriggers(std::vector& triggers) CombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode( "enemy out of melee", { NextAction("reach melee", ACTION_HIGH + 1) })); + triggers.push_back(new TriggerNode( + "fear sleep sap", { NextAction("berserker rage", ACTION_EMERGENCY + 1) })); } class WarrirorAoeStrategyActionNodeFactory : public NamedObjectFactory diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.h index 5a336be20..f58c4eaf6 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/GenericWarriorStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GENERICWARRIORSTRATEGY_H -#define _PLAYERBOT_GENERICWARRIORSTRATEGY_H +#ifndef PLAYERBOTS_GENERICWARRIORSTRATEGY_H +#define PLAYERBOTS_GENERICWARRIORSTRATEGY_H #include "CombatStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp index e6daa2646..a2eed5cd3 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TankWarriorStrategy.h" @@ -19,12 +20,10 @@ public: creators["heroic throw taunt"] = &heroic_throw_taunt; creators["taunt"] = &taunt; creators["taunt spell"] = &taunt; - creators["vigilance"] = &vigilance; - creators["enraged regeneration"] = &enraged_regeneration; } private: - static ActionNode* heroic_throw_taunt(PlayerbotAI* botAI) + static ActionNode* heroic_throw_taunt(PlayerbotAI* /*botAI*/) { return new ActionNode( "heroic throw", @@ -34,7 +33,7 @@ private: ); } - static ActionNode* heroic_throw_on_snare_target(PlayerbotAI* botAI) + static ActionNode* heroic_throw_on_snare_target(PlayerbotAI* /*botAI*/) { return new ActionNode( "heroic throw on snare target", @@ -44,7 +43,7 @@ private: ); } - static ActionNode* last_stand(PlayerbotAI* botAI) + static ActionNode* last_stand(PlayerbotAI* /*botAI*/) { return new ActionNode( "last stand", @@ -54,7 +53,7 @@ private: ); } - static ActionNode* devastate(PlayerbotAI* botAI) + static ActionNode* devastate(PlayerbotAI* /*botAI*/) { return new ActionNode( "devastate", @@ -64,7 +63,7 @@ private: ); } - static ActionNode* commanding_shout(PlayerbotAI* botAI) + static ActionNode* commanding_shout(PlayerbotAI* /*botAI*/) { return new ActionNode( "commanding shout", @@ -74,7 +73,7 @@ private: ); } - static ActionNode* sunder_armor(PlayerbotAI* botAI) + static ActionNode* sunder_armor(PlayerbotAI* /*botAI*/) { return new ActionNode( "sunder armor", @@ -84,7 +83,7 @@ private: ); } - static ActionNode* charge(PlayerbotAI* botAI) + static ActionNode* charge(PlayerbotAI* /*botAI*/) { return new ActionNode( "charge", @@ -94,7 +93,7 @@ private: ); } - static ActionNode* taunt(PlayerbotAI* botAI) + static ActionNode* taunt(PlayerbotAI* /*botAI*/) { return new ActionNode( "taunt", @@ -103,26 +102,6 @@ private: /*C*/ {} ); } - - static ActionNode* vigilance(PlayerbotAI* botAI) - { - return new ActionNode( - "vigilance", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } - - static ActionNode* enraged_regeneration(PlayerbotAI* botAI) - { - return new ActionNode( - "enraged regeneration", - /*P*/ {}, - /*A*/ {}, - /*C*/ {} - ); - } }; TankWarriorStrategy::TankWarriorStrategy(PlayerbotAI* botAI) : GenericWarriorStrategy(botAI) diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.h index b52824297..d8cc13879 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/TankWarriorStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TANKWARRIORSTRATEGY_H -#define _PLAYERBOT_TANKWARRIORSTRATEGY_H +#ifndef PLAYERBOTS_TANKWARRIORSTRATEGY_H +#define PLAYERBOTS_TANKWARRIORSTRATEGY_H #include "GenericWarriorStrategy.h" #include "WarriorTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp new file mode 100644 index 000000000..aeff2c088 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.cpp @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "WarriorPullStrategy.h" + +#include "PlayerbotAI.h" + +std::string WarriorPullStrategy::GetPullActionName() const +{ + Unit* target = GetTarget(); + if (!target) + return PullStrategy::GetPullActionName(); + + if (botAI->CanCastSpell("heroic throw", target)) + return "heroic throw"; + + return PullStrategy::GetPullActionName(); +} diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h new file mode 100644 index 000000000..1db283c77 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/Strategy/WarriorPullStrategy.h @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_WARRIORPULLSTRATEGY_H +#define PLAYERBOTS_WARRIORPULLSTRATEGY_H + +#include "PullStrategy.h" + +class WarriorPullStrategy : public PullStrategy +{ +public: + WarriorPullStrategy(PlayerbotAI* botAI) : PullStrategy(botAI, "shoot") {} + + std::string GetPullActionName() const override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp deleted file mode 100644 index 5aa419c92..000000000 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 "WarriorTriggers.h" - -#include "Playerbots.h" - -bool BloodrageBuffTrigger::IsActive() -{ - return AI_VALUE2(uint8, "health", "self target") >= sPlayerbotAIConfig.mediumHealth && - AI_VALUE2(uint8, "rage", "self target") < 20; -} - -bool VigilanceTrigger::IsActive() -{ - if (!bot->HasSpell(50720)) - { - return false; - } - - Group* group = bot->GetGroup(); - if (!group) - { - return false; - } - - Player* currentVigilanceTarget = nullptr; - Player* mainTank = nullptr; - Player* assistTank1 = nullptr; - Player* assistTank2 = nullptr; - Player* highestGearScorePlayer = nullptr; - uint32 highestGearScore = 0; - - // Iterate once through the group to gather all necessary information - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || member == bot || !member->IsAlive()) - continue; - - // Check if member has Vigilance applied by the bot - if (!currentVigilanceTarget && botAI->HasAura("vigilance", member, false, true)) - { - currentVigilanceTarget = member; - } - - // Identify Main Tank - if (!mainTank && botAI->IsMainTank(member)) - { - mainTank = member; - } - - // Identify Assist Tanks - if (assistTank1 == nullptr && botAI->IsAssistTankOfIndex(member, 0)) - { - assistTank1 = member; - } - else if (assistTank2 == nullptr && botAI->IsAssistTankOfIndex(member, 1)) - { - assistTank2 = member; - } - - // Determine Highest Gear Score - uint32 gearScore = botAI->GetEquipGearScore(member/*, false, false*/); - if (gearScore > highestGearScore) - { - highestGearScore = gearScore; - highestGearScorePlayer = member; - } - } - - // Determine the highest-priority target - Player* highestPriorityTarget = mainTank ? mainTank : - (assistTank1 ? assistTank1 : - (assistTank2 ? assistTank2 : highestGearScorePlayer)); - - // Trigger if no Vigilance is active or the current target is not the highest-priority target - if (!currentVigilanceTarget || currentVigilanceTarget != highestPriorityTarget) - { - return true; - } - - return false; // No need to reassign Vigilance -} - -bool ShatteringThrowTrigger::IsActive() -{ - // Spell cooldown check - if (!bot->HasSpell(64382)) - { - return false; - } - - // Spell cooldown check - if (bot->HasSpellCooldown(64382)) - { - return false; - } - - GuidVector enemies = AI_VALUE(GuidVector, "possible targets"); - - for (ObjectGuid const& guid : enemies) - { - Unit* enemy = botAI->GetUnit(guid); - if (!enemy || !enemy->IsAlive() || enemy->IsFriendlyTo(bot)) - continue; - - // Check if the enemy is within 25 yards and has the specific auras - if (bot->IsWithinDistInMap(enemy, 25.0f) && - (enemy->HasAura(642) || // Divine Shield - enemy->HasAura(45438) || // Ice Block - enemy->HasAura(41450))) // Blessing of Protection - { - return true; - } - } - - return false; // No valid targets within range -} diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Action/WarriorActions.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorActions.cpp similarity index 69% rename from modules/mod-playerbots/src/Ai/Class/Warrior/Action/WarriorActions.cpp rename to modules/mod-playerbots/src/Ai/Class/Warrior/WarriorActions.cpp index 9f15cf767..73f768938 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Action/WarriorActions.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorActions.cpp @@ -1,14 +1,73 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WarriorActions.h" +#include "AiFactory.h" #include "Playerbots.h" +namespace +{ +constexpr uint32 SPELL_RETALIATION = 20230; +constexpr uint32 SPELL_DIVINE_SHIELD = 642; +constexpr uint32 SPELL_ICE_BLOCK = 45438; +constexpr uint32 SPELL_BLESSING_OF_PROTECTION = 41450; +constexpr uint32 SPELL_SHATTERING_THROW = 64382; +} + +bool CastBerserkerRageAction::isPossible() +{ + if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true)) + return false; + + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + if (!spellId) + return false; + + if (!bot->HasSpell(spellId)) + return false; + + if (bot->HasSpellCooldown(spellId)) + return false; + + return true; +} + +bool CastBerserkerRageAction::isUseful() +{ + return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) || + bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED)) + && !botAI->HasAura("berserker rage", bot) + && CastSpellAction::isUseful(); +} + bool CastSunderArmorAction::isUseful() { + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (!botAI->IsTank(bot, false)) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive() || !member->IsInWorld() || + member->GetMapId() != bot->GetMapId()) + { + continue; + } + + if (member->getClass() == CLASS_WARRIOR && + botAI->IsTank(member, false)) + return false; + } + } + Aura* aura = botAI->GetAura("sunder armor", GetTarget(), false, true); return !aura || aura->GetStackAmount() < 5 || aura->GetDuration() <= 6000; } @@ -104,14 +163,8 @@ bool CastVigilanceAction::Execute(Event /*event*/) bool CastRetaliationAction::isUseful() { - // Spell cooldown check - if (!bot->HasSpell(20230)) - { - return false; - } - - // Spell cooldown check - if (bot->HasSpellCooldown(20230)) + if (!bot->HasSpell(SPELL_RETALIATION) || bot->HasSpellCooldown(SPELL_RETALIATION) || + bot->HasAura(SPELL_RETALIATION)) { return false; } @@ -150,8 +203,8 @@ bool CastRetaliationAction::isUseful() break; } - // Only cast Retaliation if there are at least 2 melee attackers and the buff is not active - return meleeAttackers >= 2 && !botAI->HasAura("retaliation", bot); + // Only cast Retaliation if there are at least 2 melee attackers + return meleeAttackers >= 2; } Unit* CastShatteringThrowAction::GetTarget() @@ -165,15 +218,15 @@ Unit* CastShatteringThrowAction::GetTarget() continue; if (bot->IsWithinDistInMap(enemy, 25.0f) && - (enemy->HasAura(642) || // Divine Shield - enemy->HasAura(45438) || // Ice Block - enemy->HasAura(41450))) // Blessing of Protection + (enemy->HasAura(SPELL_DIVINE_SHIELD) || + enemy->HasAura(SPELL_ICE_BLOCK) || + enemy->HasAura(SPELL_BLESSING_OF_PROTECTION))) { return enemy; } } - return nullptr; // No valid target + return nullptr; } bool CastShatteringThrowAction::Execute(Event /*event*/) @@ -187,7 +240,7 @@ bool CastShatteringThrowAction::Execute(Event /*event*/) bool CastShatteringThrowAction::isUseful() { - if (!bot->HasSpell(64382) || bot->HasSpellCooldown(64382)) + if (!bot->HasSpell(SPELL_SHATTERING_THROW) || bot->HasSpellCooldown(SPELL_SHATTERING_THROW)) return false; GuidVector enemies = AI_VALUE(GuidVector, "possible targets"); @@ -198,17 +251,16 @@ bool CastShatteringThrowAction::isUseful() if (!enemy || !enemy->IsAlive() || enemy->IsFriendlyTo(bot)) continue; - // Check if the enemy is within 25 yards and has the specific auras if (bot->IsWithinDistInMap(enemy, 25.0f) && - (enemy->HasAura(642) || // Divine Shield - enemy->HasAura(45438) || // Ice Block - enemy->HasAura(41450))) // Blessing of Protection + (enemy->HasAura(SPELL_DIVINE_SHIELD) || + enemy->HasAura(SPELL_ICE_BLOCK) || + enemy->HasAura(SPELL_BLESSING_OF_PROTECTION))) { return true; } } - return false; // No valid targets within range + return false; } bool CastShatteringThrowAction::isPossible() diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Action/WarriorActions.h b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorActions.h similarity index 90% rename from modules/mod-playerbots/src/Ai/Class/Warrior/Action/WarriorActions.h rename to modules/mod-playerbots/src/Ai/Class/Warrior/WarriorActions.h index 7910fc0d8..9c533550f 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Action/WarriorActions.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WARRIORACTIONS_H -#define _PLAYERBOT_WARRIORACTIONS_H +#ifndef PLAYERBOTS_WARRIORACTIONS_H +#define PLAYERBOTS_WARRIORACTIONS_H #include "AiObject.h" #include "GenericSpellActions.h" @@ -78,7 +79,15 @@ REACH_ACTION(CastInterceptAction, "intercept", 8.0f); ENEMY_HEALER_ACTION(CastInterceptOnEnemyHealerAction, "intercept"); SNARE_ACTION(CastInterceptOnSnareTargetAction, "intercept"); MELEE_ACTION(CastSlamAction, "slam"); -BUFF_ACTION(CastBerserkerRageAction, "berserker rage"); +class CastBerserkerRageAction : public CastSpellAction +{ +public: + CastBerserkerRageAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "berserker rage") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; + bool isUseful() override; +}; MELEE_ACTION(CastWhirlwindAction, "whirlwind"); MELEE_ACTION(CastPummelAction, "pummel"); ENEMY_HEALER_ACTION(CastPummelOnEnemyHealerAction, "pummel"); diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp index 22754beab..a34a2241a 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "WarriorAiObjectContext.h" @@ -10,8 +11,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 +29,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/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.h b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.h index b4f3449b7..e3222687b 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorAiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WARRIORAIOBJECTCONTEXT_H -#define _PLAYERBOT_WARRIORAIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_WARRIORAIOBJECTCONTEXT_H +#define PLAYERBOTS_WARRIORAIOBJECTCONTEXT_H #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.cpp b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.cpp new file mode 100644 index 000000000..2f70ed0e8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.cpp @@ -0,0 +1,166 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "WarriorTriggers.h" +#include "Playerbots.h" + +namespace +{ +constexpr uint32 SPELL_VIGILANCE = 50720; +constexpr uint32 SPELL_SHATTERING_THROW = 64382; +constexpr uint32 SPELL_DIVINE_SHIELD = 642; +constexpr uint32 SPELL_ICE_BLOCK = 45438; +constexpr uint32 SPELL_BLESSING_OF_PROTECTION = 41450; +constexpr uint32 SPELL_COMMANDING_PRESENCE_RANKS[] = { 12318, 12857, 12858, 12860, 12861 }; +} + +bool BloodrageBuffTrigger::IsActive() +{ + return AI_VALUE2(uint8, "health", "self target") >= sPlayerbotAIConfig.mediumHealth && + AI_VALUE2(uint8, "rage", "self target") < 20; +} + +bool VigilanceTrigger::IsActive() +{ + if (!bot->HasSpell(SPELL_VIGILANCE)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* currentVigilanceTarget = nullptr; + Player* mainTank = nullptr; + Player* assistTank1 = nullptr; + Player* assistTank2 = nullptr; + Player* highestGearScorePlayer = nullptr; + uint32 highestGearScore = 0; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (!currentVigilanceTarget && botAI->HasAura("vigilance", member, false, true)) + currentVigilanceTarget = member; + + if (!mainTank && botAI->IsMainTank(member)) + mainTank = member; + else if (!assistTank1 && botAI->IsAssistTankOfIndex(member, 0)) + assistTank1 = member; + else if (!assistTank2 && botAI->IsAssistTankOfIndex(member, 1)) + assistTank2 = member; + + uint32 gearScore = botAI->GetEquipGearScore(member); + if (gearScore > highestGearScore) + { + highestGearScore = gearScore; + highestGearScorePlayer = member; + } + } + + Player* highestPriorityTarget = mainTank ? mainTank : + (assistTank1 ? assistTank1 : + (assistTank2 ? assistTank2 : highestGearScorePlayer)); + + if (!currentVigilanceTarget || currentVigilanceTarget != highestPriorityTarget) + return true; + + return false; +} + +bool ShatteringThrowTrigger::IsActive() +{ + if (!bot->HasSpell(SPELL_SHATTERING_THROW) || bot->HasSpellCooldown(SPELL_SHATTERING_THROW)) + return false; + + GuidVector enemies = AI_VALUE(GuidVector, "possible targets"); + + for (ObjectGuid const& guid : enemies) + { + Unit* enemy = botAI->GetUnit(guid); + if (!enemy || !enemy->IsAlive() || enemy->IsFriendlyTo(bot)) + continue; + + if (bot->IsWithinDistInMap(enemy, 25.0f) && + (enemy->HasAura(SPELL_DIVINE_SHIELD) || + enemy->HasAura(SPELL_ICE_BLOCK) || + enemy->HasAura(SPELL_BLESSING_OF_PROTECTION))) + { + return true; + } + } + + return false; +} + +bool BattleShoutTrigger::IsActive() +{ + if (!BuffTrigger::IsActive()) + return false; + + uint32 battleShoutSpellId = AI_VALUE2(uint32, "spell id", "battle shout"); + if (!battleShoutSpellId) + return false; + + SpellInfo const* bsInfo = sSpellMgr->GetSpellInfo(battleShoutSpellId); + if (!bsInfo) + return false; + + int32 bsApValue = 0; + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (bsInfo->Effects[eff].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + bsApValue = bsInfo->Effects[eff].BasePoints + 1; + break; + } + } + if (!bsApValue) + return false; + + static const float commandingPresenceBonus[] = { + 0.05f, 0.10f, 0.15f, 0.20f, 0.25f }; + + float cpBonus = 0.0f; + for (int rank = 4; rank >= 0; --rank) + { + if (bot->HasAura(SPELL_COMMANDING_PRESENCE_RANKS[rank])) + { + cpBonus = commandingPresenceBonus[rank]; + break; + } + } + int32 effectiveBsAp = int32(bsApValue * (1.0f + cpBonus)); + + static const char* blessingNames[] = { + "blessing of might", "greater blessing of might", nullptr + }; + for (int i = 0; blessingNames[i] != nullptr; ++i) + { + Aura* bom = botAI->GetAura(blessingNames[i], bot); + if (!bom) + continue; + + SpellInfo const* bomInfo = bom->GetSpellInfo(); + if (!bomInfo) + continue; + + for (uint8 eff = 0; eff < MAX_SPELL_EFFECTS; ++eff) + { + if (bomInfo->Effects[eff].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + int32 bomApValue = bomInfo->Effects[eff].BasePoints + 1; + if (bomApValue >= effectiveBsAp) + return false; + break; + } + } + } + + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.h similarity index 86% rename from modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h rename to modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.h index 563d70769..fb88e5936 100644 --- a/modules/mod-playerbots/src/Ai/Class/Warrior/Trigger/WarriorTriggers.h +++ b/modules/mod-playerbots/src/Ai/Class/Warrior/WarriorTriggers.h @@ -1,15 +1,22 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WARRIORTRIGGERS_H -#define _PLAYERBOT_WARRIORTRIGGERS_H +#ifndef PLAYERBOTS_WARRIORTRIGGERS_H +#define PLAYERBOTS_WARRIORTRIGGERS_H #include "GenericTriggers.h" #include "PlayerbotAI.h" -BUFF_TRIGGER(BattleShoutTrigger, "battle shout"); +class BattleShoutTrigger : public BuffTrigger +{ +public: + BattleShoutTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "battle shout") {} + bool IsActive() override; +}; + BUFF_TRIGGER(BattleStanceTrigger, "battle stance"); BUFF_TRIGGER(DefensiveStanceTrigger, "defensive stance"); BUFF_TRIGGER(BerserkerStanceTrigger, "berserker stance"); @@ -85,4 +92,5 @@ public: // public: // SlamTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "slam!") {} // }; + #endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActionContext.h new file mode 100644 index 000000000..e9b963c1b --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActionContext.h @@ -0,0 +1,40 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ACACTIONCONTEXT_H +#define PLAYERBOTS_ACACTIONCONTEXT_H + +#include "AiObjectContext.h" +#include "Action.h" +#include "ACActions.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.cpp new file mode 100644 index 000000000..32caf89c9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.cpp @@ -0,0 +1,123 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "Playerbots.h" +#include "AiFactory.h" +#include "ACTriggers.h" +#include "ACActions.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.h b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.h new file mode 100644 index 000000000..63462cbc2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACActions.h @@ -0,0 +1,37 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ACACTIONS_H +#define PLAYERBOTS_ACACTIONS_H + +#include "AttackAction.h" +#include "MovementActions.h" +#include "ACTriggers.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.cpp new file mode 100644 index 000000000..07681fd5e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.cpp @@ -0,0 +1,51 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ACMultipliers.h" +#include "ACActions.h" +#include "ACTriggers.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.h new file mode 100644 index 000000000..cf266e2c3 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACMultipliers.h @@ -0,0 +1,19 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ACMULTIPLIERS_H +#define PLAYERBOTS_ACMULTIPLIERS_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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.cpp new file mode 100644 index 000000000..b4e0361ce --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.cpp @@ -0,0 +1,27 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ACTriggers.h" +#include "ACStrategy.h" +#include "ACMultipliers.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.h new file mode 100644 index 000000000..da5e1ccfd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACStrategy.h @@ -0,0 +1,25 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ACSTRATEGY_H +#define PLAYERBOTS_ACSTRATEGY_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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggerContext.h new file mode 100644 index 000000000..cb868c984 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggerContext.h @@ -0,0 +1,41 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ACTRIGGERCONTEXT_H +#define PLAYERBOTS_ACTRIGGERCONTEXT_H + +#include "AiObjectContext.h" +#include "TriggerContext.h" +#include "ACTriggers.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.cpp new file mode 100644 index 000000000..c766402dd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.cpp @@ -0,0 +1,40 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "Playerbots.h" +#include "ACTriggers.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/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.h new file mode 100644 index 000000000..fd9c66fe7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AC/ACTriggers.h @@ -0,0 +1,44 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ACTRIGGERS_H +#define PLAYERBOTS_ACTRIGGERS_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/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/OldKingdomActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKActionContext.h similarity index 70% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/OldKingdomActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKActionContext.h index 19426c579..6d73b8a3d 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/OldKingdomActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOKACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONOKACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AKACTIONCONTEXT_H +#define PLAYERBOTS_AKACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "OldKingdomActions.h" +#include "AKActions.h" class WotlkDungeonOKActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Action/OldKingdomActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Action/OldKingdomActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKActions.cpp index 8a5250a8d..c68d80161 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Action/OldKingdomActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "OldKingdomActions.h" +#include "AKActions.h" bool AttackNadoxGuardianAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Action/OldKingdomActions.h b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKActions.h similarity index 67% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Action/OldKingdomActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKActions.h index cba518784..4760feebd 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Action/OldKingdomActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOKACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONOKACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AKACTIONS_H +#define PLAYERBOTS_AKACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "OldKingdomTriggers.h" +#include "AKTriggers.h" class AttackNadoxGuardianAction : public AttackAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Multiplier/OldKingdomMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKMultipliers.cpp similarity index 83% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Multiplier/OldKingdomMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKMultipliers.cpp index dbca52316..6087c6db8 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Multiplier/OldKingdomMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKMultipliers.cpp @@ -1,9 +1,15 @@ -#include "OldKingdomMultipliers.h" -#include "OldKingdomActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AKMultipliers.h" +#include "AKActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "OldKingdomTriggers.h" +#include "AKTriggers.h" #include "Action.h" float ElderNadoxMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Multiplier/OldKingdomMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKMultipliers.h similarity index 68% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Multiplier/OldKingdomMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKMultipliers.h index 88ae2fa51..3b0a03739 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Multiplier/OldKingdomMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOKMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONOKMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AKMULTIPLIERS_H +#define PLAYERBOTS_AKMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Strategy/OldKingdomStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKStrategy.cpp similarity index 81% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Strategy/OldKingdomStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKStrategy.cpp index 484ea6ac4..a8dab9c56 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Strategy/OldKingdomStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKStrategy.cpp @@ -1,5 +1,11 @@ -#include "OldKingdomStrategy.h" -#include "OldKingdomMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AKStrategy.h" +#include "AKMultipliers.h" void WotlkDungeonOKStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Strategy/OldKingdomStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Strategy/OldKingdomStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKStrategy.h index 75c475559..0534fa0b0 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Strategy/OldKingdomStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOKSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONOKSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AKSTRATEGY_H +#define PLAYERBOTS_AKSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/OldKingdomTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggerContext.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/OldKingdomTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggerContext.h index 4c25b9a95..f79a21b67 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/OldKingdomTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOKTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONOKTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AKTRIGGERCONTEXT_H +#define PLAYERBOTS_AKTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "OldKingdomTriggers.h" +#include "AKTriggers.h" class WotlkDungeonOKTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Trigger/OldKingdomTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggers.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Trigger/OldKingdomTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggers.cpp index 6b72f656a..dda1c0efd 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Trigger/OldKingdomTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "OldKingdomTriggers.h" +#include "AKTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Trigger/OldKingdomTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggers.h similarity index 76% rename from modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Trigger/OldKingdomTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggers.h index 2aa77986e..e2c95f6d7 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/OldKingdom/Trigger/OldKingdomTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AK/AKTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOKTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONOKTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AKTRIGGERS_H +#define PLAYERBOTS_AKTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/AzjolNerubActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANActionContext.h similarity index 68% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/AzjolNerubActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANActionContext.h index 6306643ba..eef6f075e 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/AzjolNerubActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONANACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONANACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ANACTIONCONTEXT_H +#define PLAYERBOTS_ANACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "AzjolNerubActions.h" +#include "ANActions.h" class WotlkDungeonANActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Action/AzjolNerubActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANActions.cpp similarity index 93% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Action/AzjolNerubActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANActions.cpp index 42026cecf..70540ec46 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Action/AzjolNerubActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "AzjolNerubActions.h" +#include "ANActions.h" bool AttackWebWrapAction::isUseful() { return !botAI->IsHeal(bot); } bool AttackWebWrapAction::Execute(Event /*event*/) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Action/AzjolNerubActions.h b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANActions.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Action/AzjolNerubActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANActions.h index ef388cbdb..92057d4eb 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Action/AzjolNerubActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONANACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONANACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ANACTIONS_H +#define PLAYERBOTS_ANACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "AzjolNerubTriggers.h" +#include "ANTriggers.h" class AttackWebWrapAction : public AttackAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.cpp index 6cd6673dc..13409ba6d 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.cpp @@ -1,9 +1,15 @@ -#include "AzjolNerubMultipliers.h" -#include "AzjolNerubActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ANMultipliers.h" +#include "ANActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "AzjolNerubTriggers.h" +#include "ANTriggers.h" #include "Action.h" float KrikthirMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.h new file mode 100644 index 000000000..a24f5f289 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANMultipliers.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ANMULTIPLIERS_H +#define PLAYERBOTS_ANMULTIPLIERS_H + +#include "Multiplier.h" + +class KrikthirMultiplier : public Multiplier +{ + public: + KrikthirMultiplier(PlayerbotAI* ai) : Multiplier(ai, "krik'thir the gatewatcher") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Strategy/AzjolNerubStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANStrategy.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Strategy/AzjolNerubStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANStrategy.cpp index dbbb8f5bf..d439a5f81 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Strategy/AzjolNerubStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANStrategy.cpp @@ -1,5 +1,11 @@ -#include "AzjolNerubStrategy.h" -#include "AzjolNerubMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ANStrategy.h" +#include "ANMultipliers.h" void WotlkDungeonANStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Strategy/AzjolNerubStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Strategy/AzjolNerubStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANStrategy.h index 70196e6e4..069f912e9 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Strategy/AzjolNerubStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONANSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONANSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ANSTRATEGY_H +#define PLAYERBOTS_ANSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/AzjolNerubTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggerContext.h similarity index 74% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/AzjolNerubTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggerContext.h index a41cebfc6..6e1011b51 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/AzjolNerubTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONANTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONANTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ANTRIGGERCONTEXT_H +#define PLAYERBOTS_ANTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "AzjolNerubTriggers.h" +#include "ANTriggers.h" class WotlkDungeonANTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Trigger/AzjolNerubTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggers.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Trigger/AzjolNerubTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggers.cpp index f9ef4ff95..346cc7f02 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Trigger/AzjolNerubTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "AzjolNerubTriggers.h" +#include "ANTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Trigger/AzjolNerubTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggers.h similarity index 84% rename from modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Trigger/AzjolNerubTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggers.h index 5d12b5960..8cf0fadff 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Trigger/AzjolNerubTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/AN/ANTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONANTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONANTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ANTRIGGERS_H +#define PLAYERBOTS_ANTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.h deleted file mode 100644 index bb01115e2..000000000 --- a/modules/mod-playerbots/src/Ai/Dungeon/AzjolNerub/Multiplier/AzjolNerubMultipliers.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONANMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONANMULTIPLIERS_H - -#include "Multiplier.h" - -class KrikthirMultiplier : public Multiplier -{ - public: - KrikthirMultiplier(PlayerbotAI* ai) : Multiplier(ai, "krik'thir the gatewatcher") {} - - public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/CullingOfStratholmeActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActionContext.h similarity index 62% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/CullingOfStratholmeActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActionContext.h index 0e12c908c..48326b599 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/CullingOfStratholmeActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONCOSACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONCOSACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_COSACTIONCONTEXT_H +#define PLAYERBOTS_COSACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "CullingOfStratholmeActions.h" +#include "CoSActions.h" class WotlkDungeonCoSActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Action/CullingOfStratholmeActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActions.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Action/CullingOfStratholmeActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActions.cpp index b2732c3cf..db6717232 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Action/CullingOfStratholmeActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "CullingOfStratholmeActions.h" +#include "CoSActions.h" bool ExplodeGhoulSpreadAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Action/CullingOfStratholmeActions.h b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActions.h similarity index 61% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Action/CullingOfStratholmeActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActions.h index 989cb6ab0..0db1af5c6 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Action/CullingOfStratholmeActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSActions.h @@ -1,12 +1,18 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONCOSACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONCOSACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_COSACTIONS_H +#define PLAYERBOTS_COSACTIONS_H #include "Action.h" #include "AttackAction.h" #include "GenericSpellActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "CullingOfStratholmeTriggers.h" +#include "CoSTriggers.h" class ExplodeGhoulSpreadAction : public MovementAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.cpp similarity index 55% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.cpp index 1fbbc6b3e..8f7fadde1 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.cpp @@ -1,9 +1,15 @@ -#include "CullingOfStratholmeMultipliers.h" -#include "CullingOfStratholmeActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "CoSMultipliers.h" +#include "CoSActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "CullingOfStratholmeTriggers.h" +#include "CoSTriggers.h" #include "Action.h" float EpochMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.h new file mode 100644 index 000000000..4e190ff05 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSMultipliers.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_COSMULTIPLIERS_H +#define PLAYERBOTS_COSMULTIPLIERS_H + +#include "Multiplier.h" + +class EpochMultiplier : public Multiplier +{ + public: + EpochMultiplier(PlayerbotAI* ai) : Multiplier(ai, "chrono-lord epoch") {} + + public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSStrategy.cpp similarity index 72% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSStrategy.cpp index 90765bded..223bb6374 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSStrategy.cpp @@ -1,5 +1,11 @@ -#include "CullingOfStratholmeStrategy.h" -#include "CullingOfStratholmeMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "CoSStrategy.h" +#include "CoSMultipliers.h" void WotlkDungeonCoSStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSStrategy.h index bff126743..afea59bc4 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONCOSSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONCOSSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_COSSTRATEGY_H +#define PLAYERBOTS_COSSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/CullingOfStratholmeTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggerContext.h similarity index 62% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/CullingOfStratholmeTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggerContext.h index 6e35eac85..806449f24 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/CullingOfStratholmeTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_COSTRIGGERCONTEXT_H +#define PLAYERBOTS_COSTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "CullingOfStratholmeTriggers.h" +#include "CoSTriggers.h" class WotlkDungeonCoSTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggers.cpp similarity index 74% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggers.cpp index cb67ad077..7668cb435 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "CullingOfStratholmeTriggers.h" +#include "CoSTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggers.h similarity index 64% rename from modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggers.h index 28fafa368..877e1047a 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Trigger/CullingOfStratholmeTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/CoS/CoSTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONCOSTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_COSTRIGGERS_H +#define PLAYERBOTS_COSTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.h deleted file mode 100644 index 587b0f704..000000000 --- a/modules/mod-playerbots/src/Ai/Dungeon/CullingOfStratholme/Multiplier/CullingOfStratholmeMultipliers.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONCOSMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONCOSMULTIPLIERS_H - -#include "Multiplier.h" - -class EpochMultiplier : public Multiplier -{ - public: - EpochMultiplier(PlayerbotAI* ai) : Multiplier(ai, "chrono-lord epoch") {} - - public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/DrakTharonKeepActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActionContext.h similarity index 83% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/DrakTharonKeepActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActionContext.h index 1435eedbe..3780feb5c 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/DrakTharonKeepActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONDTKACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONDTKACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DTKACTIONCONTEXT_H +#define PLAYERBOTS_DTKACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "DrakTharonKeepActions.h" +#include "DTKActions.h" class WotlkDungeonDTKActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Action/DrakTharonKeepActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActions.cpp similarity index 95% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Action/DrakTharonKeepActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActions.cpp index 07e23699e..c888c781a 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Action/DrakTharonKeepActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "DrakTharonKeepActions.h" +#include "DTKActions.h" bool CorpseExplodeSpreadAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Action/DrakTharonKeepActions.h b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActions.h similarity index 83% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Action/DrakTharonKeepActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActions.h index 8c59a7c24..53b82d4bd 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Action/DrakTharonKeepActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKActions.h @@ -1,12 +1,18 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONDTKACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONDTKACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DTKACTIONS_H +#define PLAYERBOTS_DTKACTIONS_H #include "Action.h" #include "AttackAction.h" #include "GenericSpellActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "DrakTharonKeepTriggers.h" +#include "DTKTriggers.h" const Position NOVOS_PARTY_POSITION = Position(-378.852f, -760.349f, 28.587f); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKMultipliers.cpp similarity index 83% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKMultipliers.cpp index 8fd119dc4..1bc8a578c 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKMultipliers.cpp @@ -1,9 +1,15 @@ -#include "DrakTharonKeepMultipliers.h" -#include "DrakTharonKeepActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DTKMultipliers.h" +#include "DTKActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "DrakTharonKeepTriggers.h" +#include "DTKTriggers.h" #include "Action.h" float NovosMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKMultipliers.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKMultipliers.h index c8f3c8135..750380f07 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Multiplier/DrakTharonKeepMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONDTKMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONDTKMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DTKMULTIPLIERS_H +#define PLAYERBOTS_DTKMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Strategy/DrakTharonKeepStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKStrategy.cpp similarity index 85% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Strategy/DrakTharonKeepStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKStrategy.cpp index 791a0ea71..19c33c08e 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Strategy/DrakTharonKeepStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKStrategy.cpp @@ -1,5 +1,11 @@ -#include "DrakTharonKeepStrategy.h" -#include "DrakTharonKeepMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "DTKStrategy.h" +#include "DTKMultipliers.h" void WotlkDungeonDTKStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Strategy/DrakTharonKeepStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Strategy/DrakTharonKeepStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKStrategy.h index 41ef0ced6..568225f04 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Strategy/DrakTharonKeepStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONDTKSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONDTKSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DTKSTRATEGY_H +#define PLAYERBOTS_DTKSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/DrakTharonKeepTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggerContext.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/DrakTharonKeepTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggerContext.h index ff7739107..8880cb62f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/DrakTharonKeepTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DTKTRIGGERCONTEXT_H +#define PLAYERBOTS_DTKTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "DrakTharonKeepTriggers.h" +#include "DTKTriggers.h" class WotlkDungeonDTKTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Trigger/DrakTharonKeepTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggers.cpp similarity index 85% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Trigger/DrakTharonKeepTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggers.cpp index 406f4d433..f4382c7df 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Trigger/DrakTharonKeepTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "DrakTharonKeepTriggers.h" +#include "DTKTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Trigger/DrakTharonKeepTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggers.h similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Trigger/DrakTharonKeepTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggers.h index 627200e8d..1c90df0b0 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DraktharonKeep/Trigger/DrakTharonKeepTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DTK/DTKTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONDTKTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DTKTRIGGERS_H +#define PLAYERBOTS_DTKTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyContext.h b/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyContext.h index 3311aeee2..2afb8ce21 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyContext.h @@ -1,22 +1,29 @@ -#ifndef _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H -#define _PLAYERBOT_DUNGEONSTRATEGYCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DUNGEONSTRATEGYCONTEXT_H +#define PLAYERBOTS_DUNGEONSTRATEGYCONTEXT_H #include "Strategy.h" -#include "UtgardeKeep/Strategy/UtgardeKeepStrategy.h" -#include "Nexus/Strategy/NexusStrategy.h" -#include "AzjolNerub/Strategy/AzjolNerubStrategy.h" -#include "OldKingdom/Strategy/OldKingdomStrategy.h" -#include "DraktharonKeep/Strategy/DrakTharonKeepStrategy.h" -#include "VioletHold/Strategy/VioletHoldStrategy.h" -#include "Gundrak/Strategy/GundrakStrategy.h" -#include "HallsOfStone/Strategy/HallsOfStoneStrategy.h" -#include "HallsOfLightning/Strategy/HallsOfLightningStrategy.h" -#include "Oculus/Strategy/OculusStrategy.h" -#include "UtgardePinnacle/Strategy/UtgardePinnacleStrategy.h" -#include "CullingOfStratholme/Strategy/CullingOfStratholmeStrategy.h" -#include "ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.h" -#include "PitOfSaron/Strategy/PitOfSaronStrategy.h" -#include "TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.h" +#include "ACStrategy.h" +#include "UKStrategy.h" +#include "NexStrategy.h" +#include "ANStrategy.h" +#include "AKStrategy.h" +#include "DTKStrategy.h" +#include "VHStrategy.h" +#include "GDStrategy.h" +#include "HoSStrategy.h" +#include "HoLStrategy.h" +#include "OCStrategy.h" +#include "UPStrategy.h" +#include "CoSStrategy.h" +#include "FoSStrategy.h" +#include "PoSStrategy.h" +#include "TOCStrategy.h" /* Full list/TODO: @@ -44,7 +51,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 +72,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/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyUtils.h b/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyUtils.h index 3f67f4b4d..7a3da56de 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyUtils.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/DungeonStrategyUtils.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_DUNGEONUTILS_H -#define _PLAYERBOT_DUNGEONUTILS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_DUNGEONSTRATEGYUTILS_H +#define PLAYERBOTS_DUNGEONSTRATEGYUTILS_H template inline const T& DUNGEON_MODE(Player* bot, const T& normal5, const T& heroic10) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/ForgeOfSoulsActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActionContext.h similarity index 75% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/ForgeOfSoulsActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActionContext.h index 4797b69c1..6d2364482 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/ForgeOfSoulsActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONFOSACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONFOSACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOSACTIONCONTEXT_H +#define PLAYERBOTS_FOSACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "ForgeOfSoulsActions.h" +#include "FoSActions.h" class WotlkDungeonFoSActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Action/ForgeOfSoulsActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActions.cpp similarity index 95% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Action/ForgeOfSoulsActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActions.cpp index d83b69bb4..2ab5bb643 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Action/ForgeOfSoulsActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "ForgeOfSoulsActions.h" +#include "FoSActions.h" bool MoveFromBronjahmAction::Execute(Event /*event*/) { @@ -17,11 +23,9 @@ bool MoveFromBronjahmAction::Execute(Event /*event*/) bool AttackCorruptedSoulFragmentAction::Execute(Event /*event*/) { - Unit* currentTarget = AI_VALUE(Unit*, "current target"); GuidVector targets = AI_VALUE(GuidVector, "possible targets"); // If no valid skull target, search for corrupted soul fragment - Unit* empoweredPrince = nullptr; for (auto i = targets.begin(); i != targets.end(); ++i) { Unit* unit = botAI->GetUnit(*i); @@ -30,8 +34,6 @@ bool AttackCorruptedSoulFragmentAction::Execute(Event /*event*/) if (unit->GetEntry() == NPC_CORRUPTED_SOUL_FRAGMENT) { - empoweredPrince = unit; - // Mark corrupted soul fragment with skull if in group and not already marked if (Group* group = bot->GetGroup()) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Action/ForgeOfSoulsActions.h b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActions.h similarity index 78% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Action/ForgeOfSoulsActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActions.h index d9b762be6..a3e46a23f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Action/ForgeOfSoulsActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONFOSACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONFOSACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOSACTIONS_H +#define PLAYERBOTS_FOSACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "ForgeOfSoulsTriggers.h" +#include "FoSTriggers.h" const Position BRONJAHM_TANK_POSITION = Position(5297.920f, 2506.698f, 686.068f); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSMultipliers.cpp similarity index 75% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSMultipliers.cpp index 7873e7c1d..6a4863574 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSMultipliers.cpp @@ -1,10 +1,16 @@ -#include "ForgeOfSoulsMultipliers.h" -#include "ForgeOfSoulsActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "FoSMultipliers.h" +#include "FoSActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "ForgeOfSoulsTriggers.h" -#include "ForgeOfSoulsActions.h" +#include "FoSTriggers.h" +#include "FoSActions.h" float BronjahmMultiplier::GetValue(Action* action) { Unit* boss = AI_VALUE2(Unit *, "find target", "bronjahm"); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSMultipliers.h similarity index 56% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSMultipliers.h index 1c89bb096..9ed4906cc 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Multiplier/ForgeOfSoulsMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONFOSMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONFOSMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOSMULTIPLIERS_H +#define PLAYERBOTS_FOSMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSStrategy.cpp similarity index 71% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSStrategy.cpp index 9ff321982..7c161b48e 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSStrategy.cpp @@ -1,5 +1,11 @@ -#include "ForgeOfSoulsStrategy.h" -#include "ForgeOfSoulsMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "FoSStrategy.h" +#include "FoSMultipliers.h" void WotlkDungeonFoSStrategy::InitTriggers(std::vector& triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSStrategy.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSStrategy.h index 79d58334a..b3fdd81ee 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Strategy/ForgeOfSoulsStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONFOSSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONFOSSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOSSTRATEGY_H +#define PLAYERBOTS_FOSSTRATEGY_H #include "Multiplier.h" #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/ForgeOfSoulsTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggerContext.h similarity index 74% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/ForgeOfSoulsTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggerContext.h index 129124127..78adf866a 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/ForgeOfSoulsTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOSTRIGGERCONTEXT_H +#define PLAYERBOTS_FOSTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "ForgeOfSoulsTriggers.h" +#include "FoSTriggers.h" class WotlkDungeonFoSTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggers.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggers.cpp index eb8e66e11..2d8b70d34 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "ForgeOfSoulsTriggers.h" +#include "FoSTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggers.h similarity index 78% rename from modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggers.h index 59dd202d4..8f4f4cb9d 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/ForgeOfSouls/Trigger/ForgeOfSoulsTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/FoS/FoSTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONFOSTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_FOSTRIGGERS_H +#define PLAYERBOTS_FOSTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/GundrakActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDActionContext.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/GundrakActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDActionContext.h index 746fed394..e901a83fb 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/GundrakActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONGDACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONGDACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GDACTIONCONTEXT_H +#define PLAYERBOTS_GDACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "GundrakActions.h" +#include "GDActions.h" class WotlkDungeonGDActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Action/GundrakActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDActions.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Action/GundrakActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDActions.cpp index 775e8ef29..555f9d879 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Action/GundrakActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "GundrakActions.h" +#include "GDActions.h" bool AvoidPoisonNovaAction::Execute(Event /*event*/) { @@ -23,7 +29,6 @@ bool AttackSnakeWrapAction::Execute(Event /*event*/) Unit* boss = AI_VALUE2(Unit*, "find target", "slad'ran"); if (!boss) { return false; } - Unit* snakeWrap = nullptr; // Target is not findable from threat table using AI_VALUE2(), // therefore need to search manually for the unit name GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Action/GundrakActions.h b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDActions.h similarity index 67% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Action/GundrakActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDActions.h index 095cc5cfa..78a3bad1a 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Action/GundrakActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONGDACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONGDACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GDACTIONS_H +#define PLAYERBOTS_GDACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "GundrakTriggers.h" +#include "GDTriggers.h" class AvoidPoisonNovaAction : public MovementAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Multiplier/GundrakMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDMultipliers.cpp similarity index 82% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Multiplier/GundrakMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDMultipliers.cpp index 2d4404255..a3effad4f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Multiplier/GundrakMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDMultipliers.cpp @@ -1,9 +1,15 @@ -#include "GundrakMultipliers.h" -#include "GundrakActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GDMultipliers.h" +#include "GDActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "GundrakTriggers.h" +#include "GDTriggers.h" #include "Action.h" float SladranMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Multiplier/GundrakMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDMultipliers.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Multiplier/GundrakMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDMultipliers.h index e1ceb0b62..0856667f1 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Multiplier/GundrakMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONGDMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONGDMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GDMULTIPLIERS_H +#define PLAYERBOTS_GDMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Strategy/GundrakStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDStrategy.cpp similarity index 76% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Strategy/GundrakStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDStrategy.cpp index c8244e2ca..2f8aa5e2a 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Strategy/GundrakStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDStrategy.cpp @@ -1,5 +1,11 @@ -#include "GundrakStrategy.h" -#include "GundrakMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GDStrategy.h" +#include "GDMultipliers.h" void WotlkDungeonGDStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Strategy/GundrakStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Strategy/GundrakStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDStrategy.h index 47f8617f1..0aa434bdf 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Strategy/GundrakStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONGDSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONGDSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GDSTRATEGY_H +#define PLAYERBOTS_GDSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/GundrakTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggerContext.h similarity index 68% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/GundrakTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggerContext.h index 4d0e5dee8..6ad834f7b 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/GundrakTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONGDTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONGDTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GDTRIGGERCONTEXT_H +#define PLAYERBOTS_GDTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "GundrakTriggers.h" +#include "GDTriggers.h" class WotlkDungeonGDTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Trigger/GundrakTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggers.cpp similarity index 77% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Trigger/GundrakTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggers.cpp index b3e98cfdb..7ae7ec88d 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Trigger/GundrakTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "GundrakTriggers.h" +#include "GDTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Trigger/GundrakTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggers.h similarity index 78% rename from modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Trigger/GundrakTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggers.h index e8a5c25f1..974321002 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Gundrak/Trigger/GundrakTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/GD/GDTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONGDTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONGDTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GDTRIGGERS_H +#define PLAYERBOTS_GDTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfReflection/TODO b/modules/mod-playerbots/src/Ai/Dungeon/HallsOfReflection/TODO deleted file mode 100644 index e69de29bb..000000000 diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/HallsOfLightningActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActionContext.h similarity index 84% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/HallsOfLightningActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActionContext.h index c3073d492..edc5b741f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/HallsOfLightningActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOLACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONHOLACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOLACTIONCONTEXT_H +#define PLAYERBOTS_HOLACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "HallsOfLightningActions.h" +#include "HoLActions.h" class WotlkDungeonHoLActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Action/HallsOfLightningActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActions.cpp similarity index 94% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Action/HallsOfLightningActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActions.cpp index 7dcf64333..65c78720b 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Action/HallsOfLightningActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "HallsOfLightningActions.h" +#include "HoLActions.h" bool BjarngrimTargetAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Action/HallsOfLightningActions.h b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActions.h similarity index 86% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Action/HallsOfLightningActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActions.h index fe81aa448..83e150e97 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Action/HallsOfLightningActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOLACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONHOLACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOLACTIONS_H +#define PLAYERBOTS_HOLACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "HallsOfLightningTriggers.h" +#include "HoLTriggers.h" const Position IONAR_TANK_POSITION = Position(1078.860f, -261.928f, 61.226f); const Position DISPERSE_POSITION = Position(1161.152f, -261.584f, 53.223f); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Multiplier/HallsOfLightningMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLMultipliers.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Multiplier/HallsOfLightningMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLMultipliers.cpp index 3bf9fcfd6..a480a62d3 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Multiplier/HallsOfLightningMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLMultipliers.cpp @@ -1,9 +1,15 @@ -#include "HallsOfLightningMultipliers.h" -#include "HallsOfLightningActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HoLMultipliers.h" +#include "HoLActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "HallsOfLightningTriggers.h" +#include "HoLTriggers.h" #include "Action.h" #include "WarriorActions.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Multiplier/HallsOfLightningMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLMultipliers.h similarity index 71% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Multiplier/HallsOfLightningMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLMultipliers.h index 8cc439677..d3f66415e 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Multiplier/HallsOfLightningMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOLMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONHOLMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOLMULTIPLIERS_H +#define PLAYERBOTS_HOLMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Strategy/HallsOfLightningStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLStrategy.cpp similarity index 84% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Strategy/HallsOfLightningStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLStrategy.cpp index 1266fb0b9..3d02b1047 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Strategy/HallsOfLightningStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLStrategy.cpp @@ -1,5 +1,11 @@ -#include "HallsOfLightningStrategy.h" -#include "HallsOfLightningMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HoLStrategy.h" +#include "HoLMultipliers.h" void WotlkDungeonHoLStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Strategy/HallsOfLightningStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Strategy/HallsOfLightningStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLStrategy.h index 6463033b3..2a0d10cca 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Strategy/HallsOfLightningStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOLSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONHOLSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOLSTRATEGY_H +#define PLAYERBOTS_HOLSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/HallsOfLightningTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggerContext.h similarity index 84% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/HallsOfLightningTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggerContext.h index 74cd0cd73..3c9f37d9f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/HallsOfLightningTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOLTRIGGERCONTEXT_H +#define PLAYERBOTS_HOLTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "HallsOfLightningTriggers.h" +#include "HoLTriggers.h" class WotlkDungeonHoLTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Trigger/HallsOfLightningTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggers.cpp similarity index 90% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Trigger/HallsOfLightningTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggers.cpp index da88807ca..be2dd3e71 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Trigger/HallsOfLightningTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "HallsOfLightningTriggers.h" +#include "HoLTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Trigger/HallsOfLightningTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggers.h similarity index 88% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Trigger/HallsOfLightningTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggers.h index 7dbbc4610..5665e1e3b 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfLightning/Trigger/HallsOfLightningTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoL/HoLTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONHOLTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOLTRIGGERS_H +#define PLAYERBOTS_HOLTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/HallsOfStoneActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActionContext.h similarity index 63% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/HallsOfStoneActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActionContext.h index 40b469b40..87bafcfca 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/HallsOfStoneActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOSACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONHOSACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOSACTIONCONTEXT_H +#define PLAYERBOTS_HOSACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "HallsOfStoneActions.h" +#include "HoSActions.h" class WotlkDungeonHoSActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Action/HallsOfStoneActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActions.cpp similarity index 84% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Action/HallsOfStoneActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActions.cpp index 51f1072b5..8644cc9a6 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Action/HallsOfStoneActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "HallsOfStoneActions.h" +#include "HoSActions.h" bool ShatterSpreadAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Action/HallsOfStoneActions.h b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActions.h similarity index 59% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Action/HallsOfStoneActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActions.h index ad7058afd..8a742f3c9 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Action/HallsOfStoneActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOSACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONHOSACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOSACTIONS_H +#define PLAYERBOTS_HOSACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "HallsOfStoneTriggers.h" +#include "HoSTriggers.h" class ShatterSpreadAction : public MovementAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Multiplier/HallsOfStoneMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSMultipliers.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Multiplier/HallsOfStoneMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSMultipliers.cpp index 443905bbb..acb2de216 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Multiplier/HallsOfStoneMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSMultipliers.cpp @@ -1,9 +1,15 @@ -#include "HallsOfStoneMultipliers.h" -#include "HallsOfStoneActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HoSMultipliers.h" +#include "HoSActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "HallsOfStoneTriggers.h" +#include "HoSTriggers.h" #include "Action.h" float KrystallusMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Multiplier/HallsOfStoneMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSMultipliers.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Multiplier/HallsOfStoneMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSMultipliers.h index 543d389a8..14a9af9c7 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Multiplier/HallsOfStoneMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOSMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONHOSMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOSMULTIPLIERS_H +#define PLAYERBOTS_HOSMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Strategy/HallsOfStoneStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSStrategy.cpp similarity index 78% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Strategy/HallsOfStoneStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSStrategy.cpp index 47006a9e4..1b9f91c41 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Strategy/HallsOfStoneStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSStrategy.cpp @@ -1,5 +1,11 @@ -#include "HallsOfStoneStrategy.h" -#include "HallsOfStoneMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HoSStrategy.h" +#include "HoSMultipliers.h" void WotlkDungeonHoSStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Strategy/HallsOfStoneStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Strategy/HallsOfStoneStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSStrategy.h index ee26aa2cd..31bbff055 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Strategy/HallsOfStoneStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOSSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONHOSSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOSSTRATEGY_H +#define PLAYERBOTS_HOSSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/HallsOfStoneTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggerContext.h similarity index 63% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/HallsOfStoneTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggerContext.h index bd3d6dd01..b606b7c96 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/HallsOfStoneTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOSTRIGGERCONTEXT_H +#define PLAYERBOTS_HOSTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "HallsOfStoneTriggers.h" +#include "HoSTriggers.h" class WotlkDungeonHoSTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Trigger/HallsOfStoneTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggers.cpp similarity index 72% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Trigger/HallsOfStoneTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggers.cpp index 2e6e37b86..6bbdc50d3 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Trigger/HallsOfStoneTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "HallsOfStoneTriggers.h" +#include "HoSTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Trigger/HallsOfStoneTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggers.h similarity index 72% rename from modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Trigger/HallsOfStoneTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggers.h index 53a15b6f2..eb99d64c4 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/HallsOfStone/Trigger/HallsOfStoneTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/HoS/HoSTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONHOSTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HOSTRIGGERS_H +#define PLAYERBOTS_HOSTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/NexusActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActionContext.h similarity index 79% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/NexusActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActionContext.h index c02422625..b8d3c40f6 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/NexusActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONNEXACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONNEXACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEXACTIONCONTEXT_H +#define PLAYERBOTS_NEXACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "NexusActions.h" +#include "NexActions.h" class WotlkDungeonNexActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Action/NexusActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActions.cpp similarity index 94% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Action/NexusActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActions.cpp index 12b546b5e..02b9144ff 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Action/NexusActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "NexusActions.h" +#include "NexActions.h" bool MoveFromWhirlwindAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Action/NexusActions.h b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActions.h similarity index 79% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Action/NexusActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActions.h index 96790c367..f322cf3b7 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Action/NexusActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONNEXACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONNEXACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEXACTIONS_H +#define PLAYERBOTS_NEXACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "NexusTriggers.h" +#include "NexTriggers.h" class MoveFromWhirlwindAction : public MovementAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Multiplier/NexusMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexMultipliers.cpp similarity index 89% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Multiplier/NexusMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexMultipliers.cpp index 7adc79b45..e1804fb50 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Multiplier/NexusMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexMultipliers.cpp @@ -1,9 +1,15 @@ -#include "NexusMultipliers.h" -#include "NexusActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NexMultipliers.h" +#include "NexActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "NexusTriggers.h" +#include "NexTriggers.h" float FactionCommanderMultiplier::GetValue(Action* action) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Multiplier/NexusMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexMultipliers.h similarity index 73% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Multiplier/NexusMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexMultipliers.h index 00373ebd9..4e2e6061e 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Multiplier/NexusMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONNEXMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONNEXMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEXMULTIPLIERS_H +#define PLAYERBOTS_NEXMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Strategy/NexusStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexStrategy.cpp similarity index 89% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Strategy/NexusStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexStrategy.cpp index e633ba982..7ed3e5087 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Strategy/NexusStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexStrategy.cpp @@ -1,5 +1,11 @@ -#include "NexusStrategy.h" -#include "NexusMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NexStrategy.h" +#include "NexMultipliers.h" void WotlkDungeonNexStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Strategy/NexusStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexStrategy.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Strategy/NexusStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexStrategy.h index c78225b93..8581ae726 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Strategy/NexusStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONNEXSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONNEXSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEXSTRATEGY_H +#define PLAYERBOTS_NEXSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/NexusTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggerContext.h similarity index 83% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/NexusTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggerContext.h index 38ebbcb3d..a8fdf68dc 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/NexusTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEXTRIGGERCONTEXT_H +#define PLAYERBOTS_NEXTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "NexusTriggers.h" +#include "NexTriggers.h" class WotlkDungeonNexTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Trigger/NexusTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggers.cpp similarity index 92% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Trigger/NexusTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggers.cpp index 02be3f70e..68dd18157 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Trigger/NexusTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "NexusTriggers.h" +#include "NexTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Trigger/NexusTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggers.h similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/Nexus/Trigger/NexusTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggers.h index ac5d88a48..e7c299513 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Nexus/Trigger/NexusTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/Nex/NexTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONNEXTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEXTRIGGERS_H +#define PLAYERBOTS_NEXTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/OculusActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCActionContext.h similarity index 81% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/OculusActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCActionContext.h index fa5036e54..9d57710b9 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/OculusActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOCCACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONOCCACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OCACTIONCONTEXT_H +#define PLAYERBOTS_OCACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "OculusActions.h" +#include "OCActions.h" class WotlkDungeonOccActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCActions.cpp similarity index 97% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCActions.cpp index 385451003..1fcca1fcb 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Action/OculusActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCActions.cpp @@ -1,6 +1,12 @@ -#include "OculusTriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OCTriggers.h" #include "Playerbots.h" -#include "OculusActions.h" +#include "OCActions.h" #include "LastSpellCastValue.h" bool AvoidUnstableSphereAction::Execute(Event /*event*/) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Action/OculusActions.h b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCActions.h similarity index 85% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Action/OculusActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCActions.h index 6616ce884..ad973a806 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Action/OculusActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOCCACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONOCCACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OCACTIONS_H +#define PLAYERBOTS_OCACTIONS_H #include "Action.h" #include "AttackAction.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Multiplier/OculusMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCMultipliers.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Multiplier/OculusMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCMultipliers.cpp index f8fcdd3f1..6a7c8f75b 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Multiplier/OculusMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCMultipliers.cpp @@ -1,9 +1,15 @@ -#include "OculusMultipliers.h" -#include "OculusActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OCMultipliers.h" +#include "OCActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "OculusTriggers.h" +#include "OCTriggers.h" #include "FollowActions.h" #include "ReachTargetActions.h" #include "Playerbots.h" @@ -93,7 +99,7 @@ float EregosMultiplier::GetValue(Action* action) Unit* boss = AI_VALUE2(Unit*, "find target", "ley-guardian eregos"); if (!boss) { return 1.0f; } - if (boss->HasAura(SPELL_PLANAR_SHIFT && dynamic_cast(action))) + if (boss->HasAura(SPELL_PLANAR_SHIFT) && dynamic_cast(action)) return 0.0f; return 1.0f; diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Multiplier/OculusMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCMultipliers.h similarity index 79% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Multiplier/OculusMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCMultipliers.h index 423364b09..094115262 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Multiplier/OculusMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOCCMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONOCCMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OCMULTIPLIERS_H +#define PLAYERBOTS_OCMULTIPLIERS_H #include "Multiplier.h" #include "Unit.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Strategy/OculusStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCStrategy.cpp similarity index 84% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Strategy/OculusStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCStrategy.cpp index 4c558469e..6fc0f0e18 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Strategy/OculusStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCStrategy.cpp @@ -1,5 +1,11 @@ -#include "OculusStrategy.h" -#include "OculusMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OCStrategy.h" +#include "OCMultipliers.h" void WotlkDungeonOccStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Strategy/OculusStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Strategy/OculusStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCStrategy.h index 2accfd8f4..179c35cf5 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Strategy/OculusStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOCCSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONOCCSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OCSTRATEGY_H +#define PLAYERBOTS_OCSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/OculusTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggerContext.h similarity index 83% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/OculusTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggerContext.h index ccd2d8ca1..4531425b2 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/OculusTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OCTRIGGERCONTEXT_H +#define PLAYERBOTS_OCTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "OculusTriggers.h" +#include "OCTriggers.h" class WotlkDungeonOccTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Trigger/OculusTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggers.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Trigger/OculusTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggers.cpp index 9cc8288f3..42ffc4e98 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Trigger/OculusTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "OculusTriggers.h" +#include "OCTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" #include "Unit.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Trigger/OculusTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggers.h similarity index 92% rename from modules/mod-playerbots/src/Ai/Dungeon/Oculus/Trigger/OculusTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggers.h index 8d2d55d16..5bbb2d264 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/Oculus/Trigger/OculusTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/OC/OCTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONOCCTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OCTRIGGERS_H +#define PLAYERBOTS_OCTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/PitOfSaronActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActionContext.h similarity index 61% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/PitOfSaronActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActionContext.h index 816202a8a..4b21c0501 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/PitOfSaronActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONPOSACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONPOSACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_POSACTIONCONTEXT_H +#define PLAYERBOTS_POSACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "PitOfSaronActions.h" +#include "PoSActions.h" class WotlkDungeonPoSActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActions.cpp index 577575d42..37ec31395 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "PitOfSaronActions.h" +#include "PoSActions.h" bool IckAndKrickAction::Execute(Event /*event*/) { @@ -24,7 +30,7 @@ bool IckAndKrickAction::Execute(Event /*event*/) bool pursuit = bot->HasAura(SPELL_PURSUIT) || (!botAI->IsTank(bot) && boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_PURSUIT)); bool poisonNova = boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA_POS) || boss->FindCurrentSpellBySpellId(SPELL_POISON_NOVA_POS_HC)); - bool explosiveBarrage = orb || boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_ICK) || boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_KRICK)); + bool explosiveBarrage = orb || (boss->HasUnitState(UNIT_STATE_CASTING) && (boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_ICK) || boss->FindCurrentSpellBySpellId(SPELL_EXPLOSIVE_BARRAGE_KRICK))); bool isTank = botAI->IsTank(bot); if (pursuit && Pursuit(pursuit, boss)) @@ -134,7 +140,7 @@ bool IckAndKrickAction::PoisonNova(bool poisonNova, Unit* boss) return false; } -bool IckAndKrickAction::ExplosiveBarrage(bool explosiveBarrage, Unit* boss) +bool IckAndKrickAction::ExplosiveBarrage(bool /*explosiveBarrage*/, Unit* boss) { std::vector orbs; Unit* closestOrb = nullptr; @@ -201,8 +207,7 @@ bool IckAndKrickAction::ExplosiveBarrage(bool explosiveBarrage, Unit* boss) continue; // Check if position is within maximum allowed distance from boss - if (boss && sqrt(pow(potentialPos.GetPositionX() - boss->GetPositionX(), 2) + - pow(potentialPos.GetPositionY() - boss->GetPositionY(), 2)) > MAX_BOSS_DISTANCE) + if (boss && boss->GetDistance2d(potentialPos.GetPositionX(), potentialPos.GetPositionY()) > MAX_BOSS_DISTANCE) continue; // Score this position based on: @@ -215,8 +220,7 @@ bool IckAndKrickAction::ExplosiveBarrage(bool explosiveBarrage, Unit* boss) float minOrbDist = std::numeric_limits::max(); for (Unit* orb : orbs) { - float orbDist = sqrt(pow(potentialPos.GetPositionX() - orb->GetPositionX(), 2) + - pow(potentialPos.GetPositionY() - orb->GetPositionY(), 2)); + float orbDist = orb->GetDistance2d(potentialPos.GetPositionX(), potentialPos.GetPositionY()); minOrbDist = std::min(minOrbDist, orbDist); } score += minOrbDist * 2.0f; // Weight orb distance more heavily @@ -232,8 +236,7 @@ bool IckAndKrickAction::ExplosiveBarrage(bool explosiveBarrage, Unit* boss) // Factor in proximity to boss (closer is better, as long as we're safe from orbs) if (boss) { - float bossDist = sqrt(pow(potentialPos.GetPositionX() - boss->GetPositionX(), 2) + - pow(potentialPos.GetPositionY() - boss->GetPositionY(), 2)); + float bossDist = boss->GetDistance2d(potentialPos.GetPositionX(), potentialPos.GetPositionY()); // Add points for being closer to boss (inverse relationship) // but only if we're safely away from orbs if (minOrbDist > SAFE_DISTANCE) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.h b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActions.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActions.h index a467766f1..4fb88835c 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Action/PitOfSaronActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONPOSACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONPOSACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_POSACTIONS_H +#define PLAYERBOTS_POSACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "PitOfSaronTriggers.h" +#include "PoSTriggers.h" const Position ICKANDKRICK_TANK_POSITION = Position(816.8508f, 102.331505f, 509.1586f); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSMultipliers.cpp similarity index 76% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSMultipliers.cpp index be36e480f..988d07d0c 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSMultipliers.cpp @@ -1,9 +1,15 @@ -#include "PitOfSaronMultipliers.h" -#include "PitOfSaronActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PoSMultipliers.h" +#include "PoSActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "PitOfSaronTriggers.h" +#include "PoSTriggers.h" float IckAndKrickMultiplier::GetValue(Action* action) { @@ -27,7 +33,7 @@ float IckAndKrickMultiplier::GetValue(Action* action) return 1.0f; } -float GarfrostMultiplier::GetValue(Action* action) +float GarfrostMultiplier::GetValue(Action* /*action*/) { Unit* boss = AI_VALUE2(Unit*, "find target", "garfrost"); if (!boss) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSMultipliers.h similarity index 56% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSMultipliers.h index bcafc01a4..e595a2742 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Multiplier/PitOfSaronMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONPOSMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONPOSMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_POSMULTIPLIERS_H +#define PLAYERBOTS_POSMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Strategy/PitOfSaronStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSStrategy.cpp similarity index 60% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Strategy/PitOfSaronStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSStrategy.cpp index 641af0a6d..cc00650c4 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Strategy/PitOfSaronStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSStrategy.cpp @@ -1,5 +1,11 @@ -#include "PitOfSaronStrategy.h" -#include "PitOfSaronMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "PoSStrategy.h" +#include "PoSMultipliers.h" void WotlkDungeonPoSStrategy::InitTriggers(std::vector& triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Strategy/PitOfSaronStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSStrategy.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Strategy/PitOfSaronStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSStrategy.h index 4098378d7..a4d39d5f1 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Strategy/PitOfSaronStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONPOSSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONPOSSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_POSSTRATEGY_H +#define PLAYERBOTS_POSSTRATEGY_H #include "Multiplier.h" #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/PitOfSaronTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggerContext.h similarity index 60% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/PitOfSaronTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggerContext.h index a31bff949..290ea6107 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/PitOfSaronTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_POSTRIGGERCONTEXT_H +#define PLAYERBOTS_POSTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "PitOfSaronTriggers.h" +#include "PoSTriggers.h" class WotlkDungeonPoSTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Trigger/PitOfSaronTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggers.cpp similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Trigger/PitOfSaronTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggers.cpp index f744302cd..433c3d279 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Trigger/PitOfSaronTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "PitOfSaronTriggers.h" +#include "PoSTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Trigger/PitOfSaronTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggers.h similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Trigger/PitOfSaronTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggers.h index 9205026fb..eae2708b2 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/PitOfSaron/Trigger/PitOfSaronTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/PoS/PoSTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONPOSTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_POSTRIGGERS_H +#define PLAYERBOTS_POSTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/TrialOfTheChampionActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActionContext.h similarity index 74% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/TrialOfTheChampionActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActionContext.h index 21f877f24..c662db1e5 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/TrialOfTheChampionActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTOCACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONTOCACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TOCACTIONCONTEXT_H +#define PLAYERBOTS_TOCACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "TrialOfTheChampionActions.h" +#include "TOCActions.h" class WotlkDungeonToCActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Action/TrialOfTheChampionActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActions.cpp similarity index 96% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Action/TrialOfTheChampionActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActions.cpp index da865a2d1..05b388cc9 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Action/TrialOfTheChampionActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "TrialOfTheChampionActions.h" +#include "TOCActions.h" #include "NearestNpcsValue.h" #include "ObjectAccessor.h" #include "Vehicle.h" @@ -55,10 +61,6 @@ bool ToCLanceAction::Execute(Event /*event*/) // If we found the lance, equip it if (lanceItem) { - // Store the lance's current position - uint8 srcBag = lanceItem->GetBagSlot(); - uint8 srcSlot = lanceItem->GetSlot(); - // First unequip current weapon if it exists if (oldWeapon) bot->SwapItem(oldWeapon->GetPos(), lanceItem->GetPos()); diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Action/TrialOfTheChampionActions.h b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActions.h similarity index 81% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Action/TrialOfTheChampionActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActions.h index ab2c18ae8..a8367070b 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Action/TrialOfTheChampionActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTOCACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONTOCACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TOCACTIONS_H +#define PLAYERBOTS_TOCACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "TrialOfTheChampionTriggers.h" +#include "TOCTriggers.h" #include "MovementActions.h" #include "LastMovementValue.h" #include "ObjectGuid.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.cpp new file mode 100644 index 000000000..3ee464317 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.cpp @@ -0,0 +1,15 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TOCMultipliers.h" +#include "TOCActions.h" +#include "GenericSpellActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "TOCTriggers.h" +#include "Action.h" + +//float tocMultiplier::GetValue(Action* action) { return 1.0f; } diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.h new file mode 100644 index 000000000..6a539c5f1 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCMultipliers.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TOCMULTIPLIERS_H +#define PLAYERBOTS_TOCMULTIPLIERS_H + +#include "Multiplier.h" + +/* class tocMultiplier : public Multiplier +{ + public: + tocMultiplier(PlayerbotAI* ai) : Multiplier(ai, "toc") {} + + public: + virtual float GetValue(Action* action); +}; +*/ +#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCStrategy.cpp similarity index 70% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCStrategy.cpp index 323970d52..729ddac04 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCStrategy.cpp @@ -1,4 +1,10 @@ -#include "TrialOfTheChampionStrategy.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TOCStrategy.h" void WotlkDungeonToCStrategy::InitTriggers(std::vector &triggers) { @@ -15,6 +21,6 @@ void WotlkDungeonToCStrategy::InitTriggers(std::vector &triggers) } -void WotlkDungeonToCStrategy::InitMultipliers(std::vector &multipliers) +void WotlkDungeonToCStrategy::InitMultipliers(std::vector &/*multipliers*/) { } diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCStrategy.h similarity index 56% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCStrategy.h index 12416d5ad..fb85b823d 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Strategy/TrialOfTheChampionStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCStrategy.h @@ -1,10 +1,16 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTOCSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONTOCSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TOCSTRATEGY_H +#define PLAYERBOTS_TOCSTRATEGY_H #include "AiObjectContext.h" #include "Multiplier.h" #include "Strategy.h" -#include "TrialOfTheChampionMultipliers.h" +#include "TOCMultipliers.h" class WotlkDungeonToCStrategy : public Strategy { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/TrialOfTheChampionTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggerContext.h similarity index 75% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/TrialOfTheChampionTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggerContext.h index f1b55c523..b6636a46f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/TrialOfTheChampionTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTOCTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONTOCTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TOCTRIGGERCONTEXT_H +#define PLAYERBOTS_TOCTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "TrialOfTheChampionTriggers.h" +#include "TOCTriggers.h" class WotlkDungeonToCTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggers.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggers.cpp index 6c388382e..ebb7a12a5 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "TrialOfTheChampionTriggers.h" +#include "TOCTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggers.h similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggers.h index 0daaf914a..4afc00284 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Trigger/TrialOfTheChampionTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/TOC/TOCTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTOCTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONTOCTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TOCTRIGGERS_H +#define PLAYERBOTS_TOCTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonActionContext.h new file mode 100644 index 000000000..72bb50002 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonActionContext.h @@ -0,0 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TBCDUNGEONACTIONCONTEXT_H +#define PLAYERBOTS_TBCDUNGEONACTIONCONTEXT_H + +#include "ACActionContext.h" + +#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonTriggerContext.h new file mode 100644 index 000000000..f832abdcd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Dungeon/TbcDungeonTriggerContext.h @@ -0,0 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TBCDUNGEONTRIGGERCONTEXT_H +#define PLAYERBOTS_TBCDUNGEONTRIGGERCONTEXT_H + +#include "ACTriggerContext.h" + +#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.cpp deleted file mode 100644 index c090ef68b..000000000 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "TrialOfTheChampionMultipliers.h" -#include "TrialOfTheChampionActions.h" -#include "GenericSpellActions.h" -#include "ChooseTargetActions.h" -#include "MovementActions.h" -#include "TrialOfTheChampionTriggers.h" -#include "Action.h" - -//float tocMultiplier::GetValue(Action* action) { return 1.0f; } diff --git a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.h deleted file mode 100644 index bf9412639..000000000 --- a/modules/mod-playerbots/src/Ai/Dungeon/TrialOfTheChampion/Multiplier/TrialOfTheChampionMultipliers.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTOCMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONTOCMULTIPLIERS_H - -#include "Multiplier.h" - -/* class tocMultiplier : public Multiplier -{ - public: - tocMultiplier(PlayerbotAI* ai) : Multiplier(ai, "toc") {} - - public: - virtual float GetValue(Action* action); -}; -*/ -#endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/UtgardeKeepActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKActionContext.h similarity index 81% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/UtgardeKeepActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKActionContext.h index 0e51e56b4..7cf57d8c8 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/UtgardeKeepActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUKACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONUKACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UKACTIONCONTEXT_H +#define PLAYERBOTS_UKACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "UtgardeKeepActions.h" +#include "UKActions.h" class WotlkDungeonUKActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Action/UtgardeKeepActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Action/UtgardeKeepActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKActions.cpp index 85d8590ee..c2425b34a 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Action/UtgardeKeepActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "UtgardeKeepActions.h" +#include "UKActions.h" bool AttackFrostTombAction::isUseful() { return !botAI->IsHeal(bot); } bool AttackFrostTombAction::Execute(Event /*event*/) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Action/UtgardeKeepActions.h b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKActions.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Action/UtgardeKeepActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKActions.h index c9c77d520..c851494f9 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Action/UtgardeKeepActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUKACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONUKACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UKACTIONS_H +#define PLAYERBOTS_UKACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "UtgardeKeepTriggers.h" +#include "UKTriggers.h" class AttackFrostTombAction : public AttackAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Multiplier/UtgardeKeepMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKMultipliers.cpp similarity index 89% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Multiplier/UtgardeKeepMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKMultipliers.cpp index e9a411dac..3d1b26e3f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Multiplier/UtgardeKeepMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKMultipliers.cpp @@ -1,8 +1,14 @@ -#include "UtgardeKeepMultipliers.h" -#include "UtgardeKeepActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UKMultipliers.h" +#include "UKActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" -#include "UtgardeKeepTriggers.h" +#include "UKTriggers.h" float PrinceKelesethMultiplier::GetValue(Action* action) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Multiplier/UtgardeKeepMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKMultipliers.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Multiplier/UtgardeKeepMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKMultipliers.h index e0b8e7af0..9553e4057 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Multiplier/UtgardeKeepMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUKMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONUKMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UKMULTIPLIERS_H +#define PLAYERBOTS_UKMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Strategy/UtgardeKeepStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKStrategy.cpp similarity index 86% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Strategy/UtgardeKeepStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKStrategy.cpp index 562cb8ec5..6b819212f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Strategy/UtgardeKeepStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKStrategy.cpp @@ -1,5 +1,11 @@ -#include "UtgardeKeepStrategy.h" -#include "UtgardeKeepMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UKStrategy.h" +#include "UKMultipliers.h" void WotlkDungeonUKStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Strategy/UtgardeKeepStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Strategy/UtgardeKeepStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKStrategy.h index 41ce1e76e..0857dbc63 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Strategy/UtgardeKeepStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUKSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONUKSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UKSTRATEGY_H +#define PLAYERBOTS_UKSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/UtgardeKeepTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggerContext.h similarity index 82% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/UtgardeKeepTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggerContext.h index 52ec95dae..2231c948f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/UtgardeKeepTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUKTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONUKTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UKTRIGGERCONTEXT_H +#define PLAYERBOTS_UKTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "UtgardeKeepTriggers.h" +#include "UKTriggers.h" class WotlkDungeonUKTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Trigger/UtgardeKeepTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggers.cpp similarity index 89% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Trigger/UtgardeKeepTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggers.cpp index e19234bd0..fee073b32 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Trigger/UtgardeKeepTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "UtgardeKeepTriggers.h" +#include "UKTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Trigger/UtgardeKeepTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggers.h similarity index 88% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Trigger/UtgardeKeepTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggers.h index 237721a6e..d1ccc37b5 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardeKeep/Trigger/UtgardeKeepTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UK/UKTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUKTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONUKTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UKTRIGGERS_H +#define PLAYERBOTS_UKTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/UtgardePinnacleActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPActionContext.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/UtgardePinnacleActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPActionContext.h index 9e7532b9a..b4cb910f2 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/UtgardePinnacleActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUPACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONUPACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UPACTIONCONTEXT_H +#define PLAYERBOTS_UPACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "UtgardePinnacleActions.h" +#include "UPActions.h" class WotlkDungeonUPActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Action/UtgardePinnacleActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPActions.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Action/UtgardePinnacleActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPActions.cpp index 8025f1aac..0e730ee62 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Action/UtgardePinnacleActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "UtgardePinnacleActions.h" +#include "UPActions.h" bool AvoidFreezingCloudAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Action/UtgardePinnacleActions.h b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPActions.h similarity index 60% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Action/UtgardePinnacleActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPActions.h index 38e90dd15..ce6914a5f 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Action/UtgardePinnacleActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPActions.h @@ -1,11 +1,17 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUPACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONUPACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UPACTIONS_H +#define PLAYERBOTS_UPACTIONS_H #include "Action.h" #include "AttackAction.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "UtgardePinnacleTriggers.h" +#include "UPTriggers.h" class AvoidFreezingCloudAction : public MovementAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPMultipliers.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPMultipliers.cpp index cc34cb6a4..8dc1fac84 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPMultipliers.cpp @@ -1,9 +1,15 @@ -#include "UtgardePinnacleMultipliers.h" -#include "UtgardePinnacleActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UPMultipliers.h" +#include "UPActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "UtgardePinnacleTriggers.h" +#include "UPTriggers.h" #include "Action.h" float SkadiMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPMultipliers.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPMultipliers.h index 06cb85783..8ad6c02cb 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Multiplier/UtgardePinnacleMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUPMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONUPMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UPMULTIPLIERS_H +#define PLAYERBOTS_UPMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Strategy/UtgardePinnacleStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPStrategy.cpp similarity index 74% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Strategy/UtgardePinnacleStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPStrategy.cpp index fe104f34f..1d0834190 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Strategy/UtgardePinnacleStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPStrategy.cpp @@ -1,5 +1,11 @@ -#include "UtgardePinnacleStrategy.h" -#include "UtgardePinnacleMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UPStrategy.h" +#include "UPMultipliers.h" void WotlkDungeonUPStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Strategy/UtgardePinnacleStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Strategy/UtgardePinnacleStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPStrategy.h index 8ce816f80..ec75cada3 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Strategy/UtgardePinnacleStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUPSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONUPSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UPSTRATEGY_H +#define PLAYERBOTS_UPSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/UtgardePinnacleTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggerContext.h similarity index 68% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/UtgardePinnacleTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggerContext.h index 527453f8b..dbc690266 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/UtgardePinnacleTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUPTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONUPTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UPTRIGGERCONTEXT_H +#define PLAYERBOTS_UPTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "UtgardePinnacleTriggers.h" +#include "UPTriggers.h" class WotlkDungeonUPTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Trigger/UtgardePinnacleTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggers.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Trigger/UtgardePinnacleTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggers.cpp index dc35e8be0..50a1827cc 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Trigger/UtgardePinnacleTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "UtgardePinnacleTriggers.h" +#include "UPTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Trigger/UtgardePinnacleTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggers.h similarity index 81% rename from modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Trigger/UtgardePinnacleTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggers.h index 72bf59f8c..8359aa98e 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/UtgardePinnacle/Trigger/UtgardePinnacleTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/UP/UPTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONUPTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONUPTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_UPTRIGGERS_H +#define PLAYERBOTS_UPTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/VioletHoldActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHActionContext.h similarity index 73% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/VioletHoldActionContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHActionContext.h index 902332f15..64c7fcedd 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/VioletHoldActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONVHACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONVHACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VHACTIONCONTEXT_H +#define PLAYERBOTS_VHACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "VioletHoldActions.h" +#include "VHActions.h" class WotlkDungeonVHActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Action/VioletHoldActions.cpp b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Action/VioletHoldActions.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHActions.cpp index b61f84bdf..d9841b0ba 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Action/VioletHoldActions.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHActions.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "VioletHoldActions.h" +#include "VHActions.h" bool AttackErekemAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Action/VioletHoldActions.h b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHActions.h similarity index 76% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Action/VioletHoldActions.h rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHActions.h index fc7ce9d29..64e4419f2 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Action/VioletHoldActions.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHActions.h @@ -1,12 +1,18 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONVHACTIONS_H -#define _PLAYERBOT_WOTLKDUNGEONVHACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VHACTIONS_H +#define PLAYERBOTS_VHACTIONS_H #include "Action.h" #include "AttackAction.h" #include "GenericSpellActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "VioletHoldTriggers.h" +#include "VHTriggers.h" class AttackErekemAction : public AttackAction { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Multiplier/VioletHoldMultipliers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHMultipliers.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Multiplier/VioletHoldMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHMultipliers.cpp index 3d3a13cd0..48708e5f7 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Multiplier/VioletHoldMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHMultipliers.cpp @@ -1,9 +1,15 @@ -#include "VioletHoldMultipliers.h" -#include "VioletHoldActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "VHMultipliers.h" +#include "VHActions.h" #include "GenericSpellActions.h" #include "ChooseTargetActions.h" #include "MovementActions.h" -#include "VioletHoldTriggers.h" +#include "VHTriggers.h" #include "Action.h" float ErekemMultiplier::GetValue(Action* action) diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Multiplier/VioletHoldMultipliers.h b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHMultipliers.h similarity index 66% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Multiplier/VioletHoldMultipliers.h rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHMultipliers.h index 67952a16b..bf4b7ac62 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Multiplier/VioletHoldMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONVHMULTIPLIERS_H -#define _PLAYERBOT_WOTLKDUNGEONVHMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VHMULTIPLIERS_H +#define PLAYERBOTS_VHMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Strategy/VioletHoldStrategy.cpp b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHStrategy.cpp similarity index 81% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Strategy/VioletHoldStrategy.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHStrategy.cpp index ffc00e306..e0c431895 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Strategy/VioletHoldStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHStrategy.cpp @@ -1,5 +1,11 @@ -#include "VioletHoldStrategy.h" -#include "VioletHoldMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "VHStrategy.h" +#include "VHMultipliers.h" void WotlkDungeonVHStrategy::InitTriggers(std::vector &triggers) { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Strategy/VioletHoldStrategy.h b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHStrategy.h similarity index 58% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Strategy/VioletHoldStrategy.h rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHStrategy.h index a06f64ce3..e9fbccc55 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Strategy/VioletHoldStrategy.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHStrategy.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONVHSTRATEGY_H -#define _PLAYERBOT_WOTLKDUNGEONVHSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VHSTRATEGY_H +#define PLAYERBOTS_VHSTRATEGY_H #include "Multiplier.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/VioletHoldTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggerContext.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/VioletHoldTriggerContext.h rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggerContext.h index 45d298121..fa4049180 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/VioletHoldTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggerContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONVHTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONVHTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VHTRIGGERCONTEXT_H +#define PLAYERBOTS_VHTRIGGERCONTEXT_H #include "NamedObjectContext.h" #include "AiObjectContext.h" -#include "VioletHoldTriggers.h" +#include "VHTriggers.h" class WotlkDungeonVHTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Trigger/VioletHoldTriggers.cpp b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggers.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Trigger/VioletHoldTriggers.cpp rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggers.cpp index 87284b0e4..67fe600ed 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Trigger/VioletHoldTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggers.cpp @@ -1,5 +1,11 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "VioletHoldTriggers.h" +#include "VHTriggers.h" #include "AiObject.h" #include "AiObjectContext.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Trigger/VioletHoldTriggers.h b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggers.h similarity index 82% rename from modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Trigger/VioletHoldTriggers.h rename to modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggers.h index b5a306d42..542285990 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/VioletHold/Trigger/VioletHoldTriggers.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/VH/VHTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONVHTRIGGERS_H -#define _PLAYERBOT_WOTLKDUNGEONVHTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VHTRIGGERS_H +#define PLAYERBOTS_VHTRIGGERS_H #include "Trigger.h" #include "PlayerbotAIConfig.h" diff --git a/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonActionContext.h b/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonActionContext.h index c7202de61..5b425aa51 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonActionContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonActionContext.h @@ -1,21 +1,27 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONACTIONCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "UtgardeKeep/UtgardeKeepActionContext.h" -#include "Nexus/NexusActionContext.h" -#include "AzjolNerub/AzjolNerubActionContext.h" -#include "OldKingdom/OldKingdomActionContext.h" -#include "DraktharonKeep/DrakTharonKeepActionContext.h" -#include "VioletHold/VioletHoldActionContext.h" -#include "Gundrak/GundrakActionContext.h" -#include "HallsOfStone/HallsOfStoneActionContext.h" -#include "HallsOfLightning/HallsOfLightningActionContext.h" -#include "Oculus/OculusActionContext.h" -#include "UtgardePinnacle/UtgardePinnacleActionContext.h" -#include "CullingOfStratholme/CullingOfStratholmeActionContext.h" -#include "ForgeOfSouls/ForgeOfSoulsActionContext.h" -#include "PitOfSaron/PitOfSaronActionContext.h" -#include "TrialOfTheChampion/TrialOfTheChampionActionContext.h" +#ifndef PLAYERBOTS_WOTLKDUNGEONACTIONCONTEXT_H +#define PLAYERBOTS_WOTLKDUNGEONACTIONCONTEXT_H + +#include "UKActionContext.h" +#include "NexActionContext.h" +#include "ANActionContext.h" +#include "AKActionContext.h" +#include "DTKActionContext.h" +#include "VHActionContext.h" +#include "GDActionContext.h" +#include "HoSActionContext.h" +#include "HoLActionContext.h" +#include "OCActionContext.h" +#include "UPActionContext.h" +#include "CoSActionContext.h" +#include "FoSActionContext.h" +#include "PoSActionContext.h" +#include "TOCActionContext.h" // #include "HallsOfReflection/HallsOfReflectionActionContext.h" #endif diff --git a/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonTriggerContext.h b/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonTriggerContext.h index 630aecbd9..1b91c9c82 100644 --- a/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Dungeon/WotlkDungeonTriggerContext.h @@ -1,21 +1,27 @@ -#ifndef _PLAYERBOT_WOTLKDUNGEONTRIGGERCONTEXT_H -#define _PLAYERBOT_WOTLKDUNGEONTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "UtgardeKeep/UtgardeKeepTriggerContext.h" -#include "Nexus/NexusTriggerContext.h" -#include "AzjolNerub/AzjolNerubTriggerContext.h" -#include "OldKingdom/OldKingdomTriggerContext.h" -#include "DraktharonKeep/DrakTharonKeepTriggerContext.h" -#include "VioletHold/VioletHoldTriggerContext.h" -#include "Gundrak/GundrakTriggerContext.h" -#include "HallsOfStone/HallsOfStoneTriggerContext.h" -#include "HallsOfLightning/HallsOfLightningTriggerContext.h" -#include "Oculus/OculusTriggerContext.h" -#include "UtgardePinnacle/UtgardePinnacleTriggerContext.h" -#include "CullingOfStratholme/CullingOfStratholmeTriggerContext.h" -#include "ForgeOfSouls/ForgeOfSoulsTriggerContext.h" -#include "PitOfSaron/PitOfSaronTriggerContext.h" -#include "TrialOfTheChampion/TrialOfTheChampionTriggerContext.h" +#ifndef PLAYERBOTS_WOTLKDUNGEONTRIGGERCONTEXT_H +#define PLAYERBOTS_WOTLKDUNGEONTRIGGERCONTEXT_H + +#include "UKTriggerContext.h" +#include "NexTriggerContext.h" +#include "ANTriggerContext.h" +#include "AKTriggerContext.h" +#include "DTKTriggerContext.h" +#include "VHTriggerContext.h" +#include "GDTriggerContext.h" +#include "HoSTriggerContext.h" +#include "HoLTriggerContext.h" +#include "OCTriggerContext.h" +#include "UPTriggerContext.h" +#include "CoSTriggerContext.h" +#include "FoSTriggerContext.h" +#include "PoSTriggerContext.h" +#include "TOCTriggerContext.h" // #include "HallsOfReflection/HallsOfReflectionTriggerContext.h" #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.h deleted file mode 100644 index 59a9b4376..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef _PLAYERBOT_RAIDAQ20ACTIONS_H -#define _PLAYERBOT_RAIDAQ20ACTIONS_H - -#include "MovementActions.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" - -class Aq20UseCrystalAction : public MovementAction -{ -public: - Aq20UseCrystalAction(PlayerbotAI* botAI, std::string const name = "aq20 use crystal") : MovementAction(botAI, name) {} - bool Execute(Event event) override; -}; -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20ActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20ActionContext.h similarity index 50% rename from modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20ActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20ActionContext.h index ea3afcf41..5c28e880c 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20ActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20ActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_RAIDAQ20ACTIONCONTEXT_H -#define _PLAYERBOT_RAIDAQ20ACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AQ20ACTIONCONTEXT_H +#define PLAYERBOTS_AQ20ACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "RaidAq20Actions.h" +#include "Aq20Actions.h" class RaidAq20ActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.cpp b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.cpp similarity index 86% rename from modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.cpp rename to modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.cpp index 5d9b4aaa3..27d679c82 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Action/RaidAq20Actions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.cpp @@ -1,6 +1,12 @@ -#include "RaidAq20Actions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidAq20Utils.h" +#include "Aq20Actions.h" + +#include "Aq20Utils.h" bool Aq20UseCrystalAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.h new file mode 100644 index 000000000..3762a4946 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Actions.h @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AQ20ACTIONS_H +#define PLAYERBOTS_AQ20ACTIONS_H + +#include "MovementActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" + +class Aq20UseCrystalAction : public MovementAction +{ +public: + Aq20UseCrystalAction(PlayerbotAI* botAI, std::string const name = "aq20 use crystal") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.cpp new file mode 100644 index 000000000..bc018a0e9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.cpp @@ -0,0 +1,17 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "Aq20Strategy.h" + +#include "Strategy.h" + +void RaidAq20Strategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode("aq20 move to crystal", + { NextAction("aq20 use crystal", ACTION_RAID) })); + +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.h similarity index 53% rename from modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h rename to modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.h index 97ff7453a..0a5ae8706 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Strategy.h @@ -1,8 +1,12 @@ -#ifndef _PLAYERBOT_RAIDAQ20STRATEGY_H -#define _PLAYERBOT_RAIDAQ20STRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AQ20STRATEGY_H +#define PLAYERBOTS_AQ20STRATEGY_H -#include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidAq20Strategy : public Strategy diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20TriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20TriggerContext.h new file mode 100644 index 000000000..b7269e948 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20TriggerContext.h @@ -0,0 +1,25 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AQ20TRIGGERCONTEXT_H +#define PLAYERBOTS_AQ20TRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "Aq20Triggers.h" + +class RaidAq20TriggerContext : public NamedObjectContext +{ +public: + RaidAq20TriggerContext() + { + creators["aq20 move to crystal"] = &RaidAq20TriggerContext::move_to_crystal; + } + +private: + static Trigger* move_to_crystal(PlayerbotAI* ai) { return new Aq20MoveToCrystalTrigger(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.cpp similarity index 82% rename from modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.cpp index 913cb2690..ed8b0adff 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.cpp @@ -1,6 +1,12 @@ -#include "RaidAq20Triggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidAq20Utils.h" +#include "Aq20Triggers.h" + +#include "Aq20Utils.h" bool Aq20MoveToCrystalTrigger::IsActive() { diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.h new file mode 100644 index 000000000..ef3f00f95 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Triggers.h @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AQ20TRIGGERS_H +#define PLAYERBOTS_AQ20TRIGGERS_H + +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "Trigger.h" + +class Aq20MoveToCrystalTrigger : public Trigger +{ +public: + Aq20MoveToCrystalTrigger(PlayerbotAI* botAI) : Trigger(botAI, "aq20 move to crystal") {} + bool IsActive() override; +}; +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.cpp b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.cpp similarity index 79% rename from modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.cpp rename to modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.cpp index 41637af7b..24de20269 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.cpp @@ -1,4 +1,10 @@ -#include "RaidAq20Utils.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "Aq20Utils.h" #include "SpellAuras.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.h new file mode 100644 index 000000000..6293976a1 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Aq20/Aq20Utils.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_AQ20UTILS_H +#define PLAYERBOTS_AQ20UTILS_H + +#include "GameObject.h" +#include "Unit.h" + +class RaidAq20Utils +{ +public: + static bool IsOssirianBuffActive(Unit* ossirian); + static int32 GetOssirianDebuffTimeRemaining(Unit* ossirian); + static GameObject* GetNearestCrystal(Unit* ossirian); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h deleted file mode 100644 index b49ae1c6b..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/RaidAq20TriggerContext.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H - -#include "AiObjectContext.h" -#include "NamedObjectContext.h" -#include "RaidAq20Triggers.h" - -class RaidAq20TriggerContext : public NamedObjectContext -{ -public: - RaidAq20TriggerContext() - { - creators["aq20 move to crystal"] = &RaidAq20TriggerContext::move_to_crystal; - } - -private: - static Trigger* move_to_crystal(PlayerbotAI* ai) { return new Aq20MoveToCrystalTrigger(ai); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.cpp deleted file mode 100644 index 93e0462ca..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Strategy/RaidAq20Strategy.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "RaidAq20Strategy.h" - -#include "Strategy.h" - -void RaidAq20Strategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back( - new TriggerNode("aq20 move to crystal", - { NextAction("aq20 use crystal", ACTION_RAID) })); - -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.h deleted file mode 100644 index 219de8f48..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Trigger/RaidAq20Triggers.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef _PLAYERBOT_RAIDAQ20TRIGGERS_H -#define _PLAYERBOT_RAIDAQ20TRIGGERS_H - -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "Trigger.h" - -class Aq20MoveToCrystalTrigger : public Trigger -{ -public: - Aq20MoveToCrystalTrigger(PlayerbotAI* botAI) : Trigger(botAI, "aq20 move to crystal") {} - bool IsActive() override; -}; -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.h b/modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.h deleted file mode 100644 index b78340d0e..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Aq20/Util/RaidAq20Utils.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _PLAYERBOT_RAIDAQ20UTILS_H -#define _PLAYERBOT_RAIDAQ20UTILS_H - -#include "GameObject.h" -#include "Unit.h" - -class RaidAq20Utils -{ -public: - static bool IsOssirianBuffActive(Unit* ossirian); - static int32 GetOssirianDebuffTimeRemaining(Unit* ossirian); - static GameObject* GetNearestCrystal(Unit* ossirian); -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTActionContext.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTActionContext.h new file mode 100644 index 000000000..b49c25017 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTActionContext.h @@ -0,0 +1,417 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTACTIONCONTEXT_H +#define PLAYERBOTS_BTACTIONCONTEXT_H + +#include "NamedObjectContext.h" +#include "BTActions.h" + +class RaidBlackTempleActionContext : public NamedObjectContext +{ +public: + RaidBlackTempleActionContext() + { + // General + creators["black temple erase timers and trackers"] = + &RaidBlackTempleActionContext::black_temple_erase_timers_and_trackers; + + // High Warlord Naj'entus + creators["high warlord naj'entus misdirect boss to main tank"] = + &RaidBlackTempleActionContext::high_warlord_najentus_misdirect_boss_to_main_tank; + + creators["high warlord naj'entus tanks position boss"] = + &RaidBlackTempleActionContext::high_warlord_najentus_tanks_position_boss; + + creators["high warlord naj'entus disperse ranged"] = + &RaidBlackTempleActionContext::high_warlord_najentus_disperse_ranged; + + creators["high warlord naj'entus remove impaling spine"] = + &RaidBlackTempleActionContext::high_warlord_najentus_remove_impaling_spine; + + creators["high warlord naj'entus throw impaling spine"] = + &RaidBlackTempleActionContext::high_warlord_najentus_throw_impaling_spine; + + // Supremus + creators["supremus misdirect boss to main tank"] = + &RaidBlackTempleActionContext::supremus_misdirect_boss_to_main_tank; + + creators["supremus disperse ranged"] = + &RaidBlackTempleActionContext::supremus_disperse_ranged; + + creators["supremus kite boss"] = + &RaidBlackTempleActionContext::supremus_kite_boss; + + creators["supremus move away from volcanos"] = + &RaidBlackTempleActionContext::supremus_move_away_from_volcanos; + + creators["supremus manage phase timer"] = + &RaidBlackTempleActionContext::supremus_manage_phase_timer; + + // Shade of Akama + creators["shade of akama melee dps prioritize channelers"] = + &RaidBlackTempleActionContext::shade_of_akama_melee_dps_prioritize_channelers; + + // Teron Gorefiend + creators["teron gorefiend misdirect boss to main tank"] = + &RaidBlackTempleActionContext::teron_gorefiend_misdirect_boss_to_main_tank; + + creators["teron gorefiend tanks position boss"] = + &RaidBlackTempleActionContext::teron_gorefiend_tanks_position_boss; + + creators["teron gorefiend position ranged on balcony"] = + &RaidBlackTempleActionContext::teron_gorefiend_position_ranged_on_balcony; + + creators["teron gorefiend avoid shadow of death"] = + &RaidBlackTempleActionContext::teron_gorefiend_avoid_shadow_of_death; + + creators["teron gorefiend move to corner to die"] = + &RaidBlackTempleActionContext::teron_gorefiend_move_to_corner_to_die; + + creators["teron gorefiend control and destroy shadowy constructs"] = + &RaidBlackTempleActionContext::teron_gorefiend_control_and_destroy_shadowy_constructs; + + // Gurtogg Bloodboil + creators["gurtogg bloodboil misdirect boss to main tank"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_misdirect_boss_to_main_tank; + + creators["gurtogg bloodboil tanks position boss"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_tanks_position_boss; + + creators["gurtogg bloodboil rotate ranged groups"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_rotate_ranged_groups; + + creators["gurtogg bloodboil ranged move away from enraged player"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_ranged_move_away_from_enraged_player; + + creators["gurtogg bloodboil manage phase timer"] = + &RaidBlackTempleActionContext::gurtogg_bloodboil_manage_phase_timer; + + // Reliquary of Souls + creators["reliquary of souls misdirect boss to main tank"] = + &RaidBlackTempleActionContext::reliquary_of_souls_misdirect_boss_to_main_tank; + + creators["reliquary of souls adjust distance from suffering"] = + &RaidBlackTempleActionContext::reliquary_of_souls_adjust_distance_from_suffering; + + creators["reliquary of souls healers dps suffering"] = + &RaidBlackTempleActionContext::reliquary_of_souls_healers_dps_suffering; + + creators["reliquary of souls spellsteal rune shield"] = + &RaidBlackTempleActionContext::reliquary_of_souls_spellsteal_rune_shield; + + creators["reliquary of souls spell reflect deaden"] = + &RaidBlackTempleActionContext::reliquary_of_souls_spell_reflect_deaden; + + // Mother Shahraz + creators["mother shahraz misdirect boss to main tank"] = + &RaidBlackTempleActionContext::mother_shahraz_misdirect_boss_to_main_tank; + + creators["mother shahraz tanks position boss under pillar"] = + &RaidBlackTempleActionContext::mother_shahraz_tanks_position_boss_under_pillar; + + creators["mother shahraz melee dps wait at safe position"] = + &RaidBlackTempleActionContext::mother_shahraz_melee_dps_wait_at_safe_position; + + creators["mother shahraz position ranged under pillar"] = + &RaidBlackTempleActionContext::mother_shahraz_position_ranged_under_pillar; + + creators["mother shahraz run away to break fatal attraction"] = + &RaidBlackTempleActionContext::mother_shahraz_run_away_to_break_fatal_attraction; + + // Illidari Council + creators["illidari council misdirect bosses to tanks"] = + &RaidBlackTempleActionContext::illidari_council_misdirect_bosses_to_tanks; + + creators["illidari council main tank position gathios"] = + &RaidBlackTempleActionContext::illidari_council_main_tank_position_gathios; + + creators["illidari council main tank reflect judgement of command"] = + &RaidBlackTempleActionContext::illidari_council_main_tank_reflect_judgement_of_command; + + creators["illidari council first assist tank focus malande"] = + &RaidBlackTempleActionContext::illidari_council_first_assist_tank_focus_malande; + + creators["illidari council second assist tank position darkshadow"] = + &RaidBlackTempleActionContext::illidari_council_second_assist_tank_position_darkshadow; + + creators["illidari council mage tank position zerevor"] = + &RaidBlackTempleActionContext::illidari_council_mage_tank_position_zerevor; + + creators["illidari council position mage tank healer"] = + &RaidBlackTempleActionContext::illidari_council_position_mage_tank_healer; + + creators["illidari council disperse ranged"] = + &RaidBlackTempleActionContext::illidari_council_disperse_ranged; + + creators["illidari council command pets to attack gathios"] = + &RaidBlackTempleActionContext::illidari_council_command_pets_to_attack_gathios; + + creators["illidari council assign dps targets"] = + &RaidBlackTempleActionContext::illidari_council_assign_dps_targets; + + creators["illidari council manage dps timer"] = + &RaidBlackTempleActionContext::illidari_council_manage_dps_timer; + + // Illidan Stormrage + creators["illidan stormrage misdirect to tank"] = + &RaidBlackTempleActionContext::illidan_stormrage_misdirect_to_tank; + + creators["illidan stormrage main tank reposition boss"] = + &RaidBlackTempleActionContext::illidan_stormrage_main_tank_reposition_boss; + + creators["illidan stormrage isolate bot with parasite"] = + &RaidBlackTempleActionContext::illidan_stormrage_isolate_bot_with_parasite; + + creators["illidan stormrage set earthbind totem"] = + &RaidBlackTempleActionContext::illidan_stormrage_set_earthbind_totem; + + creators["illidan stormrage assist tanks handle flames of azzinoth"] = + &RaidBlackTempleActionContext::illidan_stormrage_assist_tanks_handle_flames_of_azzinoth; + + creators["illidan stormrage control pet aggression"] = + &RaidBlackTempleActionContext::illidan_stormrage_control_pet_aggression; + + creators["illidan stormrage position above grate"] = + &RaidBlackTempleActionContext::illidan_stormrage_position_above_grate; + + creators["illidan stormrage remove dark barrage"] = + &RaidBlackTempleActionContext::illidan_stormrage_remove_dark_barrage; + + creators["illidan stormrage move away from landing point"] = + &RaidBlackTempleActionContext::illidan_stormrage_move_away_from_landing_point; + + creators["illidan stormrage disperse ranged"] = + &RaidBlackTempleActionContext::illidan_stormrage_disperse_ranged; + + creators["illidan stormrage melee go somewhere to not die"] = + &RaidBlackTempleActionContext::illidan_stormrage_melee_go_somewhere_to_not_die; + + creators["illidan stormrage warlock tank handle demon boss"] = + &RaidBlackTempleActionContext::illidan_stormrage_warlock_tank_handle_demon_boss; + + creators["illidan stormrage dps prioritize adds"] = + &RaidBlackTempleActionContext::illidan_stormrage_dps_prioritize_adds; + + creators["illidan stormrage use shadow trap"] = + &RaidBlackTempleActionContext::illidan_stormrage_use_shadow_trap; + + creators["illidan stormrage manage dps timer and rti"] = + &RaidBlackTempleActionContext::illidan_stormrage_manage_dps_timer_and_rti; + + creators["illidan stormrage destroy hazards"] = + &RaidBlackTempleActionContext::illidan_stormrage_destroy_hazards; + + creators["illidan stormrage handle adds cheat"] = + &RaidBlackTempleActionContext::illidan_stormrage_handle_adds_cheat; + } + +private: + // General + static Action* black_temple_erase_timers_and_trackers(PlayerbotAI* botAI) { + return new BlackTempleEraseTimersAndTrackersAction(botAI); + } + + // High Warlord Naj'entus + static Action* high_warlord_najentus_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new HighWarlordNajentusMisdirectBossToMainTankAction(botAI); + } + static Action* high_warlord_najentus_tanks_position_boss(PlayerbotAI* botAI) { + return new HighWarlordNajentusTanksPositionBossAction(botAI); + } + static Action* high_warlord_najentus_disperse_ranged(PlayerbotAI* botAI) { + return new HighWarlordNajentusDisperseRangedAction(botAI); + } + static Action* high_warlord_najentus_remove_impaling_spine(PlayerbotAI* botAI) { + return new HighWarlordNajentusRemoveImpalingSpineAction(botAI); + } + static Action* high_warlord_najentus_throw_impaling_spine(PlayerbotAI* botAI) { + return new HighWarlordNajentusThrowImpalingSpineAction(botAI); + } + + // Supremus + static Action* supremus_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new SupremusMisdirectBossToMainTankAction(botAI); + } + static Action* supremus_disperse_ranged(PlayerbotAI* botAI) { + return new SupremusDisperseRangedAction(botAI); + } + static Action* supremus_kite_boss(PlayerbotAI* botAI) { + return new SupremusKiteBossAction(botAI); + } + static Action* supremus_move_away_from_volcanos(PlayerbotAI* botAI) { + return new SupremusMoveAwayFromVolcanosAction(botAI); + } + static Action* supremus_manage_phase_timer(PlayerbotAI* botAI) { + return new SupremusManagePhaseTimerAction(botAI); + } + + // Shade of Akama + static Action* shade_of_akama_melee_dps_prioritize_channelers(PlayerbotAI* botAI) { + return new ShadeOfAkamaMeleeDpsPrioritizeChannelersAction(botAI); + } + + // Teron Gorefiend + static Action* teron_gorefiend_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new TeronGorefiendMisdirectBossToMainTankAction(botAI); + } + static Action* teron_gorefiend_tanks_position_boss(PlayerbotAI* botAI) { + return new TeronGorefiendTanksPositionBossAction(botAI); + } + static Action* teron_gorefiend_position_ranged_on_balcony(PlayerbotAI* botAI) { + return new TeronGorefiendPositionRangedOnBalconyAction(botAI); + } + static Action* teron_gorefiend_avoid_shadow_of_death(PlayerbotAI* botAI) { + return new TeronGorefiendAvoidShadowOfDeathAction(botAI); + } + static Action* teron_gorefiend_move_to_corner_to_die(PlayerbotAI* botAI) { + return new TeronGorefiendMoveToCornerToDieAction(botAI); + } + static Action* teron_gorefiend_control_and_destroy_shadowy_constructs(PlayerbotAI* botAI) { + return new TeronGorefiendControlAndDestroyShadowyConstructsAction(botAI); + } + + // Gurtogg Bloodboil + static Action* gurtogg_bloodboil_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new GurtoggBloodboilMisdirectBossToMainTankAction(botAI); + } + static Action* gurtogg_bloodboil_tanks_position_boss(PlayerbotAI* botAI) { + return new GurtoggBloodboilTanksPositionBossAction(botAI); + } + static Action* gurtogg_bloodboil_rotate_ranged_groups(PlayerbotAI* botAI) { + return new GurtoggBloodboilRotateRangedGroupsAction(botAI); + } + static Action* gurtogg_bloodboil_ranged_move_away_from_enraged_player(PlayerbotAI* botAI) { + return new GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction(botAI); + } + static Action* gurtogg_bloodboil_manage_phase_timer(PlayerbotAI* botAI) { + return new GurtoggBloodboilManagePhaseTimerAction(botAI); + } + + // Reliquary of Souls + static Action* reliquary_of_souls_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsMisdirectBossToMainTankAction(botAI); + } + static Action* reliquary_of_souls_adjust_distance_from_suffering(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsAdjustDistanceFromSufferingAction(botAI); + } + static Action* reliquary_of_souls_healers_dps_suffering(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsHealersDpsSufferingAction(botAI); + } + static Action* reliquary_of_souls_spellsteal_rune_shield(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsSpellstealRuneShieldAction(botAI); + } + static Action* reliquary_of_souls_spell_reflect_deaden(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsSpellReflectDeadenAction(botAI); + } + + // Mother Shahraz + static Action* mother_shahraz_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new MotherShahrazMisdirectBossToMainTankAction(botAI); + } + static Action* mother_shahraz_tanks_position_boss_under_pillar(PlayerbotAI* botAI) { + return new MotherShahrazTanksPositionBossUnderPillarAction(botAI); + } + static Action* mother_shahraz_melee_dps_wait_at_safe_position(PlayerbotAI* botAI) { + return new MotherShahrazMeleeDpsWaitAtSafePositionAction(botAI); + } + static Action* mother_shahraz_position_ranged_under_pillar(PlayerbotAI* botAI) { + return new MotherShahrazPositionRangedUnderPillarAction(botAI); + } + static Action* mother_shahraz_run_away_to_break_fatal_attraction(PlayerbotAI* botAI) { + return new MotherShahrazRunAwayToBreakFatalAttractionAction(botAI); + } + + // Illidari Council + static Action* illidari_council_misdirect_bosses_to_tanks(PlayerbotAI* botAI) { + return new IllidariCouncilMisdirectBossesToTanksAction(botAI); + } + static Action* illidari_council_main_tank_position_gathios(PlayerbotAI* botAI) { + return new IllidariCouncilMainTankPositionGathiosAction(botAI); + } + static Action* illidari_council_main_tank_reflect_judgement_of_command(PlayerbotAI* botAI) { + return new IllidariCouncilMainTankReflectJudgementOfCommandAction(botAI); + } + static Action* illidari_council_first_assist_tank_focus_malande(PlayerbotAI* botAI) { + return new IllidariCouncilFirstAssistTankFocusMalandeAction(botAI); + } + static Action* illidari_council_second_assist_tank_position_darkshadow(PlayerbotAI* botAI) { + return new IllidariCouncilSecondAssistTankPositionDarkshadowAction(botAI); + } + static Action* illidari_council_mage_tank_position_zerevor(PlayerbotAI* botAI) { + return new IllidariCouncilMageTankPositionZerevorAction(botAI); + } + static Action* illidari_council_position_mage_tank_healer(PlayerbotAI* botAI) { + return new IllidariCouncilPositionMageTankHealerAction(botAI); + } + static Action* illidari_council_disperse_ranged(PlayerbotAI* botAI) { + return new IllidariCouncilDisperseRangedAction(botAI); + } + static Action* illidari_council_command_pets_to_attack_gathios(PlayerbotAI* botAI) { + return new IllidariCouncilCommandPetsToAttackGathiosAction(botAI); + } + static Action* illidari_council_assign_dps_targets(PlayerbotAI* botAI) { + return new IllidariCouncilAssignDpsTargetsAction(botAI); + } + static Action* illidari_council_manage_dps_timer(PlayerbotAI* botAI) { + return new IllidariCouncilManageDpsTimerAction(botAI); + } + + // Illidan Stormrage + static Action* illidan_stormrage_misdirect_to_tank(PlayerbotAI* botAI) { + return new IllidanStormrageMisdirectToTankAction(botAI); + } + static Action* illidan_stormrage_main_tank_reposition_boss(PlayerbotAI* botAI) { + return new IllidanStormrageMainTankRepositionBossAction(botAI); + } + static Action* illidan_stormrage_isolate_bot_with_parasite(PlayerbotAI* botAI) { + return new IllidanStormrageIsolateBotWithParasiteAction(botAI); + } + static Action* illidan_stormrage_set_earthbind_totem(PlayerbotAI* botAI) { + return new IllidanStormrageSetEarthbindTotemAction(botAI); + } + static Action* illidan_stormrage_assist_tanks_handle_flames_of_azzinoth(PlayerbotAI* botAI) { + return new IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction(botAI); + } + static Action* illidan_stormrage_control_pet_aggression(PlayerbotAI* botAI) { + return new IllidanStormrageControlPetAggressionAction(botAI); + } + static Action* illidan_stormrage_position_above_grate(PlayerbotAI* botAI) { + return new IllidanStormragePositionAboveGrateAction(botAI); + } + static Action* illidan_stormrage_remove_dark_barrage(PlayerbotAI* botAI) { + return new IllidanStormrageRemoveDarkBarrageAction(botAI); + } + static Action* illidan_stormrage_move_away_from_landing_point(PlayerbotAI* botAI) { + return new IllidanStormrageMoveAwayFromLandingPointAction(botAI); + } + static Action* illidan_stormrage_disperse_ranged(PlayerbotAI* botAI) { + return new IllidanStormrageDisperseRangedAction(botAI); + } + static Action* illidan_stormrage_melee_go_somewhere_to_not_die(PlayerbotAI* botAI) { + return new IllidanStormrageMeleeGoSomewhereToNotDieAction(botAI); + } + static Action* illidan_stormrage_warlock_tank_handle_demon_boss(PlayerbotAI* botAI) { + return new IllidanStormrageWarlockTankHandleDemonBossAction(botAI); + } + static Action* illidan_stormrage_dps_prioritize_adds(PlayerbotAI* botAI) { + return new IllidanStormrageDpsPrioritizeAddsAction(botAI); + } + static Action* illidan_stormrage_use_shadow_trap(PlayerbotAI* botAI) { + return new IllidanStormrageUseShadowTrapAction(botAI); + } + static Action* illidan_stormrage_manage_dps_timer_and_rti(PlayerbotAI* botAI) { + return new IllidanStormrageManageDpsTimerAndRtiAction(botAI); + } + static Action* illidan_stormrage_destroy_hazards(PlayerbotAI* botAI) { + return new IllidanStormrageDestroyHazardsAction(botAI); + } + static Action* illidan_stormrage_handle_adds_cheat(PlayerbotAI* botAI) { + return new IllidanStormrageHandleAddsCheatAction(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTActions.cpp b/modules/mod-playerbots/src/Ai/Raid/BT/BTActions.cpp new file mode 100644 index 000000000..0a8b1f027 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTActions.cpp @@ -0,0 +1,3052 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BTActions.h" + +#include + +#include "CreatureAI.h" +#include "Playerbots.h" +#include "BTHelpers.h" +#include "RaidBossHelpers.h" + +using namespace BlackTempleHelpers; + +// General + +bool BlackTempleEraseTimersAndTrackersAction::Execute(Event /*event*/) +{ + const ObjectGuid guid = bot->GetGUID(); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + if (botAI->IsTank(bot)) + { + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "illidan stormrage") && + !AI_VALUE2(Unit*, "find target", "flame of azzinoth")) + { + if (illidanBossDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (illidanFlameDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (illidanLastPhase.erase(instanceId) > 0) + erased = true; + if (illidanShadowTrapGuid.erase(guid) > 0) + erased = true; + if (illidanShadowTrapDestination.erase(guid) > 0) + erased = true; + if (flameTankWaypointIndex.erase(guid) > 0) + erased = true; + if (westFlameGuid.erase(instanceId) > 0) + erased = true; + if (eastFlameGuid.erase(instanceId) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + if (councilDpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (gathiosTankStep.erase(guid) > 0) + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "mother shahraz") && + shahrazTankStep.erase(guid) > 0) + { + erased = true; + } + return erased; + } + else if (botAI->IsHeal(bot)) + { + if (zerevorHealStep.erase(guid) > 0) + return true; + else + return false; + } + else + { + bool erased = false; + if (!AI_VALUE2(Unit*, "find target", "supremus") && + supremusPhaseTimer.erase(instanceId) > 0) + { + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "ashtongue channeler") && + hasReachedAkamaChannelerPosition.erase(guid) > 0) + { + erased = true; + } + if (!AI_VALUE2(Unit*, "find target", "gurtogg bloodboil") && + gurtoggPhaseTimer.erase(instanceId) > 0) + { + erased = true; + } + return erased; + } +} + +// High Warlord Naj'entus + +bool HighWarlordNajentusMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", najentus)) + { + return botAI->CastSpell("steady shot", najentus); + } + + return false; +} + +bool HighWarlordNajentusTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + if (AI_VALUE(Unit*, "current target") != najentus) + return Attack(najentus); + + if (najentus->GetVictim() == bot && bot->IsWithinMeleeRange(najentus)) + { + const Position& position = NAJENTUS_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool HighWarlordNajentusDisperseRangedAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + constexpr uint32 minInterval = 1000; + + constexpr float safeDistFromBoss = 10.0f; + if (bot->GetExactDist2d(najentus) < safeDistFromBoss && + FleePosition(najentus->GetPosition(), safeDistFromBoss, minInterval)) + { + return true; + } + + constexpr float safeDistFromPlayer = 7.0f; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval); + + return false; +} + +bool HighWarlordNajentusRemoveImpalingSpineAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* impaledPlayer = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(static_cast(BlackTempleSpells::SPELL_IMPALING_SPINE))) + { + impaledPlayer = member; + break; + } + } + if (!impaledPlayer) + return false; + + constexpr float searchRadius = 30.0f; + GameObject* spineGo = bot->FindNearestGameObject( + static_cast(BlackTempleObjects::GO_NAJENTUS_SPINE), searchRadius, true); + if (!spineGo) + return false; + + if (bot->GetExactDist2d(spineGo) > 3.0f) + { + const uint32 delay = urand(2000, 3000); + const ObjectGuid spineGuid = spineGo->GetGUID(); + + botAI->AddTimedEvent( + [this, spineGuid]() + { + if (GameObject* targetSpine = botAI->GetGameObject(spineGuid)) + { + MoveTo(BLACK_TEMPLE_MAP_ID, targetSpine->GetPositionX(), + targetSpine->GetPositionY(), bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); + } + }, + delay); + + return true; + } + else + { + const uint32 delay = urand(1000, 2000); + const ObjectGuid spineGuid = spineGo->GetGUID(); + + botAI->AddTimedEvent( + [this, spineGuid]() + { + if (GameObject* targetSpine = botAI->GetGameObject(spineGuid)) + targetSpine->Use(bot); + }, + delay); + + return true; + } + + return false; +} + +bool HighWarlordNajentusThrowImpalingSpineAction::Execute(Event /*event*/) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus) + return false; + + if (bot->GetExactDist2d(najentus) > 24.0f) + { + const float angle = atan2(bot->GetPositionY() - najentus->GetPositionY(), + bot->GetPositionX() - najentus->GetPositionX()); + const float targetX = najentus->GetPositionX() + 23.0f * std::cos(angle); + const float targetY = najentus->GetPositionY() + 23.0f * std::sin(angle); + + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); + } + + if (bot->GetItemByEntry(static_cast(BlackTempleItems::ITEM_NAJENTUS_SPINE))) + { + const uint32 delay = urand(500, 1500); + const ObjectGuid najentusGuid = najentus->GetGUID(); + + botAI->AddTimedEvent( + [this, najentusGuid]() + { + Item* targetSpine = bot->GetItemByEntry( + static_cast(BlackTempleItems::ITEM_NAJENTUS_SPINE)); + Unit* targetNajentus = botAI->GetUnit(najentusGuid); + if (targetSpine && targetNajentus) + botAI->ImbueItem(targetSpine, targetNajentus); + }, + delay); + + return true; + } + + return false; +} + +// Supremus + +bool SupremusMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref && hunters.size() < 3; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER) + hunters.push_back(member); + } + + if (hunters.empty()) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); + + Player* misdirectTarget = nullptr; + if (bot == hunters[0] && mainTank) + misdirectTarget = mainTank; + else if (hunters.size() > 1 && bot == hunters[1] && firstAssistTank) + misdirectTarget = firstAssistTank; + else if (hunters.size() > 2 && bot == hunters[2] && secondAssistTank) + misdirectTarget = secondAssistTank; + + if (!misdirectTarget) + return false; + + if (botAI->CanCastSpell("misdirection", misdirectTarget)) + return botAI->CastSpell("misdirection", misdirectTarget); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", supremus)) + { + return botAI->CastSpell("steady shot", supremus); + } + + return false; +} + +bool SupremusDisperseRangedAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 8.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool SupremusKiteBossAction::Execute(Event /*event*/) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + constexpr float safeDistance = 25.0f; + const float currentDistance = bot->GetDistance2d(supremus); + if (currentDistance < safeDistance) + return MoveAway(supremus, safeDistance - currentDistance); + + return false; +} + +bool SupremusMoveAwayFromVolcanosAction::Execute(Event /*event*/) +{ + auto const& volcanos = GetAllSupremusVolcanos(); + if (volcanos.empty()) + return false; + + constexpr float hazardRadius = 16.0f; + bool inDanger = false; + for (Unit* volcano : volcanos) + { + if (bot->GetDistance2d(volcano) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + constexpr float maxRadius = 40.0f; + Position safestPos = FindSafestNearbyPosition(volcanos, maxRadius, hazardRadius); + + return MoveTo(BLACK_TEMPLE_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +Position SupremusMoveAwayFromVolcanosAction::FindSafestNearbyPosition( + const std::vector& volcanos, float maxRadius, float hazardRadius) +{ + constexpr float searchStep = M_PI / 8.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = 0.0f; + distance <= maxRadius; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + float x = bot->GetPositionX() + distance * std::cos(angle); + float y = bot->GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* volcano : volcanos) + { + if (volcano->GetDistance2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = + IsPathSafeFromVolcanos(bot->GetPosition(), testPos, volcanos, hazardRadius); + if (pathSafe || !foundSafe) + { + float moveDistance = bot->GetExactDist2d(x, y); + + if (pathSafe && (!foundSafe || moveDistance < minMoveDistance)) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = true; + } + else if (!foundSafe && moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + } + } + } + + if (foundSafe) + break; + } + + return bestPos; +} + +bool SupremusMoveAwayFromVolcanosAction::IsPathSafeFromVolcanos(const Position& start, + const Position& end, const std::vector& volcanos, float hazardRadius) +{ + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* volcano : volcanos) + { + float distToVol = volcano->GetDistance2d(checkX, checkY); + if (distToVol < hazardRadius) + return false; + } + } + + return true; +} + +std::vector SupremusMoveAwayFromVolcanosAction::GetAllSupremusVolcanos() +{ + std::vector volcanos; + constexpr float searchRadius = 40.0f; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, static_cast( + BlackTempleNpcs::NPC_SUPREMUS_VOLCANO), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + volcanos.push_back(creature); + } + + return volcanos; +} + +bool SupremusManagePhaseTimerAction::Execute(Event /*event*/) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + supremusPhaseTimer.try_emplace( + supremus->GetMap()->GetInstanceId(), std::time(nullptr)); + + return false; +} + +// Shade of Akama + +bool ShadeOfAkamaMeleeDpsPrioritizeChannelersAction::Execute(Event /*event*/) +{ + if (!hasReachedAkamaChannelerPosition.count(bot->GetGUID())) + { + const Position &position = AKAMA_CHANNELER_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + else + { + hasReachedAkamaChannelerPosition.insert(bot->GetGUID()); + } + } + + constexpr float searchRadius = 30.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(BlackTempleNpcs::NPC_ASHTONGUE_CHANNELER), searchRadius); + + std::vector channelers; + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + channelers.push_back(creature); + } + + if (channelers.empty()) + return false; + + std::sort(channelers.begin(), channelers.end(), + [](Creature* first, Creature* second) { return first->GetGUID() < second->GetGUID(); }); + + Creature* const channeler = channelers.front(); + + if (MarkTargetWithSkull(bot, channeler)) + return true; + + if (AI_VALUE(Unit*, "current target") != channeler) + return Attack(channeler); + + return false; +} + +// Teron Gorefiend + +bool TeronGorefiendMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", gorefiend)) + { + return botAI->CastSpell("steady shot", gorefiend); + } + + return false; +} + +bool TeronGorefiendTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + if (MarkTargetWithSkull(bot, gorefiend)) + return true; + + if (AI_VALUE(Unit*, "current target") != gorefiend) + return Attack(gorefiend); + + if (gorefiend->GetVictim() == bot && bot->IsWithinMeleeRange(gorefiend)) + { + const Position& position = GOREFIEND_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Assume positions in arc at the edge of the balcony (farthest from Constructs) +bool TeronGorefiendPositionRangedOnBalconyAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector rangedMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + + if (rangedMembers.empty()) + return false; + + size_t count = rangedMembers.size(); + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = (findIt != rangedMembers.end()) ? + std::distance(rangedMembers.begin(), findIt) : 0; + + constexpr float arcSpan = 2.0f * M_PI / 5.0f; + constexpr float arcCenter = 6.279f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + constexpr float radius = 12.0f; + const float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + const float targetX = GOREFIEND_TANK_POSITION.GetPositionX() + radius * std::cos(angle); + const float targetY = GOREFIEND_TANK_POSITION.GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool TeronGorefiendAvoidShadowOfDeathAction::Execute(Event /*event*/) +{ + switch (bot->getClass()) + { + case CLASS_HUNTER: + return botAI->CanCastSpell("feign death", bot) && + botAI->CastSpell("feign death", bot); + + case CLASS_MAGE: + return botAI->CanCastSpell("ice block", bot) && + botAI->CastSpell("ice block", bot); + + case CLASS_PALADIN: + return botAI->CanCastSpell("divine shield", bot) && + botAI->CastSpell("divine shield", bot); + + case CLASS_ROGUE: + return botAI->CanCastSpell("vanish", bot) && + botAI->CastSpell("vanish", bot); + + default: + return false; + } +} + +bool TeronGorefiendMoveToCornerToDieAction::Execute(Event /*event*/) +{ + const Position& position = GOREFIEND_DIE_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool TeronGorefiendControlAndDestroyShadowyConstructsAction::Execute(Event /*event*/) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + Unit* spirit = bot->GetCharm(); + if (!spirit) + return false; + + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + Unit* priorityTarget = nullptr; + uint32 highestHp = std::numeric_limits::min(); + + float closestToGorefiend = std::numeric_limits::max(); + + for (auto guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || + unit->GetEntry() != static_cast(BlackTempleNpcs::NPC_SHADOWY_CONSTRUCT)) + continue; + + uint32 hp = unit->GetHealth(); + float distToGorefiend = gorefiend->GetExactDist2d(unit); + + if (hp > highestHp) + { + highestHp = hp; + priorityTarget = unit; + closestToGorefiend = distToGorefiend; + } + else if ((hp == highestHp) && (distToGorefiend < closestToGorefiend)) + { + priorityTarget = unit; + closestToGorefiend = distToGorefiend; + } + } + + if (priorityTarget) + { + const float distToTarget = spirit->GetExactDist2d(priorityTarget); + constexpr float desiredDist = 10.0f; + if (distToTarget > desiredDist) + { + const float moveDist = distToTarget - desiredDist + 2.0f; + const float dX = priorityTarget->GetPositionX() - spirit->GetPositionX(); + const float dY = priorityTarget->GetPositionY() - spirit->GetPositionY(); + const float moveX = spirit->GetPositionX() + (dX / distToTarget) * moveDist; + const float moveY = spirit->GetPositionY() + (dY / distToTarget) * moveDist; + + spirit->GetMotionMaster()->MovePoint(0, moveX, moveY, spirit->GetPositionZ()); + return true; + } + + // Adding cooldowns manually is needed due to the charmed creature not observing cooldowns, + // including the GCD. The ordering, including repeating some spells, is the product of testing + // to try to keep the bot from breaking chains with volley, which tends to happen when volley + // is cast before chains (maybe due to projectile travel time?) + if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS)) && + priorityTarget->GetHealthPct() == 100.0f) + { + spirit->CastSpell( + priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), 0, 15000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE))) + { + spirit->CastSpell( + priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), 0, 1000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS))) + { + spirit->CastSpell( + priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS), 0, 15000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE))) + { + spirit->CastSpell + (priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_LANCE), 0, 1000); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_VOLLEY)) && + !priorityTarget->HasAura(static_cast(BlackTempleSpells::SPELL_SPIRIT_CHAINS))) + { + spirit->CastSpell + (priorityTarget, static_cast(BlackTempleSpells::SPELL_SPIRIT_VOLLEY), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_VOLLEY), 0, 15000); + return true; + } + } + else + { + const float distToGorefiend = spirit->GetExactDist2d(gorefiend); + constexpr float targetDist = 5.0f; + if (distToGorefiend > targetDist) + { + const float moveDist = distToGorefiend - targetDist; + const float dX = gorefiend->GetPositionX() - spirit->GetPositionX(); + const float dY = gorefiend->GetPositionY() - spirit->GetPositionY(); + const float moveX = spirit->GetPositionX() + (dX / distToGorefiend) * moveDist; + const float moveY = spirit->GetPositionY() + (dY / distToGorefiend) * moveDist; + + spirit->GetMotionMaster()->MovePoint(0, moveX, moveY, spirit->GetPositionZ()); + return true; + } + else if (!spirit->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_SPIRIT_STRIKE))) + { + spirit->CastSpell( + gorefiend, static_cast(BlackTempleSpells::SPELL_SPIRIT_STRIKE), true); + spirit->AddSpellCooldown( + static_cast(BlackTempleSpells::SPELL_SPIRIT_STRIKE), 0, 1000); + return true; + } + } + + return false; +} + +// Gurtogg Bloodboil + +bool GurtoggBloodboilMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", gurtogg)) + { + return botAI->CastSpell("steady shot", gurtogg); + } + + return false; +} + +bool GurtoggBloodboilTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + if (AI_VALUE(Unit*, "current target") != gurtogg) + return Attack(gurtogg); + + Unit* victim = gurtogg->GetVictim(); + Player* playerVictim = victim ? victim->ToPlayer() : nullptr; + if (playerVictim && botAI->IsTank(playerVictim) && bot->IsWithinMeleeRange(gurtogg)) + { + const Position& position = GURTOGG_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 2.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool GurtoggBloodboilRotateRangedGroupsAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + auto groups = GetGurtoggRangedRotationGroups(bot); + int activeGroup = GetGurtoggActiveRotationGroup(gurtogg); + + bool inActiveGroup = false; + if (activeGroup >= 0 && (size_t)activeGroup < groups.size()) + { + auto const& group = groups[activeGroup]; + inActiveGroup = std::find(group.begin(), group.end(), bot) != group.end(); + } + + const Position& nearPosition = GURTOGG_RANGED_POSITION; + const Position& farPosition = GURTOGG_SOAKER_POSITION; + constexpr float distFromPos = 2.0f; + + if (inActiveGroup && bot->GetExactDist2d(farPosition) > distFromPos) + { + return MoveInside(BLACK_TEMPLE_MAP_ID, farPosition.GetPositionX(), + farPosition.GetPositionY(), bot->GetPositionZ(), + distFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (!inActiveGroup && bot->GetExactDist2d(nearPosition) > distFromPos) + { + return MoveInside(BLACK_TEMPLE_MAP_ID, nearPosition.GetPositionX(), + nearPosition.GetPositionY(), bot->GetPositionZ(), + distFromPos, MovementPriority::MOVEMENT_FORCED); + } + + return false; +} + +bool GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* enragedPlayer = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura( + static_cast(BlackTempleSpells::SPELL_PLAYER_FEL_RAGE))) + { + enragedPlayer = member; + break; + } + } + + constexpr float safeDistance = 20.0f; + constexpr uint32 minInterval = 0; + if (enragedPlayer && bot->GetExactDist2d(enragedPlayer) < safeDistance) + return FleePosition(enragedPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool GurtoggBloodboilManagePhaseTimerAction::Execute(Event /*event*/) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + const time_t now = std::time(nullptr); + const uint32 instanceId = gurtogg->GetMap()->GetInstanceId(); + + if (gurtogg->HasAura(static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE))) + { + return gurtoggPhaseTimer.erase(instanceId) > 0; + } + else + { + auto [it, inserted] = gurtoggPhaseTimer.try_emplace(instanceId, now); + return inserted; + } +} + +// Reliquary of Souls + +bool ReliquaryOfSoulsMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + Unit* anger = AI_VALUE2(Unit*, "find target", "essence of anger"); + + if (!desire && !anger) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + Unit* target = desire ? desire : anger; + + if (target->GetHealthPct() > 95.0f) + { + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", target)) + { + return botAI->CastSpell("steady shot", target); + } + } + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::Execute(Event /*event*/) +{ + Unit* suffering = AI_VALUE2(Unit*, "find target", "essence of suffering"); + if (!suffering) + return false; + + if (botAI->IsTank(bot) && bot->GetHealthPct() > 25.0f) + return TanksMoveToMinimumRange(suffering); + else if (botAI->IsMelee(bot) && bot->GetVictim() != suffering) + return MeleeDpsStayAtMaximumRange(suffering); + else if (botAI->IsRanged(bot)) + return RangedMoveAwayFromBoss(suffering); + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::TanksMoveToMinimumRange(Unit* suffering) +{ + const float distanceToBoss = bot->GetExactDist2d(suffering); + if (distanceToBoss > 2.0f) + { + const float dX = suffering->GetPositionX() - bot->GetPositionX(); + const float dY = suffering->GetPositionY() - bot->GetPositionY(); + const float targetX = bot->GetPositionX() + (dX / distanceToBoss); + const float targetY = bot->GetPositionY() + (dY / distanceToBoss); + + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::MeleeDpsStayAtMaximumRange(Unit* suffering) +{ + const float desiredDist = bot->GetMeleeRange(suffering); + const float behindAngle = Position::NormalizeOrientation(suffering->GetOrientation() + M_PI); + const float targetX = suffering->GetPositionX() + desiredDist * std::cos(behindAngle); + const float targetY = suffering->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > 0.25f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool ReliquaryOfSoulsAdjustDistanceFromSufferingAction::RangedMoveAwayFromBoss(Unit* suffering) +{ + constexpr float safeDistance = 15.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetExactDist2d(suffering) < safeDistance) + return FleePosition(suffering->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool ReliquaryOfSoulsHealersDpsSufferingAction::Execute(Event /*event*/) +{ + Unit* suffering = AI_VALUE2(Unit*, "find target", "essence of suffering"); + if (!suffering) + return false; + + if (bot->getClass() == CLASS_DRUID) + { + if (botAI->HasAura("tree of life", bot)) + botAI->RemoveAura("tree of life"); + + bool casted = false; + + if (botAI->CanCastSpell("barkskin", bot) && + botAI->CastSpell("barkskin", bot)) + casted = true; + + if (botAI->CanCastSpell("wrath", suffering) && + botAI->CastSpell("wrath", suffering)) + casted = true; + + return casted; + } + else if (bot->getClass() == CLASS_PALADIN) + { + bool casted = false; + + if (botAI->CanCastSpell("avenging wrath", bot) && + botAI->CastSpell("avenging wrath", bot)) + casted = true; + + if (botAI->CanCastSpell("consecration", bot) && + botAI->CastSpell("consecration", bot)) + casted = true; + + if (botAI->CanCastSpell("exorcism", suffering) && + botAI->CastSpell("exorcism", suffering)) + casted = true; + + if (botAI->CanCastSpell("hammer of wrath", suffering) && + botAI->CastSpell("hammer of wrath", suffering)) + casted = true; + + if (botAI->CanCastSpell("holy shock", suffering) && + botAI->CastSpell("holy shock", suffering)) + casted = true; + + if (botAI->CanCastSpell("judgement of light", suffering) && + botAI->CastSpell("judgement of light", suffering)) + casted = true; + + return casted; + } + else if (bot->getClass() == CLASS_PRIEST) + { + if (botAI->CanCastSpell("smite", suffering)) + return botAI->CastSpell("smite", suffering); + } + else if (bot->getClass() == CLASS_SHAMAN) + { + bool casted = false; + + if (botAI->CanCastSpell("earth shock", suffering) && + botAI->CastSpell("earth shock", suffering)) + casted = true; + + if (botAI->CanCastSpell("chain lightning", suffering) && + botAI->CastSpell("chain lightning", suffering)) + casted = true; + + if (botAI->CanCastSpell("lightning bolt", suffering) && + botAI->CastSpell("lightning bolt", suffering)) + casted = true; + + return casted; + } + + return false; +} + +bool ReliquaryOfSoulsSpellstealRuneShieldAction::Execute(Event /*event*/) +{ + if (Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + desire && botAI->CanCastSpell("spellsteal", desire)) + { + return botAI->CastSpell("spellsteal", desire); + } + + return false; +} + +bool ReliquaryOfSoulsSpellReflectDeadenAction::Execute(Event /*event*/) +{ + if (botAI->CanCastSpell("spell reflection", bot)) + return botAI->CastSpell("spell reflection", bot); + + return false; +} + +// Mother Shahraz + +bool MotherShahrazMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", shahraz)) + { + return botAI->CastSpell("steady shot", shahraz); + } + + return false; +} + +bool MotherShahrazTanksPositionBossUnderPillarAction::Execute(Event /*event*/) +{ + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz) + return false; + + if (AI_VALUE(Unit*, "current target") != shahraz) + return Attack(shahraz); + + Unit* victim = shahraz->GetVictim(); + Player* playerVictim = victim ? victim->ToPlayer() : nullptr; + if (playerVictim && botAI->IsTank(playerVictim)) + { + const ObjectGuid guid = bot->GetGUID(); + auto it = shahrazTankStep.try_emplace( + guid, TankPositionState::MovingToTransition).first; + TankPositionState state = it->second; + + constexpr float maxDistance = 0.5f; + const Position& position = state == TankPositionState::MovingToTransition ? + SHAHRAZ_TRANSITION_POSITION : SHAHRAZ_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position); + + if (distToPosition > maxDistance && bot->IsWithinMeleeRange(shahraz)) + { + const bool backwards = (shahraz->GetVictim() == bot); + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, backwards); + } + + if (state == TankPositionState::MovingToTransition && distToPosition <= maxDistance) + shahrazTankStep[guid] = TankPositionState::MovingToFinal; + + if (state != TankPositionState::MovingToTransition && distToPosition <= maxDistance) + { + const float orientation = atan2(shahraz->GetPositionY() - bot->GetPositionY(), + shahraz->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); + shahrazTankStep[guid] = TankPositionState::Positioned; + } + } + + return false; +} + +bool MotherShahrazMeleeDpsWaitAtSafePositionAction::Execute(Event /*event*/) +{ + return MoveTo(BLACK_TEMPLE_MAP_ID, SHAHRAZ_RANGED_POSITION.GetPositionX(), + SHAHRAZ_RANGED_POSITION.GetPositionY(), bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); +} + +// This doesn't matter for bots since they don't take fall damage, and it's actually easier +// to tank her closer to her starting position, but I want to simulate a player strategy +bool MotherShahrazPositionRangedUnderPillarAction::Execute(Event /*event*/) +{ + const Position& position = SHAHRAZ_RANGED_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 1.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool MotherShahrazRunAwayToBreakFatalAttractionAction::Execute(Event /*event*/) +{ + std::vector attractedPlayers = GetAttractedPlayers(); + if (attractedPlayers.size() < 2) + return false; + + float centerX = 0.0f, centerY = 0.0f; + for (Player* member : attractedPlayers) + { + centerX += member->GetPositionX(); + centerY += member->GetPositionY(); + } + centerX /= attractedPlayers.size(); + centerY /= attractedPlayers.size(); + + auto botIt = std::find(attractedPlayers.begin(), attractedPlayers.end(), bot); + if (botIt == attractedPlayers.end()) + return false; + + const float spreadAngle = + 2.0f * M_PI * std::distance(attractedPlayers.begin(), botIt) / attractedPlayers.size(); + + constexpr float maxSpreadDistance = 35.0f; + constexpr float distanceStep = 1.0f; + float lastValidX = bot->GetPositionX(); + float lastValidY = bot->GetPositionY(); + float lastValidZ = bot->GetPositionZ(); + + for (float currentDistance = distanceStep; + currentDistance <= maxSpreadDistance; + currentDistance += distanceStep) + { + float testX = centerX + std::cos(spreadAngle) * currentDistance; + float testY = centerY + std::sin(spreadAngle) * currentDistance; + float testZ = lastValidZ; + + if (!bot->GetMap()->CheckCollisionAndGetValidCoords( + bot, bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), testX, testY, testZ)) + { + break; + } + + lastValidX = testX; + lastValidY = testY; + lastValidZ = testZ; + } + + if (MoveTo(BLACK_TEMPLE_MAP_ID, lastValidX, lastValidY, lastValidZ, false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false)) + { + return true; + } + else + { + // In case bots get stuck, try a 5-yard random move + const float angle = frand(0.0f, 2.0f * M_PI); + constexpr float dist = 5.0f; + float randX = bot->GetPositionX() + std::cos(angle) * dist; + float randY = bot->GetPositionY() + std::sin(angle) * dist; + float randZ = lastValidZ; + bot->GetMap()->CheckCollisionAndGetValidCoords( + bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + randX, randY, randZ); + + return MoveTo(BLACK_TEMPLE_MAP_ID, randX, randY, randZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); + } +} + +std::vector MotherShahrazRunAwayToBreakFatalAttractionAction::GetAttractedPlayers() +{ + std::vector attractedPlayers; + Group* group = bot->GetGroup(); + if (!group) + return attractedPlayers; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura(static_cast( + BlackTempleSpells::SPELL_FATAL_ATTRACTION))) + { + attractedPlayers.push_back(member); + } + } + + std::sort(attractedPlayers.begin(), attractedPlayers.end(), + [](Player* a, Player* b) { + return a->GetGUID().GetCounter() < b->GetGUID().GetCounter(); + }); + + return attractedPlayers; +} + +// Illidari Council + +bool IllidariCouncilMisdirectBossesToTanksAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && + GET_PLAYERBOT_AI(member)) + { + hunters.push_back(member); + } + + if (hunters.size() >= 4) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* councilTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + councilTarget = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + tankTarget = GetZerevorMageTank(bot); + } + else if (hunterIndex == 1) + { + councilTarget = AI_VALUE2(Unit*, "find target", "lady malande"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupAssistTank(botAI, bot, 0)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + councilTarget = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupMainTank(botAI, bot)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 3) + { + councilTarget = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupAssistTank(botAI, bot, 1)) + { + tankTarget = member; + break; + } + } + } + + if (!councilTarget || !tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", councilTarget)) + { + return botAI->CastSpell("steady shot", councilTarget); + } + + return false; +} + +bool IllidariCouncilMainTankPositionGathiosAction::Execute(Event /*event*/) +{ + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios) + return false; + + // Failsafe for if bot falls through the floor, which tends to happen upon the pull + if (bot->GetPositionZ() < COUNCIL_FLOOR_Z_THRESHOLD) + { + bot->TeleportTo(BLACK_TEMPLE_MAP_ID, gathios->GetPositionX(), gathios->GetPositionY(), + gathios->GetPositionZ(), bot->GetOrientation()); + } + + if (MarkTargetWithSquare(bot, gathios)) + return true; + + SetRtiTarget(botAI, "square", gathios); + + if (AI_VALUE(Unit*, "current target") != gathios) + return Attack(gathios); + + const ObjectGuid guid = bot->GetGUID(); + uint8 index = gathiosTankStep.count(guid) ? gathiosTankStep[guid] : 0; + + const Position& position = GATHIOS_TANK_POSITIONS[index]; + + constexpr float maxDistance = 2.0f; + float distToPosition = bot->GetExactDist2d(position); + + if (gathios->GetVictim() == bot && bot->IsWithinMeleeRange(gathios)) + { + if (distToPosition <= maxDistance && HasDangerousCouncilAura(bot)) + { + index = (index + 1) % 4; + gathiosTankStep[guid] = index; + const Position& newPosition = GATHIOS_TANK_POSITIONS[index]; + const float newDistToPosition = bot->GetExactDist2d(newPosition); + if (newDistToPosition > maxDistance) + { + const float dX = newPosition.GetPositionX() - bot->GetPositionX(); + const float dY = newPosition.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, newDistToPosition); + const float moveX = bot->GetPositionX() + (dX / newDistToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / newDistToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + else if (distToPosition > maxDistance) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool IllidariCouncilMainTankReflectJudgementOfCommandAction::Execute(Event /*event*/) +{ + if (botAI->CanCastSpell("spell reflection", bot)) + return botAI->CastSpell("spell reflection", bot); + + return false; +} + +bool IllidariCouncilFirstAssistTankFocusMalandeAction::Execute(Event /*event*/) +{ + Unit* malande = AI_VALUE2(Unit*, "find target", "lady malande"); + if (!malande) + return false; + + // Failsafe for if bot falls through the floor, which tends to happen upon the pull + if (bot->GetPositionZ() < COUNCIL_FLOOR_Z_THRESHOLD) + { + bot->TeleportTo(BLACK_TEMPLE_MAP_ID, malande->GetPositionX(), malande->GetPositionY(), + malande->GetPositionZ(), bot->GetOrientation()); + } + + if (MarkTargetWithStar(bot, malande)) + return true; + + SetRtiTarget(botAI, "star", malande); + + if (AI_VALUE(Unit*, "current target") != malande) + return Attack(malande); + + return false; +} + +bool IllidariCouncilSecondAssistTankPositionDarkshadowAction::Execute(Event /*event*/) +{ + Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + if (!darkshadow) + return false; + + // Failsafe for if bot falls through the floor, which tends to happen upon the pull + if (bot->GetPositionZ() < COUNCIL_FLOOR_Z_THRESHOLD) + { + bot->TeleportTo(BLACK_TEMPLE_MAP_ID, darkshadow->GetPositionX(), darkshadow->GetPositionY(), + darkshadow->GetPositionZ(), bot->GetOrientation()); + } + + if (MarkTargetWithCircle(bot, darkshadow)) + return true; + + SetRtiTarget(botAI, "circle", darkshadow); + + if (AI_VALUE(Unit*, "current target") != darkshadow) + return Attack(darkshadow); + + if (darkshadow->GetVictim() == bot) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + const float distToPosition = bot->GetExactDist2d(mainTank->GetPositionX(), + mainTank->GetPositionY()); + if (distToPosition > 2.0f) + { + const float dX = mainTank->GetPositionX() - bot->GetPositionX(); + const float dY = mainTank->GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + const bool backwards = bot->GetExactDist2d(mainTank) < 10.0f; + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, backwards); + } + } + + return false; +} + +bool IllidariCouncilMageTankPositionZerevorAction::Execute(Event /*event*/) +{ + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (!zerevor) + return false; + + if (zerevor->HasAura(static_cast(BlackTempleSpells::SPELL_DAMPEN_MAGIC)) && + botAI->CanCastSpell("spellsteal", zerevor)) + { + return botAI->CastSpell("spellsteal", zerevor); + } + + if (MarkTargetWithTriangle(bot, zerevor)) + return true; + + SetRtiTarget(botAI, "triangle", zerevor); + + if (AI_VALUE(Unit*, "current target") != zerevor) + return Attack(zerevor); + + if (zerevor->GetVictim() == bot) + { + const Position& position = ZEREVOR_TANK_POSITION; + const float distToPosition = bot->GetExactDist2d(position.GetPositionX(), + position.GetPositionY()); + if (distToPosition > 2.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(10.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool IllidariCouncilPositionMageTankHealerAction::Execute(Event /*event*/) +{ + Player* mageTank = GetZerevorMageTank(bot); + if (!mageTank) + return false; + + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (!zerevor || zerevor->GetVictim() != mageTank) + return false; + + const ObjectGuid guid = bot->GetGUID(); + uint8 index = zerevorHealStep.count(guid) ? zerevorHealStep[guid] : 0; + + const Position& position = ZEREVOR_HEALER_POSITIONS[index]; + + constexpr float maxDistance = 1.0f; + const float distToPosition = bot->GetExactDist2d(position); + + if (distToPosition <= maxDistance && HasDangerousCouncilAura(bot)) + { + index = (index + 1) % 2; + zerevorHealStep[guid] = index; + const Position& newPosition = ZEREVOR_HEALER_POSITIONS[index]; + const float newDistToPosition = bot->GetExactDist2d(newPosition); + if (newDistToPosition > maxDistance) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, newPosition.GetPositionX(), + newPosition.GetPositionY(), bot->GetPositionZ(), + false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else if (distToPosition > maxDistance) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(10.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool IllidariCouncilDisperseRangedAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 4.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} + +bool IllidariCouncilCommandPetsToAttackGathiosAction::Execute(Event /*event*/) +{ + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios) + return false; + + Pet* pet = bot->GetPet(); + if (pet && pet->IsAlive() && pet->GetVictim() != gathios) + { + pet->ClearUnitState(UNIT_STATE_FOLLOW); + pet->AttackStop(); + pet->SetTarget(gathios->GetGUID()); + + if (pet->GetCharmInfo()) + { + pet->GetCharmInfo()->SetIsCommandAttack(true); + pet->GetCharmInfo()->SetIsAtStay(false); + pet->GetCharmInfo()->SetIsFollowing(false); + pet->GetCharmInfo()->SetIsCommandFollow(false); + pet->GetCharmInfo()->SetIsReturning(false); + + pet->AI()->AttackStart(gathios); + return true; + } + } + + return false; +} + +bool IllidariCouncilAssignDpsTargetsAction::Execute(Event /*event*/) +{ + Unit* malande = AI_VALUE2(Unit*, "find target", "lady malande"); + if (!malande) + return false; + + bool shouldAttackMalande = false; + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (zerevor && zerevor->GetExactDist2d(malande) < 15.0f) + { + shouldAttackMalande = false; + } + else if (bot->getClass() == CLASS_ROGUE || + (bot->getClass() == CLASS_WARRIOR && botAI->IsDps(bot))) + { + shouldAttackMalande = !malande->HasAura( + static_cast(BlackTempleSpells::SPELL_BLESSING_OF_PROTECTION)); + } + else if (bot->getClass() == CLASS_SHAMAN && botAI->IsDps(bot)) + { + shouldAttackMalande = !malande->HasAura( + static_cast(BlackTempleSpells::SPELL_BLESSING_OF_SPELL_WARDING)); + } + + if (shouldAttackMalande) + { + SetRtiTarget(botAI, "star", malande); + + if (AI_VALUE(Unit*, "current target") != malande) + return Attack(malande); + } + else if (Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + darkshadow && !darkshadow->HasAura( + static_cast(BlackTempleSpells::SPELL_VANISH))) + { + SetRtiTarget(botAI, "circle", darkshadow); + + if (AI_VALUE(Unit*, "current target") != darkshadow) + return Attack(darkshadow); + } + else if (Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + SetRtiTarget(botAI, "square", gathios); + + if (AI_VALUE(Unit*, "current target") != gathios) + return Attack(gathios); + } + + return false; +} + +bool IllidariCouncilManageDpsTimerAction::Execute(Event /*event*/) +{ + if (Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + return councilDpsWaitTimer.try_emplace( + gathios->GetMap()->GetInstanceId(), std::time(nullptr)).second; + } + + return false; +} + +// Illidan Stormrage + +bool IllidanStormrageMisdirectToTankAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + int phase = GetIllidanPhase(illidan); + + if (phase == 2 && TryMisdirectToFlameTanks(group)) + return true; + + return phase == 4 && TryMisdirectToWarlockTank(illidan); +} + +bool IllidanStormrageMisdirectToTankAction::TryMisdirectToFlameTanks(Group* group) +{ + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && + GET_PLAYERBOT_AI(member)) + { + hunters.push_back(member); + } + + if (hunters.size() >= 2) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + auto [eastFlame, westFlame] = GetFlamesOfAzzinoth(bot); + if (!eastFlame || !westFlame || eastFlame == westFlame) + return false; + + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); + if (!firstAssistTank || !secondAssistTank) + return false; + + if (hunters.size() == 1) + { + if (eastFlame->GetHealthPct() < 99.0f) + return false; + + if (botAI->CanCastSpell("misdirection", secondAssistTank)) + return botAI->CastSpell("misdirection", secondAssistTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", eastFlame)) + { + return botAI->CastSpell("steady shot", eastFlame); + } + + return false; + } + + Player* tankTarget = nullptr; + Unit* flame = nullptr; + + if (hunterIndex == 0) + { + tankTarget = secondAssistTank; + flame = eastFlame; + } + else if (hunterIndex == 1) + { + tankTarget = firstAssistTank; + flame = westFlame; + } + else + return false; + + if (!tankTarget || !tankTarget->IsAlive() || flame->GetHealthPct() < 90.0f) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", flame)) + { + return botAI->CastSpell("steady shot", flame); + } + + return false; +} + +bool IllidanStormrageMisdirectToTankAction::TryMisdirectToWarlockTank(Unit* illidan) +{ + Player* warlockTank = GetIllidanWarlockTank(bot); + if (!warlockTank) + return false; + + if (botAI->CanCastSpell("misdirection", warlockTank)) + return botAI->CastSpell("misdirection", warlockTank); + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", illidan)) + { + return botAI->CastSpell("steady shot", illidan); + } + + return false; +} + +bool IllidanStormrageMainTankRepositionBossAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + if (AI_VALUE(Unit*, "current target") != illidan) + return Attack(illidan); + + if (GetIllidanPhase(illidan) == 5) + { + GameObject* trap = FindNearestTrap(botAI, bot); + if (trap && bot->GetExactDist2d(trap) < 40.0f && illidan->GetVictim() == bot) + return MoveToShadowTrap(trap); + } + else + { + illidanShadowTrapGuid.erase(bot->GetGUID()); + illidanShadowTrapDestination.erase(bot->GetGUID()); + } + + if (illidan->GetVictim() != bot) + { + illidanShadowTrapGuid.erase(bot->GetGUID()); + illidanShadowTrapDestination.erase(bot->GetGUID()); + return false; + } + + auto const& flameCrashes = GetAllFlameCrashes(bot); + if (flameCrashes.empty()) + return false; + + constexpr float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameCrash : flameCrashes) + { + if (bot->GetDistance2d(flameCrash) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + constexpr float maxRadius = 30.0f; + Position safestPos = FindSafestNearbyPosition(flameCrashes, maxRadius, hazardRadius); + + return MoveTo(BLACK_TEMPLE_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, true); +} + +bool IllidanStormrageMainTankRepositionBossAction::MoveToShadowTrap(GameObject* trap) +{ + if (!trap) + return false; + + ObjectGuid const botGuid = bot->GetGUID(); + ObjectGuid const trapGuid = trap->GetGUID(); + Position target; + + auto const cachedTrapIt = illidanShadowTrapGuid.find(botGuid); + auto const cachedDestinationIt = illidanShadowTrapDestination.find(botGuid); + if (cachedTrapIt != illidanShadowTrapGuid.end() && + cachedDestinationIt != illidanShadowTrapDestination.end() && + cachedTrapIt->second == trapGuid) + { + target = cachedDestinationIt->second; + } + else + { + const float trapX = trap->GetPositionX(); + const float trapY = trap->GetPositionY(); + + const float distToTrap = trap->GetExactDist2d(bot); + if (distToTrap <= 0.0f) + return false; + + constexpr float distBeyondTrap = 6.0f; + + const float dx = trapX - bot->GetPositionX(); + const float dy = trapY - bot->GetPositionY(); + const float targetX = trapX + (dx / distToTrap) * distBeyondTrap; + const float targetY = trapY + (dy / distToTrap) * distBeyondTrap; + + target = Position(targetX, targetY, trap->GetPositionZ()); + illidanShadowTrapGuid[botGuid] = trapGuid; + illidanShadowTrapDestination[botGuid] = target; + } + + const float targetDist = bot->GetExactDist2d(target); + + if (targetDist > 2.0f && bot->GetHealthPct() > 50.0f) + { + const float dX = target.GetPositionX() - bot->GetPositionX(); + const float dY = target.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, targetDist); + const float moveX = bot->GetPositionX() + (dX / targetDist) * moveDist; + const float moveY = bot->GetPositionY() + (dY / targetDist) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, trap->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +Position IllidanStormrageMainTankRepositionBossAction::FindSafestNearbyPosition( + const std::vector& flameCrashes, float maxRadius, float hazardRadius) +{ + constexpr float searchStep = M_PI / 16.0f; + constexpr float minDistance = 2.0f; + constexpr float distanceStep = 1.0f; + + float backwardsAngle = Position::NormalizeOrientation(bot->GetOrientation() + M_PI); + + Position bestPos; + float bestAngleDiff = M_PI * 2.0f; + float bestDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; distance <= maxRadius; distance += distanceStep) + { + for (float angleOffset = 0.0f; angleOffset < 2 * M_PI; angleOffset += searchStep) + { + for (int sign = -1; sign <= 1; sign += 2) + { + const float testAngle = + Position::NormalizeOrientation(backwardsAngle + sign * angleOffset); + const float x = bot->GetPositionX() + distance * std::cos(testAngle); + const float y = bot->GetPositionY() + distance * std::sin(testAngle); + + Position testPos(x, y, bot->GetPositionZ()); + + bool isSafe = true; + for (Unit* flameCrash : flameCrashes) + { + if (flameCrash->GetDistance2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + if (!isSafe) + continue; + + bool pathSafe = IsPathSafeFromFlameCrashes(bot->GetPosition(), testPos, + flameCrashes, hazardRadius); + + float angleDiff = std::abs(Position::NormalizeOrientation( + testAngle - backwardsAngle)); + if (angleDiff > M_PI) + angleDiff = 2 * M_PI - angleDiff; + + if (pathSafe && (!foundSafe || angleDiff < bestAngleDiff || + (angleDiff == bestAngleDiff && distance < bestDistance))) + { + bestPos = testPos; + bestAngleDiff = angleDiff; + bestDistance = distance; + foundSafe = true; + } + else if (!foundSafe && angleDiff < bestAngleDiff) + { + bestPos = testPos; + bestAngleDiff = angleDiff; + bestDistance = distance; + } + } + if (foundSafe) + break; + } + if (foundSafe) + break; + } + + return bestPos; +} + +bool IllidanStormrageMainTankRepositionBossAction::IsPathSafeFromFlameCrashes( + const Position& start, const Position& end, const std::vector& flameCrashes, + float hazardRadius) +{ + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* flameCrash : flameCrashes) + { + float distToFlameCrash = flameCrash->GetDistance2d(checkX, checkY); + if (distToFlameCrash < hazardRadius) + return false; + } + } + + return true; +} + +bool IllidanStormrageIsolateBotWithParasiteAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + + if (phase == 1) + { + constexpr float safeDistance = 15.0f; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + { + const float currentDistance = bot->GetExactDist2d(nearestPlayer); + if (currentDistance < safeDistance) + return MoveAway(nearestPlayer, safeDistance - currentDistance); + } + } + else + { + const float angle = illidan->GetOrientation() + M_PI; + constexpr float distBehindIllidan = 35.0f; + + const float targetX = illidan->GetPositionX() + std::cos(angle) * distBehindIllidan; + const float targetY = illidan->GetPositionY() + std::sin(angle) * distBehindIllidan; + const Position target(targetX, targetY, bot->GetPositionZ()); + + if (HasParasiticShadowfiend(bot)) + return InfectedBotMoveFromGroup(illidan, target); + + if (GetIllidanTrapperHunter(bot) == bot) + return FreezeTrapShadowfiend(bot, illidan, target); + } + + return false; +} + +bool IllidanStormrageIsolateBotWithParasiteAction::InfectedBotMoveFromGroup( + Unit*, const Position& target) +{ + if (bot->GetExactDist2d(target) < 1.0f) + return false; + + return MoveTo(BLACK_TEMPLE_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool IllidanStormrageIsolateBotWithParasiteAction::FreezeTrapShadowfiend( + Player* bot, Unit*, const Position& target) +{ + if (bot->HasSpellCooldown(static_cast(BlackTempleSpells::SPELL_FROST_TRAP))) + return false; + + Player* infected = GetBotWithParasiticShadowfiend(bot); + if (!infected) + return false; + + if (bot->GetExactDist2d(target) > 2.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (bot->GetExactDist2d(infected) < 2.0f && + botAI->CanCastSpell(static_cast(BlackTempleSpells::SPELL_FROST_TRAP), bot)) + { + return botAI->CastSpell(static_cast(BlackTempleSpells::SPELL_FROST_TRAP), bot); + } + + return false; +} + +bool IllidanStormrageSetEarthbindTotemAction::Execute(Event /*event*/) +{ + return botAI->CanCastSpell("earthbind totem", bot) && + botAI->CastSpell("earthbind totem", bot); +} + +bool IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction::Execute(Event /*event*/) +{ + auto [eastFlame, westFlame] = GetFlamesOfAzzinoth(bot); + // The second assist tank's flame is killed first; this is so that if the tank + // for the second flame dies after the first flame is down, the dead flame's + // tank will become the first assist tank and take over the remaining flame + if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + if (eastFlame && westFlame) + { + if (AI_VALUE(Unit*, "current target") != eastFlame) + return Attack(eastFlame); + + if (eastFlame->GetVictim() != bot) + { + if (!bot->IsWithinMeleeRange(eastFlame)) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, eastFlame->GetPositionX(), + eastFlame->GetPositionY(), eastFlame->GetPositionZ(), + false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; + } + } + else if (!eastFlame && !westFlame) + { + // (1) Before flames spawn, go to the waiting position + // (2) If both flames are dead and the waiting position is too close to hazards, + // move to a grate position + std::list demonFires; + constexpr float searchRadius = 40.0f; + bot->GetCreatureListWithEntryInGrid( + demonFires, static_cast(BlackTempleNpcs::NPC_DEMON_FIRE), searchRadius); + + const Position& pos = demonFires.empty() ? + ILLIDAN_E_GLAIVE_WAITING_POSITION : ILLIDAN_E_GRATE_POSITION; + + if (bot->GetExactDist2d(pos.GetPositionX(), pos.GetPositionY()) > 0.5f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, pos.GetPositionX(), pos.GetPositionY(), + pos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + // After the first flame dies, its tank waits with other bots + else if (!eastFlame && westFlame) + { + const Position& pos = ILLIDAN_E_GRATE_POSITION; + if (bot->GetExactDist2d(pos.GetPositionX(), pos.GetPositionY()) > 0.5f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, pos.GetPositionX(), pos.GetPositionY(), + pos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (westFlame) + { + if (AI_VALUE(Unit*, "current target") != westFlame) + return Attack(westFlame); + + if (westFlame->GetVictim() != bot) + { + if (!bot->IsWithinMeleeRange(westFlame)) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, westFlame->GetPositionX(), + westFlame->GetPositionY(), westFlame->GetPositionZ(), + false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; + } + } + else + { + // (1) Before flames spawn, go to the waiting position + // (2) If both flames are dead and the waiting position is too close to hazards, + // move to a grate position + std::list demonFires; + constexpr float searchRadius = 40.0f; + bot->GetCreatureListWithEntryInGrid( + demonFires, static_cast(BlackTempleNpcs::NPC_DEMON_FIRE), searchRadius); + + const Position& pos = demonFires.empty() ? + ILLIDAN_W_GLAIVE_WAITING_POSITION : ILLIDAN_W_GRATE_POSITION; + + if (bot->GetExactDist2d(pos.GetPositionX(), pos.GetPositionY()) > 0.5f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, pos.GetPositionX(), pos.GetPositionY(), + pos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + } + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + EyeBlastDangerArea dangerArea = GetEyeBlastDangerArea(bot); + + // Only consider the eye blast if its trigger NPC is within 30 yards of the tank + constexpr float eyeBlastTriggerRadius = 30.0f; + if (dangerArea.width > 0.0f && + bot->GetExactDist2d(dangerArea.start) <= eyeBlastTriggerRadius) + { + return RepositionToAvoidEyeBlast(illidan, dangerArea); + } + else + { + return RepositionToAvoidBlaze(eastFlame, westFlame); + } + + return false; +} + +bool IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction::RepositionToAvoidEyeBlast( + Unit*, const EyeBlastDangerArea& dangerArea) +{ + if (!IsPositionInEyeBlastDangerArea(bot->GetPosition(), dangerArea)) + return false; + + const float dx = dangerArea.end.GetPositionX() - dangerArea.start.GetPositionX(); + const float dy = dangerArea.end.GetPositionY() - dangerArea.start.GetPositionY(); + const float length = dangerArea.start.GetExactDist2d(dangerArea.end); + + const float px = bot->GetPositionX(); + const float py = bot->GetPositionY(); + const float sx = dangerArea.start.GetPositionX(); + const float sy = dangerArea.start.GetPositionY(); + + const float projection = std::clamp( + ((px - sx) * dx + (py - sy) * dy) / (length * length), 0.0f, 1.0f); + const float closestX = sx + projection * dx; + const float closestY = sy + projection * dy; + + const float distToLine = bot->GetExactDist2d(closestX, closestY); + const float moveDist = (dangerArea.width - distToLine) + 0.5f; + if (moveDist <= 0.0f) + return false; + + const float rawDirX = px - closestX; + const float rawDirY = py - closestY; + const float rawDirLength = std::sqrt(rawDirX * rawDirX + rawDirY * rawDirY); + const float dirX = rawDirLength == 0.0f ? -(dy / length) : rawDirX / rawDirLength; + const float dirY = rawDirLength == 0.0f ? dx / length : rawDirY / rawDirLength; + + const float safeX = px + dirX * moveDist; + const float safeY = py + dirY * moveDist; + const float safeZ = bot->GetPositionZ(); + const Position safePosition(safeX, safeY, safeZ); + + constexpr float minGrateDistance = 10.0f; + const bool tooCloseToNorthGrate = + safePosition.GetExactDist2d(ILLIDAN_N_GRATE_POSITION) < minGrateDistance; + const bool tooCloseToEastGrate = + safePosition.GetExactDist2d(ILLIDAN_E_GRATE_POSITION) < minGrateDistance; + const bool tooCloseToWestGrate = + safePosition.GetExactDist2d(ILLIDAN_W_GRATE_POSITION) < minGrateDistance; + + if (tooCloseToNorthGrate || tooCloseToEastGrate || tooCloseToWestGrate) + return false; + + return MoveTo(BLACK_TEMPLE_MAP_ID, safeX, safeY, safeZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction::RepositionToAvoidBlaze( + Unit* eastFlame, Unit* westFlame) +{ + const std::array* waypoints = nullptr; + constexpr size_t numWaypoints = 7; + + if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + if (!eastFlame || eastFlame->GetVictim() != bot || + !bot->IsWithinMeleeRange(eastFlame)) + { + return false; + } + waypoints = &E_GLAIVE_TANK_POSITIONS; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + if (!westFlame || westFlame->GetVictim() != bot || + !bot->IsWithinMeleeRange(westFlame)) + { + return false; + } + waypoints = &W_GLAIVE_TANK_POSITIONS; + } + + if (!waypoints) + return false; + + size_t& waypointIndex = flameTankWaypointIndex[bot->GetGUID()]; + const Position& target = (*waypoints)[waypointIndex]; + + auto const& npcs = + botAI->GetAiObjectContext()->GetValue("possible triggers")->Get(); + + bool blazeNearby = false; + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == static_cast(BlackTempleNpcs::NPC_BLAZE) && + bot->GetDistance2d(unit) <= 8.0f) + { + blazeNearby = true; + break; + } + } + + float distToPosition = + bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + if (blazeNearby && distToPosition <= 0.2f) + { + waypointIndex = (waypointIndex + 1) % numWaypoints; + const Position& newTarget = (*waypoints)[waypointIndex]; + const float distToNewPosition = + bot->GetExactDist2d(newTarget.GetPositionX(), newTarget.GetPositionY()); + + if (distToNewPosition > 0.2f) + { + const float dX = newTarget.GetPositionX() - bot->GetPositionX(); + const float dY = newTarget.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToNewPosition); + const float moveX = bot->GetPositionX() + (dX / distToNewPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToNewPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + else if (distToPosition > 0.2f) + { + const float dX = target.GetPositionX() - bot->GetPositionX(); + const float dY = target.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(3.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(BLACK_TEMPLE_MAP_ID, moveX, moveY, + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +// Pets grab aggro right away during Phase 2 and wipe the raid if not put on passive +// Just like players, pets cannot melee Illidan during Phase 4 +bool IllidanStormrageControlPetAggressionAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + Pet* pet = bot->GetPet(); + if (!pet) + return false; + + int phase = GetIllidanPhase(illidan); + + if ((phase == 2 || phase == 4) && + pet->GetReactState() != REACT_PASSIVE) + { + pet->AttackStop(); + pet->SetReactState(REACT_PASSIVE); + } + else if (pet->GetReactState() == REACT_PASSIVE) + { + pet->SetReactState(REACT_DEFENSIVE); + } + + return false; +} + +bool IllidanStormragePositionAboveGrateAction::Execute(Event /*event*/) +{ + const std::array& gratePositions = GRATE_POSITIONS; + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector bots; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && !botAI->IsAssistTankOfIndex(member, 0, true) && + !botAI->IsAssistTankOfIndex(member, 1, true)) + { + bots.push_back(member); + } + } + + if (bots.empty()) + return false; + + std::sort(bots.begin(), bots.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + auto it = std::find(bots.begin(), bots.end(), bot); + if (it == bots.end()) + return false; + + const size_t botIndex = std::distance(bots.begin(), it); + const uint8 index = botIndex % gratePositions.size(); + + const Position& position = gratePositions[index]; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 0.2f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IllidanStormrageRemoveDarkBarrageAction::Execute(Event /*event*/) +{ + switch (bot->getClass()) + { + case CLASS_MAGE: + return botAI->CanCastSpell("ice block", bot) && + botAI->CastSpell("ice block", bot); + + case CLASS_PALADIN: + return botAI->CanCastSpell("divine shield", bot) && + botAI->CastSpell("divine shield", bot); + + case CLASS_ROGUE: + return botAI->CanCastSpell("cloak of shadows", bot) && + botAI->CastSpell("cloak of shadows", bot); + + default: + return false; + } +} + +bool IllidanStormrageMoveAwayFromLandingPointAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + constexpr float safeDistance = 20.0f; + const float currentDistance = bot->GetExactDist2d(illidan); + if (currentDistance < safeDistance) + return MoveAway(illidan, safeDistance - currentDistance); + + return false; +} + +// NOTE: Illidan's bounding radius is 0.459f, and combatreach is 7.5f +bool IllidanStormrageDisperseRangedAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + int phase = GetIllidanPhase(illidan); + + if (phase == 4) + { + return SpreadInCircleInDemonPhase(illidan, group); + } + else if (GetBotWithParasiticShadowfiend(bot) == bot || + (GetIllidanTrapperHunter(bot) == bot && + GetBotWithParasiticShadowfiend(bot))) + { + return false; + } + else + { + return FanOutBehindInHumanPhase(illidan, group); + } +} + +bool IllidanStormrageDisperseRangedAction::FanOutBehindInHumanPhase( + Unit* illidan, Group* group) +{ + auto const& flameCrashes = GetAllFlameCrashes(bot); + + std::vector healers; + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + if (botAI->IsHeal(member)) + healers.push_back(member); + else + rangedDps.push_back(member); + } + + constexpr float arcSpan = M_PI; + const float arcCenter = illidan->GetOrientation() + M_PI; + const float arcStart = arcCenter - arcSpan / 2.0f; + + const float radius = botAI->IsHeal(bot) ? 18.0f : 25.0f; + auto& bots = botAI->IsHeal(bot) ? healers : rangedDps; + const size_t count = bots.size(); + auto findIt = std::find(bots.begin(), bots.end(), bot); + const size_t botIndex = (findIt != bots.end()) ? + std::distance(bots.begin(), findIt) : 0; + + const float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / + static_cast(count - 1)); + + const float targetX = illidan->GetPositionX() + radius * std::cos(angle); + const float targetY = illidan->GetPositionY() + radius * std::sin(angle); + + constexpr float hazardRadius = 12.0f; + bool safe = true; + for (Unit* flameCrash : flameCrashes) + { + if (flameCrash->GetDistance2d(targetX, targetY) < hazardRadius) + { + safe = false; + break; + } + } + + if (!safe) + return false; + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + return MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT, + true, false); + } + + return false; +} + +bool IllidanStormrageDisperseRangedAction::SpreadInCircleInDemonPhase( + Unit* illidan, Group* group) +{ + Player* warlockTank = GetIllidanWarlockTank(bot); + if (!warlockTank) + { + constexpr float safeDistFromBoss = 24.0f; + if (bot->GetExactDist2d(illidan) < safeDistFromBoss) + { + constexpr uint32 minInterval = 0; + if (FleePosition(illidan->GetPosition(), safeDistFromBoss, minInterval)) + return true; + } + + constexpr float safeDistFromPlayer = 6.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval); + + return false; + } + + std::vector rangedBots; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedBots.push_back(member); + } + + if (rangedBots.empty()) + return false; + + const size_t count = rangedBots.size(); + auto findIt = std::find(rangedBots.begin(), rangedBots.end(), bot); + const size_t botIndex = (findIt != rangedBots.end()) ? + std::distance(rangedBots.begin(), findIt) : 0; + + const float dx = warlockTank->GetPositionX() - illidan->GetPositionX(); + const float dy = warlockTank->GetPositionY() - illidan->GetPositionY(); + const float warlockAngle = std::atan2(dy, dx); + + constexpr float forbiddenArc = (2.0f / 3.0f) * M_PI; + constexpr float allowedArc = (4.0f / 3.0f) * M_PI; + + const float arcStart = Position::NormalizeOrientation(warlockAngle + forbiddenArc / 2.0f); + constexpr float radius = 25.0f; + + const float angle = (count == 1) ? + Position::NormalizeOrientation(arcStart + allowedArc / 2.0f) : + Position::NormalizeOrientation( + arcStart + allowedArc * static_cast(botIndex) / + static_cast(count - 1)); + + const float targetX = illidan->GetPositionX() + radius * std::cos(angle); + const float targetY = illidan->GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + if (MoveTo(BLACK_TEMPLE_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false)) + { + return true; + } + else + { + constexpr float safeDistFromTank = 25.0f; + const float currentDistFromTank = bot->GetExactDist2d(warlockTank); + if (currentDistFromTank < safeDistFromTank) + return MoveAway(warlockTank, safeDistFromTank - currentDistFromTank); + } + } + + return false; +} + +// Melee cannot attack Demon Form Illidan +bool IllidanStormrageMeleeGoSomewhereToNotDieAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + constexpr float demonSearchRadius = 25.0f; + constexpr float shadowfiendSearchRadius = 15.0f; + + Unit* illidanVictim = illidan->GetVictim(); + // But they can attack Shadow Demons and Shadowfiends, if far enough from Illidan + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), demonSearchRadius, true); + + if (shadowDemon && shadowDemon->GetDistance2d(illidan) > 15.0f && + (!illidanVictim || shadowDemon->GetDistance2d(illidanVictim) > 24.0f)) + { + return false; + } + else + { + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + shadowfiendSearchRadius, true); + + if (shadowfiend && shadowfiend->GetDistance2d(illidan) > 15.0f && + shadowfiend->GetHealthPct() < 30.0f && + (!illidanVictim || shadowfiend->GetDistance2d(illidanVictim) > 24.0f)) + { + return false; + } + } + + // 30y is closer than ideal but is a compromise to allow melee to reach targets in time + constexpr float safeDistFromBoss = 30.0f; + const float currentDistFromBoss = bot->GetExactDist2d(illidan); + if (currentDistFromBoss < safeDistFromBoss) + MoveAway(illidan, safeDistFromBoss - currentDistFromBoss); + + if (Player* warlockTank = GetIllidanWarlockTank(bot)) + { + constexpr float safeDistFromTank = 25.0f; + const float currentDistFromTank = bot->GetExactDist2d(warlockTank); + if (currentDistFromTank < safeDistFromTank) + MoveAway(warlockTank, safeDistFromTank - currentDistFromTank); + } + + constexpr float safeDistFromPlayer = 6.0f; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + MoveAway(nearestPlayer, safeDistFromPlayer - bot->GetDistance2d(nearestPlayer)); + + return true; +} + +bool IllidanStormrageWarlockTankHandleDemonBossAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + constexpr float safeDistance = 24.0f; + const float currentDistance = bot->GetExactDist2d(illidan); + if (currentDistance < safeDistance && + MoveAway(illidan, safeDistance - currentDistance)) + { + return true; + } + + if (botAI->CanCastSpell("shadow ward", bot) && + botAI->CastSpell("shadow ward", bot)) + { + return true; + } + + if (botAI->CanCastSpell("searing pain", illidan)) + return botAI->CastSpell("searing pain", illidan); + + return false; +} + +bool IllidanStormrageDpsPrioritizeAddsAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + + std::vector targets; + + if (phase == 4) + { + constexpr float searchRadius = 35.0f; + + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), searchRadius, true); + + if (GetIllidanWarlockTank(bot) == bot) + { + targets = { shadowDemon, illidan }; + } + else + { + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true); + + if (botAI->IsRanged(bot)) + { + if (shadowDemon) + targets = { shadowDemon }; + else if (shadowfiend && bot->GetDistance2d(shadowfiend) > 10.0f) + targets = { shadowfiend }; + else + targets = { illidan }; + } + else if (botAI->IsMelee(bot)) + { + targets = { shadowDemon, shadowfiend }; + } + } + } + else if (botAI->IsRanged(bot)) + { + if (phase == 1 || phase == 3 || phase == 5) + { + constexpr float searchRadius = 35.0f; + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true); + + if (shadowfiend && bot->GetDistance2d(shadowfiend) > 10.0f) + targets = { shadowfiend }; + else + targets = { illidan }; + } + else if (phase == 2) + { + constexpr float searchRadius = 20.0f; + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true); + + if (shadowfiend && bot->GetDistance2d(shadowfiend) > 5.0f) + { + targets = { shadowfiend }; + } + else + { + auto [eastFlame, westFlame] = GetFlamesOfAzzinoth(bot); + targets = { eastFlame, westFlame }; + } + } + } + + for (Unit* candidate : targets) + { + if (candidate && candidate->IsAlive()) + { + if (AI_VALUE(Unit*, "current target") != candidate) + return Attack(candidate); + + return false; + } + } + + return false; +} + +bool IllidanStormrageUseShadowTrapAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + GameObject* trap = FindNearestTrap(botAI, bot); + if (!trap || illidan->GetExactDist2d(trap) >= 4.0f) + return false; + + if (bot->GetExactDist2d(trap) < 3.0f) + { + trap->Use(bot); + return true; + } + else + { + return MoveTo(BLACK_TEMPLE_MAP_ID, trap->GetPositionX(), trap->GetPositionY(), + trap->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IllidanStormrageManageDpsTimerAndRtiAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + const time_t now = std::time(nullptr); + const uint32 instanceId = illidan->GetMap()->GetInstanceId(); + + bool updated = false; + const int phase = GetIllidanPhase(illidan); + int lastPhase = -1; + if (auto const it = illidanLastPhase.find(instanceId); it != illidanLastPhase.end()) + lastPhase = it->second; + const bool phaseChanged = lastPhase != phase; + illidanLastPhase[instanceId] = phase; + + if (phaseChanged) + { + if (phase == 1 || phase == 3 || phase == 4 || phase == 5) + { + illidanBossDpsWaitTimer[instanceId] = now; + updated = true; + } + else if (phase == 2) + { + if (illidanBossDpsWaitTimer.erase(instanceId) > 0) + updated = true; + } + + if (phase != 2 && illidanFlameDpsWaitTimer.erase(instanceId) > 0) + updated = true; + } + + if (phase == 2) + { + if (eastFlameGuid.find(instanceId) == eastFlameGuid.end() && + westFlameGuid.find(instanceId) == westFlameGuid.end()) + { + std::list creatureList; + constexpr float searchRadius = 50.0f; + illidan->GetCreatureListWithEntryInGrid(creatureList, static_cast( + BlackTempleNpcs::NPC_FLAME_OF_AZZINOTH), searchRadius); + + std::vector flames; + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + flames.push_back(creature); + } + + if (flames.size() == 2) + { + const float eastDist0 = + flames[0]->GetExactDist2d(ILLIDAN_E_GLAIVE_WAITING_POSITION); + const float eastDist1 = + flames[1]->GetExactDist2d(ILLIDAN_E_GLAIVE_WAITING_POSITION); + + if (eastDist0 < eastDist1) + { + eastFlameGuid[instanceId] = flames[0]->GetGUID(); + westFlameGuid[instanceId] = flames[1]->GetGUID(); + } + else + { + eastFlameGuid[instanceId] = flames[1]->GetGUID(); + westFlameGuid[instanceId] = flames[0]->GetGUID(); + } + + illidanFlameDpsWaitTimer[instanceId] = now; + + updated = true; + } + } + } + else + { + if (eastFlameGuid.erase(instanceId) > 0) + updated = true; + if (westFlameGuid.erase(instanceId) > 0) + updated = true; + } + + return updated; +} + +bool IllidanStormrageDestroyHazardsAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + constexpr float searchRadius = 50.0f; + std::list hazards; + std::vector entries; + + if (phase == 2) + { + entries = { + static_cast(BlackTempleNpcs::NPC_FLAME_CRASH) + }; + } + else if (phase == 4) + { + entries = { + static_cast(BlackTempleNpcs::NPC_FLAME_CRASH) + }; + } + else if (phase == 0) + { + entries = { + static_cast(BlackTempleNpcs::NPC_DEMON_FIRE), + static_cast(BlackTempleNpcs::NPC_BLAZE) + }; + } + + if (!entries.empty()) + bot->GetCreatureListWithEntryInGrid(hazards, entries, searchRadius); + + for (Creature* creature : hazards) + { + if (creature && creature->IsAlive()) + { + creature->Kill(bot, creature); + return true; + } + } + + return false; +} + +// Reduce Shadow Demon to 25% health and kill residual Shadowfiends in Phase 2 +bool IllidanStormrageHandleAddsCheatAction::Execute(Event /*event*/) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + if (GetIllidanPhase(illidan) == 2) + { + constexpr float searchRadius = 20.0f; + if (Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), + searchRadius, true)) + { + shadowfiend->Kill(bot, shadowfiend); + return true; + } + } + else + { + constexpr float searchRadius = 75.0f; + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), searchRadius, true); + + if (shadowDemon && shadowDemon->GetHealthPct() > 25.0f) + { + uint32 desiredDamage = 0; + const uint32 quarterHealth = shadowDemon->GetMaxHealth() / 4; + if (shadowDemon->GetHealth() > quarterHealth) + desiredDamage = shadowDemon->GetHealth() - quarterHealth; + + Unit::DealDamage(bot, shadowDemon, desiredDamage, nullptr, DIRECT_DAMAGE, + SPELL_SCHOOL_MASK_NORMAL, nullptr, false, false, nullptr); + return true; + } + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTActions.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTActions.h new file mode 100644 index 000000000..a79cda6b7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTActions.h @@ -0,0 +1,566 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTACTIONS_H +#define PLAYERBOTS_BTACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" +#include "BTHelpers.h" + +namespace BlackTempleHelpers +{ + struct EyeBlastDangerArea; +} + +// General + +class BlackTempleEraseTimersAndTrackersAction : public Action +{ +public: + BlackTempleEraseTimersAndTrackersAction( + PlayerbotAI* botAI) : Action(botAI, "black temple erase timers and trackers") {} + bool Execute(Event event) override; +}; + +// High Warlord Naj'entus + +class HighWarlordNajentusMisdirectBossToMainTankAction : public AttackAction +{ +public: + HighWarlordNajentusMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "high warlord naj'entus misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusTanksPositionBossAction : public AttackAction +{ +public: + HighWarlordNajentusTanksPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "high warlord naj'entus tanks position boss") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusDisperseRangedAction : public MovementAction +{ +public: + HighWarlordNajentusDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "naj'entus disperse ranged") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusRemoveImpalingSpineAction : public MovementAction +{ +public: + HighWarlordNajentusRemoveImpalingSpineAction( + PlayerbotAI* botAI) : MovementAction(botAI, "high warlord naj'entus remove impaling spine") {} + bool Execute(Event event) override; +}; + +class HighWarlordNajentusThrowImpalingSpineAction : public MovementAction +{ +public: + HighWarlordNajentusThrowImpalingSpineAction( + PlayerbotAI* botAI) : MovementAction(botAI, "high warlord naj'entus throw impaling spine") {} + bool Execute(Event event) override; +}; + +// Supremus + +class SupremusMisdirectBossToMainTankAction : public AttackAction +{ +public: + SupremusMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "supremus misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class SupremusDisperseRangedAction : public MovementAction +{ +public: + SupremusDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "supremus disperse ranged") {} + bool Execute(Event event) override; +}; + +class SupremusKiteBossAction : public MovementAction +{ +public: + SupremusKiteBossAction( + PlayerbotAI* botAI) : MovementAction(botAI, "supremus kite boss") {} + bool Execute(Event event) override; +}; + +class SupremusMoveAwayFromVolcanosAction : public MovementAction +{ +public: + SupremusMoveAwayFromVolcanosAction( + PlayerbotAI* botAI) : MovementAction(botAI, "supremus move away from volcanos") {} + bool Execute(Event event) override; + +private: + Position FindSafestNearbyPosition( + const std::vector& volcanos, float maxRadius, float hazardRadius); + bool IsPathSafeFromVolcanos(const Position& start, + const Position& end, const std::vector& volcanos, float hazardRadius); + std::vector GetAllSupremusVolcanos(); +}; + +class SupremusManagePhaseTimerAction : public Action +{ +public: + SupremusManagePhaseTimerAction( + PlayerbotAI* botAI) : Action(botAI, "supremus manage phase timer") {} + bool Execute(Event event) override; +}; + +// Shade of Akama + +class ShadeOfAkamaMeleeDpsPrioritizeChannelersAction : public AttackAction +{ +public: + ShadeOfAkamaMeleeDpsPrioritizeChannelersAction( + PlayerbotAI* botAI) : AttackAction(botAI, "shade of akama melee dps prioritize channelers") {} + bool Execute(Event event) override; +}; + +// Teron Gorefiend + +class TeronGorefiendMisdirectBossToMainTankAction : public AttackAction +{ +public: + TeronGorefiendMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "teron gorefiend misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendTanksPositionBossAction : public AttackAction +{ +public: + TeronGorefiendTanksPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "teron gorefiend tanks position boss") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendPositionRangedOnBalconyAction : public MovementAction +{ +public: + TeronGorefiendPositionRangedOnBalconyAction( + PlayerbotAI* botAI) : MovementAction(botAI, "teron gorefiend position ranged on balcony") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendAvoidShadowOfDeathAction : public Action +{ +public: + TeronGorefiendAvoidShadowOfDeathAction( + PlayerbotAI* botAI) : Action(botAI, "teron gorefiend avoid shadow of death") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendMoveToCornerToDieAction : public MovementAction +{ +public: + TeronGorefiendMoveToCornerToDieAction( + PlayerbotAI* botAI) : MovementAction(botAI, "teron gorefiend move to corner to die") {} + bool Execute(Event event) override; +}; + +class TeronGorefiendControlAndDestroyShadowyConstructsAction : public MovementAction +{ +public: + TeronGorefiendControlAndDestroyShadowyConstructsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "teron gorefiend control and destroy shadowy constructs") {} + bool Execute(Event event) override; +}; + +// Gurtogg Bloodboil + +class GurtoggBloodboilMisdirectBossToMainTankAction : public AttackAction +{ +public: + GurtoggBloodboilMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "gurtogg bloodboil misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilTanksPositionBossAction : public AttackAction +{ +public: + GurtoggBloodboilTanksPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "gurtogg bloodboil tanks position boss") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilRotateRangedGroupsAction : public MovementAction +{ +public: + GurtoggBloodboilRotateRangedGroupsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "gurtogg bloodboil rotate ranged groups") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction : public MovementAction +{ +public: + GurtoggBloodboilRangedMoveAwayFromEnragedPlayerAction( + PlayerbotAI* botAI) : MovementAction(botAI, "gurtogg bloodboil ranged move away from enraged player") {} + bool Execute(Event event) override; +}; + +class GurtoggBloodboilManagePhaseTimerAction : public Action +{ +public: + GurtoggBloodboilManagePhaseTimerAction( + PlayerbotAI* botAI) : Action(botAI, "gurtogg bloodboil manage phase timer") {} + bool Execute(Event event) override; +}; + +// Reliquary of Souls + +class ReliquaryOfSoulsMisdirectBossToMainTankAction : public AttackAction +{ +public: + ReliquaryOfSoulsMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "reliquary of souls misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class ReliquaryOfSoulsAdjustDistanceFromSufferingAction : public MovementAction +{ +public: + ReliquaryOfSoulsAdjustDistanceFromSufferingAction( + PlayerbotAI* botAI) : MovementAction(botAI, "reliquary of souls adjust distance from suffering") {} + bool Execute(Event event) override; + +private: + bool TanksMoveToMinimumRange(Unit* suffering); + bool MeleeDpsStayAtMaximumRange(Unit* suffering); + bool RangedMoveAwayFromBoss(Unit* suffering); +}; + +class ReliquaryOfSoulsHealersDpsSufferingAction : public Action +{ +public: + ReliquaryOfSoulsHealersDpsSufferingAction( + PlayerbotAI* botAI) : Action(botAI, "reliquary of souls healers dps suffering") {} + bool Execute(Event event) override; +}; + +class ReliquaryOfSoulsSpellstealRuneShieldAction : public Action +{ +public: + ReliquaryOfSoulsSpellstealRuneShieldAction( + PlayerbotAI* botAI) : Action(botAI, "reliquary of souls spellsteal rune shield") {} + bool Execute(Event event) override; +}; + +class ReliquaryOfSoulsSpellReflectDeadenAction : public Action +{ +public: + ReliquaryOfSoulsSpellReflectDeadenAction( + PlayerbotAI* botAI) : Action(botAI, "reliquary of souls spell reflect deaden") {} + bool Execute(Event event) override; +}; + +// Mother Shahraz + +class MotherShahrazMisdirectBossToMainTankAction : public AttackAction +{ +public: + MotherShahrazMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "mother shahraz misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class MotherShahrazTanksPositionBossUnderPillarAction : public AttackAction +{ +public: + MotherShahrazTanksPositionBossUnderPillarAction( + PlayerbotAI* botAI) : AttackAction(botAI, "mother shahraz tanks position boss under pillar") {} + bool Execute(Event event) override; +}; + +class MotherShahrazMeleeDpsWaitAtSafePositionAction : public MovementAction +{ +public: + MotherShahrazMeleeDpsWaitAtSafePositionAction( + PlayerbotAI* botAI) : MovementAction(botAI, "mother shahraz melee dps wait at safe position") {} + bool Execute(Event event) override; +}; + +class MotherShahrazPositionRangedUnderPillarAction : public MovementAction +{ +public: + MotherShahrazPositionRangedUnderPillarAction( + PlayerbotAI* botAI) : MovementAction(botAI, "mother shahraz position ranged under pillar") {} + bool Execute(Event event) override; +}; + +class MotherShahrazRunAwayToBreakFatalAttractionAction : public MovementAction +{ +public: + MotherShahrazRunAwayToBreakFatalAttractionAction( + PlayerbotAI* botAI) : MovementAction(botAI, "mother shahraz run away to break fatal attraction") {} + bool Execute(Event event) override; + +private: + std::vector GetAttractedPlayers(); +}; + +// Illidari Council + +class IllidariCouncilMisdirectBossesToTanksAction : public AttackAction +{ +public: + IllidariCouncilMisdirectBossesToTanksAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council misdirect bosses to tanks") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilMainTankPositionGathiosAction : public AttackAction +{ +public: + IllidariCouncilMainTankPositionGathiosAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council main tank position gathios") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilMainTankReflectJudgementOfCommandAction : public Action +{ +public: + IllidariCouncilMainTankReflectJudgementOfCommandAction( + PlayerbotAI* botAI) : Action(botAI, "illidari council main tank reflect judgement of command") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilFirstAssistTankFocusMalandeAction : public AttackAction +{ +public: + IllidariCouncilFirstAssistTankFocusMalandeAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council first assist tank focus malande") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilSecondAssistTankPositionDarkshadowAction : public AttackAction +{ +public: + IllidariCouncilSecondAssistTankPositionDarkshadowAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council second assist tank position darkshadow") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilMageTankPositionZerevorAction : public AttackAction +{ +public: + IllidariCouncilMageTankPositionZerevorAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council mage tank position zerevor") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilPositionMageTankHealerAction : public AttackAction +{ +public: + IllidariCouncilPositionMageTankHealerAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council position mage tank healer") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilAssignDpsTargetsAction : public AttackAction +{ +public: + IllidariCouncilAssignDpsTargetsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council assign dps targets") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilDisperseRangedAction : public MovementAction +{ +public: + IllidariCouncilDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidari council disperse ranged") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilCommandPetsToAttackGathiosAction : public AttackAction +{ +public: + IllidariCouncilCommandPetsToAttackGathiosAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidari council command pets to attack gathios") {} + bool Execute(Event event) override; +}; + +class IllidariCouncilManageDpsTimerAction : public Action +{ +public: + IllidariCouncilManageDpsTimerAction( + PlayerbotAI* botAI) : Action(botAI, "illidari council manage dps timer") {} + bool Execute(Event event) override; +}; + +// Illidan Stormrage + +class IllidanStormrageMisdirectToTankAction : public AttackAction +{ +public: + IllidanStormrageMisdirectToTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage misdirect to tank") {} + bool Execute(Event event) override; + +private: + bool TryMisdirectToFlameTanks(Group* group); + bool TryMisdirectToWarlockTank(Unit* illidan); +}; + +class IllidanStormrageMainTankRepositionBossAction : public AttackAction +{ +public: + IllidanStormrageMainTankRepositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage main tank reposition boss") {} + bool Execute(Event event) override; + +private: + bool MoveToShadowTrap(GameObject* trap); + Position FindSafestNearbyPosition( + const std::vector& flameCrashes, float maxRadius, float hazardRadius); + bool IsPathSafeFromFlameCrashes(const Position& start, + const Position& end, const std::vector& flameCrashes, float hazardRadius); +}; + +class IllidanStormrageIsolateBotWithParasiteAction : public MovementAction +{ +public: + IllidanStormrageIsolateBotWithParasiteAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage isolate bot with parasite") {} + bool Execute(Event event) override; + +private: + bool InfectedBotMoveFromGroup(Unit* illidan, const Position& targetPos); + bool FreezeTrapShadowfiend(Player* bot, Unit* illidan, const Position& targetPos); +}; + +class IllidanStormrageSetEarthbindTotemAction : public Action +{ +public: + IllidanStormrageSetEarthbindTotemAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage set earthbind totem") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction : public AttackAction +{ +public: + IllidanStormrageAssistTanksHandleFlamesOfAzzinothAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage assist tanks handle flames of azzinoth") {} + bool Execute(Event event) override; + +private: + bool RepositionToAvoidEyeBlast(Unit* illidan, const BlackTempleHelpers::EyeBlastDangerArea& dangerArea); + bool RepositionToAvoidBlaze(Unit* eastFlame, Unit* westFlame); +}; + +class IllidanStormrageControlPetAggressionAction : public Action +{ +public: + IllidanStormrageControlPetAggressionAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage control pet aggression") {} + bool Execute(Event event) override; +}; + +class IllidanStormragePositionAboveGrateAction : public MovementAction +{ +public: + IllidanStormragePositionAboveGrateAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage position above grate") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageRemoveDarkBarrageAction : public Action +{ +public: + IllidanStormrageRemoveDarkBarrageAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage remove dark barrage") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageMoveAwayFromLandingPointAction : public MovementAction +{ +public: + IllidanStormrageMoveAwayFromLandingPointAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage move away from landing point") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageDisperseRangedAction : public MovementAction +{ +public: + IllidanStormrageDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage disperse ranged") {} + bool Execute(Event event) override; + +private: + bool FanOutBehindInHumanPhase(Unit* illidan, Group* group); + bool SpreadInCircleInDemonPhase(Unit* illidan, Group* group); +}; + +class IllidanStormrageMeleeGoSomewhereToNotDieAction : public MovementAction +{ +public: + IllidanStormrageMeleeGoSomewhereToNotDieAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage melee go somewhere to not die") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageWarlockTankHandleDemonBossAction : public AttackAction +{ +public: + IllidanStormrageWarlockTankHandleDemonBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage warlock tank handle demon boss") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageDpsPrioritizeAddsAction : public AttackAction +{ +public: + IllidanStormrageDpsPrioritizeAddsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "illidan stormrage dps prioritize adds") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageUseShadowTrapAction : public MovementAction +{ +public: + IllidanStormrageUseShadowTrapAction( + PlayerbotAI* botAI) : MovementAction(botAI, "illidan stormrage use shadow trap") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageManageDpsTimerAndRtiAction : public Action +{ +public: + IllidanStormrageManageDpsTimerAndRtiAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage manage dps timer and rti") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageDestroyHazardsAction : public Action +{ +public: + IllidanStormrageDestroyHazardsAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage destroy hazards") {} + bool Execute(Event event) override; +}; + +class IllidanStormrageHandleAddsCheatAction : public Action +{ +public: + IllidanStormrageHandleAddsCheatAction( + PlayerbotAI* botAI) : Action(botAI, "illidan stormrage handle adds cheat") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.cpp new file mode 100644 index 000000000..4be581978 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.cpp @@ -0,0 +1,469 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BTHelpers.h" + +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace BlackTempleHelpers +{ + // High Warlord Naj'entus + const Position NAJENTUS_TANK_POSITION = { 438.515f, 772.436f, 11.931f }; + + // Supremus + std::unordered_map supremusPhaseTimer; + + bool HasSupremusVolcanoNearby(PlayerbotAI*, Player* bot) + { + constexpr float searchRadius = 20.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast( + BlackTempleNpcs::NPC_SUPREMUS_VOLCANO), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + return true; + } + + return false; + } + + // Shade of Akama + const Position AKAMA_CHANNELER_POSITION = { 467.851f, 401.622f, 118.538f }; + + std::unordered_set hasReachedAkamaChannelerPosition; + + // Teron Gorefiend + const Position GOREFIEND_TANK_POSITION = { 597.653f, 402.284f, 187.090f }; + const Position GOREFIEND_DIE_POSITION = { 525.709f, 377.177f, 193.203f }; + + // Gurtogg Bloodboil + const Position GURTOGG_TANK_POSITION = { 735.987f, 272.451f, 063.554f }; + const Position GURTOGG_RANGED_POSITION = { 762.265f, 277.183f, 063.781f }; + const Position GURTOGG_SOAKER_POSITION = { 769.348f, 280.116f, 063.780f }; + + std::unordered_map gurtoggPhaseTimer; + + std::vector> GetGurtoggRangedRotationGroups(Player* bot) + { + Group* group = bot->GetGroup(); + std::vector rangedMembers; + std::vector> groups(3); + + if (!group) + return groups; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive()) + { + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (memberAI && memberAI->IsRanged(member)) + rangedMembers.push_back(member); + } + } + + for (size_t i = 0; i < rangedMembers.size(); ++i) + { + groups[i / 5].push_back(rangedMembers[i]); + if (groups[2].size() == 5) + break; + } + + return groups; + } + + int GetGurtoggActiveRotationGroup(Unit* gurtogg) + { + if (!gurtogg) + return -1; + + auto it = gurtoggPhaseTimer.find(gurtogg->GetMap()->GetInstanceId()); + if (it == gurtoggPhaseTimer.end()) + return -1; + + const time_t now = std::time(nullptr); + const time_t elapsed = now - it->second; + const int groupIndex = (elapsed % 30) / 10; // 3 groups, swapping every 10 seconds + + return groupIndex; + } + + // Mother Shahraz + const Position SHAHRAZ_TANK_POSITION = { 960.438f, 178.989f, 192.826f }; + const Position SHAHRAZ_TRANSITION_POSITION = { 951.327f, 179.550f, 192.550f }; + const Position SHAHRAZ_RANGED_POSITION = { 935.267f, 175.459f, 192.821f }; + std::unordered_map shahrazTankStep; + + TankPositionState GetShahrazTankPositionState(PlayerbotAI* botAI, Player* bot) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return TankPositionState::Unknown; + + auto it = shahrazTankStep.find(mainTank->GetGUID()); + if (it != shahrazTankStep.end()) + return it->second; + + return TankPositionState::Unknown; + } + + // Illidari Council + const std::array GATHIOS_TANK_POSITIONS = {{ + { 662.977f, 296.246f, 271.688f }, + { 636.238f, 283.719f, 271.629f }, + { 655.571f, 261.377f, 271.687f }, + { 673.789f, 274.139f, 271.689f } + }}; + const Position ZEREVOR_TANK_POSITION = { 686.219f, 377.644f, 271.689f }; + const std::array ZEREVOR_HEALER_POSITIONS = {{ + { 661.385f, 351.219f, 271.690f }, + { 667.003f, 363.768f, 271.690f } + }}; + const Position MALANDE_TANK_POSITION = { 690.590f, 299.790f, 277.443f }; + + std::unordered_map councilDpsWaitTimer; + std::unordered_map gathiosTankStep; + std::unordered_map zerevorHealStep; + + // (1) First priority is an assistant Mage (real player or bot) + // (2) If no assistant Mage, then look for any Mage bot + Player* GetZerevorMageTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* fallbackMage = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + if (!fallbackMage && GET_PLAYERBOT_AI(member)) + fallbackMage = member; + } + + return fallbackMage; + } + + bool HasDangerousCouncilAura(Unit* unit) + { + static const std::array dangerousAuras = + { + static_cast(BlackTempleSpells::SPELL_CONSECRATION), + static_cast(BlackTempleSpells::SPELL_BLIZZARD), + static_cast(BlackTempleSpells::SPELL_FLAMESTRIKE) + }; + + for (uint32 aura : dangerousAuras) + { + if (unit->HasAura(aura)) + return true; + } + + return false; + } + + // Illidan Stormrage + const Position ILLIDAN_LANDING_POSITION = { 676.648f, 304.761f, 354.189f }; + const Position ILLIDAN_N_GRATE_POSITION = { 682.100f, 306.000f, 353.192f }; + const Position ILLIDAN_E_GRATE_POSITION = { 673.500f, 298.500f, 353.192f }; + const Position ILLIDAN_W_GRATE_POSITION = { 672.400f, 312.500f, 353.192f }; + const std::array GRATE_POSITIONS = {{ + ILLIDAN_N_GRATE_POSITION, + ILLIDAN_E_GRATE_POSITION, + ILLIDAN_W_GRATE_POSITION + }}; + + const Position ILLIDAN_E_GLAIVE_WAITING_POSITION = { 677.656f, 294.066f, 353.192f }; + const std::array E_GLAIVE_TANK_POSITIONS = {{ + { 683.000f, 295.000f, 354.000f }, + { 696.969f, 300.982f, 354.302f }, + { 691.112f, 287.461f, 354.363f }, + { 676.674f, 280.797f, 354.268f }, + { 664.414f, 284.834f, 354.271f }, + { 656.826f, 295.113f, 354.165f }, + { 665.000f, 304.000f, 354.000f } + }}; + + const Position ILLIDAN_W_GLAIVE_WAITING_POSITION = { 676.102f, 316.305f, 353.192f }; + const std::array W_GLAIVE_TANK_POSITIONS = {{ + { 697.208f, 313.475f, 354.234f }, + { 681.000f, 318.000f, 354.000f }, + { 664.000f, 307.000f, 354.000f }, + { 656.161f, 314.132f, 354.092f }, + { 665.080f, 326.905f, 354.128f }, + { 678.809f, 329.968f, 354.387f }, + { 690.889f, 324.277f, 354.204f } + }}; + + std::unordered_map flameTankWaypointIndex; + std::unordered_map illidanShadowTrapGuid; + std::unordered_map illidanShadowTrapDestination; + std::unordered_map illidanLastPhase; + std::unordered_map illidanBossDpsWaitTimer; + std::unordered_map illidanFlameDpsWaitTimer; + std::unordered_map eastFlameGuid; + std::unordered_map westFlameGuid; + + int GetIllidanPhase(Unit* illidan) + { + if (!illidan || illidan->GetHealth() == 1 || illidan->HasAura( + static_cast(BlackTempleSpells::SPELL_SHADOW_PRISON))) + { + return -1; + } + + // Transitioning from Phase 2 to Phase 3 + float x, y, z; + illidan->GetMotionMaster()->GetDestination(x, y, z); + Position dest(x, y, z); + if ((dest.GetExactDist2d(ILLIDAN_LANDING_POSITION) < 0.2f || + illidan->GetExactDist2d(ILLIDAN_LANDING_POSITION) < 0.2f) && + illidan->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + { + return 0; + } + + // Phase 2: Flying + if (illidan->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return 2; + + // Phase 1: Health > 65% + if (illidan->GetHealthPct() > 65.0f) + return 1; + + // Phase 4: Demon Form + if (!illidan->HasAura(static_cast(BlackTempleSpells::SPELL_CAGED)) && + (illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_FORM)) || + illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_TRANSFORM_1)) || + illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_TRANSFORM_2)) || + illidan->HasAura(static_cast(BlackTempleSpells::SPELL_DEMON_TRANSFORM_3)))) + { + return 4; + } + + // Phase 3: Normal (ground, 65-30%, not demon) + if (illidan->GetHealthPct() > 30.0f) + return 3; + + // Phase 5: Health <= 30% + if (illidan->GetHealthPct() <= 30.0f) + return 5; + + return -1; + } + + std::vector GetAllFlameCrashes(Player* bot) + { + std::vector flameCrashes; + std::list creatureList; + constexpr float searchRadius = 30.0f; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(BlackTempleNpcs::NPC_FLAME_CRASH), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + flameCrashes.push_back(creature); + } + + return flameCrashes; + } + + std::pair GetFlamesOfAzzinoth(Player* bot) + { + Unit* eastFlame = nullptr; + Unit* westFlame = nullptr; + + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + if (eastFlameGuid.find(instanceId) != eastFlameGuid.end()) + { + if (Unit* unit = ObjectAccessor::GetUnit(*bot, eastFlameGuid[instanceId])) + { + if (unit->IsAlive()) + eastFlame = unit; + } + } + + if (westFlameGuid.find(instanceId) != westFlameGuid.end()) + { + if (Unit* unit = ObjectAccessor::GetUnit(*bot, westFlameGuid[instanceId])) + { + if (unit->IsAlive()) + westFlame = unit; + } + } + + return { eastFlame, westFlame }; + } + + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot + Player* GetIllidanWarlockTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* fallbackWarlock = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_WARLOCK) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + if (!fallbackWarlock && GET_PLAYERBOT_AI(member)) + fallbackWarlock = member; + } + + return fallbackWarlock; + } + + bool HasParasiticShadowfiend(Player* member) + { + if (!member) + return false; + + constexpr uint32 shadowfiendAura1 = + static_cast(BlackTempleSpells::SPELL_PARASITIC_SHADOWFIEND_1); + constexpr uint32 shadowfiendAura2 = + static_cast(BlackTempleSpells::SPELL_PARASITIC_SHADOWFIEND_2); + + return member->HasAura(shadowfiendAura1) || member->HasAura(shadowfiendAura2); + } + + // Get the first bot hunter that doesn't have Parasitic Shadowfiend + Player* GetIllidanTrapperHunter(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && + GET_PLAYERBOT_AI(member) && !HasParasiticShadowfiend(member)) + { + return member; + } + } + + return nullptr; + } + + Player* GetBotWithParasiticShadowfiend(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + HasParasiticShadowfiend(member)) + { + return member; + } + } + + return nullptr; + } + + EyeBlastDangerArea GetEyeBlastDangerArea(Player* bot) + { + constexpr float searchRadius = 100.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(BlackTempleNpcs::NPC_ILLIDAN_DB_TARGET), searchRadius); + + Creature* eyeBlastTrigger = nullptr; + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + { + eyeBlastTrigger = creature; + break; + } + } + + if (!eyeBlastTrigger) + return {}; + + Position startPos = Position(eyeBlastTrigger->GetPositionX(), eyeBlastTrigger->GetPositionY(), + eyeBlastTrigger->GetPositionZ()); + + float destX, destY, destZ; + eyeBlastTrigger->GetMotionMaster()->GetDestination(destX, destY, destZ); + Position endPos(destX, destY, destZ); + + if (startPos.GetExactDist2d(endPos) < 0.1f) + return {}; + + constexpr float eyeBlastWidth = 9.0f; + return { startPos, endPos, eyeBlastWidth }; + } + + bool IsPositionInEyeBlastDangerArea(const Position& pos, const EyeBlastDangerArea& area) + { + const float dx = area.end.GetPositionX() - area.start.GetPositionX(); + const float dy = area.end.GetPositionY() - area.start.GetPositionY(); + const float length = area.start.GetExactDist2d(area.end.GetPositionX(), area.end.GetPositionY()); + + if (length < 0.1f) + return false; + + const float projectionFactor = ( + (pos.GetPositionX() - area.start.GetPositionX()) * dx + ( + pos.GetPositionY() - area.start.GetPositionY()) * dy) / (length * length); + + const float clampedProjectionFactor = std::clamp(projectionFactor, 0.0f, 1.0f); + + const float closestX = area.start.GetPositionX() + clampedProjectionFactor * dx; + const float closestY = area.start.GetPositionY() + clampedProjectionFactor * dy; + + const float distToLine = pos.GetExactDist2d(closestX, closestY); + + return distToLine < area.width; + } + + GameObject* FindNearestTrap(PlayerbotAI* botAI, Player*) + { + GuidVector const& gos = + botAI->GetAiObjectContext()->GetValue("nearest game objects")->Get(); + + GameObject* nearestTrap = nullptr; + for (ObjectGuid const& guid : gos) + { + GameObject* go = botAI->GetGameObject(guid); + if (go && go->isSpawned() && + go->GetEntry() == static_cast(BlackTempleObjects::GO_SHADOW_TRAP)) + { + nearestTrap = go; + break; + } + } + + return nearestTrap; + } +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.h new file mode 100644 index 000000000..6d1cbc416 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTHelpers.h @@ -0,0 +1,217 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTHELPERS_H +#define PLAYERBOTS_BTHELPERS_H + +#include +#include +#include +#include +#include + +#include "Common.h" +#include "ObjectGuid.h" +#include "Position.h" + +class GameObject; +class Player; +class PlayerbotAI; +class Unit; + +namespace BlackTempleHelpers +{ + +enum class BlackTempleSpells : uint32 +{ + // High Warlord Naj'entus + SPELL_IMPALING_SPINE = 39837, + SPELL_TIDAL_SHIELD = 39872, + + // Supremus + SPELL_SNARE_SELF = 41922, + + // Teron Gorefiend + SPELL_SHADOW_OF_DEATH = 40251, + SPELL_SPIRITUAL_VENGEANCE = 40268, + + SPELL_SPIRIT_LANCE = 40157, + SPELL_SPIRIT_CHAINS = 40175, + SPELL_SPIRIT_VOLLEY = 40314, + SPELL_SPIRIT_STRIKE = 40325, + + // Gurtogg Bloodboil + SPELL_BOSS_FEL_RAGE = 40594, + SPELL_PLAYER_FEL_RAGE = 40604, + SPELL_BLOODBOIL = 42005, + + // Reliquary of Souls + SPELL_DEADEN = 41410, + SPELL_RUNE_SHIELD = 41431, + + // Mother Shahraz + SPELL_FATAL_ATTRACTION = 41001, + + // Gathios the Shatterer + SPELL_BLESSING_OF_PROTECTION = 41450, + SPELL_BLESSING_OF_SPELL_WARDING = 41451, + SPELL_JUDGEMENT = 41467, + SPELL_SEAL_OF_COMMAND = 41469, + SPELL_CONSECRATION = 41541, + + // Veras Darkshadow + SPELL_VANISH = 41476, + + // High Nethermancer Zerevor + SPELL_DAMPEN_MAGIC = 41478, + SPELL_FLAMESTRIKE = 41481, + SPELL_BLIZZARD = 41482, + + // Illidan Stormrage + SPELL_DEMON_TRANSFORM_1 = 40511, + SPELL_DEMON_TRANSFORM_2 = 40398, + SPELL_DEMON_TRANSFORM_3 = 40510, + SPELL_DEMON_FORM = 40506, + SPELL_DARK_BARRAGE = 40585, + SPELL_SHADOW_PRISON = 40647, + SPELL_CAGED = 40695, + SPELL_PARASITIC_SHADOWFIEND_1 = 41917, // cast by Illidan (primary infection) + SPELL_PARASITIC_SHADOWFIEND_2 = 41914, // cast by Shadowfiend on contact (secondary infection) + + // Hunter + SPELL_FROST_TRAP = 13809, + SPELL_MISDIRECTION = 35079, + + // Shaman + SPELL_EARTHBIND_TOTEM = 2484, +}; + +enum class BlackTempleNpcs : uint32 +{ + // Supremus + NPC_SUPREMUS_VOLCANO = 23085, + + // Shade of Akama + NPC_ASHTONGUE_CHANNELER = 23421, + + // Teron Gorefiend + NPC_SHADOWY_CONSTRUCT = 23111, + + // Illidan Stormrage + NPC_FLAME_OF_AZZINOTH = 22997, + NPC_DEMON_FIRE = 23069, + NPC_ILLIDAN_DB_TARGET = 23070, + NPC_BLAZE = 23259, + NPC_FLAME_CRASH = 23336, + NPC_SHADOW_DEMON = 23375, + NPC_PARASITIC_SHADOWFIEND = 23498, +}; + +enum class BlackTempleItems : uint32 +{ + // High Warlord Naj'entus + ITEM_NAJENTUS_SPINE = 32408, +}; + +enum class BlackTempleObjects : uint32 +{ + // High Warlord Naj'entus + GO_NAJENTUS_SPINE = 185584, + + // Illidan Stormrage + GO_SHADOW_TRAP = 185916, +}; + +enum class TankPositionState : uint8 +{ + MovingToTransition = 0, + MovingToFinal = 1, + Positioned = 2, + Unknown = 255, +}; + +constexpr uint32 BLACK_TEMPLE_MAP_ID = 564; + +// High Warlord Naj'entus +extern const Position NAJENTUS_TANK_POSITION; + +// Supremus +extern std::unordered_map supremusPhaseTimer; +bool HasSupremusVolcanoNearby(PlayerbotAI* botAI, Player* bot); + +// Shade of Akama +extern const Position AKAMA_CHANNELER_POSITION; +extern std::unordered_set hasReachedAkamaChannelerPosition; + +// Teron Gorefiend +extern const Position GOREFIEND_TANK_POSITION; +extern const Position GOREFIEND_DIE_POSITION; + +// Gurtogg Bloodboil +extern const Position GURTOGG_TANK_POSITION; +extern const Position GURTOGG_RANGED_POSITION; +extern const Position GURTOGG_SOAKER_POSITION; +extern std::unordered_map gurtoggPhaseTimer; +std::vector> GetGurtoggRangedRotationGroups(Player* bot); +int GetGurtoggActiveRotationGroup(Unit* gurtogg); + +// Mother Shahraz +extern const Position SHAHRAZ_TANK_POSITION; +extern const Position SHAHRAZ_TRANSITION_POSITION; +extern const Position SHAHRAZ_RANGED_POSITION; +extern std::unordered_map shahrazTankStep; +TankPositionState GetShahrazTankPositionState(PlayerbotAI* botAI, Player* bot); + +// Illidari Council +constexpr float COUNCIL_FLOOR_Z_THRESHOLD = 270.000f; +extern const std::array GATHIOS_TANK_POSITIONS; +extern const Position MALANDE_TANK_POSITION; +extern const Position ZEREVOR_TANK_POSITION; +extern const std::array ZEREVOR_HEALER_POSITIONS; +extern std::unordered_map councilDpsWaitTimer; +extern std::unordered_map gathiosTankStep; +extern std::unordered_map zerevorHealStep; +Player* GetZerevorMageTank(Player* bot); +bool HasDangerousCouncilAura(Unit* unit); + +// Illidan Stormrage +extern const Position ILLIDAN_LANDING_POSITION; +extern const Position ILLIDAN_N_GRATE_POSITION; +extern const Position ILLIDAN_E_GRATE_POSITION; +extern const Position ILLIDAN_W_GRATE_POSITION; +extern const std::array GRATE_POSITIONS; +extern const Position ILLIDAN_E_GLAIVE_WAITING_POSITION; +extern const std::array E_GLAIVE_TANK_POSITIONS; +extern const Position ILLIDAN_W_GLAIVE_WAITING_POSITION; +extern const std::array W_GLAIVE_TANK_POSITIONS; +extern std::unordered_map flameTankWaypointIndex; +extern std::unordered_map illidanShadowTrapGuid; +extern std::unordered_map illidanShadowTrapDestination; +extern std::unordered_map illidanLastPhase; +extern std::unordered_map illidanBossDpsWaitTimer; +extern std::unordered_map illidanFlameDpsWaitTimer; +extern std::unordered_map eastFlameGuid; +extern std::unordered_map westFlameGuid; +int GetIllidanPhase(Unit* illidan); +std::vector GetAllFlameCrashes(Player* bot); +std::pair GetFlamesOfAzzinoth(Player* bot); +Player* GetIllidanWarlockTank(Player* bot); +bool HasParasiticShadowfiend(Player* member); +Player* GetIllidanTrapperHunter(Player* bot); +Player* GetBotWithParasiticShadowfiend(Player* bot); +struct EyeBlastDangerArea +{ + Position start; + Position end; + float width; +}; +EyeBlastDangerArea GetEyeBlastDangerArea(Player* bot); +bool IsPositionInEyeBlastDangerArea(const Position& pos, const EyeBlastDangerArea& area); +GameObject* FindNearestTrap(PlayerbotAI* botAI, Player* bot); + +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.cpp new file mode 100644 index 000000000..3f2cab6bd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.cpp @@ -0,0 +1,787 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BTMultipliers.h" + +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "DruidShapeshiftActions.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "PriestActions.h" +#include "BTActions.h" +#include "BTHelpers.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" +#include "WipeAction.h" + +using namespace BlackTempleHelpers; + +static bool IsDpsCooldownAction(Action* action) +{ + return dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action); +} + +// High Warlord Naj'entus + +float HighWarlordNajentusDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus || najentus->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float HighWarlordNajentusDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "high warlord naj'entus")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +// Supremus + +float SupremusDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus || supremus->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float SupremusFocusOnAvoidanceInPhase2Multiplier::GetValue(Action* action) +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus || supremus->GetVictim() != bot || + !supremus->HasAura(static_cast(BlackTempleSpells::SPELL_SNARE_SELF))) + { + return 1.0f; + } + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float SupremusHitboxIsBuggedMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_ROGUE || + !AI_VALUE2(Unit*, "find target", "supremus")) + { + return 1.0f; + } + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Teron Gorefiend + +float TeronGorefiendDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend || gorefiend->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float TeronGorefiendControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "teron gorefiend")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (botAI->IsRanged(bot) && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float TeronGorefiendMarkedBotOnlyMoveToDieMultiplier::GetValue(Action* action) +{ + Aura* aura = bot->GetAura( + static_cast(BlackTempleSpells::SPELL_SHADOW_OF_DEATH)); + if (!aura || aura->GetDuration() >= 15000) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + else if (!dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(static_cast(BlackTempleSpells::SPELL_SPIRITUAL_VENGEANCE)) || + dynamic_cast(action)) + { + return 1.0f; + } + + if (!dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float TeronGorefiendDisableAttackingConstructsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "teron gorefiend")) + return 1.0f; + + if (bot->GetVictim() != nullptr && + dynamic_cast(action)) + { + return 0.0f; + } + + if (!botAI->IsRangedDps(bot)) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (castSpellAction && + castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + { + return 0.0f; + } + + return 1.0f; +} + +// Gurtogg Bloodboil + +float GurtoggBloodboilDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg || gurtogg->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float GurtoggBloodboilControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "gurtogg bloodboil")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (bot->HasAura(static_cast(BlackTempleSpells::SPELL_PLAYER_FEL_RAGE)) && + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +// Reliquary of Souls + +float ReliquaryOfSoulsDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* suffering = AI_VALUE2(Unit*, "find target", "essence of suffering"); + if (!suffering || suffering->GetHealthPct() < 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float ReliquaryOfSoulsDontWasteHealingMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "essence of suffering")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 1.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +// Mother Shahraz + +float MotherShahrazDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz || shahraz->GetHealthPct() < 90.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float MotherShahrazControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "mother shahraz")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "mother shahraz") || + !bot->HasAura(static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION))) + { + return 1.0f; + } + + if (dynamic_cast(action)) + return 1.0f; + + if (!dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Illidari Council + +float IllidariCouncilDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios || gathios->GetHealthPct() < 90.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidariCouncilDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + { + return 1.0f; + } + + if (bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidariCouncilControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (botAI->IsAssistHealOfIndex(bot, 0, true) && + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + if (!botAI->IsAssistTankOfIndex(bot, 0, false) && + dynamic_cast(action)) + { + return 0.0f; + } + + if ((botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, false) || + botAI->IsAssistTankOfIndex(bot, 1, false) || + GetZerevorMageTank(bot) == bot) && + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidariCouncilControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + { + return 1.0f; + } + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float IllidariCouncilDisableIceBlockMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_MAGE || + !AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + { + return 1.0f; + } + + if (GetZerevorMageTank(bot) != bot) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float IllidariCouncilDisableArcaneShotOnZerevorMultiplier::GetValue(Action* action) +{ + Unit* zerevor = AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); + if (!zerevor) + return 1.0f; + + Unit* target = AI_VALUE(Unit*, "current target"); + if (!target || target->GetGUID() != zerevor->GetGUID()) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float IllidariCouncilWaitForDpsMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, false) || + botAI->IsAssistTankOfIndex(bot, 1, false) || + GetZerevorMageTank(bot) == bot) + { + return 1.0f; + } + + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 5; + + auto it = councilDpsWaitTimer.find(gathios->GetMap()->GetInstanceId()); + if (it == councilDpsWaitTimer.end() || (now - it->second) >= dpsWaitSeconds) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +// Illidan Stormrage + +float IllidanStormrageDelayDpsCooldownsMultiplier::GetValue(Action* action) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return 1.0f; + + if (illidan->GetHealthPct() > 62.0f && + (dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + + if (illidan->GetHealthPct() <= 62.0f || illidan->GetHealthPct() > 95.0f) + return 1.0f; + + if (IsDpsCooldownAction(action) || + (botAI->IsDps(bot) && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageControlTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (GetIllidanPhase(illidan) != 2) + return 1.0f; + + if (botAI->IsMainTank(bot)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + } + else if (botAI->IsAssistTankOfIndex(bot, 0, false) || + botAI->IsAssistTankOfIndex(bot, 1, false)) + { + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageDisableDefaultTargetingMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + int phase = GetIllidanPhase(illidan); + + if (phase == 4 && dynamic_cast(action)) + return 0.0f; + + if (botAI->IsRangedDps(bot)) + { + if (phase != 2) + context->GetValue("neglect threat")->Set(true); + + if (dynamic_cast(action)) + return 0.0f; + } + + constexpr float searchRadius = 40.0f; + Unit* shadowDemon = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_SHADOW_DEMON), searchRadius); + Unit* shadowfiend = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_PARASITIC_SHADOWFIEND), searchRadius); + + if (((shadowDemon && bot->GetTarget() == shadowDemon->GetGUID()) || + (shadowfiend && bot->GetTarget() == shadowfiend->GetGUID())) && + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageControlNonTankMovementMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot)) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + int phase = GetIllidanPhase(illidan); + + if (phase == 2 && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + + if (phase == 4 && botAI->IsHeal(bot) && + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageUseEarthbindTotemMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || GetIllidanPhase(illidan) == 2) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float IllidanStormrageWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + const uint32 instanceId = illidan->GetMap()->GetInstanceId(); + + int phase = GetIllidanPhase(illidan); + + if ((phase == 1 || phase == 3 || phase == 5) && + !botAI->IsMainTank(bot)) + { + constexpr uint8 humanoidPhaseDpsWaitSeconds = 3; + auto it = illidanBossDpsWaitTimer.find(instanceId); + + if ((it == illidanBossDpsWaitTimer.end() || + (now - it->second) < humanoidPhaseDpsWaitSeconds) && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + { + return 0.0f; + } + } + + if (phase == 4 && GetIllidanWarlockTank(bot) != bot) + { + constexpr uint8 demonPhaseDpsWaitSeconds = 8; + auto it = illidanBossDpsWaitTimer.find(instanceId); + + if ((it == illidanBossDpsWaitTimer.end() || + (now - it->second) < demonPhaseDpsWaitSeconds) && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + { + return 0.0f; + } + } + + if (AI_VALUE2(Unit*, "find target", "flame of azzinoth") && + !botAI->IsAssistTankOfIndex(bot, 0, true) && + !botAI->IsAssistTankOfIndex(bot, 1, true)) + { + constexpr uint8 flamePhaseDpsWaitSeconds = 6; + auto it = illidanFlameDpsWaitTimer.find(instanceId); + + if ((it == illidanFlameDpsWaitTimer.end() || + (now - it->second) < flamePhaseDpsWaitSeconds) && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + { + return 0.0f; + } + } + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.h new file mode 100644 index 000000000..9a0877078 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTMultipliers.h @@ -0,0 +1,268 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTMULTIPLIERS_H +#define PLAYERBOTS_BTMULTIPLIERS_H + +#include "Multiplier.h" + +// High Warlord Naj'entus + +class HighWarlordNajentusDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + HighWarlordNajentusDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high warlord naj'entus delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighWarlordNajentusDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + HighWarlordNajentusDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high warlord naj'entus disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + +// Supremus + +class SupremusDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + SupremusDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "supremus delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class SupremusFocusOnAvoidanceInPhase2Multiplier : public Multiplier +{ +public: + SupremusFocusOnAvoidanceInPhase2Multiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "supremus focus on avoidance in phase 2 multiplier") {} + virtual float GetValue(Action* action); +}; + +class SupremusHitboxIsBuggedMultiplier : public Multiplier +{ +public: + SupremusHitboxIsBuggedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "supremus hitbox is bugged multiplier") {} + virtual float GetValue(Action* action); +}; + +// Teron Gorefiend + +class TeronGorefiendDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + TeronGorefiendDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendControlMovementMultiplier : public Multiplier +{ +public: + TeronGorefiendControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendMarkedBotOnlyMoveToDieMultiplier : public Multiplier +{ +public: + TeronGorefiendMarkedBotOnlyMoveToDieMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend marked bot only move to die multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier : public Multiplier +{ +public: + TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend spirits attack only shadowy constructs multiplier") {} + virtual float GetValue(Action* action); +}; + +class TeronGorefiendDisableAttackingConstructsMultiplier : public Multiplier +{ +public: + TeronGorefiendDisableAttackingConstructsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "teron gorefiend disable attacking constructs multiplier") {} + virtual float GetValue(Action* action); +}; + +// Gurtogg Bloodboil + +class GurtoggBloodboilDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + GurtoggBloodboilDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gurtogg bloodboil delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class GurtoggBloodboilControlMovementMultiplier : public Multiplier +{ +public: + GurtoggBloodboilControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gurtogg bloodboil control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +// Reliquary of Souls + +class ReliquaryOfSoulsDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + ReliquaryOfSoulsDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "reliquary of souls delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class ReliquaryOfSoulsDontWasteHealingMultiplier : public Multiplier +{ +public: + ReliquaryOfSoulsDontWasteHealingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "reliquary of souls don't waste healing multiplier") {} + virtual float GetValue(Action* action); +}; + +// Mother Shahraz + +class MotherShahrazDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + MotherShahrazDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "mother shahraz delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class MotherShahrazControlMovementMultiplier : public Multiplier +{ +public: + MotherShahrazControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "mother shahraz control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier : public Multiplier +{ +public: + MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "mother shahraz bots with fatal attraction only run away multiplier") {} + virtual float GetValue(Action* action); +}; + +// Illidari Council + +class IllidariCouncilDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + IllidariCouncilDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilDisableTankActionsMultiplier : public Multiplier +{ +public: + IllidariCouncilDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council disable tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilControlMovementMultiplier : public Multiplier +{ +public: + IllidariCouncilControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilControlMisdirectionMultiplier : public Multiplier +{ +public: + IllidariCouncilControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilDisableArcaneShotOnZerevorMultiplier : public Multiplier +{ +public: + IllidariCouncilDisableArcaneShotOnZerevorMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council disable arcane shot on zerevor multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilDisableIceBlockMultiplier : public Multiplier +{ +public: + IllidariCouncilDisableIceBlockMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council disable ice block multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidariCouncilWaitForDpsMultiplier : public Multiplier +{ +public: + IllidariCouncilWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidari council wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +// Illidan Stormrage + +class IllidanStormrageDelayDpsCooldownsMultiplier : public Multiplier +{ +public: + IllidanStormrageDelayDpsCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage delay dps cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageControlTankActionsMultiplier : public Multiplier +{ +public: + IllidanStormrageControlTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage control tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageDisableDefaultTargetingMultiplier : public Multiplier +{ +public: + IllidanStormrageDisableDefaultTargetingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage disable default targeting multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageControlNonTankMovementMultiplier : public Multiplier +{ +public: + IllidanStormrageControlNonTankMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage control non-tank movement multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageUseEarthbindTotemMultiplier : public Multiplier +{ +public: + IllidanStormrageUseEarthbindTotemMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage use earthbind totem multiplier") {} + virtual float GetValue(Action* action); +}; + +class IllidanStormrageWaitForDpsMultiplier : public Multiplier +{ +public: + IllidanStormrageWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "illidan stormrage wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.cpp new file mode 100644 index 000000000..47dbd9dd8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.cpp @@ -0,0 +1,254 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BTStrategy.h" + +#include "BTMultipliers.h" + +void RaidBlackTempleStrategy::InitTriggers(std::vector& triggers) +{ + // General + triggers.push_back(new TriggerNode("black temple bot is not in combat", { + NextAction("black temple erase timers and trackers", ACTION_EMERGENCY + 11) })); + + // High Warlord Naj'entus + triggers.push_back(new TriggerNode("high warlord naj'entus pulling boss", { + NextAction("high warlord naj'entus misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus boss engaged by tanks", { + NextAction("high warlord naj'entus tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus casts needle spines", { + NextAction("high warlord naj'entus disperse ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus player is impaled", { + NextAction("high warlord naj'entus remove impaling spine", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("high warlord naj'entus boss has tidal shield", { + NextAction("high warlord naj'entus throw impaling spine", ACTION_RAID + 2) })); + + // Supremus + triggers.push_back(new TriggerNode("supremus pulling boss or changing phase", { + NextAction("supremus misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("supremus boss engaged by ranged", { + NextAction("supremus disperse ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("supremus boss is fixated on bot", { + NextAction("supremus kite boss", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("supremus volcano is nearby", { + NextAction("supremus move away from volcanos", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("supremus need to manage phase timer", { + NextAction("supremus manage phase timer", ACTION_EMERGENCY + 10) })); + + // Shade of Akama + triggers.push_back(new TriggerNode("shade of akama killing channelers starts phase 2", { + NextAction("shade of akama melee dps prioritize channelers", ACTION_RAID + 1) })); + + // Teron Gorefiend + triggers.push_back(new TriggerNode("teron gorefiend pulling boss", { + NextAction("teron gorefiend misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("teron gorefiend boss engaged by tanks", { + NextAction("teron gorefiend tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("teron gorefiend boss engaged by ranged", { + NextAction("teron gorefiend position ranged on balcony", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("teron gorefiend boss is casting shadow of death", { + NextAction("teron gorefiend avoid shadow of death", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("teron gorefiend bot has shadow of death", { + NextAction("teron gorefiend move to corner to die", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("teron gorefiend bot transformed into vengeful spirit", { + NextAction("teron gorefiend control and destroy shadowy constructs", ACTION_EMERGENCY + 10) })); + + // Gurtogg Bloodboil + triggers.push_back(new TriggerNode("gurtogg bloodboil pulling boss", { + NextAction("gurtogg bloodboil misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil boss engaged by tanks", { + NextAction("gurtogg bloodboil tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil boss casts bloodboil", { + NextAction("gurtogg bloodboil rotate ranged groups", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil bot has fel rage", { + NextAction("gurtogg bloodboil ranged move away from enraged player", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("gurtogg bloodboil need to manage phase timer", { + NextAction("gurtogg bloodboil manage phase timer", ACTION_EMERGENCY + 10) })); + + // Reliquary of Souls + triggers.push_back(new TriggerNode("reliquary of souls aggro resets upon phase change", { + NextAction("reliquary of souls misdirect boss to main tank", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of suffering fixates on closest target", { + NextAction("reliquary of souls adjust distance from suffering", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of suffering disables healing", { + NextAction("reliquary of souls healers dps suffering", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of desire has rune shield", { + NextAction("reliquary of souls spellsteal rune shield", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("reliquary of souls essence of desire casting deaden", { + NextAction("reliquary of souls spell reflect deaden", ACTION_EMERGENCY + 6) })); + + // Mother Shahraz + triggers.push_back(new TriggerNode("mother shahraz pulling boss", { + NextAction("mother shahraz misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("mother shahraz boss engaged by tanks", { + NextAction("mother shahraz tanks position boss under pillar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("mother shahraz tanks are positioning boss", { + NextAction("mother shahraz melee dps wait at safe position", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("mother shahraz sinister beam knocks back players", { + NextAction("mother shahraz position ranged under pillar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("mother shahraz bots are linked by fatal attraction", { + NextAction("mother shahraz run away to break fatal attraction", ACTION_EMERGENCY + 10) })); + + // Illidari Council + triggers.push_back(new TriggerNode("illidari council pulling bosses", { + NextAction("illidari council misdirect bosses to tanks", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("illidari council gathios engaged by main tank", { + NextAction("illidari council main tank position gathios", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council gathios casting judgement of command", { + NextAction("illidari council main tank reflect judgement of command", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidari council malande engaged by first assist tank", { + NextAction("illidari council first assist tank focus malande", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council darkshadow engaged by second assist tank", { + NextAction("illidari council second assist tank position darkshadow", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council zerevor engaged by mage tank", { + NextAction("illidari council mage tank position zerevor", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("illidari council mage tank needs dedicated healer", { + NextAction("illidari council position mage tank healer", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council zerevor casts dangerous aoes", { + NextAction("illidari council disperse ranged", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("illidari council pets screw up the pull", { + NextAction("illidari council command pets to attack gathios", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("illidari council determining dps assignments", { + NextAction("illidari council assign dps targets", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidari council need to manage dps timer", { + NextAction("illidari council manage dps timer", ACTION_EMERGENCY + 10) })); + + // Illidan Stormrage + triggers.push_back(new TriggerNode("illidan stormrage tank needs aggro", { + NextAction("illidan stormrage misdirect to tank", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss casts flame crash in front of main tank", { + NextAction("illidan stormrage main tank reposition boss", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage bot has parasitic shadowfiend", { + NextAction("illidan stormrage isolate bot with parasite", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("illidan stormrage parasitic shadowfiends run wild", { + NextAction("illidan stormrage set earthbind totem", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss summoned flames of azzinoth", { + NextAction("illidan stormrage assist tanks handle flames of azzinoth", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage pets die to fire", { + NextAction("illidan stormrage control pet aggression", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("illidan stormrage grate is safe from flames", { + NextAction("illidan stormrage position above grate", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("illidan stormrage bot struck by dark barrage", { + NextAction("illidan stormrage remove dark barrage", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss is preparing to land", { + NextAction("illidan stormrage move away from landing point", ACTION_EMERGENCY + 3) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss deals splash damage", { + NextAction("illidan stormrage disperse ranged", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("illidan stormrage this expansion hates melee", { + NextAction("illidan stormrage melee go somewhere to not die", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss transforms into demon", { + NextAction("illidan stormrage warlock tank handle demon boss", ACTION_EMERGENCY + 9) })); + + triggers.push_back(new TriggerNode("illidan stormrage boss spawns adds", { + NextAction("illidan stormrage dps prioritize adds", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage maiev placed shadow trap", { + NextAction("illidan stormrage use shadow trap", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("illidan stormrage need to manage dps timer and rti", { + NextAction("illidan stormrage manage dps timer and rti", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("illidan stormrage need to clear hazards between phases", { + NextAction("illidan stormrage destroy hazards", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("illidan stormrage cheat", { + NextAction("illidan stormrage handle adds cheat", ACTION_EMERGENCY + 10) })); +} + +void RaidBlackTempleStrategy::InitMultipliers(std::vector& multipliers) +{ + // High Warlord Naj'entus + multipliers.push_back(new HighWarlordNajentusDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new HighWarlordNajentusDisableCombatFormationMoveMultiplier(botAI)); + + // Supremus + multipliers.push_back(new SupremusDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new SupremusFocusOnAvoidanceInPhase2Multiplier(botAI)); + multipliers.push_back(new SupremusHitboxIsBuggedMultiplier(botAI)); + + // Teron Gorefiend + multipliers.push_back(new TeronGorefiendDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendControlMovementMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendMarkedBotOnlyMoveToDieMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendSpiritsAttackOnlyShadowyConstructsMultiplier(botAI)); + multipliers.push_back(new TeronGorefiendDisableAttackingConstructsMultiplier(botAI)); + + // Gurtogg Bloodboil + multipliers.push_back(new GurtoggBloodboilDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new GurtoggBloodboilControlMovementMultiplier(botAI)); + + // Reliquary of Souls + multipliers.push_back(new ReliquaryOfSoulsDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new ReliquaryOfSoulsDontWasteHealingMultiplier(botAI)); + + // Mother Shahraz + multipliers.push_back(new MotherShahrazDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new MotherShahrazControlMovementMultiplier(botAI)); + multipliers.push_back(new MotherShahrazBotsWithFatalAttractionOnlyRunAwayMultiplier(botAI)); + + // Illidari Council + multipliers.push_back(new IllidariCouncilDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilControlMovementMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilDisableArcaneShotOnZerevorMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilDisableIceBlockMultiplier(botAI)); + multipliers.push_back(new IllidariCouncilWaitForDpsMultiplier(botAI)); + + // Illidan Stormrage + multipliers.push_back(new IllidanStormrageDelayDpsCooldownsMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageControlTankActionsMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageDisableDefaultTargetingMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageControlNonTankMovementMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageUseEarthbindTotemMultiplier(botAI)); + multipliers.push_back(new IllidanStormrageWaitForDpsMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.h new file mode 100644 index 000000000..6c1158809 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTStrategy.h @@ -0,0 +1,23 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTSTRATEGY_H +#define PLAYERBOTS_BTSTRATEGY_H + +#include "Strategy.h" + +class RaidBlackTempleStrategy : public Strategy +{ +public: + RaidBlackTempleStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "blacktemple"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggerContext.h new file mode 100644 index 000000000..f1ad8d12c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggerContext.h @@ -0,0 +1,417 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTTRIGGERCONTEXT_H +#define PLAYERBOTS_BTTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "BTTriggers.h" + +class RaidBlackTempleTriggerContext : public NamedObjectContext +{ +public: + RaidBlackTempleTriggerContext() + { + // General + creators["black temple bot is not in combat"] = + &RaidBlackTempleTriggerContext::black_temple_bot_is_not_in_combat; + + // High Warlord Naj'entus + creators["high warlord naj'entus pulling boss"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_pulling_boss; + + creators["high warlord naj'entus boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_boss_engaged_by_tanks; + + creators["high warlord naj'entus casts needle spines"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_casts_needle_spines; + + creators["high warlord naj'entus player is impaled"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_player_is_impaled; + + creators["high warlord naj'entus boss has tidal shield"] = + &RaidBlackTempleTriggerContext::high_warlord_najentus_boss_has_tidal_shield; + + // Supremus + creators["supremus pulling boss or changing phase"] = + &RaidBlackTempleTriggerContext::supremus_pulling_boss_or_changing_phase; + + creators["supremus boss engaged by ranged"] = + &RaidBlackTempleTriggerContext::supremus_boss_engaged_by_ranged; + + creators["supremus boss is fixated on bot"] = + &RaidBlackTempleTriggerContext::supremus_boss_is_fixated_on_bot; + + creators["supremus volcano is nearby"] = + &RaidBlackTempleTriggerContext::supremus_volcano_is_nearby; + + creators["supremus need to manage phase timer"] = + &RaidBlackTempleTriggerContext::supremus_need_to_manage_phase_timer; + + // Shade of Akama + creators["shade of akama killing channelers starts phase 2"] = + &RaidBlackTempleTriggerContext::shade_of_akama_killing_channelers_starts_phase_2; + + // Teron Gorefiend + creators["teron gorefiend pulling boss"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_pulling_boss; + + creators["teron gorefiend boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_boss_engaged_by_tanks; + + creators["teron gorefiend boss engaged by ranged"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_boss_engaged_by_ranged; + + creators["teron gorefiend boss is casting shadow of death"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_boss_is_casting_shadow_of_death; + + creators["teron gorefiend bot has shadow of death"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_bot_has_shadow_of_death; + + creators["teron gorefiend bot transformed into vengeful spirit"] = + &RaidBlackTempleTriggerContext::teron_gorefiend_bot_transformed_into_vengeful_spirit; + + // Gurtogg Bloodboil + creators["gurtogg bloodboil pulling boss"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_pulling_boss; + + creators["gurtogg bloodboil boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_boss_engaged_by_tanks; + + creators["gurtogg bloodboil boss casts bloodboil"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_boss_casts_bloodboil; + + creators["gurtogg bloodboil bot has fel rage"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_bot_has_fel_rage; + + creators["gurtogg bloodboil need to manage phase timer"] = + &RaidBlackTempleTriggerContext::gurtogg_bloodboil_need_to_manage_phase_timer; + + // Reliquary of Souls + creators["reliquary of souls aggro resets upon phase change"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_aggro_resets_upon_phase_change; + + creators["reliquary of souls essence of suffering fixates on closest target"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_suffering_fixates_on_closest_target; + + creators["reliquary of souls essence of suffering disables healing"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_suffering_disables_healing; + + creators["reliquary of souls essence of desire has rune shield"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_desire_has_rune_shield; + + creators["reliquary of souls essence of desire casting deaden"] = + &RaidBlackTempleTriggerContext::reliquary_of_souls_essence_of_desire_casting_deaden; + + // Mother Shahraz + creators["mother shahraz pulling boss"] = + &RaidBlackTempleTriggerContext::mother_shahraz_pulling_boss; + + creators["mother shahraz boss engaged by tanks"] = + &RaidBlackTempleTriggerContext::mother_shahraz_boss_engaged_by_tanks; + + creators["mother shahraz tanks are positioning boss"] = + &RaidBlackTempleTriggerContext::mother_shahraz_tanks_are_positioning_boss; + + creators["mother shahraz sinister beam knocks back players"] = + &RaidBlackTempleTriggerContext::mother_shahraz_sinister_beam_knocks_back_players; + + creators["mother shahraz bots are linked by fatal attraction"] = + &RaidBlackTempleTriggerContext::mother_shahraz_bots_are_linked_by_fatal_attraction; + + // Illidari Council + creators["illidari council pulling bosses"] = + &RaidBlackTempleTriggerContext::illidari_council_pulling_bosses; + + creators["illidari council gathios engaged by main tank"] = + &RaidBlackTempleTriggerContext::illidari_council_gathios_engaged_by_main_tank; + + creators["illidari council gathios casting judgement of command"] = + &RaidBlackTempleTriggerContext::illidari_council_gathios_casting_judgement_of_command; + + creators["illidari council malande engaged by first assist tank"] = + &RaidBlackTempleTriggerContext::illidari_council_malande_engaged_by_first_assist_tank; + + creators["illidari council darkshadow engaged by second assist tank"] = + &RaidBlackTempleTriggerContext::illidari_council_darkshadow_engaged_by_second_assist_tank; + + creators["illidari council zerevor engaged by mage tank"] = + &RaidBlackTempleTriggerContext::illidari_council_zerevor_engaged_by_mage_tank; + + creators["illidari council mage tank needs dedicated healer"] = + &RaidBlackTempleTriggerContext::illidari_council_mage_tank_needs_dedicated_healer; + + creators["illidari council zerevor casts dangerous aoes"] = + &RaidBlackTempleTriggerContext::illidari_council_zerevor_casts_dangerous_aoes; + + creators["illidari council pets screw up the pull"] = + &RaidBlackTempleTriggerContext::illidari_council_pets_screw_up_the_pull; + + creators["illidari council determining dps assignments"] = + &RaidBlackTempleTriggerContext::illidari_council_determining_dps_assignments; + + creators["illidari council need to manage dps timer"] = + &RaidBlackTempleTriggerContext::illidari_council_need_to_manage_dps_timer; + + // Illidan Stormrage + creators["illidan stormrage tank needs aggro"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_tank_needs_aggro; + + creators["illidan stormrage boss casts flame crash in front of main tank"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_casts_flame_crash_in_front_of_main_tank; + + creators["illidan stormrage bot has parasitic shadowfiend"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_bot_has_parasitic_shadowfiend; + + creators["illidan stormrage parasitic shadowfiends run wild"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_parasitic_shadowfiends_run_wild; + + creators["illidan stormrage boss summoned flames of azzinoth"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_summoned_flames_of_azzinoth; + + creators["illidan stormrage pets die to fire"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_pets_die_to_fire; + + creators["illidan stormrage grate is safe from flames"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_grate_is_safe_from_flames; + + creators["illidan stormrage bot struck by dark barrage"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_bot_struck_by_dark_barrage; + + creators["illidan stormrage boss is preparing to land"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_is_preparing_to_land; + + creators["illidan stormrage boss deals splash damage"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_deals_splash_damage; + + creators["illidan stormrage this expansion hates melee"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_this_expansion_hates_melee; + + creators["illidan stormrage boss transforms into demon"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_transforms_into_demon; + + creators["illidan stormrage boss spawns adds"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_boss_spawns_adds; + + creators["illidan stormrage maiev placed shadow trap"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_maiev_placed_shadow_trap; + + creators["illidan stormrage need to manage dps timer and rti"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_need_to_manage_dps_timer_and_rti; + + creators["illidan stormrage need to clear hazards between phases"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_need_to_clear_hazards_between_phases; + + creators["illidan stormrage cheat"] = + &RaidBlackTempleTriggerContext::illidan_stormrage_cheat; + } + +private: + // General + static Trigger* black_temple_bot_is_not_in_combat(PlayerbotAI* botAI) { + return new BlackTempleBotIsNotInCombatTrigger(botAI); + } + + // High Warlord Naj'entus + static Trigger* high_warlord_najentus_pulling_boss(PlayerbotAI* botAI) { + return new HighWarlordNajentusPullingBossTrigger(botAI); + } + static Trigger* high_warlord_najentus_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new HighWarlordNajentusBossEngagedByTanksTrigger(botAI); + } + static Trigger* high_warlord_najentus_casts_needle_spines(PlayerbotAI* botAI) { + return new HighWarlordNajentusCastsNeedleSpinesTrigger(botAI); + } + static Trigger* high_warlord_najentus_player_is_impaled(PlayerbotAI* botAI) { + return new HighWarlordNajentusPlayerIsImpaledTrigger(botAI); + } + static Trigger* high_warlord_najentus_boss_has_tidal_shield(PlayerbotAI* botAI) { + return new HighWarlordNajentusBossHasTidalShieldTrigger(botAI); + } + + // Supremus + static Trigger* supremus_pulling_boss_or_changing_phase(PlayerbotAI* botAI) { + return new SupremusPullingBossOrChangingPhaseTrigger(botAI); + } + static Trigger* supremus_boss_engaged_by_ranged(PlayerbotAI* botAI) { + return new SupremusBossEngagedByRangedTrigger(botAI); + } + static Trigger* supremus_boss_is_fixated_on_bot(PlayerbotAI* botAI) { + return new SupremusBossIsFixatedOnBotTrigger(botAI); + } + static Trigger* supremus_volcano_is_nearby(PlayerbotAI* botAI) { + return new SupremusVolcanoIsNearbyTrigger(botAI); + } + static Trigger* supremus_need_to_manage_phase_timer(PlayerbotAI* botAI) { + return new SupremusNeedToManagePhaseTimerTrigger(botAI); + } + + // Shade of Akama + static Trigger* shade_of_akama_killing_channelers_starts_phase_2(PlayerbotAI* botAI) { + return new ShadeOfAkamaKillingChannelersStartsPhase2Trigger(botAI); + } + + // Teron Gorefiend + static Trigger* teron_gorefiend_pulling_boss(PlayerbotAI* botAI) { + return new TeronGorefiendPullingBossTrigger(botAI); + } + static Trigger* teron_gorefiend_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new TeronGorefiendBossEngagedByTanksTrigger(botAI); + } + static Trigger* teron_gorefiend_boss_engaged_by_ranged(PlayerbotAI* botAI) { + return new TeronGorefiendBossEngagedByRangedTrigger(botAI); + } + static Trigger* teron_gorefiend_boss_is_casting_shadow_of_death(PlayerbotAI* botAI) { + return new TeronGorefiendBossIsCastingShadowOfDeathTrigger(botAI); + } + static Trigger* teron_gorefiend_bot_has_shadow_of_death(PlayerbotAI* botAI) { + return new TeronGorefiendBotHasShadowOfDeathTrigger(botAI); + } + static Trigger* teron_gorefiend_bot_transformed_into_vengeful_spirit(PlayerbotAI* botAI) { + return new TeronGorefiendBotTransformedIntoVengefulSpiritTrigger(botAI); + } + + // Gurtogg Bloodboil + static Trigger* gurtogg_bloodboil_pulling_boss(PlayerbotAI* botAI) { + return new GurtoggBloodboilPullingBossTrigger(botAI); + } + static Trigger* gurtogg_bloodboil_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new GurtoggBloodboilBossEngagedByTanksTrigger(botAI); + } + static Trigger* gurtogg_bloodboil_boss_casts_bloodboil(PlayerbotAI* botAI) { + return new GurtoggBloodboilBossCastsBloodboilTrigger(botAI); + } + static Trigger* gurtogg_bloodboil_bot_has_fel_rage(PlayerbotAI* botAI) { + return new GurtoggBloodboilBotHasFelRageTrigger(botAI); + } + static Trigger* gurtogg_bloodboil_need_to_manage_phase_timer(PlayerbotAI* botAI) { + return new GurtoggBloodboilNeedToManagePhaseTimerTrigger(botAI); + } + + // Reliquary of Souls + static Trigger* reliquary_of_souls_aggro_resets_upon_phase_change(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger(botAI); + } + static Trigger* reliquary_of_souls_essence_of_suffering_fixates_on_closest_target(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger(botAI); + } + static Trigger* reliquary_of_souls_essence_of_suffering_disables_healing(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger(botAI); + } + static Trigger* reliquary_of_souls_essence_of_desire_has_rune_shield(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger(botAI); + } + static Trigger* reliquary_of_souls_essence_of_desire_casting_deaden(PlayerbotAI* botAI) { + return new ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger(botAI); + } + + // Mother Shahraz + static Trigger* mother_shahraz_pulling_boss(PlayerbotAI* botAI) { + return new MotherShahrazPullingBossTrigger(botAI); + } + static Trigger* mother_shahraz_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new MotherShahrazBossEngagedByTanksTrigger(botAI); + } + static Trigger* mother_shahraz_tanks_are_positioning_boss(PlayerbotAI* botAI) { + return new MotherShahrazTanksArePositioningBossTrigger(botAI); + } + static Trigger* mother_shahraz_sinister_beam_knocks_back_players(PlayerbotAI* botAI) { + return new MotherShahrazSinisterBeamKnocksBackPlayersTrigger(botAI); + } + static Trigger* mother_shahraz_bots_are_linked_by_fatal_attraction(PlayerbotAI* botAI) { + return new MotherShahrazBotsAreLinkedByFatalAttractionTrigger(botAI); + } + + // Illidari Council + static Trigger* illidari_council_pulling_bosses(PlayerbotAI* botAI) { + return new IllidariCouncilPullingBossesTrigger(botAI); + } + static Trigger* illidari_council_gathios_engaged_by_main_tank(PlayerbotAI* botAI) { + return new IllidariCouncilGathiosEngagedByMainTankTrigger(botAI); + } + static Trigger* illidari_council_gathios_casting_judgement_of_command(PlayerbotAI* botAI) { + return new IllidariCouncilGathiosCastingJudgementOfCommandTrigger(botAI); + } + static Trigger* illidari_council_malande_engaged_by_first_assist_tank(PlayerbotAI* botAI) { + return new IllidariCouncilMalandeEngagedByFirstAssistTankTrigger(botAI); + } + static Trigger* illidari_council_darkshadow_engaged_by_second_assist_tank(PlayerbotAI* botAI) { + return new IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger(botAI); + } + static Trigger* illidari_council_zerevor_engaged_by_mage_tank(PlayerbotAI* botAI) { + return new IllidariCouncilZerevorEngagedByMageTankTrigger(botAI); + } + static Trigger* illidari_council_mage_tank_needs_dedicated_healer(PlayerbotAI* botAI) { + return new IllidariCouncilMageTankNeedsDedicatedHealerTrigger(botAI); + } + static Trigger* illidari_council_zerevor_casts_dangerous_aoes(PlayerbotAI* botAI) { + return new IllidariCouncilZerevorCastsDangerousAoesTrigger(botAI); + } + static Trigger* illidari_council_pets_screw_up_the_pull(PlayerbotAI* botAI) { + return new IllidariCouncilPetsScrewUpThePullTrigger(botAI); + } + static Trigger* illidari_council_determining_dps_assignments(PlayerbotAI* botAI) { + return new IllidariCouncilDeterminingDpsAssignmentsTrigger(botAI); + } + static Trigger* illidari_council_need_to_manage_dps_timer(PlayerbotAI* botAI) { + return new IllidariCouncilNeedToManageDpsTimerTrigger(botAI); + } + + // Illidan Stormrage + static Trigger* illidan_stormrage_tank_needs_aggro(PlayerbotAI* botAI) { + return new IllidanStormrageTankNeedsAggroTrigger(botAI); + } + static Trigger* illidan_stormrage_boss_casts_flame_crash_in_front_of_main_tank(PlayerbotAI* botAI) { + return new IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger(botAI); + } + static Trigger* illidan_stormrage_bot_has_parasitic_shadowfiend(PlayerbotAI* botAI) { + return new IllidanStormrageBotHasParasiticShadowfiendTrigger(botAI); + } + static Trigger* illidan_stormrage_parasitic_shadowfiends_run_wild(PlayerbotAI* botAI) { + return new IllidanStormrageParasiticShadowfiendsRunWildTrigger(botAI); + } + static Trigger* illidan_stormrage_boss_summoned_flames_of_azzinoth(PlayerbotAI* botAI) { + return new IllidanStormrageBossSummonedFlamesOfAzzinothTrigger(botAI); + } + static Trigger* illidan_stormrage_pets_die_to_fire(PlayerbotAI* botAI) { + return new IllidanStormragePetsDieToFireTrigger(botAI); + } + static Trigger* illidan_stormrage_grate_is_safe_from_flames(PlayerbotAI* botAI) { + return new IllidanStormrageGrateIsSafeFromFlamesTrigger(botAI); + } + static Trigger* illidan_stormrage_bot_struck_by_dark_barrage(PlayerbotAI* botAI) { + return new IllidanStormrageBotStruckByDarkBarrageTrigger(botAI); + } + static Trigger* illidan_stormrage_boss_is_preparing_to_land(PlayerbotAI* botAI) { + return new IllidanStormrageBossIsPreparingToLandTrigger(botAI); + } + static Trigger* illidan_stormrage_boss_deals_splash_damage(PlayerbotAI* botAI) { + return new IllidanStormrageBossDealsSplashDamageTrigger(botAI); + } + static Trigger* illidan_stormrage_this_expansion_hates_melee(PlayerbotAI* botAI) { + return new IllidanStormrageThisExpansionHatesMeleeTrigger(botAI); + } + static Trigger* illidan_stormrage_boss_transforms_into_demon(PlayerbotAI* botAI) { + return new IllidanStormrageBossTransformsIntoDemonTrigger(botAI); + } + static Trigger* illidan_stormrage_boss_spawns_adds(PlayerbotAI* botAI) { + return new IllidanStormrageBossSpawnsAddsTrigger(botAI); + } + static Trigger* illidan_stormrage_maiev_placed_shadow_trap(PlayerbotAI* botAI) { + return new IllidanStormrageMaievPlacedShadowTrapTrigger(botAI); + } + static Trigger* illidan_stormrage_need_to_manage_dps_timer_and_rti(PlayerbotAI* botAI) { + return new IllidanStormrageNeedToManageDpsTimerAndRtiTrigger(botAI); + } + static Trigger* illidan_stormrage_need_to_clear_hazards_between_phases(PlayerbotAI* botAI) { + return new IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger(botAI); + } + static Trigger* illidan_stormrage_cheat(PlayerbotAI* botAI) { + return new IllidanStormrageCheatTrigger(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.cpp new file mode 100644 index 000000000..5fc88ed3e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.cpp @@ -0,0 +1,846 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BTTriggers.h" + +#include "AiFactory.h" +#include "Playerbots.h" +#include "BTActions.h" +#include "BTHelpers.h" +#include "RaidBossHelpers.h" +#include "SharedDefines.h" + +using namespace BlackTempleHelpers; + +// General + +bool BlackTempleBotIsNotInCombatTrigger::IsActive() +{ + return bot->GetMapId() == BLACK_TEMPLE_MAP_ID && + !AI_VALUE2(bool, "combat", "self target"); +} + +// High Warlord Naj'entus + +bool HighWarlordNajentusPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + return najentus && najentus->GetHealthPct() > 95.0f; +} + +bool HighWarlordNajentusBossEngagedByTanksTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); +} + +bool HighWarlordNajentusCastsNeedleSpinesTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); +} + +bool HighWarlordNajentusPlayerIsImpaledTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "high warlord naj'entus")) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* impaledPlayer = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; + + if (member->HasAura( + static_cast(BlackTempleSpells::SPELL_IMPALING_SPINE))) + { + impaledPlayer = member; + break; + } + } + + Player* closestBot = nullptr; + float closestDist = std::numeric_limits::max(); + + if (impaledPlayer) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == impaledPlayer || + !GET_PLAYERBOT_AI(member) || botAI->IsTank(member)) + { + continue; + } + + float dist = member->GetDistance(impaledPlayer); + if (dist < closestDist) + { + closestDist = dist; + closestBot = member; + } + } + } + + return closestBot == bot; +} + +bool HighWarlordNajentusBossHasTidalShieldTrigger::IsActive() +{ + Unit* najentus = AI_VALUE2(Unit*, "find target", "high warlord naj'entus"); + if (!najentus || !najentus->HasAura( + static_cast(BlackTempleSpells::SPELL_TIDAL_SHIELD))) + { + return false; + } + + return botAI->HasItemInInventory( + static_cast(BlackTempleItems::ITEM_NAJENTUS_SPINE)); +} + +// Supremus + +bool SupremusPullingBossOrChangingPhaseTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + if (!supremus) + return false; + + auto it = supremusPhaseTimer.find(supremus->GetMap()->GetInstanceId()); + if (it == supremusPhaseTimer.end()) + return false; + + const time_t now = time(nullptr); + const time_t elapsed = now - it->second; + + // Active during first 10 seconds, or during 60-70, 120-130, etc. + return (elapsed < 10) || ((elapsed % 60) < 10 && elapsed >= 60); +} + +bool SupremusBossEngagedByRangedTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + return supremus && !supremus->HasAura( + static_cast(BlackTempleSpells::SPELL_SNARE_SELF)); +} + +bool SupremusBossIsFixatedOnBotTrigger::IsActive() +{ + Unit* supremus = AI_VALUE2(Unit*, "find target", "supremus"); + return supremus && supremus->GetVictim() == bot && + supremus->HasAura(static_cast( + BlackTempleSpells::SPELL_SNARE_SELF)); +} + +bool SupremusVolcanoIsNearbyTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "supremus") && + HasSupremusVolcanoNearby(botAI, bot); +} + +bool SupremusNeedToManagePhaseTimerTrigger::IsActive() +{ + return IsMechanicTrackerBot(bot, BLACK_TEMPLE_MAP_ID) && + AI_VALUE2(Unit*, "find target", "supremus"); +} + +// Shade of Akama + +bool ShadeOfAkamaKillingChannelersStartsPhase2Trigger::IsActive() +{ + if (!botAI->IsDps(bot) || !botAI->IsMelee(bot)) + return false; + + constexpr float searchRadius = 30.0f; + Unit* channeler = bot->FindNearestCreature( + static_cast(BlackTempleNpcs::NPC_ASHTONGUE_CHANNELER), + searchRadius, true); + + return channeler && !channeler->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE); +} + +// Teron Gorefiend + +bool TeronGorefiendPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* gorefiend = + AI_VALUE2(Unit*, "find target", "teron gorefiend"); + + return gorefiend && gorefiend->GetHealthPct() > 95.0f; +} + +bool TeronGorefiendBossEngagedByTanksTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "teron gorefiend"); +} + +bool TeronGorefiendBossEngagedByRangedTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "teron gorefiend"); +} + +bool TeronGorefiendBossIsCastingShadowOfDeathTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER && bot->getClass() != CLASS_MAGE && + bot->getClass() != CLASS_PALADIN && bot->getClass() != CLASS_ROGUE) + { + return false; + } + + Unit* gorefiend = AI_VALUE2(Unit*, "find target", "teron gorefiend"); + if (!gorefiend) + return false; + + if (botAI->HasAura("feign death", bot)) + { + botAI->RemoveAura("feign death"); + return true; + } + else if (botAI->HasAura("ice block", bot)) + { + botAI->RemoveAura("ice block"); + return true; + } + else if (!botAI->IsHeal(bot) && botAI->HasAura("divine shield", bot)) + { + botAI->RemoveAura("divine shield"); + return true; + } + + if (!gorefiend->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* spell = gorefiend->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell || spell->m_spellInfo->Id != + static_cast(BlackTempleSpells::SPELL_SHADOW_OF_DEATH)) + { + return false; + } + + Unit* target = spell->m_targets.GetUnitTarget(); + return target && target->GetGUID() == bot->GetGUID(); +} + +bool TeronGorefiendBotHasShadowOfDeathTrigger::IsActive() +{ + Aura* aura = bot->GetAura( + static_cast(BlackTempleSpells::SPELL_SHADOW_OF_DEATH)); + return aura && aura->GetDuration() < 12000; +} + +bool TeronGorefiendBotTransformedIntoVengefulSpiritTrigger::IsActive() +{ + return bot->HasAura( + static_cast(BlackTempleSpells::SPELL_SPIRITUAL_VENGEANCE)); +} + +// Gurtogg Bloodboil + +bool GurtoggBloodboilPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg) + return false; + + auto it = gurtoggPhaseTimer.find(gurtogg->GetMap()->GetInstanceId()); + if (it == gurtoggPhaseTimer.end()) + return false; + + const time_t elapsed = std::time(nullptr) - it->second; + return elapsed < 10; +} + +bool GurtoggBloodboilBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + return gurtogg && !gurtogg->HasAura( + static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE)); +} + +bool GurtoggBloodboilBossCastsBloodboilTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + return gurtogg && !gurtogg->HasAura( + static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE)); +} + +bool GurtoggBloodboilBotHasFelRageTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* gurtogg = AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); + if (!gurtogg || !gurtogg->HasAura( + static_cast(BlackTempleSpells::SPELL_BOSS_FEL_RAGE))) + { + return false; + } + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasAura + (static_cast(BlackTempleSpells::SPELL_PLAYER_FEL_RAGE))) + { + return true; + } + } + } + + return false; +} + +bool GurtoggBloodboilNeedToManagePhaseTimerTrigger::IsActive() +{ + return IsMechanicTrackerBot(bot, BLACK_TEMPLE_MAP_ID) && + AI_VALUE2(Unit*, "find target", "gurtogg bloodboil"); +} + +// Reliquary of Souls + +bool ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger::IsActive() +{ + return bot->getClass() == CLASS_HUNTER && + AI_VALUE2(Unit*, "find target", "reliquary of the lost"); +} + +bool ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "essence of suffering"); +} + +bool ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger::IsActive() +{ + if (!botAI->IsHeal(bot)) + return false; + + if (bot->getClass() == CLASS_PRIEST && + AiFactory::GetPlayerSpecTab(bot) == PRIEST_TAB_DISCIPLINE) + { + return false; + } + + return AI_VALUE2(Unit*, "find target", "essence of suffering"); +} + +bool ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE) + return false; + + Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + return desire && desire->HasAura( + static_cast(BlackTempleSpells::SPELL_RUNE_SHIELD)); +} + +bool ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger::IsActive() +{ + if (!botAI->IsTank(bot) || bot->getClass() != CLASS_WARRIOR) + return false; + + Unit* desire = AI_VALUE2(Unit*, "find target", "essence of desire"); + if (!desire || !desire->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* spell = desire->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell || spell->m_spellInfo->Id != + static_cast(BlackTempleSpells::SPELL_DEADEN)) + { + return false; + } + + Unit* target = spell->m_targets.GetUnitTarget(); + return target && target->GetGUID() == bot->GetGUID(); +} + +// Mother Shahraz + +bool MotherShahrazPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + return shahraz && shahraz->GetHealthPct() > 95.0f; +} + +bool MotherShahrazBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "mother shahraz")) + return false; + + return !bot->HasAura( + static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION)); +} + +bool MotherShahrazTanksArePositioningBossTrigger::IsActive() +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* shahraz = AI_VALUE2(Unit*, "find target", "mother shahraz"); + if (!shahraz || shahraz->GetHealthPct() < 90.0f) + return false; + + TankPositionState tankState = GetShahrazTankPositionState(botAI, bot); + return tankState != TankPositionState::Positioned; +} + +bool MotherShahrazSinisterBeamKnocksBackPlayersTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "mother shahraz")) + return false; + + return !bot->HasAura( + static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION)); +} + +bool MotherShahrazBotsAreLinkedByFatalAttractionTrigger::IsActive() +{ + return bot->HasAura( + static_cast(BlackTempleSpells::SPELL_FATAL_ATTRACTION)); +} + +// Illidari Council + +bool IllidariCouncilPullingBossesTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + return gathios && gathios->GetHealthPct() > 95.0f; +} + +bool IllidariCouncilGathiosEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "gathios the shatterer"); +} + +bool IllidariCouncilGathiosCastingJudgementOfCommandTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARRIOR || !botAI->IsMainTank(bot)) + return false; + + Unit* gathios = AI_VALUE2(Unit*, "find target", "gathios the shatterer"); + if (!gathios || !gathios->HasUnitState(UNIT_STATE_CASTING) || !gathios->HasAura( + static_cast(BlackTempleSpells::SPELL_SEAL_OF_COMMAND))) + { + return false; + } + + Spell* spell = gathios->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell || spell->m_spellInfo->Id != + static_cast(BlackTempleSpells::SPELL_JUDGEMENT)) + { + return false; + } + + Unit* target = spell->m_targets.GetUnitTarget(); + return target && target->GetGUID() == bot->GetGUID(); +} + +bool IllidariCouncilMalandeEngagedByFirstAssistTankTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 0, false) && + AI_VALUE2(Unit*, "find target", "lady malande"); +} + +bool IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, false)) + return false; + + Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + return darkshadow && !darkshadow->HasAura( + static_cast(BlackTempleSpells::SPELL_VANISH)); +} + +bool IllidariCouncilZerevorEngagedByMageTankTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE || GetZerevorMageTank(bot) != bot) + return false; + + return AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); +} + +bool IllidariCouncilMageTankNeedsDedicatedHealerTrigger::IsActive() +{ + return botAI->IsAssistHealOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "high nethermancer zerevor"); +} + +bool IllidariCouncilZerevorCastsDangerousAoesTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "high nethermancer zerevor")) + return false; + + return !HasDangerousCouncilAura(bot); +} + +bool IllidariCouncilPetsScrewUpThePullTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER && bot->getClass() != CLASS_WARLOCK) + return false; + + Pet* pet = bot->GetPet(); + if (!pet || !pet->IsAlive()) + return false; + + return AI_VALUE2(Unit*, "find target", "gathios the shatterer"); +} + +bool IllidariCouncilDeterminingDpsAssignmentsTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "gathios the shatterer")) + return false; + + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, false) || + GetZerevorMageTank(bot) == bot) + { + return false; + } + + Unit* darkshadow = AI_VALUE2(Unit*, "find target", "veras darkshadow"); + if (botAI->IsTank(bot) && botAI->IsAssistTankOfIndex(bot, 1, false) && + darkshadow && !darkshadow->HasAura( + static_cast(BlackTempleSpells::SPELL_VANISH))) + { + return false; + } + + return true; +} + +bool IllidariCouncilNeedToManageDpsTimerTrigger::IsActive() +{ + return IsMechanicTrackerBot(bot, BLACK_TEMPLE_MAP_ID) && + AI_VALUE2(Unit*, "find target", "gathios the shatterer"); +} + +// Illidan Stormrage + +bool IllidanStormrageTankNeedsAggroTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && illidan->GetHealth() > 1; +} + +bool IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + return phase == 1 || phase == 3 || phase == 5; +} + +bool IllidanStormrageBotHasParasiticShadowfiendTrigger::IsActive() +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1 || + illidan->GetVictim() == bot) + { + return false; + } + + int phase = GetIllidanPhase(illidan); + if (phase == 2 || phase == 4) + return false; + + if (botAI->IsMainTank(bot)) + return false; + + if (phase == 5 && FindNearestTrap(botAI, bot)) + return false; + + Player* infected = GetBotWithParasiticShadowfiend(bot); + if (!infected) + return false; + + if (infected == bot || + (phase != 1 && bot->getClass() == CLASS_HUNTER)) + { + return true; + } + + return false; +} + +bool IllidanStormrageParasiticShadowfiendsRunWildTrigger::IsActive() +{ + if (bot->getClass() != CLASS_SHAMAN) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1 || GetIllidanPhase(illidan) == 2) + return false; + + ObjectGuid guid = bot->m_SummonSlot[SUMMON_SLOT_TOTEM_EARTH]; + if (guid.IsEmpty()) + return true; + + Creature* totem = bot->GetMap()->GetCreature(guid); + return !totem || totem->GetDistance(bot) > 20.0f || + totem->GetUInt32Value(UNIT_CREATED_BY_SPELL) != + static_cast(BlackTempleSpells::SPELL_EARTHBIND_TOTEM); +} + +bool IllidanStormrageBossSummonedFlamesOfAzzinothTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, true) && + !botAI->IsAssistTankOfIndex(bot, 1, true)) + { + return false; + } + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 2; +} + +bool IllidanStormragePetsDieToFireTrigger::IsActive() +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + Pet* pet = bot->GetPet(); + return pet && pet->IsAlive(); +} + +bool IllidanStormrageGrateIsSafeFromFlamesTrigger::IsActive() +{ + if (botAI->IsAssistTankOfIndex(bot, 0, true) || + botAI->IsAssistTankOfIndex(bot, 1, true)) + { + return false; + } + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 2; +} + +bool IllidanStormrageBotStruckByDarkBarrageTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE && bot->getClass() != CLASS_PALADIN && + bot->getClass() != CLASS_ROGUE) + { + return false; + } + + if (!AI_VALUE2(Unit*, "find target", "illidan stormrage")) + return false; + + if (botAI->HasAura("ice block", bot)) + { + botAI->RemoveAura("ice block"); + return true; + } + else if (!botAI->IsHeal(bot) && botAI->HasAura("divine shield", bot)) + { + botAI->RemoveAura("divine shield"); + return true; + } + + return bot->HasAura( + static_cast(BlackTempleSpells::SPELL_DARK_BARRAGE)); +} + +bool IllidanStormrageBossIsPreparingToLandTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 0; +} + +bool IllidanStormrageBossDealsSplashDamageTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->HasAura + (static_cast(BlackTempleSpells::SPELL_CAGED))) + { + return false; + } + + int phase = GetIllidanPhase(illidan); + + if (phase == 4 && GetIllidanWarlockTank(bot) == bot) + return false; + + return phase == 3 || phase == 4 || phase == 5; +} + +bool IllidanStormrageThisExpansionHatesMeleeTrigger::IsActive() +{ + if (!botAI->IsMelee(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && GetIllidanPhase(illidan) == 4; +} + +bool IllidanStormrageBossTransformsIntoDemonTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || GetIllidanPhase(illidan) != 4) + return false; + + return GetIllidanWarlockTank(bot) == bot; +} + +bool IllidanStormrageBossSpawnsAddsTrigger::IsActive() +{ + if (botAI->IsHeal(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + if (botAI->IsTank(bot) && GetIllidanPhase(illidan) != 4) + return false; + + return true; +} + +bool IllidanStormrageMaievPlacedShadowTrapTrigger::IsActive() +{ + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1 || + GetIllidanPhase(illidan) != 5) + { + return false; + } + + GameObject* trap = FindNearestTrap(botAI, bot); + if (!trap) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* closestBot = nullptr; + float closestDist = std::numeric_limits::max(); + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || illidan->GetVictim() == member || + !GET_PLAYERBOT_AI(member) || botAI->IsMainTank(member)) + { + continue; + } + + float dist = member->GetDistance(trap); + if (dist < closestDist) + { + closestDist = dist; + closestBot = member; + } + } + + return closestBot == bot; +} + +bool IllidanStormrageNeedToManageDpsTimerAndRtiTrigger::IsActive() +{ + if (!IsMechanicTrackerBot(bot, BLACK_TEMPLE_MAP_ID)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + return illidan && illidan->GetHealth() > 1; +} + +// Destroying hazards behind phases is not gated behind CheatMask +// The strategy simply cannot work without doing this +bool IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger::IsActive() +{ + if (!IsMechanicTrackerBot(bot, BLACK_TEMPLE_MAP_ID)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan || illidan->GetHealth() == 1) + return false; + + int phase = GetIllidanPhase(illidan); + return phase == 0 || phase == 2 || phase == 4; +} + +bool IllidanStormrageCheatTrigger::IsActive() +{ + if (!botAI->HasCheat(BotCheatMask::raid) || !botAI->IsDps(bot)) + return false; + + Unit* illidan = AI_VALUE2(Unit*, "find target", "illidan stormrage"); + if (!illidan) + return false; + + int phase = GetIllidanPhase(illidan); + return phase == 2 || phase == 4; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.h b/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.h new file mode 100644 index 000000000..dcf5006fc --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BT/BTTriggers.h @@ -0,0 +1,519 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BTTRIGGERS_H +#define PLAYERBOTS_BTTRIGGERS_H + +#include "Trigger.h" + +// General + +class BlackTempleBotIsNotInCombatTrigger : public Trigger +{ +public: + BlackTempleBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "black temple bot is not in combat") {} + bool IsActive() override; +}; + +// High Warlord Naj'entus + +class HighWarlordNajentusPullingBossTrigger : public Trigger +{ +public: + HighWarlordNajentusPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus pulling boss") {} + bool IsActive() override; +}; + +class HighWarlordNajentusBossEngagedByTanksTrigger : public Trigger +{ +public: + HighWarlordNajentusBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus boss engaged by tanks") {} + bool IsActive() override; +}; + +class HighWarlordNajentusCastsNeedleSpinesTrigger : public Trigger +{ +public: + HighWarlordNajentusCastsNeedleSpinesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus casts needle spines") {} + bool IsActive() override; +}; + +class HighWarlordNajentusPlayerIsImpaledTrigger : public Trigger +{ +public: + HighWarlordNajentusPlayerIsImpaledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus player is impaled") {} + bool IsActive() override; +}; + +class HighWarlordNajentusBossHasTidalShieldTrigger : public Trigger +{ +public: + HighWarlordNajentusBossHasTidalShieldTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high warlord naj'entus boss has tidal shield") {} + bool IsActive() override; +}; + +// Supremus + +class SupremusPullingBossOrChangingPhaseTrigger : public Trigger +{ +public: + SupremusPullingBossOrChangingPhaseTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus pulling boss or changing phase") {} + bool IsActive() override; +}; + +class SupremusBossEngagedByRangedTrigger : public Trigger +{ +public: + SupremusBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus boss engaged by ranged") {} + bool IsActive() override; +}; + +class SupremusBossIsFixatedOnBotTrigger : public Trigger +{ +public: + SupremusBossIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus boss is fixated on bot") {} + bool IsActive() override; +}; + +class SupremusVolcanoIsNearbyTrigger : public Trigger +{ +public: + SupremusVolcanoIsNearbyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus volcano is nearby") {} + bool IsActive() override; +}; + +class SupremusNeedToManagePhaseTimerTrigger : public Trigger +{ +public: + SupremusNeedToManagePhaseTimerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "supremus need to manage phase timer") {} + bool IsActive() override; +}; + +// Shade of Akama + +class ShadeOfAkamaKillingChannelersStartsPhase2Trigger : public Trigger +{ +public: + ShadeOfAkamaKillingChannelersStartsPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "shade of akama killing channelers starts phase 2") {} + bool IsActive() override; +}; + +// Teron Gorefiend +class TeronGorefiendPullingBossTrigger : public Trigger +{ +public: + TeronGorefiendPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend pulling boss") {} + bool IsActive() override; +}; + +class TeronGorefiendBossEngagedByTanksTrigger : public Trigger +{ +public: + TeronGorefiendBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend boss engaged by tanks") {} + bool IsActive() override; +}; + +class TeronGorefiendBossEngagedByRangedTrigger : public Trigger +{ +public: + TeronGorefiendBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend boss engaged by ranged") {} + bool IsActive() override; +}; + +class TeronGorefiendBossIsCastingShadowOfDeathTrigger : public Trigger +{ +public: + TeronGorefiendBossIsCastingShadowOfDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend boss is casting shadow of death") {} + bool IsActive() override; +}; + +class TeronGorefiendBotHasShadowOfDeathTrigger : public Trigger +{ +public: + TeronGorefiendBotHasShadowOfDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend bot has shadow of death") {} + bool IsActive() override; +}; + +class TeronGorefiendBotTransformedIntoVengefulSpiritTrigger : public Trigger +{ +public: + TeronGorefiendBotTransformedIntoVengefulSpiritTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "teron gorefiend bot transformed into vengeful spirit") {} + bool IsActive() override; +}; + +// Gurtogg Bloodboil + +class GurtoggBloodboilPullingBossTrigger : public Trigger +{ +public: + GurtoggBloodboilPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil pulling boss") {} + bool IsActive() override; +}; + +class GurtoggBloodboilBossEngagedByTanksTrigger : public Trigger +{ +public: + GurtoggBloodboilBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil boss engaged by tanks") {} + bool IsActive() override; +}; + +class GurtoggBloodboilBossCastsBloodboilTrigger : public Trigger +{ +public: + GurtoggBloodboilBossCastsBloodboilTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil boss casts bloodboil") {} + bool IsActive() override; +}; + +class GurtoggBloodboilBotHasFelRageTrigger : public Trigger +{ +public: + GurtoggBloodboilBotHasFelRageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil bot has fel rage") {} + bool IsActive() override; +}; + +class GurtoggBloodboilNeedToManagePhaseTimerTrigger : public Trigger +{ +public: + GurtoggBloodboilNeedToManagePhaseTimerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gurtogg bloodboil need to manage phase timer") {} + bool IsActive() override; +}; + +// Reliquary of Souls + +class ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger : public Trigger +{ +public: + ReliquaryOfSoulsAggroResetsUponPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls aggro resets upon phase change") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfSufferingFixatesOnClosestTargetTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of suffering fixates on closest target") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfSufferingDisablesHealingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of suffering disables healing") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfDesireHasRuneShieldTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of desire has rune shield") {} + bool IsActive() override; +}; + +class ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger : public Trigger +{ +public: + ReliquaryOfSoulsEssenceOfDesireCastingDeadenTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "reliquary of souls essence of desire casting deaden") {} + bool IsActive() override; +}; + +// Mother Shahraz + +class MotherShahrazPullingBossTrigger : public Trigger +{ +public: + MotherShahrazPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz pulling boss") {} + bool IsActive() override; +}; + +class MotherShahrazBossEngagedByTanksTrigger : public Trigger +{ +public: + MotherShahrazBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz boss engaged by tanks") {} + bool IsActive() override; +}; + +class MotherShahrazTanksArePositioningBossTrigger : public Trigger +{ +public: + MotherShahrazTanksArePositioningBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz tanks are positioning boss") {} + bool IsActive() override; +}; + +class MotherShahrazSinisterBeamKnocksBackPlayersTrigger : public Trigger +{ +public: + MotherShahrazSinisterBeamKnocksBackPlayersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz sinister beam knocks back players") {} + bool IsActive() override; +}; + +class MotherShahrazBotsAreLinkedByFatalAttractionTrigger : public Trigger +{ +public: + MotherShahrazBotsAreLinkedByFatalAttractionTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "mother shahraz bots are linked by fatal attraction") {} + bool IsActive() override; +}; + +// Illidari Council + +class IllidariCouncilPullingBossesTrigger : public Trigger +{ +public: + IllidariCouncilPullingBossesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council pulling bosses") {} + bool IsActive() override; +}; + +class IllidariCouncilGathiosEngagedByMainTankTrigger : public Trigger +{ +public: + IllidariCouncilGathiosEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council gathios engaged by main tank") {} + bool IsActive() override; +}; + +class IllidariCouncilGathiosCastingJudgementOfCommandTrigger : public Trigger +{ +public: + IllidariCouncilGathiosCastingJudgementOfCommandTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council gathios casting judgement of command") {} + bool IsActive() override; +}; + +class IllidariCouncilMalandeEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + IllidariCouncilMalandeEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council malande engaged by first assist tank") {} + bool IsActive() override; +}; + +class IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + IllidariCouncilDarkshadowEngagedBySecondAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council darkshadow engaged by second assist tank") {} + bool IsActive() override; +}; + +class IllidariCouncilZerevorEngagedByMageTankTrigger : public Trigger +{ +public: + IllidariCouncilZerevorEngagedByMageTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council zerevor engaged by mage tank") {} + bool IsActive() override; +}; + +class IllidariCouncilMageTankNeedsDedicatedHealerTrigger : public Trigger +{ +public: + IllidariCouncilMageTankNeedsDedicatedHealerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council mage tank needs dedicated healer") {} + bool IsActive() override; +}; + +class IllidariCouncilZerevorCastsDangerousAoesTrigger : public Trigger +{ +public: + IllidariCouncilZerevorCastsDangerousAoesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council zerevor casts dangerous aoes") {} + bool IsActive() override; +}; + +class IllidariCouncilPetsScrewUpThePullTrigger : public Trigger +{ +public: + IllidariCouncilPetsScrewUpThePullTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council pets screw up the pull") {} + bool IsActive() override; +}; + +class IllidariCouncilNeedToManageDpsTimerTrigger : public Trigger +{ +public: + IllidariCouncilNeedToManageDpsTimerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council need to manage dps timer") {} + bool IsActive() override; +}; + +class IllidariCouncilDeterminingDpsAssignmentsTrigger : public Trigger +{ +public: + IllidariCouncilDeterminingDpsAssignmentsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidari council determining dps assignments") {} + bool IsActive() override; +}; + +// Illidan Stormrage + +class IllidanStormrageTankNeedsAggroTrigger : public Trigger +{ +public: + IllidanStormrageTankNeedsAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage tank needs aggro") {} + bool IsActive() override; +}; + +class IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger : public Trigger +{ +public: + IllidanStormrageBossCastsFlameCrashInFrontOfMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss casts flame crash in front of main tank") {} + bool IsActive() override; +}; + +class IllidanStormrageBotHasParasiticShadowfiendTrigger : public Trigger +{ +public: + IllidanStormrageBotHasParasiticShadowfiendTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage bot has parasitic shadowfiend") {} + bool IsActive() override; +}; + +class IllidanStormrageParasiticShadowfiendsRunWildTrigger : public Trigger +{ +public: + IllidanStormrageParasiticShadowfiendsRunWildTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage parasitic shadowfiends run wild") {} + bool IsActive() override; +}; + +class IllidanStormrageBossSummonedFlamesOfAzzinothTrigger : public Trigger +{ +public: + IllidanStormrageBossSummonedFlamesOfAzzinothTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss summoned flames of azzinoth") {} + bool IsActive() override; +}; + +class IllidanStormragePetsDieToFireTrigger : public Trigger +{ +public: + IllidanStormragePetsDieToFireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage pets die to fire") {} + bool IsActive() override; +}; + +class IllidanStormrageGrateIsSafeFromFlamesTrigger : public Trigger +{ +public: + IllidanStormrageGrateIsSafeFromFlamesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage grate is safe from flames") {} + bool IsActive() override; +}; + +class IllidanStormrageBotStruckByDarkBarrageTrigger : public Trigger +{ +public: + IllidanStormrageBotStruckByDarkBarrageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage bot struck by dark barrage") {} + bool IsActive() override; +}; + +class IllidanStormrageBossIsPreparingToLandTrigger : public Trigger +{ +public: + IllidanStormrageBossIsPreparingToLandTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss is preparing to land") {} + bool IsActive() override; +}; + +class IllidanStormrageBossDealsSplashDamageTrigger : public Trigger +{ +public: + IllidanStormrageBossDealsSplashDamageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss deals splash damage") {} + bool IsActive() override; +}; + +class IllidanStormrageThisExpansionHatesMeleeTrigger : public Trigger +{ +public: + IllidanStormrageThisExpansionHatesMeleeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage this expansion hates melee") {} + bool IsActive() override; +}; + +class IllidanStormrageBossTransformsIntoDemonTrigger : public Trigger +{ +public: + IllidanStormrageBossTransformsIntoDemonTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss transforms into demon") {} + bool IsActive() override; +}; + +class IllidanStormrageBossSpawnsAddsTrigger : public Trigger +{ +public: + IllidanStormrageBossSpawnsAddsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage boss spawns adds") {} + bool IsActive() override; +}; + +class IllidanStormrageMaievPlacedShadowTrapTrigger : public Trigger +{ +public: + IllidanStormrageMaievPlacedShadowTrapTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage maiev placed shadow trap") {} + bool IsActive() override; +}; + +class IllidanStormrageNeedToManageDpsTimerAndRtiTrigger : public Trigger +{ +public: + IllidanStormrageNeedToManageDpsTimerAndRtiTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage need to manage dps timer and rti") {} + bool IsActive() override; +}; + +class IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger : public Trigger +{ +public: + IllidanStormrageNeedToClearHazardsBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage need to clear hazards between phases") {} + bool IsActive() override; +}; + +class IllidanStormrageCheatTrigger : public Trigger +{ +public: + IllidanStormrageCheatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "illidan stormrage cheat") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActionContext.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActionContext.h new file mode 100644 index 000000000..2b7df59a2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActionContext.h @@ -0,0 +1,49 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLACTIONCONTEXT_H +#define PLAYERBOTS_BWLACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "BWLActions.h" + +class RaidBwlActionContext : public NamedObjectContext +{ +public: + RaidBwlActionContext() + { + creators["bwl check onyxia scale cloak"] = &RaidBwlActionContext::bwl_check_onyxia_scale_cloak; + creators["bwl turn off suppression device"] = &RaidBwlActionContext::bwl_turn_off_suppression_device; + + creators["bwl razorgore fire resistance"] = &RaidBwlActionContext::bwl_razorgore_fire_resistance_action; + creators["bwl razorgore avoid aoe"] = &RaidBwlActionContext::bwl_razorgore_avoid_aoe; + creators["bwl razorgore mark boss"] = &RaidBwlActionContext::bwl_razorgore_mark_boss; + + creators["bwl vaelastrasz fire resistance"] = &RaidBwlActionContext::bwl_vaelastrasz_fire_resistance_action; + creators["bwl vaelastrasz move away"] = &RaidBwlActionContext::bwl_vaelastrasz_move_away; + + creators["bwl use hourglass sand"] = &RaidBwlActionContext::bwl_use_hourglass_sand; + creators["bwl nefarian fear ward"] = &RaidBwlActionContext::bwl_nefarian_fear_ward; + creators["bwl death talon wyrmguard tank move away"] = &RaidBwlActionContext::bwl_death_talon_wyrmguard_tank_move_away; + creators["bwl death talon wyrmguard ranged move away"] = &RaidBwlActionContext::bwl_death_talon_wyrmguard_ranged_move_away; + } + +private: + static Action* bwl_check_onyxia_scale_cloak(PlayerbotAI* ai) { return new BwlOnyxiaScaleCloakAuraCheckAction(ai); } + static Action* bwl_turn_off_suppression_device(PlayerbotAI* ai) { return new BwlTurnOffSuppressionDeviceAction(ai); } + static Action* bwl_razorgore_fire_resistance_action(PlayerbotAI* ai) { return new BossFireResistanceAction(ai, "razorgore the untamed"); } + static Action* bwl_razorgore_avoid_aoe(PlayerbotAI* ai) { return new BwlRazorgoreAvoidAoeAction(ai); } + static Action* bwl_razorgore_mark_boss(PlayerbotAI* ai) { return new BwlRazorgoreMarkBossAction(ai); } + static Action* bwl_vaelastrasz_fire_resistance_action(PlayerbotAI* ai) { return new BossFireResistanceAction(ai, "vaelastrasz the corrupt"); } + static Action* bwl_vaelastrasz_move_away(PlayerbotAI* ai) { return new BwlVaelastraszMoveAwayAction(ai); } + static Action* bwl_use_hourglass_sand(PlayerbotAI* ai) { return new BwlUseHourglassSandAction(ai); } + static Action* bwl_nefarian_fear_ward(PlayerbotAI* ai) { return new BwlNefarianFearWardAction(ai); } + static Action* bwl_death_talon_wyrmguard_tank_move_away(PlayerbotAI* ai) { return new BwlDeathTalonWyrmguardTankMoveAwayAction(ai); } + static Action* bwl_death_talon_wyrmguard_ranged_move_away(PlayerbotAI* ai) { return new BwlDeathTalonWyrmguardRangedMoveAwayAction(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.cpp b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.cpp new file mode 100644 index 000000000..e0f30778a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.cpp @@ -0,0 +1,368 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BWLActions.h" + +#include "RtiTargetValue.h" +#include "Playerbots.h" +#include "BWLHelpers.h" +#include "RaidBossHelpers.h" + +using namespace BlackwingLairHelpers; + +static constexpr float INCREMENTAL_MOVE_STEP_DISTANCE = 3.0f; + +static constexpr float RAZORGORE_CONE_RADIUS = 15.0f; +static constexpr float RAZORGORE_CONE_ARC = M_PI; +static constexpr float RAZORGORE_BACK_FUZZ_ARC = M_PI_4; +static constexpr float RAZORGORE_RANGE_DISTANCE = 15.0f; +static constexpr float RAZORGORE_MELEE_DISTANCE = 3.0f; + +static constexpr float VAELASTRASZ_REPULSION_RANGE = 20.0f; +static constexpr float VAELASTRASZ_TAIL_SWEEP_RECOVERY_RANGE = 30.0f; +static constexpr float VAELASTRASZ_RANGED_BIAS_THRESHOLD = 25.0f; +static constexpr float VAELASTRASZ_RANGED_BIAS_FLEE_WEIGHT = 0.7f; +static constexpr float VAELASTRASZ_RANGED_BIAS_BOSS_WEIGHT = 1.0f - VAELASTRASZ_RANGED_BIAS_FLEE_WEIGHT; + +// General + +bool BwlOnyxiaScaleCloakAuraCheckAction::Execute(Event /*event*/) +{ + bot->AddAura(static_cast(BlackwingLairSpells::SPELL_ONYXIA_SCALE_CLOAK), bot); + return true; +} + +bool BwlOnyxiaScaleCloakAuraCheckAction::isUseful() +{ + return !bot->HasAura(static_cast(BlackwingLairSpells::SPELL_ONYXIA_SCALE_CLOAK)); +} + +bool BwlTurnOffSuppressionDeviceAction::Execute(Event /*event*/) +{ + GuidVector gos = AI_VALUE(GuidVector, "nearest game objects"); + for (auto i = gos.begin(); i != gos.end(); ++i) + { + GameObject* go = botAI->GetGameObject(*i); + if (IsActiveSuppressionDeviceInRange(go, bot)) + { + go->SetGoState(GO_STATE_ACTIVE); + } + } + return true; +} + +// Razorgore the Untamed + +bool BwlRazorgoreAvoidAoeAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorgore the untamed"); + if (!boss) + return false; + + // The current target should not move. + // Also prevents non-tanks from rotating the boss if they have aggro. + if (boss->GetVictim() == bot) + return false; + + float distance = bot->GetDistance2d(boss); + + // Bot is too close and standing in Razorgore's frontal cone + if (distance <= RAZORGORE_CONE_RADIUS && boss->HasInArc(RAZORGORE_CONE_ARC, bot)) + { + // Target position: directly behind the boss with a small random fuzz + // to prevent all bots stacking on the same spot + float moveAngle = boss->GetOrientation() + M_PI + frand(-RAZORGORE_BACK_FUZZ_ARC, RAZORGORE_BACK_FUZZ_ARC); + float razorgoreRadius = PlayerbotAI::IsRanged(bot) ? RAZORGORE_RANGE_DISTANCE : RAZORGORE_MELEE_DISTANCE; + + float targetMoveX = boss->GetPositionX() + razorgoreRadius * cos(moveAngle); + float targetMoveY = boss->GetPositionY() + razorgoreRadius * sin(moveAngle); + + // Move incrementally toward the target. Allows course correction. + float dX = targetMoveX - bot->GetPositionX(); + float dY = targetMoveY - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + if (dist == 0.0f) + dist = 0.1f; + + float moveX = bot->GetPositionX() + (dX / dist) * INCREMENTAL_MOVE_STEP_DISTANCE; + float moveY = bot->GetPositionY() + (dY / dist) * INCREMENTAL_MOVE_STEP_DISTANCE; + + return MoveTo(boss->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true); + } + + // Ranged outside the cone but too close => back off to avoid War Stomp + if (PlayerbotAI::IsRanged(bot)) + { + float distToTravel = RAZORGORE_RANGE_DISTANCE - distance; + if (distToTravel > 0) + return MoveAway(boss, distToTravel); + } + + return false; +} + +bool BwlRazorgoreMarkBossAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "razorgore the untamed"); + if (!boss) + return false; + + if (AreRazorgoreEggsAlive(botAI)) + { + if (IsRazorgoreOffTank(bot)) + { + if (MarkTargetWithMoon(bot, boss)) + return true; + + SetRtiTarget(botAI, "moon", boss); + + if (AI_VALUE(Unit*, "current target") != boss) + return Attack(boss); + } + } + else + { + ClearTargetIcon(bot, RtiTargetValue::moonIndex); + } + + return false; +} + +bool BwlRazorgoreMarkBossAction::isUseful() +{ + return PlayerbotAI::IsTank(bot); +} + +// Vaelastrasz the Corrupt + +bool BwlVaelastraszMoveAwayAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "vaelastrasz the corrupt"); + + // Current target with Burning Adrenaline stays put when Vaelastrasz is alive. + if (boss && boss->IsAlive() && boss->GetVictim() == bot) + return false; + + float fleeX, fleeY; + if (!CalculateFleeDirection(boss, fleeX, fleeY)) + return false; + + return MoveAlongFleeDirection(boss, fleeX, fleeY); +} + +bool BwlVaelastraszMoveAwayAction::CalculateFleeDirection(const Unit* boss, float& fleeX, float& fleeY) const +{ + fleeX = 0.0f; + fleeY = 0.0f; + bool bossDead = !boss || !boss->IsAlive(); + + if (const Group* group = bot->GetGroup()) + { + for (const GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* p = gref->GetSource(); + if (!p || p == bot || !p->IsAlive() || p->GetMapId() != bot->GetMapId()) + continue; + + // Boss alive: ignore other Burning Adrenaline bots so they can group together + if (!bossDead && p->HasAura(static_cast(BlackwingLairSpells::SPELL_BURNING_ADRENALINE))) + continue; + + // Compute a flee direction via weighted repulsion from other bots. + // Weighted repulsion: closer = stronger push (linear falloff to 20y) + float dist = bot->GetDistance2d(p); + if (dist < VAELASTRASZ_REPULSION_RANGE && dist > 0.1f) + { + float weight = 1.0f - dist / VAELASTRASZ_REPULSION_RANGE; + fleeX += (bot->GetPositionX() - p->GetPositionX()) / dist * weight; + fleeY += (bot->GetPositionY() - p->GetPositionY()) / dist * weight; + } + } + } + + float fleeLen = sqrt(fleeX * fleeX + fleeY * fleeY); + + // No repulsion + if (fleeLen < 0.1f) + { + // Tail Sweep recovery: move back toward boss + if (!bossDead && bot->GetDistance2d(boss) > VAELASTRASZ_TAIL_SWEEP_RECOVERY_RANGE) + { + float toBossX = boss->GetPositionX() - bot->GetPositionX(); + float toBossY = boss->GetPositionY() - bot->GetPositionY(); + float dist = sqrt(toBossX * toBossX + toBossY * toBossY); + if (dist > 0.1f) + { + fleeX = toBossX / dist; + fleeY = toBossY / dist; + return true; + } + } + return false; + } + + fleeX /= fleeLen; + fleeY /= fleeLen; + + // Ranged: blend flee direction toward boss when >25y to stay in cast range + if (!bossDead && PlayerbotAI::IsRanged(bot)) + { + float bossDist = bot->GetDistance2d(boss); + if (bossDist > VAELASTRASZ_RANGED_BIAS_THRESHOLD) + { + float toBossX = (boss->GetPositionX() - bot->GetPositionX()) / bossDist; + float toBossY = (boss->GetPositionY() - bot->GetPositionY()) / bossDist; + if (fleeX * toBossX + fleeY * toBossY < 0.0f) + { + fleeX = fleeX * VAELASTRASZ_RANGED_BIAS_FLEE_WEIGHT + toBossX * VAELASTRASZ_RANGED_BIAS_BOSS_WEIGHT; + fleeY = fleeY * VAELASTRASZ_RANGED_BIAS_FLEE_WEIGHT + toBossY * VAELASTRASZ_RANGED_BIAS_BOSS_WEIGHT; + float len = sqrt(fleeX * fleeX + fleeY * fleeY); + fleeX /= len; + fleeY /= len; + } + } + } + + return true; +} + +bool BwlVaelastraszMoveAwayAction::MoveAlongFleeDirection(const Unit* boss, float fleeX, float fleeY) +{ + bool bossDead = !boss || !boss->IsAlive(); + float baseAngle = atan2(fleeY, fleeX); + + // Pass 1: ranged try LOS positions first + // Pass 2: everyone, no LOS req + for (bool requireLos : {true, false}) + { + // Ranged bots try LOS positions first, then fall back to no-LOS. + if (requireLos && (!PlayerbotAI::IsRanged(bot) || bossDead)) + continue; + + // Try 5 angles (0°, ±45°, ±90°) around the flee direction to slide along walls. + static constexpr float angleOffsets[] = {0.0f, M_PI_4, -M_PI_4, M_PI_2, -M_PI_2}; + for (float offset : angleOffsets) + { + float angle = baseAngle + offset; + float targetX = bot->GetPositionX() + INCREMENTAL_MOVE_STEP_DISTANCE * cos(angle); + float targetY = bot->GetPositionY() + INCREMENTAL_MOVE_STEP_DISTANCE * sin(angle); + + // Reject if LOS to boss would break (only checked in Pass 1 for ranged) + if (requireLos && boss && !boss->IsWithinLOS(targetX, targetY, bot->GetPositionZ())) + continue; + + if (MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true)) + return true; + } + } + + return false; +} + +// Chromaggus + +bool BwlUseHourglassSandAction::Execute(Event /*event*/) +{ + return botAI->CastSpell(static_cast(BlackwingLairSpells::SPELL_HOURGLASS_SAND), bot); +} + +bool BwlNefarianFearWardAction::Execute(Event /*event*/) +{ + Unit* nefarian = AI_VALUE2(Unit*, "find target", "nefarian"); + if (!nefarian) + return false; + + Unit* victim = nefarian->GetVictim(); + if (!victim) + return false; + + return botAI->CastSpell("fear ward", victim); +} + +// Trash + +static constexpr float WYRMGUARD_SAFE_DISTANCE = 16.0f; + +Unit* BwlDeathTalonWyrmguardTankMoveAwayAction::GetTarget() +{ + // Find a nearby wyrmguard within 16 yards being tanked by someone else + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) + { + Unit* unit = ref->GetOwner(); + if (!unit || !unit->IsAlive() || unit->GetEntry() != static_cast(BlackwingLairNPCs::NPC_DEATH_TALON_WYRMGUARD)) + continue; + if (bot->GetDistance2d(unit) > WYRMGUARD_SAFE_DISTANCE) + continue; + Unit* victim = unit->GetVictim(); + if (victim && victim != bot && victim->IsPlayer() && PlayerbotAI::IsTank(victim->ToPlayer())) + return unit; + } + return nullptr; +} + +bool BwlDeathTalonWyrmguardTankMoveAwayAction::isUseful() +{ + if (!GetTarget()) + return false; + + // Must be actively tanking a wyrmguard before moving away from another tank's + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) + { + Unit* unit = ref->GetOwner(); + if (unit && unit->IsAlive() && unit->GetEntry() == static_cast(BlackwingLairNPCs::NPC_DEATH_TALON_WYRMGUARD) && + unit->GetVictim() == bot) + return true; + } + return false; +} + +bool BwlDeathTalonWyrmguardTankMoveAwayAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + if (!target) + return false; + + float distToTravel = WYRMGUARD_SAFE_DISTANCE - bot->GetDistance2d(target); + if (distToTravel <= 0.0f) + return false; + + return MoveAway(target, distToTravel); +} + +Unit* BwlDeathTalonWyrmguardRangedMoveAwayAction::GetTarget() +{ + // Find the closest wyrmguard within 16 yards + Unit* closest = nullptr; + float closestDist = WYRMGUARD_SAFE_DISTANCE; + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) + { + Unit* unit = ref->GetOwner(); + if (!unit || !unit->IsAlive() || unit->GetEntry() != static_cast(BlackwingLairNPCs::NPC_DEATH_TALON_WYRMGUARD)) + continue; + float dist = bot->GetDistance2d(unit); + if (dist < closestDist) + { + closestDist = dist; + closest = unit; + } + } + return closest; +} + +bool BwlDeathTalonWyrmguardRangedMoveAwayAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + if (!target) + return false; + + float distToTravel = WYRMGUARD_SAFE_DISTANCE - bot->GetDistance2d(target); + if (distToTravel <= 0.0f) + return false; + + return MoveAway(target, distToTravel); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.h new file mode 100644 index 000000000..c2bc0c8f1 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLActions.h @@ -0,0 +1,96 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLACTIONS_H +#define PLAYERBOTS_BWLACTIONS_H + +#include "Action.h" +#include "MovementActions.h" +#include "AttackAction.h" + +// General + +class BwlOnyxiaScaleCloakAuraCheckAction : public Action +{ +public: + BwlOnyxiaScaleCloakAuraCheckAction(PlayerbotAI* botAI) : Action(botAI, "bwl onyxia scale cloak aura check") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class BwlTurnOffSuppressionDeviceAction : public Action +{ +public: + BwlTurnOffSuppressionDeviceAction(PlayerbotAI* botAI) : Action(botAI, "bwl turn off suppression device") {} + bool Execute(Event event) override; +}; + +// Razorgore the Untamed + +class BwlRazorgoreAvoidAoeAction : public MovementAction +{ +public: + BwlRazorgoreAvoidAoeAction(PlayerbotAI* botAI) : MovementAction(botAI, "bwl razorgore avoid aoe") {} + bool Execute(Event event) override; +}; + +class BwlRazorgoreMarkBossAction : public AttackAction +{ +public: + BwlRazorgoreMarkBossAction(PlayerbotAI* botAI) : AttackAction(botAI, "bwl razorgore mark boss") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +// Vaelastrasz the Corrupt + +class BwlVaelastraszMoveAwayAction : public MovementAction +{ +public: + BwlVaelastraszMoveAwayAction(PlayerbotAI* botAI) : MovementAction(botAI, "bwl vaelastrasz move away") {} + bool Execute(Event event) override; + +private: + bool CalculateFleeDirection(const Unit* boss, float& fleeX, float& fleeY) const; + bool MoveAlongFleeDirection(const Unit* boss, float fleeX, float fleeY); +}; + +// Chromaggus + +class BwlUseHourglassSandAction : public Action +{ +public: + BwlUseHourglassSandAction(PlayerbotAI* botAI) : Action(botAI, "bwl use hourglass sand") {} + bool Execute(Event event) override; +}; + +class BwlNefarianFearWardAction : public Action +{ +public: + BwlNefarianFearWardAction(PlayerbotAI* botAI) : Action(botAI, "bwl nefarian fear ward") {} + bool Execute(Event event) override; +}; + +// Trash + +class BwlDeathTalonWyrmguardTankMoveAwayAction : public MovementAction +{ +public: + BwlDeathTalonWyrmguardTankMoveAwayAction(PlayerbotAI* botAI) : MovementAction(botAI, "bwl death talon wyrmguard tank move away") {} + Unit* GetTarget() override; + bool isUseful() override; + bool Execute(Event event) override; +}; + +class BwlDeathTalonWyrmguardRangedMoveAwayAction : public MovementAction +{ +public: + BwlDeathTalonWyrmguardRangedMoveAwayAction(PlayerbotAI* botAI) : MovementAction(botAI, "bwl death talon wyrmguard ranged move away") {} + Unit* GetTarget() override; + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.cpp new file mode 100644 index 000000000..679a29acc --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.cpp @@ -0,0 +1,61 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BWLHelpers.h" + +#include "AiObjectContext.h" +#include "Group.h" + +namespace BlackwingLairHelpers +{ + bool IsActiveSuppressionDeviceInRange(const GameObject* go, const Player* bot) + { + constexpr float suppressionDeviceInteractionDistance = 15.0f; + return go && + go->GetEntry() == static_cast(BlackwingLairGameObjects::GO_SUPPRESSION_DEVICE) && + go->GetDistance(bot) < suppressionDeviceInteractionDistance && + go->GetGoState() == GO_STATE_READY; + } + + bool AreRazorgoreEggsAlive(PlayerbotAI* botAI) + { + GuidVector gos = botAI->GetAiObjectContext()->GetValue("nearest game objects")->Get(); + for (auto const& guid : gos) + { + const GameObject* go = botAI->GetGameObject(guid); + if (go && go->GetEntry() == static_cast(BlackwingLairGameObjects::GO_BLACK_DRAGON_EGG)) + return true; + } + return false; + } + + bool IsRazorgoreOffTank(Player* bot) + { + return PlayerbotAI::IsAssistTankOfIndex(bot, 0, true); + } + + bool IsNonBABotNearPosition(const Player* bot, Position const& position, float distance) + { + const Group* group = bot->GetGroup(); + if (!group) + return false; + + for (const GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + const Player* p = gref->GetSource(); + if (!p || p == bot || !p->IsAlive() || p->GetMapId() != bot->GetMapId()) + continue; + + if (p->HasAura(static_cast(BlackwingLairSpells::SPELL_BURNING_ADRENALINE))) + continue; + + if (p->GetDistance2d(position.GetPositionX(), position.GetPositionY()) < distance) + return true; + } + + return false; + } +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.h new file mode 100644 index 000000000..633aa6386 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLHelpers.h @@ -0,0 +1,55 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLHELPERS_H +#define PLAYERBOTS_BWLHELPERS_H + +#include "Player.h" +#include "PlayerbotAI.h" + +namespace BlackwingLairHelpers +{ + enum class BlackwingLairSpells : uint32 + { + // General + SPELL_ONYXIA_SCALE_CLOAK = 22683, + + // Razorgore the Untamed + SPELL_MINDCONTROL = 19832, + + // Vaelastrasz the Corrupt + SPELL_BURNING_ADRENALINE = 18173, + + // Chromaggus + SPELL_BROOD_AFFLICTION_BRONZE = 23170, + SPELL_HOURGLASS_SAND = 23645, + + // Nefarian + SPELL_WILD_MAGIC = 23410 + }; + + enum class BlackwingLairGameObjects : uint32 + { + // General + GO_SUPPRESSION_DEVICE = 179784, + + // Razorgore the Untamed + GO_BLACK_DRAGON_EGG = 177807 + }; + + enum class BlackwingLairNPCs : uint32 + { + // Trash + NPC_DEATH_TALON_WYRMGUARD = 12460 + }; + + bool IsActiveSuppressionDeviceInRange(const GameObject* go, const Player* bot); + bool AreRazorgoreEggsAlive(PlayerbotAI* botAI); + bool IsRazorgoreOffTank(Player* bot); + bool IsNonBABotNearPosition(const Player* bot, Position const& position, float distance); +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.cpp new file mode 100644 index 000000000..2ad371028 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.cpp @@ -0,0 +1,91 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BWLMultipliers.h" + +#include "BWLActions.h" +#include "BWLHelpers.h" +#include "ChooseTargetActions.h" +#include "GenericActions.h" +#include "HunterActions.h" +#include "Playerbots.h" +#include "ReachTargetActions.h" + +using namespace BlackwingLairHelpers; + +static constexpr float VAELASTRASZ_BA_SAFE_DISTANCE = 20.0f; +static constexpr float VAELASTRASZ_BA_BOSS_DISTANCE = 30.0f; + +float RazorgoreTankMultiplier::GetValue(Action* action) +{ + if (!PlayerbotAI::IsTank(bot)) + return 1.0f; + + Unit* boss = AI_VALUE2(Unit*, "find target", "razorgore the untamed"); + if (!boss) + return 1.0f; + + if (AreRazorgoreEggsAlive(botAI)) + { + // Off-tank picks up boss, blocks TankAssistAction to avoid changing targets + if (IsRazorgoreOffTank(bot) && bot->GetVictim() != nullptr && dynamic_cast(action)) + return 0.0f; + return 1.0f; + } + + // All eggs destroyed. Tanks not targeted by the boss don't run into the Cleave. + if (boss->GetVictim() != bot && dynamic_cast(action)) + return 0.0f; + return 1.0f; +} + +float VaelastraszTankMultiplier::GetValue(Action* action) +{ + if (!PlayerbotAI::IsTank(bot)) + return 1.0f; + + Unit* boss = AI_VALUE2(Unit*, "find target", "vaelastrasz the corrupt"); + if (!boss) + return 1.0f; + + // Stay at rear flank position and don't move in front of the boss. + if (dynamic_cast(action)) + { + Unit* victim = boss->GetVictim(); + Player* victimPlayer = victim ? victim->ToPlayer() : nullptr; + if (victim != bot && victimPlayer && PlayerbotAI::IsTank(victimPlayer)) + return 0.0f; + } + + return 1.0f; +} + +float VaelastraszBurningAdrenalineMultiplier::GetValue(Action* action) +{ + if (bot->HasAura(static_cast(BlackwingLairSpells::SPELL_BURNING_ADRENALINE))) + { + if (dynamic_cast(action)) + { + if (dynamic_cast(action)) + { + // Allow movement when boss is dead or bot was knocked away + Unit* boss = AI_VALUE2(Unit*, "find target", "vaelastrasz the corrupt"); + if (!boss || !boss->IsAlive() || bot->GetDistance2d(boss) > VAELASTRASZ_BA_BOSS_DISTANCE) + return 1.0f; + } + + // Block ALL other movement once bot is far enough from non-BA bots + if (!IsNonBABotNearPosition(bot, bot->GetPosition(), VAELASTRASZ_BA_SAFE_DISTANCE) || + !dynamic_cast(action)) + return 0.0f; + } + // Also block charge actions + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.h new file mode 100644 index 000000000..7a063a933 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLMultipliers.h @@ -0,0 +1,36 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLMULTIPLIERS_H +#define PLAYERBOTS_BWLMULTIPLIERS_H + +#include "Multiplier.h" + +class RazorgoreTankMultiplier : public Multiplier +{ +public: + RazorgoreTankMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "razorgore tank multiplier") {} + float GetValue(Action* action) override; +}; + +class VaelastraszTankMultiplier : public Multiplier +{ +public: + VaelastraszTankMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "vaelastrasz tank multiplier") {} + float GetValue(Action* action) override; +}; + +class VaelastraszBurningAdrenalineMultiplier : public Multiplier +{ +public: + VaelastraszBurningAdrenalineMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "vaelastrasz burning adrenaline multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.cpp new file mode 100644 index 000000000..1cf87663c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.cpp @@ -0,0 +1,51 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BWLStrategy.h" + +#include "BWLMultipliers.h" + +void RaidBwlStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode("often", { + NextAction("bwl check onyxia scale cloak", ACTION_RAID) })); + triggers.push_back(new TriggerNode("bwl suppression device", { + NextAction("bwl turn off suppression device", ACTION_RAID) })); + + triggers.push_back(new TriggerNode("bwl razorgore fire resistance", { + NextAction("bwl razorgore fire resistance", ACTION_RAID) })); + triggers.push_back(new TriggerNode("bwl razorgore not mind controlled", { + NextAction("bwl razorgore avoid aoe", ACTION_RAID) })); + triggers.push_back(new TriggerNode("bwl razorgore not mind controlled", { + NextAction("bwl razorgore mark boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("bwl vaelastrasz fire resistance", { + NextAction("bwl vaelastrasz fire resistance", ACTION_RAID) })); + triggers.push_back(new TriggerNode("bwl vaelastrasz positioning", { + NextAction("rear flank", ACTION_MOVE + 4) })); + triggers.push_back(new TriggerNode("bwl vaelastrasz burning adrenaline", { + NextAction("bwl vaelastrasz move away", ACTION_RAID + 5) })); + + triggers.push_back(new TriggerNode("bwl affliction bronze", { + NextAction("bwl use hourglass sand", ACTION_RAID) })); + + triggers.push_back(new TriggerNode("bwl wild magic", { + NextAction("ice block", ACTION_RAID) })); + triggers.push_back(new TriggerNode("bwl nefarian fear ward", { + NextAction("bwl nefarian fear ward", ACTION_RAID) })); + + triggers.push_back(new TriggerNode("bwl death talon wyrmguard tank", { + NextAction("bwl death talon wyrmguard tank move away", ACTION_RAID) })); + triggers.push_back(new TriggerNode("bwl death talon wyrmguard ranged", { + NextAction("bwl death talon wyrmguard ranged move away", ACTION_RAID) })); +} + +void RaidBwlStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new RazorgoreTankMultiplier(botAI)); + multipliers.push_back(new VaelastraszTankMultiplier(botAI)); + multipliers.push_back(new VaelastraszBurningAdrenalineMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.h new file mode 100644 index 000000000..0119eb10c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLStrategy.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLSTRATEGY_H +#define PLAYERBOTS_BWLSTRATEGY_H + +#include "Strategy.h" + +class RaidBwlStrategy : public Strategy +{ +public: + RaidBwlStrategy(PlayerbotAI* ai) : Strategy(ai) {} + std::string const getName() override { return "bwl"; } + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggerContext.h new file mode 100644 index 000000000..24ed1617c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggerContext.h @@ -0,0 +1,48 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLTRIGGERCONTEXT_H +#define PLAYERBOTS_BWLTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "BWLTriggers.h" + +class RaidBwlTriggerContext : public NamedObjectContext +{ +public: + RaidBwlTriggerContext() + { + creators["bwl suppression device"] = &RaidBwlTriggerContext::bwl_suppression_device; + + creators["bwl razorgore fire resistance"] = &RaidBwlTriggerContext::bwl_razorgore_fire_resistance_trigger; + creators["bwl razorgore not mind controlled"] = &RaidBwlTriggerContext::bwl_razorgore_not_mind_controlled; + + creators["bwl vaelastrasz fire resistance"] = &RaidBwlTriggerContext::bwl_vaelastrasz_fire_resistance_trigger; + creators["bwl vaelastrasz positioning"] = &RaidBwlTriggerContext::bwl_vaelastrasz_positioning; + creators["bwl vaelastrasz burning adrenaline"] = &RaidBwlTriggerContext::bwl_vaelastrasz_burning_adrenaline; + + creators["bwl affliction bronze"] = &RaidBwlTriggerContext::bwl_affliction_bronze; + creators["bwl wild magic"] = &RaidBwlTriggerContext::bwl_wild_magic; + creators["bwl nefarian fear ward"] = &RaidBwlTriggerContext::bwl_nefarian_fear_ward; + creators["bwl death talon wyrmguard tank"] = &RaidBwlTriggerContext::bwl_death_talon_wyrmguard_tank; + creators["bwl death talon wyrmguard ranged"] = &RaidBwlTriggerContext::bwl_death_talon_wyrmguard_ranged; + } + +private: + static Trigger* bwl_suppression_device(PlayerbotAI* ai) { return new BwlSuppressionDeviceTrigger(ai); } + static Trigger* bwl_razorgore_fire_resistance_trigger(PlayerbotAI* ai) { return new BossFireResistanceTrigger(ai, "razorgore the untamed"); } + static Trigger* bwl_razorgore_not_mind_controlled(PlayerbotAI* ai) { return new BwlRazorgoreNotMindControlledTrigger(ai); } + static Trigger* bwl_vaelastrasz_fire_resistance_trigger(PlayerbotAI* ai) { return new BossFireResistanceTrigger(ai, "vaelastrasz the corrupt"); } + static Trigger* bwl_vaelastrasz_positioning(PlayerbotAI* ai) { return new BwlVaelastraszPositioningTrigger(ai); } + static Trigger* bwl_vaelastrasz_burning_adrenaline(PlayerbotAI* ai) { return new BwlVaelastraszBurningAdrenalineTrigger(ai); } + static Trigger* bwl_affliction_bronze(PlayerbotAI* ai) { return new BwlAfflictionBronzeTrigger(ai); } + static Trigger* bwl_wild_magic(PlayerbotAI* ai) { return new BwlWildMagicTrigger(ai); } + static Trigger* bwl_nefarian_fear_ward(PlayerbotAI* ai) { return new BwlNefarianFearWardTrigger(ai); } + static Trigger* bwl_death_talon_wyrmguard_tank(PlayerbotAI* ai) { return new BwlDeathTalonWyrmguardTankTrigger(ai); } + static Trigger* bwl_death_talon_wyrmguard_ranged(PlayerbotAI* ai) { return new BwlDeathTalonWyrmguardRangedTrigger(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.cpp new file mode 100644 index 000000000..e75b795ad --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.cpp @@ -0,0 +1,99 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BWLTriggers.h" + +#include "Playerbots.h" +#include "BWLHelpers.h" + +using namespace BlackwingLairHelpers; + +// General + +bool BwlSuppressionDeviceTrigger::IsActive() +{ + // Until MoP, only rogues could disarm suppression devices. + // If raid cheats are enabled, any bot can disarm the devices. + if (botAI->HasCheat(BotCheatMask::raid) || bot->IsClass(CLASS_ROGUE)) + { + GuidVector gos = AI_VALUE(GuidVector, "nearest game objects"); + for (auto i = gos.begin(); i != gos.end(); ++i) + { + const GameObject* go = botAI->GetGameObject(*i); + if (IsActiveSuppressionDeviceInRange(go, bot)) + return true; + } + } + return false; +} + +// Razorgore the Untamed + +bool BwlRazorgoreNotMindControlledTrigger::IsActive() +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "razorgore the untamed")) + return !boss->HasAura(static_cast(BlackwingLairSpells::SPELL_MINDCONTROL)); + return false; +} + +// Vaelastrasz the Corrupt + +bool BwlVaelastraszPositioningTrigger::IsActive() +{ + // Prevent non-tanks from rotating the boss while the tanks gain thread. + if (Unit* boss = AI_VALUE2(Unit*, "find target", "vaelastrasz the corrupt")) + return boss->GetVictim() != bot; + return false; +} + +bool BwlVaelastraszBurningAdrenalineTrigger::IsActive() +{ + // No check for Vaelastrasz, because bots may still have burning adrenaline even after Vaelastrasz died. + return bot->HasAura(static_cast(BlackwingLairSpells::SPELL_BURNING_ADRENALINE)); +} + +// Chromaggus + +bool BwlAfflictionBronzeTrigger::IsActive() +{ + return bot->HasAura(static_cast(BlackwingLairSpells::SPELL_BROOD_AFFLICTION_BRONZE)); +} + +// Nefarian + +bool BwlWildMagicTrigger::IsActive() +{ + return bot->getClass() == CLASS_MAGE && + bot->HasAura(static_cast(BlackwingLairSpells::SPELL_WILD_MAGIC)); +} + +bool BwlNefarianFearWardTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* nefarian = AI_VALUE2(Unit*, "find target", "nefarian"); + if (!nefarian || !nefarian->IsInCombat()) + return false; + + Unit* victim = nefarian->GetVictim(); + if (!victim) + return false; + + return !botAI->HasAura("fear ward", victim); +} + +// Trash + +bool BwlDeathTalonWyrmguardTankTrigger::IsActive() +{ + return PlayerbotAI::IsTank(bot) && AI_VALUE2(Unit*, "find target", "death talon wyrmguard"); +} + +bool BwlDeathTalonWyrmguardRangedTrigger::IsActive() +{ + return PlayerbotAI::IsRanged(bot) && AI_VALUE2(Unit*, "find target", "death talon wyrmguard"); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.h b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.h new file mode 100644 index 000000000..84be389c2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/BWL/BWLTriggers.h @@ -0,0 +1,87 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BWLTRIGGERS_H +#define PLAYERBOTS_BWLTRIGGERS_H + +#include "Trigger.h" + +// General + +class BwlSuppressionDeviceTrigger : public Trigger +{ +public: + BwlSuppressionDeviceTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl suppression device") {} + bool IsActive() override; +}; + +// Razorgore the Untamed + +class BwlRazorgoreNotMindControlledTrigger : public Trigger +{ +public: + BwlRazorgoreNotMindControlledTrigger(PlayerbotAI* botAI, const std::string& name = "bwl razorgore not mind controlled") : Trigger(botAI, name) {} + bool IsActive() override; +}; + +// Vaelastrasz the Corrupt + +class BwlVaelastraszPositioningTrigger : public Trigger +{ +public: + BwlVaelastraszPositioningTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl vaelastrasz positioning") {} + bool IsActive() override; +}; + +class BwlVaelastraszBurningAdrenalineTrigger : public Trigger +{ +public: + BwlVaelastraszBurningAdrenalineTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl vaelastrasz burning adrenaline") {} + bool IsActive() override; +}; + +// Chromaggus + +class BwlAfflictionBronzeTrigger : public Trigger +{ +public: + BwlAfflictionBronzeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl affliction bronze") {} + bool IsActive() override; +}; + +// Nefarian + +class BwlWildMagicTrigger : public Trigger +{ +public: + BwlWildMagicTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl wild magic") {} + bool IsActive() override; +}; + +class BwlNefarianFearWardTrigger : public Trigger +{ +public: + BwlNefarianFearWardTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl nefarian fear ward") {} + bool IsActive() override; +}; + +// Trash + +class BwlDeathTalonWyrmguardTankTrigger : public Trigger +{ +public: + BwlDeathTalonWyrmguardTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl death talon wyrmguard tank") {} + bool IsActive() override; +}; + +class BwlDeathTalonWyrmguardRangedTrigger : public Trigger +{ +public: + BwlDeathTalonWyrmguardRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl death talon wyrmguard ranged") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.cpp b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.cpp deleted file mode 100644 index 9014345cc..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "RaidBwlActions.h" - -#include "Playerbots.h" - -bool BwlOnyxiaScaleCloakAuraCheckAction::Execute(Event /*event*/) -{ - bot->AddAura(22683, bot); - return true; -} - -bool BwlOnyxiaScaleCloakAuraCheckAction::isUseful() { return !bot->HasAura(22683); } - -bool BwlTurnOffSuppressionDeviceAction::Execute(Event /*event*/) -{ - GuidVector gos = AI_VALUE(GuidVector, "nearest game objects"); - for (GuidVector::iterator i = gos.begin(); i != gos.end(); i++) - { - GameObject* go = botAI->GetGameObject(*i); - if (!go) - { - continue; - } - if (go->GetEntry() != 179784 || go->GetDistance(bot) >= 15.0f || go->GetGoState() != GO_STATE_READY) - { - continue; - } - go->SetGoState(GO_STATE_ACTIVE); - } - return true; -} - -bool BwlUseHourglassSandAction::Execute(Event /*event*/) -{ - return botAI->CastSpell(23645, bot); -} diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.h b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.h deleted file mode 100644 index 24dcf3996..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Action/RaidBwlActions.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef _PLAYERBOT_RAIDBWLACTIONS_H -#define _PLAYERBOT_RAIDBWLACTIONS_H - -#include "Action.h" -#include "AttackAction.h" -#include "GenericActions.h" -#include "MovementActions.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" - -class BwlOnyxiaScaleCloakAuraCheckAction : public Action -{ -public: - BwlOnyxiaScaleCloakAuraCheckAction(PlayerbotAI* botAI) : Action(botAI, "bwl onyxia scale cloak aura check") {} - bool Execute(Event event) override; - bool isUseful() override; -}; - -class BwlTurnOffSuppressionDeviceAction : public Action -{ -public: - BwlTurnOffSuppressionDeviceAction(PlayerbotAI* botAI) : Action(botAI, "bwl turn off suppression device") {} - bool Execute(Event event) override; -}; - -class BwlUseHourglassSandAction : public Action -{ -public: - BwlUseHourglassSandAction(PlayerbotAI* botAI) : Action(botAI, "bwl use hourglass sand") {} - bool Execute(Event event) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlActionContext.h b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlActionContext.h deleted file mode 100644 index 7b73f410f..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlActionContext.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef _PLAYERBOT_RAIDBWLACTIONCONTEXT_H -#define _PLAYERBOT_RAIDBWLACTIONCONTEXT_H - -#include "Action.h" -#include "NamedObjectContext.h" -#include "RaidBwlActions.h" - -class RaidBwlActionContext : public NamedObjectContext -{ -public: - RaidBwlActionContext() - { - creators["bwl check onyxia scale cloak"] = &RaidBwlActionContext::bwl_check_onyxia_scale_cloak; - creators["bwl turn off suppression device"] = &RaidBwlActionContext::bwl_turn_off_suppression_device; - creators["bwl use hourglass sand"] = &RaidBwlActionContext::bwl_use_hourglass_sand; - } - -private: - static Action* bwl_check_onyxia_scale_cloak(PlayerbotAI* botAI) { return new BwlOnyxiaScaleCloakAuraCheckAction(botAI); } - static Action* bwl_turn_off_suppression_device(PlayerbotAI* botAI) { return new BwlTurnOffSuppressionDeviceAction(botAI); } - static Action* bwl_use_hourglass_sand(PlayerbotAI* botAI) { return new BwlUseHourglassSandAction(botAI); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h deleted file mode 100644 index aa6b57c9f..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H - -#include "AiObjectContext.h" -#include "NamedObjectContext.h" -#include "RaidBwlTriggers.h" - -class RaidBwlTriggerContext : public NamedObjectContext -{ -public: - RaidBwlTriggerContext() - { - creators["bwl suppression device"] = &RaidBwlTriggerContext::bwl_suppression_device; - creators["bwl affliction bronze"] = &RaidBwlTriggerContext::bwl_affliction_bronze; - } - -private: - static Trigger* bwl_suppression_device(PlayerbotAI* ai) { return new BwlSuppressionDeviceTrigger(ai); } - static Trigger* bwl_affliction_bronze(PlayerbotAI* ai) { return new BwlAfflictionBronzeTrigger(ai); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.cpp deleted file mode 100644 index c65a80ecb..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "RaidBwlStrategy.h" - -#include "Strategy.h" - -void RaidBwlStrategy::InitTriggers(std::vector& triggers) -{ - triggers.push_back(new TriggerNode("often", - { NextAction("bwl check onyxia scale cloak", ACTION_RAID) })); - - triggers.push_back(new TriggerNode("bwl suppression device", - { NextAction("bwl turn off suppression device", ACTION_RAID) })); - - triggers.push_back(new TriggerNode("bwl affliction bronze", - { NextAction("bwl use hourglass sand", ACTION_RAID) })); -} diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h deleted file mode 100644 index 4308871c8..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Strategy/RaidBwlStrategy.h +++ /dev/null @@ -1,18 +0,0 @@ - -#ifndef _PLAYERBOT_RAIDBWLSTRATEGY_H -#define _PLAYERBOT_RAIDBWLSTRATEGY_H - -#include "AiObjectContext.h" -#include "Multiplier.h" -#include "Strategy.h" - -class RaidBwlStrategy : public Strategy -{ -public: - RaidBwlStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "bwl"; } - virtual void InitTriggers(std::vector& triggers) override; - // virtual void InitMultipliers(std::vector &multipliers) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.cpp deleted file mode 100644 index e34e7a9a6..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "RaidBwlTriggers.h" - -#include "SharedDefines.h" - -bool BwlSuppressionDeviceTrigger::IsActive() -{ - GuidVector gos = AI_VALUE(GuidVector, "nearest game objects"); - for (GuidVector::iterator i = gos.begin(); i != gos.end(); i++) - { - GameObject* go = botAI->GetGameObject(*i); - if (!go) - { - continue; - } - if (go->GetEntry() != 179784 || go->GetDistance(bot) >= 15.0f || go->GetGoState() != GO_STATE_READY) - { - continue; - } - return true; - } - return false; -} - -bool BwlAfflictionBronzeTrigger::IsActive() { return bot->HasAura(23170); } diff --git a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.h b/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.h deleted file mode 100644 index fc19acd5d..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/BlackwingLair/Trigger/RaidBwlTriggers.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef _PLAYERBOT_RAIDBWLTRIGGERS_H -#define _PLAYERBOT_RAIDBWLTRIGGERS_H - -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "Trigger.h" - -class BwlSuppressionDeviceTrigger : public Trigger -{ -public: - BwlSuppressionDeviceTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl suppression device") {} - bool IsActive() override; -}; - -class BwlAfflictionBronzeTrigger : public Trigger -{ -public: - BwlAfflictionBronzeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "bwl affliction bronze") {} - bool IsActive() override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/RaidEoEActionContext.h b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEActionContext.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/RaidEoEActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoEActionContext.h index 620610e91..e0086096d 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/RaidEoEActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_RAIDEOEACTIONCONTEXT_H -#define _PLAYERBOT_RAIDEOEACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_EOEACTIONCONTEXT_H +#define PLAYERBOTS_EOEACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "RaidEoEActions.h" +#include "EoEActions.h" class RaidEoEActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Action/RaidEoEActions.cpp b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEActions.cpp similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Action/RaidEoEActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoEActions.cpp index b08db3f33..0783d97ba 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Action/RaidEoEActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEActions.cpp @@ -1,6 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Playerbots.h" -#include "RaidEoEActions.h" -#include "RaidEoETriggers.h" +#include "EoEActions.h" +#include "EoETriggers.h" bool MalygosPositionAction::Execute(Event /*event*/) { diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Action/RaidEoEActions.h b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEActions.h similarity index 85% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Action/RaidEoEActions.h rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoEActions.h index 0d5b6fcc3..98e7a8f4a 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Action/RaidEoEActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDEOEACTIONS_H -#define _PLAYERBOT_RAIDEOEACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_EOEACTIONS_H +#define PLAYERBOTS_EOEACTIONS_H #include "AttackAction.h" #include "GenericSpellActions.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.cpp similarity index 85% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.cpp index a2d8a3f1d..f1d46ea51 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.cpp @@ -1,4 +1,10 @@ -#include "RaidEoEMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "EoEMultipliers.h" #include "ChooseTargetActions.h" #include "DKActions.h" @@ -9,8 +15,8 @@ #include "GenericSpellActions.h" #include "MovementActions.h" #include "PaladinActions.h" -#include "RaidEoEActions.h" -#include "RaidEoETriggers.h" +#include "EoEActions.h" +#include "EoETriggers.h" #include "ReachTargetActions.h" #include "ScriptedCreature.h" #include "WarriorActions.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.h new file mode 100644 index 000000000..f802d57ef --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEMultipliers.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_EOEMULTIPLIERS_H +#define PLAYERBOTS_EOEMULTIPLIERS_H + +#include "Multiplier.h" + +class MalygosMultiplier : public Multiplier +{ +public: + MalygosMultiplier(PlayerbotAI* ai) : Multiplier(ai, "malygos") {} + +public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEStrategy.cpp similarity index 69% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoEStrategy.cpp index 3c0ff7ffd..7f0953f7e 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEStrategy.cpp @@ -1,5 +1,11 @@ -#include "RaidEoEStrategy.h" -#include "RaidEoEMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "EoEStrategy.h" +#include "EoEMultipliers.h" #include "Strategy.h" void RaidEoEStrategy::InitTriggers(std::vector& triggers) diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEStrategy.h similarity index 53% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoEStrategy.h index eb7a147bd..9f5d6c4b1 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Strategy/RaidEoEStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoEStrategy.h @@ -1,8 +1,12 @@ -#ifndef _PLAYERBOT_RAIDEOESTRATEGY_H -#define _PLAYERBOT_RAIDEOESTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_EOESTRATEGY_H +#define PLAYERBOTS_EOESTRATEGY_H -#include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidEoEStrategy : public Strategy diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggerContext.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggerContext.h index 0c58f6cbf..fd564aaa5 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggerContext.h @@ -1,9 +1,14 @@ -#ifndef _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_EOETRIGGERCONTEXT_H +#define PLAYERBOTS_EOETRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "NamedObjectContext.h" -#include "RaidEoETriggers.h" +#include "EoETriggers.h" class RaidEoETriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Trigger/RaidEoETriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggers.cpp similarity index 79% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Trigger/RaidEoETriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggers.cpp index f07456d04..6e00f00b6 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Trigger/RaidEoETriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggers.cpp @@ -1,4 +1,10 @@ -#include "RaidEoETriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "EoETriggers.h" #include "SharedDefines.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Trigger/RaidEoETriggers.h b/modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggers.h similarity index 83% rename from modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Trigger/RaidEoETriggers.h rename to modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggers.h index 3d01daa59..6bfec6b3c 100644 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Trigger/RaidEoETriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/EoE/EoETriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDEOETRIGGERS_H -#define _PLAYERBOT_RAIDEOETRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_EOETRIGGERS_H +#define PLAYERBOTS_EOETRIGGERS_H #include "PlayerbotAI.h" #include "Playerbots.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.h deleted file mode 100644 index 05db5ee9e..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/EyeOfEternity/Multiplier/RaidEoEMultipliers.h +++ /dev/null @@ -1,16 +0,0 @@ - -#ifndef _PLAYERRBOT_RAIDEOEMULTIPLIERS_H -#define _PLAYERRBOT_RAIDEOEMULTIPLIERS_H - -#include "Multiplier.h" - -class MalygosMultiplier : public Multiplier -{ -public: - MalygosMultiplier(PlayerbotAI* ai) : Multiplier(ai, "malygos") {} - -public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActionContext.h new file mode 100644 index 000000000..c465de7b5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActionContext.h @@ -0,0 +1,105 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULACTIONCONTEXT_H +#define PLAYERBOTS_GRUULACTIONCONTEXT_H + +#include "GruulActions.h" +#include "NamedObjectContext.h" + +class RaidGruulsLairActionContext : public NamedObjectContext +{ +public: + RaidGruulsLairActionContext() + { + // High King Maulgar + creators["high king maulgar main tank attack maulgar"] = + &RaidGruulsLairActionContext::high_king_maulgar_main_tank_attack_maulgar; + + creators["high king maulgar first assist tank attack olm"] = + &RaidGruulsLairActionContext::high_king_maulgar_first_assist_tank_attack_olm; + + creators["high king maulgar second assist tank attack blindeye"] = + &RaidGruulsLairActionContext::high_king_maulgar_second_assist_tank_attack_blindeye; + + creators["high king maulgar mage tank attack krosh"] = + &RaidGruulsLairActionContext::high_king_maulgar_mage_tank_attack_krosh; + + creators["high king maulgar moonkin tank attack kiggler"] = + &RaidGruulsLairActionContext::high_king_maulgar_moonkin_tank_attack_kiggler; + + creators["high king maulgar assign dps priority"] = + &RaidGruulsLairActionContext::high_king_maulgar_assign_dps_priority; + + creators["high king maulgar run away from whirlwind"] = + &RaidGruulsLairActionContext::high_king_maulgar_run_away_from_whirlwind; + + creators["high king maulgar move away from blast nova danger"] = + &RaidGruulsLairActionContext::high_king_maulgar_move_away_from_blast_nova_danger; + + creators["high king maulgar banish fel stalker"] = + &RaidGruulsLairActionContext::high_king_maulgar_banish_fel_stalker; + + creators["high king maulgar misdirect ogres to tanks"] = + &RaidGruulsLairActionContext::high_king_maulgar_misdirect_ogres_to_tanks; + + // Gruul the Dragonkiller + creators["gruul the dragonkiller tanks position boss"] = + &RaidGruulsLairActionContext::gruul_the_dragonkiller_tanks_position_boss; + + creators["gruul the dragonkiller spread ranged"] = + &RaidGruulsLairActionContext::gruul_the_dragonkiller_spread_ranged; + + creators["gruul the dragonkiller shatter spread"] = + &RaidGruulsLairActionContext::gruul_the_dragonkiller_shatter_spread; + } + +private: + // High King Maulgar + static Action* high_king_maulgar_main_tank_attack_maulgar(PlayerbotAI* botAI) { + return new HighKingMaulgarMainTankAttackMaulgarAction(botAI); + } + static Action* high_king_maulgar_first_assist_tank_attack_olm(PlayerbotAI* botAI) { + return new HighKingMaulgarFirstAssistTankAttackOlmAction(botAI); + } + static Action* high_king_maulgar_second_assist_tank_attack_blindeye(PlayerbotAI* botAI) { + return new HighKingMaulgarSecondAssistTankAttackBlindeyeAction(botAI); + } + static Action* high_king_maulgar_mage_tank_attack_krosh(PlayerbotAI* botAI) { + return new HighKingMaulgarMageTankAttackKroshAction(botAI); + } + static Action* high_king_maulgar_moonkin_tank_attack_kiggler(PlayerbotAI* botAI) { + return new HighKingMaulgarMoonkinTankAttackKigglerAction(botAI); + } + static Action* high_king_maulgar_assign_dps_priority(PlayerbotAI* botAI) { + return new HighKingMaulgarAssignDPSPriorityAction(botAI); + } + static Action* high_king_maulgar_run_away_from_whirlwind(PlayerbotAI* botAI) { + return new HighKingMaulgarRunAwayFromWhirlwindAction(botAI); + } + static Action* high_king_maulgar_move_away_from_blast_nova_danger(PlayerbotAI* botAI) { + return new HighKingMaulgarMoveAwayFromBlastNovaDangerAction(botAI); + } + static Action* high_king_maulgar_banish_fel_stalker(PlayerbotAI* botAI) { + return new HighKingMaulgarBanishFelStalkerAction(botAI); + } + static Action* high_king_maulgar_misdirect_ogres_to_tanks(PlayerbotAI* botAI) { + return new HighKingMaulgarMisdirectOgresToTanksAction(botAI); + } + + // Gruul the Dragonkiller + static Action* gruul_the_dragonkiller_tanks_position_boss(PlayerbotAI* botAI) { + return new GruulTheDragonkillerTanksPositionBossAction(botAI); + } + static Action* gruul_the_dragonkiller_spread_ranged(PlayerbotAI* botAI) { + return new GruulTheDragonkillerSpreadRangedAction(botAI); + } + static Action* gruul_the_dragonkiller_shatter_spread(PlayerbotAI* botAI) { + return new GruulTheDragonkillerShatterSpreadAction(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.cpp new file mode 100644 index 000000000..67830c70d --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.cpp @@ -0,0 +1,609 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GruulActions.h" +#include "GruulHelpers.h" +#include "CreatureAI.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" +#include "Unit.h" + +using namespace GruulsLairHelpers; + +// High King Maulgar Actions + +// Main tank on Maulgar +bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event /*event*/) +{ + Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; + + if (MarkTargetWithSquare(bot, maulgar)) + return true; + + SetRtiTarget(botAI, "square", maulgar); + + if (AI_VALUE(Unit*, "current target") != maulgar) + return Attack(maulgar); + + if (maulgar->GetVictim() == bot) + { + const Position& position = MAULGAR_TANK_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// First offtank on Olm +bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event /*event*/) +{ + Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); + if (!olm) + return false; + + if (MarkTargetWithCircle(bot, olm)) + return true; + + SetRtiTarget(botAI, "circle", olm); + + if (AI_VALUE(Unit*, "current target") != olm) + return Attack(olm); + + if (olm->GetVictim() == bot) + { + const Position& position = OLM_TANK_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Second offtank on Blindeye +bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event /*event*/) +{ + Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + if (!blindeye) + return false; + + if (MarkTargetWithStar(bot, blindeye)) + return true; + + SetRtiTarget(botAI, "star", blindeye); + + if (AI_VALUE(Unit*, "current target") != blindeye) + return Attack(blindeye); + + if (blindeye->GetVictim() == bot) + { + const Position& position = BLINDEYE_TANK_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Mage with highest max HP on Krosh +bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event /*event*/) +{ + Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + if (!krosh) + return false; + + if (MarkTargetWithTriangle(bot, krosh)) + return true; + + SetRtiTarget(botAI, "triangle", krosh); + + if (krosh->HasAura(static_cast(GruulsLairSpells::SPELL_SPELL_SHIELD)) && + botAI->CanCastSpell("spellsteal", krosh)) + { + return botAI->CastSpell("spellsteal", krosh); + } + + if (!bot->HasAura(static_cast(GruulsLairSpells::SPELL_SPELL_SHIELD)) && + botAI->CanCastSpell("fire ward", bot)) + { + return botAI->CastSpell("fire ward", bot); + } + + if (AI_VALUE(Unit*, "current target") != krosh) + return Attack(krosh); + + if (krosh->GetVictim() == bot) + { + const Position& position = KROSH_TANK_POSITION; + const float distanceKroshToPosition = + krosh->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + constexpr float minDistance = 17.0f; + constexpr float maxDistance = 30.0f; + + if (distanceKroshToPosition > minDistance && distanceKroshToPosition < maxDistance && + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 1.0f) + { + return MoveTo(GRUULS_LAIR_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + constexpr float safeDistance = 15.0f; + const float currentDistance = bot->GetDistance2d(krosh); + + if (currentDistance < safeDistance) + { + botAI->InterruptSpell(); + return MoveAway(krosh, safeDistance - currentDistance); + } + } + } + + return false; +} + +// Moonkin with highest max HP on Kiggler +bool HighKingMaulgarMoonkinTankAttackKigglerAction::Execute(Event /*event*/) +{ + Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); + if (!kiggler) + return false; + + if (MarkTargetWithDiamond(bot, kiggler)) + return true; + + SetRtiTarget(botAI, "diamond", kiggler); + + if (AI_VALUE(Unit*, "current target") != kiggler) + return Attack(kiggler); + + if (kiggler->GetVictim() == bot) + { + constexpr float safeDistance = 28.5f; + const float currentDistance = bot->GetDistance2d(kiggler); + + if (currentDistance < safeDistance) + { + botAI->InterruptSpell(); + return MoveAway(kiggler, safeDistance - currentDistance); + } + } + + return false; +} + +bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Blindeye + if (Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer")) + { + if (MarkTargetWithStar(bot, blindeye)) + return true; + + SetRtiTarget(botAI, "star", blindeye); + + if (AI_VALUE(Unit*, "current target") != blindeye) + return Attack(blindeye); + + return false; + } + + // Target priority 2: Olm + if (Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner")) + { + if (MarkTargetWithCircle(bot, olm)) + return true; + + SetRtiTarget(botAI, "circle", olm); + + if (AI_VALUE(Unit*, "current target") != olm) + return Attack(olm); + + return false; + } + + // Target priority 3a: Krosh (ranged only) + if (Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + krosh && botAI->IsRanged(bot)) + { + if (MarkTargetWithTriangle(bot, krosh)) + return true; + + SetRtiTarget(botAI, "triangle", krosh); + + if (AI_VALUE(Unit*, "current target") != krosh) + return Attack(krosh); + + return false; + } + + // Target priority 3b: Kiggler + if (Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed")) + { + if (MarkTargetWithDiamond(bot, kiggler)) + return true; + + SetRtiTarget(botAI, "diamond", kiggler); + + if (AI_VALUE(Unit*, "current target") != kiggler) + return Attack(kiggler); + + return false; + } + + // Target priority 4: Maulgar + if (Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar")) + { + if (MarkTargetWithSquare(bot, maulgar)) + return true; + + SetRtiTarget(botAI, "square", maulgar); + + if (AI_VALUE(Unit*, "current target") != maulgar) + return Attack(maulgar); + } + + return false; +} + +bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; + + constexpr float safeDistance = 8.0f; + const float currentDistance = bot->GetDistance2d(maulgar); + + if (currentDistance < safeDistance) + { + botAI->InterruptSpell(); + return MoveAway(maulgar, safeDistance - currentDistance); + } + + return false; +} + +bool HighKingMaulgarMoveAwayFromBlastNovaDangerAction::Execute(Event /*event*/) +{ + Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + if (!krosh) + return false; + + constexpr float safeDistance = 20.0f; + constexpr uint32 minInterval = 1000; + const float currentDistance = bot->GetDistance2d(krosh); + + if (currentDistance < safeDistance) + { + botAI->InterruptSpell(); + return FleePosition(krosh->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +bool HighKingMaulgarBanishFelStalkerAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector felStalkers; + std::list creatureList; + constexpr float searchRadius = 50.0f; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(GruulsLairNpcs::NPC_WILD_FEL_STALKER), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + felStalkers.push_back(creature); + } + + std::vector warlocks; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_WARLOCK && + GET_PLAYERBOT_AI(member)) + { + warlocks.push_back(member); + } + } + + int warlockIndex = -1; + for (size_t i = 0; i < warlocks.size(); ++i) + { + if (warlocks[i] == bot) + { + warlockIndex = static_cast(i); + break; + } + } + + const uint8 felStalkersSize = felStalkers.size(); + + if (warlockIndex >= 0 && warlockIndex < felStalkersSize) + { + Unit* assignedFelStalker = felStalkers[warlockIndex]; + if (!botAI->HasAura("banish", assignedFelStalker) && + botAI->CanCastSpell("banish", assignedFelStalker)) + { + return botAI->CastSpell("banish", assignedFelStalker); + } + } + + return false; +} + +// Hunter 1: Misdirect Blindeye to second offtank +// Hunter 2: Misdirect Olm to first offtank +// Hunter 3: Misdirect Kiggler to Moonkin tank +// Hunter 4: Misdirect Krosh to Mage tank +bool HighKingMaulgarMisdirectOgresToTanksAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && + GET_PLAYERBOT_AI(member)) + { + hunters.push_back(member); + } + + if (hunters.size() >= 4) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* ogreTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0) + { + ogreTarget = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + tankTarget = GetGroupAssistTank(botAI, bot, 1); + } + else if (hunterIndex == 1) + { + ogreTarget = AI_VALUE2(Unit*, "find target", "olm the summoner"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetGroupAssistTank(botAI, bot, 0)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 2) + { + ogreTarget = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetKigglerMoonkinTank(bot)) + { + tankTarget = member; + break; + } + } + } + else if (hunterIndex == 3) + { + ogreTarget = AI_VALUE2(Unit*, "find target", "krosh firehand"); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + if (Player* member = GetKroshMageTank(bot)) + { + tankTarget = member; + break; + } + } + } + + if (!ogreTarget || !tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(static_cast(GruulsLairSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", ogreTarget)) + { + return botAI->CastSpell("steady shot", ogreTarget); + } + + return false; +} + +// Gruul the Dragonkiller Actions + +// Position in center of the room +bool GruulTheDragonkillerTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul) + return false; + + if (AI_VALUE(Unit*, "current target") != gruul) + return Attack(gruul); + + if (gruul->GetVictim() == bot) + { + const Position& position = GRUUL_TANK_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(5.0f, distToPosition); + const float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Ranged will take initial positions around the middle of the room, 25-40 yards from center +// Ranged should spread out 10 yards from each other +bool GruulTheDragonkillerSpreadRangedAction::Execute(Event /*event*/) +{ + Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul) + return false; + + if (gruul->GetHealth() == gruul->GetMaxHealth()) + _hasReachedInitialPosition = false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector members; + Player* closestMember = nullptr; + float closestDist = std::numeric_limits::max(); + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + members.push_back(member); + + if (member != bot) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) + { + closestDist = dist; + closestMember = member; + } + } + } + + const Position& position = GRUUL_TANK_POSITION; + + if (_initialPosition.GetPositionX() == 0.0f && _initialPosition.GetPositionY() == 0.0f) + { + auto it = std::find(members.begin(), members.end(), bot); + uint8 botIndex = (it != members.end()) ? std::distance(members.begin(), it) : 0; + uint8 count = members.size(); + + constexpr float minRadius = 25.0f; + constexpr float maxRadius = 40.0f; + float angle = 2 * M_PI * botIndex / count; + float radius = minRadius + static_cast(rand()) / + static_cast(RAND_MAX) * (maxRadius - minRadius); + float targetX = position.GetPositionX() + radius * cos(angle); + float targetY = position.GetPositionY() + radius * sin(angle); + + _initialPosition = Position(targetX, targetY, position.GetPositionZ()); + } + + if (!_hasReachedInitialPosition) + { + const float distanceToTarget = + bot->GetExactDist2d(_initialPosition.GetPositionX(), _initialPosition.GetPositionY()); + if (distanceToTarget > 2.0f) + { + float destX = _initialPosition.GetPositionX(); + float destY = _initialPosition.GetPositionY(); + float destZ = _initialPosition.GetPositionZ(); + + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), + bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) + { + return false; + } + + return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + _hasReachedInitialPosition = true; + } + else + { + constexpr float minSpreadDistance = 10.0f; + constexpr uint32 minInterval = 1000; + if (closestMember && closestDist < minSpreadDistance) + return FleePosition(closestMember->GetPosition(), minSpreadDistance, minInterval); + } + + return false; +} + +// Try to get away from other group members when Ground Slam is cast +bool GruulTheDragonkillerShatterSpreadAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 10.0f; + constexpr uint32 minInterval = 0; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.h new file mode 100644 index 000000000..81fd8b5a6 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulActions.h @@ -0,0 +1,122 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULACTIONS_H +#define PLAYERBOTS_GRUULACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +class HighKingMaulgarMainTankAttackMaulgarAction : public AttackAction +{ +public: + HighKingMaulgarMainTankAttackMaulgarAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar main tank attack maulgar") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarFirstAssistTankAttackOlmAction : public AttackAction +{ +public: + HighKingMaulgarFirstAssistTankAttackOlmAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar first assist tank attack olm") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarSecondAssistTankAttackBlindeyeAction : public AttackAction +{ +public: + HighKingMaulgarSecondAssistTankAttackBlindeyeAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar second assist tank attack blindeye") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarMageTankAttackKroshAction : public AttackAction +{ +public: + HighKingMaulgarMageTankAttackKroshAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar mage tank attack krosh") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarMoonkinTankAttackKigglerAction : public AttackAction +{ +public: + HighKingMaulgarMoonkinTankAttackKigglerAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar moonkin tank attack kiggler") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarAssignDPSPriorityAction : public AttackAction +{ +public: + HighKingMaulgarAssignDPSPriorityAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar assign dps priority") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarRunAwayFromWhirlwindAction : public MovementAction +{ +public: + HighKingMaulgarRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar run away from whirlwind") : MovementAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarMoveAwayFromBlastNovaDangerAction : public MovementAction +{ +public: + HighKingMaulgarMoveAwayFromBlastNovaDangerAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar move away from blast nova danger") : MovementAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarBanishFelStalkerAction : public AttackAction +{ +public: + HighKingMaulgarBanishFelStalkerAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar banish fel stalker") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class HighKingMaulgarMisdirectOgresToTanksAction : public AttackAction +{ +public: + HighKingMaulgarMisdirectOgresToTanksAction( + PlayerbotAI* botAI, std::string const name = "high king maulgar misdirect ogres to tanks") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class GruulTheDragonkillerTanksPositionBossAction : public AttackAction +{ +public: + GruulTheDragonkillerTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller tanks position boss") : AttackAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class GruulTheDragonkillerSpreadRangedAction : public MovementAction +{ +public: + GruulTheDragonkillerSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller spread ranged") : MovementAction(botAI, name) {}; + bool Execute(Event event) override; + +private: + Position _initialPosition; + bool _hasReachedInitialPosition = false; +}; + +class GruulTheDragonkillerShatterSpreadAction : public MovementAction +{ +public: + GruulTheDragonkillerShatterSpreadAction( + PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller shatter spread") : MovementAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.cpp new file mode 100644 index 000000000..17a994b54 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.cpp @@ -0,0 +1,107 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GruulHelpers.h" +#include "AiFactory.h" +#include "GroupReference.h" +#include "Playerbots.h" +#include "Unit.h" + +namespace GruulsLairHelpers +{ + +const Position MAULGAR_TANK_POSITION = { 90.686f, 167.047f, -13.234f }; +const Position OLM_TANK_POSITION = { 101.050f, 219.359f, -9.503f }; +const Position BLINDEYE_TANK_POSITION = { 99.681f, 213.989f, -10.345f }; +const Position KROSH_TANK_POSITION = { 116.880f, 166.208f, -14.231f }; +const Position MAULGAR_ROOM_CENTER = { 88.754f, 150.759f, -11.569f }; +const Position GRUUL_TANK_POSITION = { 241.238f, 365.025f, -4.220f }; + +Player* GetKroshMageTank(Player* bot) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + // (1) First loop: Return the first assistant Mage (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + } + + // (2) Fall back to bot Mage with highest HP + Player* highestHpMage = nullptr; + uint32 highestHp = 0; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || + member->getClass() != CLASS_MAGE) + { + continue; + } + + uint32 hp = member->GetMaxHealth(); + if (!highestHpMage || hp > highestHp) + { + highestHpMage = member; + highestHp = hp; + } + } + + return highestHpMage; +} + +Player* GetKigglerMoonkinTank(Player* bot) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + + // (1) First loop: Return the first assistant Moonkin (real player or bot) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID) + continue; + + if (group->IsAssistant(member->GetGUID()) && tab == DRUID_TAB_BALANCE) + return member; + } + + // (2) Fall back to bot Moonkin with highest HP + Player* highestHpMoonkin = nullptr; + uint32 highestHp = 0; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID || + !GET_PLAYERBOT_AI(member) || tab != DRUID_TAB_BALANCE) + { + continue; + } + + uint32 hp = member->GetMaxHealth(); + if (!highestHpMoonkin || hp > highestHp) + { + highestHpMoonkin = member; + highestHp = hp; + } + } + + return highestHpMoonkin; +} + +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.h new file mode 100644 index 000000000..1a83e5e55 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulHelpers.h @@ -0,0 +1,50 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULHELPERS_H +#define PLAYERBOTS_GRUULHELPERS_H + +#include "PlayerbotAI.h" + +namespace GruulsLairHelpers +{ + +enum class GruulsLairSpells : uint32 +{ + // High King Maulgar + SPELL_WHIRLWIND = 33238, + + // Krosh Firehand + SPELL_SPELL_SHIELD = 33054, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Gruul the Dragonkiller + SPELL_GROUND_SLAM_1 = 33525, + SPELL_GROUND_SLAM_2 = 39187, +}; + +enum class GruulsLairNpcs : uint32 +{ + NPC_WILD_FEL_STALKER = 18847, +}; + +constexpr uint32 GRUULS_LAIR_MAP_ID = 565; + +Player* GetKroshMageTank(Player* bot); +Player* GetKigglerMoonkinTank(Player* bot); + +extern const Position MAULGAR_TANK_POSITION; +extern const Position OLM_TANK_POSITION; +extern const Position BLINDEYE_TANK_POSITION; +extern const Position KROSH_TANK_POSITION; +extern const Position MAULGAR_ROOM_CENTER; +extern const Position GRUUL_TANK_POSITION; + +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.cpp new file mode 100644 index 000000000..63a6bde63 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.cpp @@ -0,0 +1,162 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GruulMultipliers.h" +#include "GruulActions.h" +#include "GruulHelpers.h" +#include "ChooseTargetActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "Playerbots.h" +#include "ReachTargetActions.h" +#include "ShamanActions.h" + +using namespace GruulsLairHelpers; + +float HighKingMaulgarDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + if (!blindeye || blindeye->GetHealthPct() < 75.0f) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float HighKingMaulgarControlTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "high king maulgar")) + return 1.0f; + + if (dynamic_cast(action) || + (bot->GetVictim() != nullptr && dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +float HighKingMaulgarAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar || + !maulgar->HasAura(static_cast(GruulsLairSpells::SPELL_WHIRLWIND))) + { + return 1.0f; + } + + if (AI_VALUE(Unit*, "current target") != maulgar) + return 1.0f; + + if (botAI->IsMainTank(bot)) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} + +// Arcane Shot will remove Spell Shield, which the mage tank needs to survive +float HighKingMaulgarDisableArcaneShotOnKroshMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); + if (!krosh || AI_VALUE(Unit*, "current target") != krosh) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float HighKingMaulgarDisableMageTankAoeMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_MAGE) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "krosh firehand")) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (castSpellAction && + castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + { + return 0.0f; + } + + return 1.0f; +} + +float GruulTheDragonkillerDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul || gruul->GetHealthPct() < 95.0f) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float GruulTheDragonkillerControlTankMovementMultiplier::GetValue(Action* action) +{ + Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); + if (!gruul || gruul->GetVictim() != bot) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float GruulTheDragonkillerStaySpreadForShatterMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(static_cast(GruulsLairSpells::SPELL_GROUND_SLAM_1)) && + !bot->HasAura(static_cast(GruulsLairSpells::SPELL_GROUND_SLAM_2))) + { + return 1.0f; + } + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + { + return 0.0f; + } + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.h new file mode 100644 index 000000000..c3c199310 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulMultipliers.h @@ -0,0 +1,76 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULMULTIPLIERS_H +#define PLAYERBOTS_GRUULMULTIPLIERS_H + +#include "Multiplier.h" + +class HighKingMaulgarDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + HighKingMaulgarDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar delay bloodlust and heroism multiplier") {} + float GetValue(Action* action) override; +}; + +class HighKingMaulgarControlTankActionsMultiplier : public Multiplier +{ +public: + HighKingMaulgarControlTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar control tank actions multiplier") {} + float GetValue(Action* action) override; +}; + +class HighKingMaulgarAvoidWhirlwindMultiplier : public Multiplier +{ +public: + HighKingMaulgarAvoidWhirlwindMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar avoid whirlwind multiplier") {} + float GetValue(Action* action) override; +}; + +class HighKingMaulgarDisableArcaneShotOnKroshMultiplier : public Multiplier +{ +public: + HighKingMaulgarDisableArcaneShotOnKroshMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar disable arcane shot on krosh multiplier") {} + float GetValue(Action* action) override; +}; + +class HighKingMaulgarDisableMageTankAoeMultiplier : public Multiplier +{ +public: + HighKingMaulgarDisableMageTankAoeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar disable mage tank aoe multiplier") {} + float GetValue(Action* action) override; +}; + +class GruulTheDragonkillerDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + GruulTheDragonkillerDelayBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gruul the dragonkiller delay bloodlust and heroism multiplier") {} + float GetValue(Action* action) override; +}; + +class GruulTheDragonkillerControlTankMovementMultiplier : public Multiplier +{ +public: + GruulTheDragonkillerControlTankMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gruul the dragonkiller control tank movement multiplier") {} + float GetValue(Action* action) override; +}; + +class GruulTheDragonkillerStaySpreadForShatterMultiplier : public Multiplier +{ +public: + GruulTheDragonkillerStaySpreadForShatterMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "gruul the dragonkiller stay spread for shatter multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulStrategy.cpp similarity index 51% rename from modules/mod-playerbots/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/Gruul/GruulStrategy.cpp index 249c8e8a8..11ce991af 100644 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulStrategy.cpp @@ -1,38 +1,44 @@ -#include "RaidGruulsLairStrategy.h" -#include "RaidGruulsLairMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GruulStrategy.h" +#include "GruulMultipliers.h" void RaidGruulsLairStrategy::InitTriggers(std::vector& triggers) { // High King Maulgar - triggers.push_back(new TriggerNode("high king maulgar is main tank", { + triggers.push_back(new TriggerNode("high king maulgar boss engaged by main tank", { NextAction("high king maulgar main tank attack maulgar", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high king maulgar is first assist tank", { + triggers.push_back(new TriggerNode("high king maulgar olm engaged by first assist tank", { NextAction("high king maulgar first assist tank attack olm", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high king maulgar is second assist tank", { + triggers.push_back(new TriggerNode("high king maulgar blindeye engaged by second assist tank", { NextAction("high king maulgar second assist tank attack blindeye", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high king maulgar is mage tank", { + triggers.push_back(new TriggerNode("high king maulgar krosh engaged by mage tank", { NextAction("high king maulgar mage tank attack krosh", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high king maulgar is moonkin tank", { + triggers.push_back(new TriggerNode("high king maulgar kiggler engaged by moonkin tank", { NextAction("high king maulgar moonkin tank attack kiggler", ACTION_RAID + 1) })); triggers.push_back(new TriggerNode("high king maulgar determining kill order", { NextAction("high king maulgar assign dps priority", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high king maulgar healer in danger", { - NextAction("high king maulgar healer find safe position", ACTION_RAID + 1) })); - triggers.push_back(new TriggerNode("high king maulgar boss channeling whirlwind", { - NextAction("high king maulgar run away from whirlwind", ACTION_EMERGENCY + 6) })); + NextAction("high king maulgar run away from whirlwind", ACTION_EMERGENCY + 7) })); - triggers.push_back(new TriggerNode("high king maulgar wild felstalker spawned", { - NextAction("high king maulgar banish felstalker", ACTION_RAID + 2) })); + triggers.push_back(new TriggerNode("high king maulgar krosh casts blast wave", { + NextAction("high king maulgar move away from blast nova danger", ACTION_EMERGENCY + 6) })); - triggers.push_back(new TriggerNode("high king maulgar pulling olm and blindeye", { - NextAction("high king maulgar misdirect olm and blindeye", ACTION_RAID + 2) })); + triggers.push_back(new TriggerNode("high king maulgar wild fel stalker spawned", { + NextAction("high king maulgar banish fel stalker", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("high king maulgar pulling ogre council", { + NextAction("high king maulgar misdirect ogres to tanks", ACTION_RAID + 2) })); // Gruul the Dragonkiller triggers.push_back(new TriggerNode("gruul the dragonkiller boss engaged by tanks", { @@ -47,10 +53,15 @@ void RaidGruulsLairStrategy::InitTriggers(std::vector& triggers) void RaidGruulsLairStrategy::InitMultipliers(std::vector& multipliers) { - multipliers.push_back(new HighKingMaulgarDisableTankAssistMultiplier(botAI)); + // High King Maulgar + multipliers.push_back(new HighKingMaulgarDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new HighKingMaulgarControlTankActionsMultiplier(botAI)); multipliers.push_back(new HighKingMaulgarAvoidWhirlwindMultiplier(botAI)); multipliers.push_back(new HighKingMaulgarDisableArcaneShotOnKroshMultiplier(botAI)); - multipliers.push_back(new HighKingMaulgarDisableMageTankAOEMultiplier(botAI)); - multipliers.push_back(new GruulTheDragonkillerMainTankMovementMultiplier(botAI)); - multipliers.push_back(new GruulTheDragonkillerGroundSlamMultiplier(botAI)); + multipliers.push_back(new HighKingMaulgarDisableMageTankAoeMultiplier(botAI)); + + // Gruul the Dragonkiller + multipliers.push_back(new GruulTheDragonkillerDelayBloodlustAndHeroismMultiplier(botAI)); + multipliers.push_back(new GruulTheDragonkillerControlTankMovementMultiplier(botAI)); + multipliers.push_back(new GruulTheDragonkillerStaySpreadForShatterMultiplier(botAI)); } diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulStrategy.h similarity index 53% rename from modules/mod-playerbots/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/Gruul/GruulStrategy.h index ba6f33f07..3f4fbdf68 100644 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Strategy/RaidGruulsLairStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulStrategy.h @@ -1,8 +1,13 @@ -#ifndef _PLAYERBOT_RAIDGRUULSLAIRSTRATEGY_H -#define _PLAYERBOT_RAIDGRUULSLAIRSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULSTRATEGY_H +#define PLAYERBOTS_GRUULSTRATEGY_H #include "Strategy.h" -#include "Multiplier.h" class RaidGruulsLairStrategy : public Strategy { diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggerContext.h new file mode 100644 index 000000000..341cd3d11 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggerContext.h @@ -0,0 +1,105 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULTRIGGERCONTEXT_H +#define PLAYERBOTS_GRUULTRIGGERCONTEXT_H + +#include "GruulTriggers.h" +#include "NamedObjectContext.h" + +class RaidGruulsLairTriggerContext : public NamedObjectContext +{ +public: + RaidGruulsLairTriggerContext() : NamedObjectContext() + { + // High King Maulgar + creators["high king maulgar boss engaged by main tank"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_boss_engaged_by_main_tank; + + creators["high king maulgar olm engaged by first assist tank"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_olm_engaged_by_first_assist_tank; + + creators["high king maulgar blindeye engaged by second assist tank"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_blindeye_engaged_by_second_assist_tank; + + creators["high king maulgar krosh engaged by mage tank"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_krosh_engaged_by_mage_tank; + + creators["high king maulgar kiggler engaged by moonkin tank"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_kiggler_engaged_by_moonkin_tank; + + creators["high king maulgar determining kill order"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_determining_kill_order; + + creators["high king maulgar boss channeling whirlwind"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_boss_channeling_whirlwind; + + creators["high king maulgar krosh casts blast wave"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_krosh_casts_blast_wave; + + creators["high king maulgar wild fel stalker spawned"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_wild_fel_stalker_spawned; + + creators["high king maulgar pulling ogre council"] = + &RaidGruulsLairTriggerContext::high_king_maulgar_pulling_ogre_council; + + // Gruul the Dragonkiller + creators["gruul the dragonkiller boss engaged by tanks"] = + &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_tanks; + + creators["gruul the dragonkiller boss engaged by ranged"] = + &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_ranged; + + creators["gruul the dragonkiller incoming shatter"] = + &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_incoming_shatter; + } + +private: + // High King Maulgar + static Trigger* high_king_maulgar_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new HighKingMaulgarBossEngagedByMainTankTrigger(botAI); + } + static Trigger* high_king_maulgar_olm_engaged_by_first_assist_tank(PlayerbotAI* botAI) { + return new HighKingMaulgarOlmEngagedByFirstAssistTankTrigger(botAI); + } + static Trigger* high_king_maulgar_blindeye_engaged_by_second_assist_tank(PlayerbotAI* botAI) { + return new HighKingMaulgarBlindeyeEngagedBySecondAssistTankTrigger(botAI); + } + static Trigger* high_king_maulgar_krosh_engaged_by_mage_tank(PlayerbotAI* botAI) { + return new HighKingMaulgarKroshEngagedByMageTankTrigger(botAI); + } + static Trigger* high_king_maulgar_kiggler_engaged_by_moonkin_tank(PlayerbotAI* botAI) { + return new HighKingMaulgarKigglerEngagedByMoonkinTankTrigger(botAI); + } + static Trigger* high_king_maulgar_determining_kill_order(PlayerbotAI* botAI) { + return new HighKingMaulgarDeterminingKillOrderTrigger(botAI); + } + static Trigger* high_king_maulgar_boss_channeling_whirlwind(PlayerbotAI* botAI) { + return new HighKingMaulgarBossChannelingWhirlwindTrigger(botAI); + } + static Trigger* high_king_maulgar_krosh_casts_blast_wave(PlayerbotAI* botAI) { + return new HighKingMaulgarKroshCastsBlastWaveTrigger(botAI); + } + static Trigger* high_king_maulgar_wild_fel_stalker_spawned(PlayerbotAI* botAI) { + return new HighKingMaulgarWildFelStalkerSpawnedTrigger(botAI); + } + static Trigger* high_king_maulgar_pulling_ogre_council(PlayerbotAI* botAI) { + return new HighKingMaulgarPullingOgreCouncilTrigger(botAI); + } + + // Gruul the Dragonkiller + static Trigger* gruul_the_dragonkiller_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new GruulTheDragonkillerBossEngagedByTanksTrigger(botAI); + } + static Trigger* gruul_the_dragonkiller_boss_engaged_by_ranged(PlayerbotAI* botAI) { + return new GruulTheDragonkillerBossEngagedByRangedTrigger(botAI); + } + static Trigger* gruul_the_dragonkiller_incoming_shatter(PlayerbotAI* botAI) { + return new GruulTheDragonkillerIncomingShatterTrigger(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.cpp new file mode 100644 index 000000000..248ce27e3 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.cpp @@ -0,0 +1,124 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GruulTriggers.h" +#include "GruulHelpers.h" +#include "Playerbots.h" + +using namespace GruulsLairHelpers; + +// High King Maulgar Triggers + +bool HighKingMaulgarBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "high king maulgar"); +} + +bool HighKingMaulgarOlmEngagedByFirstAssistTankTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 0, false) && + AI_VALUE2(Unit*, "find target", "olm the summoner"); +} + +bool HighKingMaulgarBlindeyeEngagedBySecondAssistTankTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 1, false) && + AI_VALUE2(Unit*, "find target", "blindeye the seer"); +} + +bool HighKingMaulgarKroshEngagedByMageTankTrigger::IsActive() +{ + return bot->getClass() == CLASS_MAGE && + AI_VALUE2(Unit*, "find target", "krosh firehand") && + GetKroshMageTank(bot) == bot; +} + +bool HighKingMaulgarKigglerEngagedByMoonkinTankTrigger::IsActive() +{ + return bot->getClass() == CLASS_DRUID && + AI_VALUE2(Unit*, "find target", "kiggler the crazed") && + GetKigglerMoonkinTank(bot) == bot; +} + +bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot) || botAI->IsMainTank(bot)) + return false; + + Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar) + return false; + + if (botAI->IsAssistTankOfIndex(bot, 0, false)) + return !AI_VALUE2(Unit*, "find target", "olm the summoner"); + + if (botAI->IsAssistTankOfIndex(bot, 1, false)) + return !AI_VALUE2(Unit*, "find target", "blindeye the seer"); + + if (bot->getClass() == CLASS_MAGE && GetKroshMageTank(bot) == bot) + return !AI_VALUE2(Unit*, "find target", "krosh firehand"); + + if (bot->getClass() == CLASS_DRUID && GetKigglerMoonkinTank(bot) == bot) + return !AI_VALUE2(Unit*, "find target", "kiggler the crazed"); + + return true; +} + +bool HighKingMaulgarBossChannelingWhirlwindTrigger::IsActive() +{ + Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); + if (!maulgar || + !maulgar->HasAura(static_cast(GruulsLairSpells::SPELL_WHIRLWIND))) + { + return false; + } + + return !botAI->IsMainTank(bot); +} + +bool HighKingMaulgarKroshCastsBlastWaveTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "krosh firehand")) + return false; + + return !botAI->IsTank(bot) && GetKroshMageTank(bot) != bot; +} + +bool HighKingMaulgarWildFelStalkerSpawnedTrigger::IsActive() +{ + return bot->getClass() == CLASS_WARLOCK && + AI_VALUE2(Unit*, "find target", "wild fel stalker"); +} + +bool HighKingMaulgarPullingOgreCouncilTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); + return blindeye && blindeye->GetHealthPct() > 80.0f; +} + +// Gruul the Dragonkiller Triggers + +bool GruulTheDragonkillerBossEngagedByTanksTrigger::IsActive() +{ + return botAI->IsTank(bot) && + AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); +} + +bool GruulTheDragonkillerBossEngagedByRangedTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); +} + +bool GruulTheDragonkillerIncomingShatterTrigger::IsActive() +{ + return bot->HasAura(static_cast(GruulsLairSpells::SPELL_GROUND_SLAM_1)) || + bot->HasAura(static_cast(GruulsLairSpells::SPELL_GROUND_SLAM_2)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.h new file mode 100644 index 000000000..447894b63 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Gruul/GruulTriggers.h @@ -0,0 +1,116 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_GRUULTRIGGERS_H +#define PLAYERBOTS_GRUULTRIGGERS_H + +#include "Trigger.h" + +class HighKingMaulgarBossEngagedByMainTankTrigger : public Trigger +{ +public: + HighKingMaulgarBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar engaged by main tank") {} + bool IsActive() override; +}; + +class HighKingMaulgarOlmEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + HighKingMaulgarOlmEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar olm engaged by first assist tank") {} + bool IsActive() override; +}; + +class HighKingMaulgarBlindeyeEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + HighKingMaulgarBlindeyeEngagedBySecondAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar blindeye engaged by second assist tank") {} + bool IsActive() override; +}; + +class HighKingMaulgarKroshEngagedByMageTankTrigger : public Trigger +{ +public: + HighKingMaulgarKroshEngagedByMageTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar krosh engaged by mage tank") {} + bool IsActive() override; +}; + +class HighKingMaulgarKigglerEngagedByMoonkinTankTrigger : public Trigger +{ +public: + HighKingMaulgarKigglerEngagedByMoonkinTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar kiggler engaged by moonkin tank") {} + bool IsActive() override; +}; + +class HighKingMaulgarDeterminingKillOrderTrigger : public Trigger +{ +public: + HighKingMaulgarDeterminingKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar determining kill order") {} + bool IsActive() override; +}; + +class HighKingMaulgarBossChannelingWhirlwindTrigger : public Trigger +{ +public: + HighKingMaulgarBossChannelingWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar boss channeling whirlwind") {} + bool IsActive() override; +}; + +class HighKingMaulgarKroshCastsBlastWaveTrigger : public Trigger +{ +public: + HighKingMaulgarKroshCastsBlastWaveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar krosh casts blast wave") {} + bool IsActive() override; +}; + +class HighKingMaulgarWildFelStalkerSpawnedTrigger : public Trigger +{ +public: + HighKingMaulgarWildFelStalkerSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar wild fel stalker spawned") {} + bool IsActive() override; +}; + +class HighKingMaulgarPullingOgreCouncilTrigger : public Trigger +{ +public: + HighKingMaulgarPullingOgreCouncilTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar pulling ogre council") {} + bool IsActive() override; +}; + +class GruulTheDragonkillerBossEngagedByTanksTrigger : public Trigger +{ +public: + GruulTheDragonkillerBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by tanks") {} + bool IsActive() override; +}; + +class GruulTheDragonkillerBossEngagedByRangedTrigger : public Trigger +{ +public: + GruulTheDragonkillerBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by ranged") {} + bool IsActive() override; +}; + +class GruulTheDragonkillerIncomingShatterTrigger : public Trigger +{ +public: + GruulTheDragonkillerIncomingShatterTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller incoming shatter") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp deleted file mode 100644 index 1a45b0c15..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.cpp +++ /dev/null @@ -1,652 +0,0 @@ -#include "RaidGruulsLairActions.h" -#include "RaidGruulsLairHelpers.h" -#include "CreatureAI.h" -#include "Playerbots.h" -#include "RaidBossHelpers.h" -#include "Unit.h" - -using namespace GruulsLairHelpers; - -// High King Maulgar Actions - -// Main tank on Maulgar -bool HighKingMaulgarMainTankAttackMaulgarAction::Execute(Event /*event*/) -{ - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - if (!maulgar) - return false; - - MarkTargetWithSquare(bot, maulgar); - SetRtiTarget(botAI, "square", maulgar); - - if (bot->GetVictim() != maulgar) - return Attack(maulgar); - - if (maulgar->GetVictim() == bot) - { - const Position& position = MAULGAR_TANK_POSITION; - const float maxDistance = 3.0f; - - float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceToPosition > maxDistance) - { - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, true); - } - } - - return false; -} - -// First offtank on Olm -bool HighKingMaulgarFirstAssistTankAttackOlmAction::Execute(Event /*event*/) -{ - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - if (!olm) - return false; - - MarkTargetWithCircle(bot, olm); - SetRtiTarget(botAI, "circle", olm); - - if (bot->GetVictim() != olm) - return Attack(olm); - - if (olm->GetVictim() == bot) - { - const Position& position = OLM_TANK_POSITION; - const float maxDistance = 3.0f; - const float olmTankLeeway = 30.0f; - - float distanceOlmToPosition = olm->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceOlmToPosition > olmTankLeeway) - { - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float moveX = bot->GetPositionX() + (dX / distanceOlmToPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceOlmToPosition) * maxDistance; - return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -// Second offtank on Blindeye -bool HighKingMaulgarSecondAssistTankAttackBlindeyeAction::Execute(Event /*event*/) -{ - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - if (!blindeye) - return false; - - MarkTargetWithStar(bot, blindeye); - SetRtiTarget(botAI, "star", blindeye); - - if (bot->GetVictim() != blindeye) - return Attack(blindeye); - - if (blindeye->GetVictim() == bot) - { - const Position& position = BLINDEYE_TANK_POSITION; - const float maxDistance = 3.0f; - - float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceToPosition > maxDistance) - { - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -// Mage with highest max HP on Krosh -bool HighKingMaulgarMageTankAttackKroshAction::Execute(Event /*event*/) -{ - Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - if (!krosh) - return false; - - MarkTargetWithTriangle(bot, krosh); - SetRtiTarget(botAI, "triangle", krosh); - - if (krosh->HasAura(SPELL_SPELL_SHIELD) && botAI->CanCastSpell("spellsteal", krosh)) - return botAI->CastSpell("spellsteal", krosh); - - if (!bot->HasAura(SPELL_SPELL_SHIELD) && botAI->CanCastSpell("fire ward", bot)) - return botAI->CastSpell("fire ward", bot); - - if (bot->GetTarget() != krosh->GetGUID()) - return Attack(krosh); - - if (krosh->GetVictim() == bot) - { - const Position& position = KROSH_TANK_POSITION; - float distanceToKrosh = krosh->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - const float minDistance = 16.0f; - const float maxDistance = 29.0f; - const float tankPositionLeeway = 1.0f; - - if (distanceToKrosh > minDistance && distanceToKrosh < maxDistance) - { - if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), tankPositionLeeway)) - { - return MoveTo(GRUULS_LAIR_MAP_ID, position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - float orientation = atan2(krosh->GetPositionY() - bot->GetPositionY(), - krosh->GetPositionX() - bot->GetPositionX()); - bot->SetFacingTo(orientation); - } - else - { - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - } - - return false; -} - -// Moonkin with highest max HP on Kiggler -bool HighKingMaulgarMoonkinTankAttackKigglerAction::Execute(Event /*event*/) -{ - Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - if (!kiggler) - return false; - - MarkTargetWithDiamond(bot, kiggler); - SetRtiTarget(botAI, "diamond", kiggler); - - if (bot->GetTarget() != kiggler->GetGUID()) - return Attack(kiggler); - - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - return false; -} - -bool HighKingMaulgarAssignDPSPriorityAction::Execute(Event /*event*/) -{ - // Target priority 1: Blindeye - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - if (blindeye) - { - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - SetRtiTarget(botAI, "star", blindeye); - - if (bot->GetTarget() != blindeye->GetGUID()) - return Attack(blindeye); - - return false; - } - - // Target priority 2: Olm - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - if (olm) - { - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - SetRtiTarget(botAI, "circle", olm); - - if (bot->GetTarget() != olm->GetGUID()) - return Attack(olm); - - return false; - } - - // Target priority 3a: Krosh (ranged only) - Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - if (krosh && botAI->IsRanged(bot)) - { - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - SetRtiTarget(botAI, "triangle", krosh); - - if (bot->GetTarget() != krosh->GetGUID()) - return Attack(krosh); - - return false; - } - - // Target priority 3b: Kiggler - Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - if (kiggler) - { - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - SetRtiTarget(botAI, "diamond", kiggler); - - if (bot->GetTarget() != kiggler->GetGUID()) - return Attack(kiggler); - - return false; - } - - // Target priority 4: Maulgar - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - if (maulgar) - { - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - SetRtiTarget(botAI, "square", maulgar); - - if (bot->GetTarget() != maulgar->GetGUID()) - return Attack(maulgar); - } - - return false; -} - -// Avoid Whirlwind and Blast Wave and generally try to stay near the center of the room -bool HighKingMaulgarHealerFindSafePositionAction::Execute(Event /*event*/) -{ - const Position& center = MAULGAR_ROOM_CENTER; - const float maxDistanceFromCenter = 50.0f; - float distToCenter = bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()); - - if (distToCenter > maxDistanceFromCenter) - { - float angle = atan2(bot->GetPositionY() - center.GetPositionY(), bot->GetPositionX() - center.GetPositionX()); - float destX = center.GetPositionX() + 40.0f * cos(angle); - float destY = center.GetPositionY() + 40.0f * sin(angle); - float destZ = center.GetPositionZ(); - - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), destX, destY, destZ)) - return false; - - return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - - Position safePos; - if (TryGetNewSafePosition(botAI, bot, safePos)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(GRUULS_LAIR_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), safePos.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); - } - - return false; -} - -// Run away from Maulgar during Whirlwind (logic for after all other ogres are dead) -bool HighKingMaulgarRunAwayFromWhirlwindAction::Execute(Event /*event*/) -{ - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - if (!maulgar) - return false; - - const float safeDistance = 10.0f; - float distance = bot->GetExactDist2d(maulgar); - - if (distance < safeDistance) - { - float angle = atan2(bot->GetPositionY() - maulgar->GetPositionY(), - bot->GetPositionX() - maulgar->GetPositionX()); - float destX = maulgar->GetPositionX() + safeDistance * cos(angle); - float destY = maulgar->GetPositionY() + safeDistance * sin(angle); - float destZ = bot->GetPositionZ(); - - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), destX, destY, destZ)) - return false; - - float destDist = maulgar->GetExactDist2d(destX, destY); - - if (destDist >= safeDistance - 0.1f) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); - return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -bool HighKingMaulgarBanishFelstalkerAction::Execute(Event /*event*/) -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector felStalkers; - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->GetEntry() == NPC_WILD_FEL_STALKER && unit->IsAlive()) - felStalkers.push_back(unit); - } - - std::vector warlocks; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->getClass() == CLASS_WARLOCK && GET_PLAYERBOT_AI(member)) - warlocks.push_back(member); - } - - int warlockIndex = -1; - for (size_t i = 0; i < warlocks.size(); ++i) - { - if (warlocks[i] == bot) - { - warlockIndex = static_cast(i); - break; - } - } - - if (warlockIndex >= 0 && warlockIndex < felStalkers.size()) - { - Unit* assignedFelStalker = felStalkers[warlockIndex]; - if (!botAI->HasAura("banish", assignedFelStalker) && botAI->CanCastSpell("banish", assignedFelStalker)) - return botAI->CastSpell("banish", assignedFelStalker); - } - - return false; -} - -// Hunter 1: Misdirect Olm to first offtank and have pet attack Blindeye -// Hunter 2: Misdirect Blindeye to second offtank -bool HighKingMaulgarMisdirectOlmAndBlindeyeAction::Execute(Event /*event*/) -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - std::vector hunters; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) - hunters.push_back(member); - } - - int hunterIndex = -1; - for (size_t i = 0; i < hunters.size(); ++i) - { - if (hunters[i] == bot) - { - hunterIndex = static_cast(i); - break; - } - } - - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - Player* olmTank = nullptr; - Player* blindeyeTank = nullptr; - - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - else if (botAI->IsAssistTankOfIndex(member, 0)) olmTank = member; - else if (botAI->IsAssistTankOfIndex(member, 1)) blindeyeTank = member; - } - - switch (hunterIndex) - { - case 0: - botAI->CastSpell("misdirection", olmTank); - if (bot->HasAura(SPELL_MISDIRECTION)) - { - Pet* pet = bot->GetPet(); - if (pet && pet->IsAlive() && pet->GetVictim() != blindeye) - { - pet->ClearUnitState(UNIT_STATE_FOLLOW); - pet->AttackStop(); - pet->SetTarget(blindeye->GetGUID()); - if (pet->GetCharmInfo()) - { - pet->GetCharmInfo()->SetIsCommandAttack(true); - pet->GetCharmInfo()->SetIsAtStay(false); - pet->GetCharmInfo()->SetIsFollowing(false); - pet->GetCharmInfo()->SetIsCommandFollow(false); - pet->GetCharmInfo()->SetIsReturning(false); - } - pet->ToCreature()->AI()->AttackStart(blindeye); - } - return botAI->CastSpell("steady shot", olm); - } - break; - - case 1: - botAI->CastSpell("misdirection", blindeyeTank); - if (bot->HasAura(SPELL_MISDIRECTION)) - return botAI->CastSpell("steady shot", blindeye); - break; - - default: - break; - } - - return false; -} - -// Gruul the Dragonkiller Actions - -// Position in center of the room -bool GruulTheDragonkillerTanksPositionBossAction::Execute(Event /*event*/) -{ - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (!gruul) - return false; - - if (bot->GetVictim() != gruul) - return Attack(gruul); - - if (gruul->GetVictim() == bot) - { - const Position& position = GRUUL_TANK_POSITION; - const float maxDistance = 5.0f; - - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float distanceToTankPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceToTankPosition > maxDistance) - { - float step = std::min(maxDistance, distanceToTankPosition); - float moveX = bot->GetPositionX() + (dX / distanceToTankPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceToTankPosition) * maxDistance; - const float moveZ = position.GetPositionZ(); - return MoveTo(GRUULS_LAIR_MAP_ID, moveX, moveY, moveZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -// Ranged will take initial positions around the middle of the room, 25-40 yards from center -// Ranged should spread out 10 yards from each other -bool GruulTheDragonkillerSpreadRangedAction::Execute(Event /*event*/) -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - static std::unordered_map initialPositions; - static std::unordered_map hasReachedInitialPosition; - - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (gruul && gruul->GetHealth() == gruul->GetMaxHealth()) - { - initialPositions.erase(bot->GetGUID()); - hasReachedInitialPosition.erase(bot->GetGUID()); - } - - const Position& position = GRUUL_TANK_POSITION; - const float centerX = position.GetPositionX(); - const float centerY = position.GetPositionY(); - const float centerZ = position.GetPositionZ(); - const float minRadius = 25.0f; - const float maxRadius = 40.0f; - - std::vector members; - Player* closestMember = nullptr; - float closestDist = std::numeric_limits::max(); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - members.push_back(member); - if (member != bot) - { - float dist = bot->GetExactDist2d(member); - if (dist < closestDist) - { - closestDist = dist; - closestMember = member; - } - } - } - - if (!initialPositions.count(bot->GetGUID())) - { - auto it = std::find(members.begin(), members.end(), bot); - uint8 botIndex = (it != members.end()) ? std::distance(members.begin(), it) : 0; - uint8 count = members.size(); - - float angle = 2 * M_PI * botIndex / count; - float radius = minRadius + static_cast(rand()) / - static_cast(RAND_MAX) * (maxRadius - minRadius); - float targetX = centerX + radius * cos(angle); - float targetY = centerY + radius * sin(angle); - - initialPositions[bot->GetGUID()] = Position(targetX, targetY, centerZ); - hasReachedInitialPosition[bot->GetGUID()] = false; - } - - Position targetPosition = initialPositions[bot->GetGUID()]; - if (!hasReachedInitialPosition[bot->GetGUID()]) - { - if (!bot->IsWithinDist2d(targetPosition.GetPositionX(), targetPosition.GetPositionY(), 2.0f)) - { - float destX = targetPosition.GetPositionX(); - float destY = targetPosition.GetPositionY(); - float destZ = targetPosition.GetPositionZ(); - - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), - bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) - return false; - - return MoveTo(GRUULS_LAIR_MAP_ID, destX, destY, destZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - - hasReachedInitialPosition[bot->GetGUID()] = true; - } - - const float minSpreadDistance = 10.0f; - const float movementThreshold = 2.0f; - - if (closestMember && closestDist < minSpreadDistance - movementThreshold) - { - return FleePosition(Position(closestMember->GetPositionX(), closestMember->GetPositionY(), - closestMember->GetPositionZ()), minSpreadDistance, 0); - } - - return false; -} - -// Try to get away from other group members when Ground Slam is cast -bool GruulTheDragonkillerShatterSpreadAction::Execute(Event /*event*/) -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - GuidVector members = AI_VALUE(GuidVector, "group members"); - Unit* closestMember = nullptr; - float closestDist = std::numeric_limits::max(); - - for (auto& member : members) - { - Unit* unit = botAI->GetUnit(member); - if (!unit || bot->GetGUID() == member) - continue; - - const float dist = bot->GetExactDist2d(unit); - if (dist < closestDist) - { - closestDist = dist; - closestMember = unit; - } - } - - if (closestMember) - { - return FleePosition(Position(closestMember->GetPositionX(), closestMember->GetPositionY(), - closestMember->GetPositionZ()), 6.0f, 0); - } - - return false; -} diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h deleted file mode 100644 index 525e0ee6d..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Action/RaidGruulsLairActions.h +++ /dev/null @@ -1,112 +0,0 @@ -#ifndef _PLAYERBOT_RAIDGRUULSLAIRACTIONS_H -#define _PLAYERBOT_RAIDGRUULSLAIRACTIONS_H - -#include "Action.h" -#include "AttackAction.h" -#include "MovementActions.h" - -class HighKingMaulgarMainTankAttackMaulgarAction : public AttackAction -{ -public: - HighKingMaulgarMainTankAttackMaulgarAction(PlayerbotAI* botAI, std::string const name = "high king maulgar main tank attack maulgar") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarFirstAssistTankAttackOlmAction : public AttackAction -{ -public: - HighKingMaulgarFirstAssistTankAttackOlmAction(PlayerbotAI* botAI, std::string const name = "high king maulgar first assist tank attack olm") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarSecondAssistTankAttackBlindeyeAction : public AttackAction -{ -public: - HighKingMaulgarSecondAssistTankAttackBlindeyeAction(PlayerbotAI* botAI, std::string const name = "high king maulgar second assist tank attack blindeye") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarMageTankAttackKroshAction : public AttackAction -{ -public: - HighKingMaulgarMageTankAttackKroshAction(PlayerbotAI* botAI, std::string const name = "high king maulgar mage tank attack krosh") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarMoonkinTankAttackKigglerAction : public AttackAction -{ -public: - HighKingMaulgarMoonkinTankAttackKigglerAction(PlayerbotAI* botAI, std::string const name = "high king maulgar moonkin tank attack kiggler") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarAssignDPSPriorityAction : public AttackAction -{ -public: - HighKingMaulgarAssignDPSPriorityAction(PlayerbotAI* botAI, std::string const name = "high king maulgar assign dps priority") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarHealerFindSafePositionAction : public MovementAction -{ -public: - HighKingMaulgarHealerFindSafePositionAction(PlayerbotAI* botAI, std::string const name = "high king maulgar healer find safe position") : MovementAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarRunAwayFromWhirlwindAction : public MovementAction -{ -public: - HighKingMaulgarRunAwayFromWhirlwindAction(PlayerbotAI* botAI, std::string const name = "high king maulgar run away from whirlwind") : MovementAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarBanishFelstalkerAction : public AttackAction -{ -public: - HighKingMaulgarBanishFelstalkerAction(PlayerbotAI* botAI, std::string const name = "high king maulgar banish felstalker") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class HighKingMaulgarMisdirectOlmAndBlindeyeAction : public AttackAction -{ -public: - HighKingMaulgarMisdirectOlmAndBlindeyeAction(PlayerbotAI* botAI, std::string const name = "high king maulgar misdirect olm and blindeye") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class GruulTheDragonkillerTanksPositionBossAction : public AttackAction -{ -public: - GruulTheDragonkillerTanksPositionBossAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller tanks position boss") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class GruulTheDragonkillerSpreadRangedAction : public MovementAction -{ -public: - GruulTheDragonkillerSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller spread ranged") : MovementAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class GruulTheDragonkillerShatterSpreadAction : public MovementAction -{ -public: - GruulTheDragonkillerShatterSpreadAction(PlayerbotAI* botAI, std::string const name = "gruul the dragonkiller shatter spread") : MovementAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp deleted file mode 100644 index 6604fc525..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "RaidGruulsLairMultipliers.h" -#include "RaidGruulsLairActions.h" -#include "RaidGruulsLairHelpers.h" -#include "ChooseTargetActions.h" -#include "DruidBearActions.h" -#include "DruidCatActions.h" -#include "GenericSpellActions.h" -#include "HunterActions.h" -#include "MageActions.h" -#include "Playerbots.h" -#include "ReachTargetActions.h" -#include "WarriorActions.h" - -using namespace GruulsLairHelpers; - -float HighKingMaulgarDisableTankAssistMultiplier::GetValue(Action* action) -{ - if (IsAnyOgreBossAlive(botAI) && dynamic_cast(action)) - return 0.0f; - - return 1.0f; -} - -// Don't run back in during Whirlwind -float HighKingMaulgarAvoidWhirlwindMultiplier::GetValue(Action* action) -{ - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - - if (maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && - !kiggler && !krosh && !olm && !blindeye) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } - - return 1.0f; -} - -// Arcane Shot will remove Spell Shield, which the mage tank needs to survive -float HighKingMaulgarDisableArcaneShotOnKroshMultiplier::GetValue(Action* action) -{ - Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - Unit* target = AI_VALUE(Unit*, "current target"); - - if (krosh && target && target->GetGUID() == krosh->GetGUID() && - dynamic_cast(action)) - return 0.0f; - - return 1.0f; -} - -float HighKingMaulgarDisableMageTankAOEMultiplier::GetValue(Action* action) -{ - if (IsKroshMageTank(botAI, bot) && - (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action))) - return 0.0f; - - return 1.0f; -} - -float GruulTheDragonkillerMainTankMovementMultiplier::GetValue(Action* action) -{ - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (!gruul) - return 1.0f; - - if (botAI->IsMainTank(bot)) - { - if (gruul->GetVictim() == bot && dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} - -float GruulTheDragonkillerGroundSlamMultiplier::GetValue(Action* action) -{ - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - if (!gruul) - return 1.0f; - - if (bot->HasAura(SPELL_GROUND_SLAM_1) || - bot->HasAura(SPELL_GROUND_SLAM_2)) - { - if ((dynamic_cast(action) && - !dynamic_cast(action)) || - dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.h deleted file mode 100644 index d67e8523e..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Multiplier/RaidGruulsLairMultipliers.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef _PLAYERBOT_RAIDGRUULSLAIRMULTIPLIERS_H -#define _PLAYERBOT_RAIDGRUULSLAIRMULTIPLIERS_H - -#include "Multiplier.h" - -class HighKingMaulgarDisableTankAssistMultiplier : public Multiplier -{ -public: - HighKingMaulgarDisableTankAssistMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar disable tank assist multiplier") {} - float GetValue(Action* action) override; -}; - -class HighKingMaulgarAvoidWhirlwindMultiplier : public Multiplier -{ -public: - HighKingMaulgarAvoidWhirlwindMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar avoid whirlwind multiplier") {} - float GetValue(Action* action) override; -}; - -class HighKingMaulgarDisableArcaneShotOnKroshMultiplier : public Multiplier -{ -public: - HighKingMaulgarDisableArcaneShotOnKroshMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar disable arcane shot on krosh multiplier") {} - float GetValue(Action* action) override; -}; - -class HighKingMaulgarDisableMageTankAOEMultiplier : public Multiplier -{ -public: - HighKingMaulgarDisableMageTankAOEMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "high king maulgar disable mage tank aoe multiplier") {} - float GetValue(Action* action) override; -}; - -class GruulTheDragonkillerMainTankMovementMultiplier : public Multiplier -{ -public: - GruulTheDragonkillerMainTankMovementMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "gruul the dragonkiller main tank movement multiplier") {} - float GetValue(Action* action) override; -}; - -class GruulTheDragonkillerGroundSlamMultiplier : public Multiplier -{ -public: - GruulTheDragonkillerGroundSlamMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "gruul the dragonkiller ground slam multiplier") {} - float GetValue(Action* action) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h deleted file mode 100644 index 3850f58c6..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef _PLAYERBOT_RAIDGRUULSLAIRACTIONCONTEXT_H -#define _PLAYERBOT_RAIDGRUULSLAIRACTIONCONTEXT_H - -#include "RaidGruulsLairActions.h" -#include "NamedObjectContext.h" - -class RaidGruulsLairActionContext : public NamedObjectContext -{ -public: - RaidGruulsLairActionContext() - { - // High King Maulgar - creators["high king maulgar main tank attack maulgar"] = &RaidGruulsLairActionContext::high_king_maulgar_main_tank_attack_maulgar; - creators["high king maulgar first assist tank attack olm"] = &RaidGruulsLairActionContext::high_king_maulgar_first_assist_tank_attack_olm; - creators["high king maulgar second assist tank attack blindeye"] = &RaidGruulsLairActionContext::high_king_maulgar_second_assist_tank_attack_blindeye; - creators["high king maulgar mage tank attack krosh"] = &RaidGruulsLairActionContext::high_king_maulgar_mage_tank_attack_krosh; - creators["high king maulgar moonkin tank attack kiggler"] = &RaidGruulsLairActionContext::high_king_maulgar_moonkin_tank_attack_kiggler; - creators["high king maulgar assign dps priority"] = &RaidGruulsLairActionContext::high_king_maulgar_assign_dps_priority; - creators["high king maulgar healer find safe position"] = &RaidGruulsLairActionContext::high_king_maulgar_healer_find_safe_position; - creators["high king maulgar run away from whirlwind"] = &RaidGruulsLairActionContext::high_king_maulgar_run_away_from_whirlwind; - creators["high king maulgar banish felstalker"] = &RaidGruulsLairActionContext::high_king_maulgar_banish_felstalker; - creators["high king maulgar misdirect olm and blindeye"] = &RaidGruulsLairActionContext::high_king_maulgar_misdirect_olm_and_blindeye; - - // Gruul the Dragonkiller - creators["gruul the dragonkiller tanks position boss"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_tanks_position_boss; - creators["gruul the dragonkiller spread ranged"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_spread_ranged; - creators["gruul the dragonkiller shatter spread"] = &RaidGruulsLairActionContext::gruul_the_dragonkiller_shatter_spread; - } - -private: - // High King Maulgar - static Action* high_king_maulgar_main_tank_attack_maulgar(PlayerbotAI* botAI) { return new HighKingMaulgarMainTankAttackMaulgarAction(botAI); } - static Action* high_king_maulgar_first_assist_tank_attack_olm(PlayerbotAI* botAI) { return new HighKingMaulgarFirstAssistTankAttackOlmAction(botAI); } - static Action* high_king_maulgar_second_assist_tank_attack_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarSecondAssistTankAttackBlindeyeAction(botAI); } - static Action* high_king_maulgar_mage_tank_attack_krosh(PlayerbotAI* botAI) { return new HighKingMaulgarMageTankAttackKroshAction(botAI); } - static Action* high_king_maulgar_moonkin_tank_attack_kiggler(PlayerbotAI* botAI) { return new HighKingMaulgarMoonkinTankAttackKigglerAction(botAI); } - static Action* high_king_maulgar_assign_dps_priority(PlayerbotAI* botAI) { return new HighKingMaulgarAssignDPSPriorityAction(botAI); } - static Action* high_king_maulgar_healer_find_safe_position(PlayerbotAI* botAI) { return new HighKingMaulgarHealerFindSafePositionAction(botAI); } - static Action* high_king_maulgar_run_away_from_whirlwind(PlayerbotAI* botAI) { return new HighKingMaulgarRunAwayFromWhirlwindAction(botAI); } - static Action* high_king_maulgar_banish_felstalker(PlayerbotAI* botAI) { return new HighKingMaulgarBanishFelstalkerAction(botAI); } - static Action* high_king_maulgar_misdirect_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarMisdirectOlmAndBlindeyeAction(botAI); } - - // Gruul the Dragonkiller - static Action* gruul_the_dragonkiller_tanks_position_boss(PlayerbotAI* botAI) { return new GruulTheDragonkillerTanksPositionBossAction(botAI); } - static Action* gruul_the_dragonkiller_spread_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerSpreadRangedAction(botAI); } - static Action* gruul_the_dragonkiller_shatter_spread(PlayerbotAI* botAI) { return new GruulTheDragonkillerShatterSpreadAction(botAI); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h deleted file mode 100644 index d12b0ce46..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef _PLAYERBOT_RAIDGRUULSLAIRTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDGRUULSLAIRTRIGGERCONTEXT_H - -#include "RaidGruulsLairTriggers.h" -#include "AiObjectContext.h" - -class RaidGruulsLairTriggerContext : public NamedObjectContext -{ -public: - RaidGruulsLairTriggerContext() : NamedObjectContext() - { - // High King Maulgar - creators["high king maulgar is main tank"] = &RaidGruulsLairTriggerContext::high_king_maulgar_is_main_tank; - creators["high king maulgar is first assist tank"] = &RaidGruulsLairTriggerContext::high_king_maulgar_is_first_assist_tank; - creators["high king maulgar is second assist tank"] = &RaidGruulsLairTriggerContext::high_king_maulgar_is_second_assist_tank; - creators["high king maulgar is mage tank"] = &RaidGruulsLairTriggerContext::high_king_maulgar_is_mage_tank; - creators["high king maulgar is moonkin tank"] = &RaidGruulsLairTriggerContext::high_king_maulgar_is_moonkin_tank; - creators["high king maulgar determining kill order"] = &RaidGruulsLairTriggerContext::high_king_maulgar_determining_kill_order; - creators["high king maulgar healer in danger"] = &RaidGruulsLairTriggerContext::high_king_maulgar_healer_in_danger; - creators["high king maulgar boss channeling whirlwind"] = &RaidGruulsLairTriggerContext::high_king_maulgar_boss_channeling_whirlwind; - creators["high king maulgar wild felstalker spawned"] = &RaidGruulsLairTriggerContext::high_king_maulgar_wild_felstalker_spawned; - creators["high king maulgar pulling olm and blindeye"] = &RaidGruulsLairTriggerContext::high_king_maulgar_pulling_olm_and_blindeye; - - // Gruul the Dragonkiller - creators["gruul the dragonkiller boss engaged by tanks"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_tanks; - creators["gruul the dragonkiller boss engaged by ranged"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_boss_engaged_by_ranged; - creators["gruul the dragonkiller incoming shatter"] = &RaidGruulsLairTriggerContext::gruul_the_dragonkiller_incoming_shatter; - } - -private: - // High King Maulgar - static Trigger* high_king_maulgar_is_main_tank(PlayerbotAI* botAI) { return new HighKingMaulgarIsMainTankTrigger(botAI); } - static Trigger* high_king_maulgar_is_first_assist_tank(PlayerbotAI* botAI) { return new HighKingMaulgarIsFirstAssistTankTrigger(botAI); } - static Trigger* high_king_maulgar_is_second_assist_tank(PlayerbotAI* botAI) { return new HighKingMaulgarIsSecondAssistTankTrigger(botAI); } - static Trigger* high_king_maulgar_is_mage_tank(PlayerbotAI* botAI) { return new HighKingMaulgarIsMageTankTrigger(botAI); } - static Trigger* high_king_maulgar_is_moonkin_tank(PlayerbotAI* botAI) { return new HighKingMaulgarIsMoonkinTankTrigger(botAI); } - static Trigger* high_king_maulgar_determining_kill_order(PlayerbotAI* botAI) { return new HighKingMaulgarDeterminingKillOrderTrigger(botAI); } - static Trigger* high_king_maulgar_healer_in_danger(PlayerbotAI* botAI) { return new HighKingMaulgarHealerInDangerTrigger(botAI); } - static Trigger* high_king_maulgar_boss_channeling_whirlwind(PlayerbotAI* botAI) { return new HighKingMaulgarBossChannelingWhirlwindTrigger(botAI); } - static Trigger* high_king_maulgar_wild_felstalker_spawned(PlayerbotAI* botAI) { return new HighKingMaulgarWildFelstalkerSpawnedTrigger(botAI); } - static Trigger* high_king_maulgar_pulling_olm_and_blindeye(PlayerbotAI* botAI) { return new HighKingMaulgarPullingOlmAndBlindeyeTrigger(botAI); } - - // Gruul the Dragonkiller - static Trigger* gruul_the_dragonkiller_boss_engaged_by_tanks(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByTanksTrigger(botAI); } - static Trigger* gruul_the_dragonkiller_boss_engaged_by_ranged(PlayerbotAI* botAI) { return new GruulTheDragonkillerBossEngagedByRangedTrigger(botAI); } - static Trigger* gruul_the_dragonkiller_incoming_shatter(PlayerbotAI* botAI) { return new GruulTheDragonkillerIncomingShatterTrigger(botAI); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp deleted file mode 100644 index 4bc5efe99..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include "RaidGruulsLairTriggers.h" -#include "RaidGruulsLairHelpers.h" -#include "Playerbots.h" - -using namespace GruulsLairHelpers; - -// High King Maulgar Triggers - -bool HighKingMaulgarIsMainTankTrigger::IsActive() -{ - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - - return botAI->IsMainTank(bot) && maulgar; -} - -bool HighKingMaulgarIsFirstAssistTankTrigger::IsActive() -{ - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - - return botAI->IsAssistTankOfIndex(bot, 0, false) && olm; -} - -bool HighKingMaulgarIsSecondAssistTankTrigger::IsActive() -{ - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - - return botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye; -} - -bool HighKingMaulgarIsMageTankTrigger::IsActive() -{ - Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - - return IsKroshMageTank(botAI, bot) && krosh; -} - -bool HighKingMaulgarIsMoonkinTankTrigger::IsActive() -{ - Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - - return IsKigglerMoonkinTank(botAI, bot) && kiggler; -} - -bool HighKingMaulgarDeterminingKillOrderTrigger::IsActive() -{ - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - Unit* kiggler = AI_VALUE2(Unit*, "find target", "kiggler the crazed"); - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - Unit* krosh = AI_VALUE2(Unit*, "find target", "krosh firehand"); - - return (botAI->IsDps(bot) || botAI->IsTank(bot)) && - !(botAI->IsMainTank(bot) && maulgar) && - !(botAI->IsAssistTankOfIndex(bot, 0, false) && olm) && - !(botAI->IsAssistTankOfIndex(bot, 1, false) && blindeye) && - !(IsKroshMageTank(botAI, bot) && krosh) && - !(IsKigglerMoonkinTank(botAI, bot) && kiggler); -} - -bool HighKingMaulgarHealerInDangerTrigger::IsActive() -{ - return botAI->IsHeal(bot) && IsAnyOgreBossAlive(botAI); -} - -bool HighKingMaulgarBossChannelingWhirlwindTrigger::IsActive() -{ - Unit* maulgar = AI_VALUE2(Unit*, "find target", "high king maulgar"); - - return maulgar && maulgar->HasAura(SPELL_WHIRLWIND) && - !botAI->IsMainTank(bot); -} - -bool HighKingMaulgarWildFelstalkerSpawnedTrigger::IsActive() -{ - Unit* felStalker = AI_VALUE2(Unit*, "find target", "wild fel stalker"); - - return felStalker && bot->getClass() == CLASS_WARLOCK; -} - -bool HighKingMaulgarPullingOlmAndBlindeyeTrigger::IsActive() -{ - Group* group = bot->GetGroup(); - if (!group || bot->getClass() != CLASS_HUNTER) - return false; - - std::vector hunters; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) - hunters.push_back(member); - } - - int hunterIndex = -1; - for (size_t i = 0; i < hunters.size(); ++i) - { - if (hunters[i] == bot) - { - hunterIndex = static_cast(i); - break; - } - } - if (hunterIndex == -1 || hunterIndex > 1) - return false; - - Unit* olm = AI_VALUE2(Unit*, "find target", "olm the summoner"); - Unit* blindeye = AI_VALUE2(Unit*, "find target", "blindeye the seer"); - Player* olmTank = nullptr; - Player* blindeyeTank = nullptr; - - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - else if (botAI->IsAssistTankOfIndex(member, 0)) olmTank = member; - else if (botAI->IsAssistTankOfIndex(member, 1)) blindeyeTank = member; - } - - switch (hunterIndex) - { - case 0: - return olm && olm->GetHealthPct() > 98.0f && - olmTank && botAI->CanCastSpell("misdirection", olmTank); - - case 1: - return blindeye && blindeye->GetHealthPct() > 90.0f && - blindeyeTank && botAI->CanCastSpell("misdirection", blindeyeTank); - - default: - break; - } - - return false; -} - -// Gruul the Dragonkiller Triggers - -bool GruulTheDragonkillerBossEngagedByTanksTrigger::IsActive() -{ - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - - return gruul && botAI->IsTank(bot); -} - -bool GruulTheDragonkillerBossEngagedByRangedTrigger::IsActive() -{ - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - - return gruul && botAI->IsRanged(bot); -} - -bool GruulTheDragonkillerIncomingShatterTrigger::IsActive() -{ - Unit* gruul = AI_VALUE2(Unit*, "find target", "gruul the dragonkiller"); - - return gruul && (bot->HasAura(SPELL_GROUND_SLAM_1) || - bot->HasAura(SPELL_GROUND_SLAM_2)); -} diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h deleted file mode 100644 index 583b8b75f..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Trigger/RaidGruulsLairTriggers.h +++ /dev/null @@ -1,97 +0,0 @@ -#ifndef _PLAYERBOT_RAIDGRUULSLAIRTRIGGERS_H -#define _PLAYERBOT_RAIDGRUULSLAIRTRIGGERS_H - -#include "Trigger.h" - -class HighKingMaulgarIsMainTankTrigger : public Trigger -{ -public: - HighKingMaulgarIsMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar is main tank") {} - bool IsActive() override; -}; - -class HighKingMaulgarIsFirstAssistTankTrigger : public Trigger -{ -public: - HighKingMaulgarIsFirstAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar is first assist tank") {} - bool IsActive() override; -}; - -class HighKingMaulgarIsSecondAssistTankTrigger : public Trigger -{ -public: - HighKingMaulgarIsSecondAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar is second assist tank") {} - bool IsActive() override; -}; - -class HighKingMaulgarIsMageTankTrigger : public Trigger -{ -public: - HighKingMaulgarIsMageTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar is mage tank") {} - bool IsActive() override; -}; - -class HighKingMaulgarIsMoonkinTankTrigger : public Trigger -{ -public: - HighKingMaulgarIsMoonkinTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar is moonkin tank") {} - bool IsActive() override; -}; - -class HighKingMaulgarDeterminingKillOrderTrigger : public Trigger -{ -public: - HighKingMaulgarDeterminingKillOrderTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar determining kill order") {} - bool IsActive() override; -}; - -class HighKingMaulgarHealerInDangerTrigger : public Trigger -{ -public: - HighKingMaulgarHealerInDangerTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar healers in danger") {} - bool IsActive() override; -}; - -class HighKingMaulgarBossChannelingWhirlwindTrigger : public Trigger -{ -public: - HighKingMaulgarBossChannelingWhirlwindTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar boss channeling whirlwind") {} - bool IsActive() override; -}; - -class HighKingMaulgarWildFelstalkerSpawnedTrigger : public Trigger -{ -public: - HighKingMaulgarWildFelstalkerSpawnedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar wild felstalker spawned") {} - bool IsActive() override; -}; - -class HighKingMaulgarPullingOlmAndBlindeyeTrigger : public Trigger -{ -public: - HighKingMaulgarPullingOlmAndBlindeyeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "high king maulgar pulling olm and blindeye") {} - bool IsActive() override; -}; - -class GruulTheDragonkillerBossEngagedByTanksTrigger : public Trigger -{ -public: - GruulTheDragonkillerBossEngagedByTanksTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by tanks") {} - bool IsActive() override; -}; - -class GruulTheDragonkillerBossEngagedByRangedTrigger : public Trigger -{ -public: - GruulTheDragonkillerBossEngagedByRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller boss engaged by ranged") {} - bool IsActive() override; -}; - -class GruulTheDragonkillerIncomingShatterTrigger : public Trigger -{ -public: - GruulTheDragonkillerIncomingShatterTrigger(PlayerbotAI* botAI) : Trigger(botAI, "gruul the dragonkiller incoming shatter") {} - bool IsActive() override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp deleted file mode 100644 index 7ed82f19e..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include "RaidGruulsLairHelpers.h" -#include "AiFactory.h" -#include "GroupReference.h" -#include "Playerbots.h" -#include "Unit.h" - -namespace GruulsLairHelpers -{ - // Olm does not chase properly due to the Core's caster movement issues - // Thus, the below "OlmTankPosition" is beyond the actual desired tanking location - // It is the spot to which the OlmTank runs to to pull Olm to a decent tanking location - // "MaulgarRoomCenter" is to keep healers in a centralized location - const Position MAULGAR_TANK_POSITION = { 90.686f, 167.047f, -13.234f }; - const Position OLM_TANK_POSITION = { 87.485f, 234.942f, -3.635f }; - const Position BLINDEYE_TANK_POSITION = { 99.681f, 213.989f, -10.345f }; - const Position KROSH_TANK_POSITION = { 116.880f, 166.208f, -14.231f }; - const Position MAULGAR_ROOM_CENTER = { 88.754f, 150.759f, -11.569f }; - const Position GRUUL_TANK_POSITION = { 241.238f, 365.025f, -4.220f }; - - bool IsAnyOgreBossAlive(PlayerbotAI* botAI) - { - const char* ogreBossNames[] = - { - "high king maulgar", - "kiggler the crazed", - "krosh firehand", - "olm the summoner", - "blindeye the seer" - }; - - for (const char* name : ogreBossNames) - { - Unit* boss = botAI->GetAiObjectContext()->GetValue("find target", name)->Get(); - if (!boss || !boss->IsAlive()) - continue; - return true; - } - - return false; - } - - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot) - { - Group* group = bot->GetGroup(); - if (!group) - return false; - - // (1) First loop: Return the first assistant Mage (real player or bot) - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member->getClass() != CLASS_MAGE) - continue; - - if (group->IsAssistant(member->GetGUID())) - return member == bot; - } - - // (2) Fall back to bot Mage with highest HP - Player* highestHpMage = nullptr; - uint32 highestHp = 0; - - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - member->getClass() != CLASS_MAGE) - continue; - - uint32 hp = member->GetMaxHealth(); - if (!highestHpMage || hp > highestHp) - { - highestHpMage = member; - highestHp = hp; - } - } - - // (3) Return the found Mage tank, or nullptr if none found - return highestHpMage == bot; - } - - bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot) - { - Group* group = bot->GetGroup(); - if (!group) - return false; - - // (1) First loop: Return the first assistant Moonkin (real player or bot) - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID) - continue; - - if (group->IsAssistant(member->GetGUID()) && - AiFactory::GetPlayerSpecTab(member) == DRUID_TAB_BALANCE) - return member == bot; - } - - // (2) Fall back to bot Moonkin with highest HP - Player* highestHpMoonkin = nullptr; - uint32 highestHp = 0; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member->getClass() != CLASS_DRUID || - !GET_PLAYERBOT_AI(member) || AiFactory::GetPlayerSpecTab(member) != DRUID_TAB_BALANCE) - continue; - - uint32 hp = member->GetMaxHealth(); - if (!highestHpMoonkin || hp > highestHp) - { - highestHpMoonkin = member; - highestHp = hp; - } - } - - // (3) Return the found Moonkin tank, or nullptr if none found - return highestHpMoonkin == bot; - } - - bool IsPositionSafe(PlayerbotAI* botAI, Player* bot, Position pos) - { - const float KROSH_SAFE_DISTANCE = 20.0f; - const float MAULGAR_SAFE_DISTANCE = 10.0f; - bool isSafe = true; - - Unit* krosh = botAI->GetAiObjectContext()->GetValue("find target", "krosh firehand")->Get(); - if (krosh && krosh->IsAlive()) - { - float dist = sqrt(pow(pos.GetPositionX() - krosh->GetPositionX(), 2) + pow(pos.GetPositionY() - krosh->GetPositionY(), 2)); - if (dist < KROSH_SAFE_DISTANCE) - isSafe = false; - } - - Unit* maulgar = botAI->GetAiObjectContext()->GetValue("find target", "high king maulgar")->Get(); - if (botAI->IsRanged(bot) && maulgar && maulgar->IsAlive()) - { - float dist = sqrt(pow(pos.GetPositionX() - maulgar->GetPositionX(), 2) + pow(pos.GetPositionY() - maulgar->GetPositionY(), 2)); - if (dist < MAULGAR_SAFE_DISTANCE) - isSafe = false; - } - - return isSafe; - } - - bool TryGetNewSafePosition(PlayerbotAI* botAI, Player* bot, Position& outPos) - { - const float SEARCH_RADIUS = 30.0f; - const uint8 NUM_POSITIONS = 32; - - outPos = { bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ() }; - if (IsPositionSafe(botAI, bot, outPos)) - { - outPos = Position(); - return false; - } - - float bestScore = std::numeric_limits::max(); - bool foundSafeSpot = false; - Position bestPos; - - for (int i = 0; i < NUM_POSITIONS; ++i) - { - float angle = 2 * M_PI * i / NUM_POSITIONS; - Position candidatePos; - candidatePos.Relocate(bot->GetPositionX() + SEARCH_RADIUS * cos(angle), - bot->GetPositionY() + SEARCH_RADIUS * sin(angle), - bot->GetPositionZ()); - - float destX = candidatePos.GetPositionX(); - float destY = candidatePos.GetPositionY(); - float destZ = candidatePos.GetPositionZ(); - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), destX, destY, destZ, true)) - continue; - - if (destX != candidatePos.GetPositionX() || destY != candidatePos.GetPositionY()) - continue; - - candidatePos.Relocate(destX, destY, destZ); - - if (IsPositionSafe(botAI, bot, candidatePos)) - { - float movementDistance = sqrt(pow(destX - bot->GetPositionX(), 2) + pow(destY - bot->GetPositionY(), 2)); - if (movementDistance < bestScore) - { - bestScore = movementDistance; - bestPos = candidatePos; - foundSafeSpot = true; - } - } - } - - if (foundSafeSpot) - { - outPos = bestPos; - return true; - } - - outPos = Position(); - return false; - } -} diff --git a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h b/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h deleted file mode 100644 index 4615a9b7a..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/GruulsLair/Util/RaidGruulsLairHelpers.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef RAID_GRUULSLAIRHELPERS_H -#define RAID_GRUULSLAIRHELPERS_H - -#include "PlayerbotAI.h" - -namespace GruulsLairHelpers -{ - enum GruulsLairSpells - { - // High King Maulgar - SPELL_WHIRLWIND = 33238, - - // Krosh Firehand - SPELL_SPELL_SHIELD = 33054, - - // Hunter - SPELL_MISDIRECTION = 35079, - - // Gruul the Dragonkiller - SPELL_GROUND_SLAM_1 = 33525, - SPELL_GROUND_SLAM_2 = 39187, - }; - - enum GruulsLairNPCs - { - NPC_WILD_FEL_STALKER = 18847, - }; - - constexpr uint32 GRUULS_LAIR_MAP_ID = 565; - - bool IsAnyOgreBossAlive(PlayerbotAI* botAI); - bool IsKroshMageTank(PlayerbotAI* botAI, Player* bot); - bool IsKigglerMoonkinTank(PlayerbotAI* botAI, Player* bot); - bool IsPositionSafe(PlayerbotAI* botAI, Player* bot, Position pos); - bool TryGetNewSafePosition(PlayerbotAI* botAI, Player* bot, Position& outPos); - - extern const Position MAULGAR_TANK_POSITION; - extern const Position OLM_TANK_POSITION; - extern const Position BLINDEYE_TANK_POSITION; - extern const Position KROSH_TANK_POSITION; - extern const Position MAULGAR_ROOM_CENTER; - extern const Position GRUUL_TANK_POSITION; -} - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActionContext.h new file mode 100644 index 000000000..d5e51ebff --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActionContext.h @@ -0,0 +1,225 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALACTIONCONTEXT_H +#define PLAYERBOTS_HYJALACTIONCONTEXT_H + +#include "HyjalActions.h" +#include "NamedObjectContext.h" + +class RaidHyjalSummitActionContext : public NamedObjectContext +{ +public: + RaidHyjalSummitActionContext() + { + // General + creators["hyjal summit erase trackers"] = + &RaidHyjalSummitActionContext::hyjal_summit_erase_trackers; + + // Rage Winterchill + creators["rage winterchill misdirect boss to main tank"] = + &RaidHyjalSummitActionContext::rage_winterchill_misdirect_boss_to_main_tank; + + creators["rage winterchill main tank position boss"] = + &RaidHyjalSummitActionContext::rage_winterchill_main_tank_position_boss; + + creators["rage winterchill spread ranged in circle"] = + &RaidHyjalSummitActionContext::rage_winterchill_spread_ranged_in_circle; + + creators["rage winterchill melee get out of death and decay"] = + &RaidHyjalSummitActionContext::rage_winterchill_melee_get_out_of_death_and_decay; + + // Anetheron + creators["anetheron misdirect boss and infernals to tanks"] = + &RaidHyjalSummitActionContext::anetheron_misdirect_boss_and_infernals_to_tanks; + + creators["anetheron main tank position boss"] = + &RaidHyjalSummitActionContext::anetheron_main_tank_position_boss; + + creators["anetheron spread ranged in circle"] = + &RaidHyjalSummitActionContext::anetheron_spread_ranged_in_circle; + + creators["anetheron bring infernal to infernal tank"] = + &RaidHyjalSummitActionContext::anetheron_bring_infernal_to_infernal_tank; + + creators["anetheron first assist tank pick up infernals"] = + &RaidHyjalSummitActionContext::anetheron_first_assist_tank_pick_up_infernals; + + creators["anetheron assign dps priority"] = + &RaidHyjalSummitActionContext::anetheron_assign_dps_priority; + + // Kaz'rogal + creators["kaz'rogal misdirect boss to main tank"] = + &RaidHyjalSummitActionContext::kazrogal_misdirect_boss_to_main_tank; + + creators["kaz'rogal main tank position boss"] = + &RaidHyjalSummitActionContext::kazrogal_main_tank_position_boss; + + creators["kaz'rogal assist tanks move in front of boss"] = + &RaidHyjalSummitActionContext::kazrogal_assist_tanks_move_in_front_of_boss; + + creators["kaz'rogal spread ranged in arc"] = + &RaidHyjalSummitActionContext::kazrogal_spread_ranged_in_arc; + + creators["kaz'rogal low mana bot take defensive measures"] = + &RaidHyjalSummitActionContext::kazrogal_low_mana_bot_take_defensive_measures; + + creators["kaz'rogal cast shadow protection spell"] = + &RaidHyjalSummitActionContext::kazrogal_cast_shadow_protection_spell; + + // Azgalor + creators["azgalor misdirect boss to main tank"] = + &RaidHyjalSummitActionContext::azgalor_misdirect_boss_to_main_tank; + + creators["azgalor main tank position boss"] = + &RaidHyjalSummitActionContext::azgalor_main_tank_position_boss; + + creators["azgalor wait at safe position"] = + &RaidHyjalSummitActionContext::azgalor_wait_at_safe_position; + + creators["azgalor disperse ranged"] = + &RaidHyjalSummitActionContext::azgalor_disperse_ranged; + + creators["azgalor melee get out of fire and swap targets"] = + &RaidHyjalSummitActionContext::azgalor_melee_get_out_of_fire_and_swap_targets; + + creators["azgalor move to doomguard tank"] = + &RaidHyjalSummitActionContext::azgalor_move_to_doomguard_tank; + + creators["azgalor first assist tank position doomguard"] = + &RaidHyjalSummitActionContext::azgalor_first_assist_tank_position_doomguard; + + creators["azgalor ranged dps prioritize doomguards"] = + &RaidHyjalSummitActionContext::azgalor_ranged_dps_prioritize_doomguards; + + // Archimonde + creators["archimonde misdirect boss to main tank"] = + &RaidHyjalSummitActionContext::archimonde_misdirect_boss_to_main_tank; + + creators["archimonde move boss to initial position"] = + &RaidHyjalSummitActionContext::archimonde_move_boss_to_initial_position; + + creators["archimonde cast fear immunity spell"] = + &RaidHyjalSummitActionContext::archimonde_cast_fear_immunity_spell; + + creators["archimonde spread to avoid air burst"] = + &RaidHyjalSummitActionContext::archimonde_spread_to_avoid_air_burst; + + creators["archimonde avoid doomfire"] = + &RaidHyjalSummitActionContext::archimonde_avoid_doomfire; + + creators["archimonde remove doomfire dot"] = + &RaidHyjalSummitActionContext::archimonde_remove_doomfire_dot; + } + +private: + // General + static Action* hyjal_summit_erase_trackers(PlayerbotAI* botAI) { + return new HyjalSummitEraseTrackersAction(botAI); + } + + // Rage Winterchill + static Action* rage_winterchill_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new RageWinterchillMisdirectBossToMainTankAction(botAI); + } + static Action* rage_winterchill_main_tank_position_boss(PlayerbotAI* botAI) { + return new RageWinterchillMainTankPositionBossAction(botAI); + } + static Action* rage_winterchill_spread_ranged_in_circle(PlayerbotAI* botAI) { + return new RageWinterchillSpreadRangedInCircleAction(botAI); + } + static Action* rage_winterchill_melee_get_out_of_death_and_decay(PlayerbotAI* botAI) { + return new RageWinterchillMeleeGetOutOfDeathAndDecayAction(botAI); + } + + // Anetheron + static Action* anetheron_misdirect_boss_and_infernals_to_tanks(PlayerbotAI* botAI) { + return new AnetheronMisdirectBossAndInfernalsToTanksAction(botAI); + } + static Action* anetheron_main_tank_position_boss(PlayerbotAI* botAI) { + return new AnetheronMainTankPositionBossAction(botAI); + } + static Action* anetheron_spread_ranged_in_circle(PlayerbotAI* botAI) { + return new AnetheronSpreadRangedInCircleAction(botAI); + } + static Action* anetheron_bring_infernal_to_infernal_tank(PlayerbotAI* botAI) { + return new AnetheronBringInfernalToInfernalTankAction(botAI); + } + static Action* anetheron_first_assist_tank_pick_up_infernals(PlayerbotAI* botAI) { + return new AnetheronFirstAssistTankPickUpInfernalsAction(botAI); + } + static Action* anetheron_assign_dps_priority(PlayerbotAI* botAI) { + return new AnetheronAssignDpsPriorityAction(botAI); + } + + // Kaz'rogal + static Action* kazrogal_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new KazrogalMisdirectBossToMainTankAction(botAI); + } + static Action* kazrogal_main_tank_position_boss(PlayerbotAI* botAI) { + return new KazrogalMainTankPositionBossAction(botAI); + } + static Action* kazrogal_assist_tanks_move_in_front_of_boss(PlayerbotAI* botAI) { + return new KazrogalAssistTanksMoveInFrontOfBossAction(botAI); + } + static Action* kazrogal_spread_ranged_in_arc(PlayerbotAI* botAI) { + return new KazrogalSpreadRangedInArcAction(botAI); + } + static Action* kazrogal_low_mana_bot_take_defensive_measures(PlayerbotAI* botAI) { + return new KazrogalLowManaBotTakeDefensiveMeasuresAction(botAI); + } + static Action* kazrogal_cast_shadow_protection_spell(PlayerbotAI* botAI) { + return new KazrogalCastShadowProtectionSpellAction(botAI); + } + + // Azgalor + static Action* azgalor_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new AzgalorMisdirectBossToMainTankAction(botAI); + } + static Action* azgalor_main_tank_position_boss(PlayerbotAI* botAI) { + return new AzgalorMainTankPositionBossAction(botAI); + } + static Action* azgalor_wait_at_safe_position(PlayerbotAI* botAI) { + return new AzgalorWaitAtSafePositionAction(botAI); + } + static Action* azgalor_disperse_ranged(PlayerbotAI* botAI) { + return new AzgalorDisperseRangedAction(botAI); + } + static Action* azgalor_melee_get_out_of_fire_and_swap_targets(PlayerbotAI* botAI) { + return new AzgalorMeleeGetOutOfFireAndSwapTargetsAction(botAI); + } + static Action* azgalor_move_to_doomguard_tank(PlayerbotAI* botAI) { + return new AzgalorMoveToDoomguardTankAction(botAI); + } + static Action* azgalor_first_assist_tank_position_doomguard(PlayerbotAI* botAI) { + return new AzgalorFirstAssistTankPositionDoomguardAction(botAI); + } + static Action* azgalor_ranged_dps_prioritize_doomguards(PlayerbotAI* botAI) { + return new AzgalorRangedDpsPrioritizeDoomguardsAction(botAI); + } + + // Archimonde + static Action* archimonde_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new ArchimondeMisdirectBossToMainTankAction(botAI); + } + static Action* archimonde_move_boss_to_initial_position(PlayerbotAI* botAI) { + return new ArchimondeMoveBossToInitialPositionAction(botAI); + } + static Action* archimonde_cast_fear_immunity_spell(PlayerbotAI* botAI) { + return new ArchimondeCastFearImmunitySpellAction(botAI); + } + static Action* archimonde_spread_to_avoid_air_burst(PlayerbotAI* botAI) { + return new ArchimondeSpreadToAvoidAirBurstAction(botAI); + } + static Action* archimonde_avoid_doomfire(PlayerbotAI* botAI) { + return new ArchimondeAvoidDoomfireAction(botAI); + } + static Action* archimonde_remove_doomfire_dot(PlayerbotAI* botAI) { + return new ArchimondeRemoveDoomfireDotAction(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.cpp new file mode 100644 index 000000000..f8047d9a0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.cpp @@ -0,0 +1,1209 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HyjalActions.h" +#include "HyjalHelpers.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" +#include "Timer.h" + +using namespace HyjalSummitHelpers; + +// General + +bool HyjalSummitEraseTrackersAction::Execute(Event /*event*/) +{ + const ObjectGuid guid = bot->GetGUID(); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + bool erased = false; + if (botAI->IsTank(bot)) + { + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + { + if (kazrogalTankStep.erase(guid) > 0) + erased = true; + + if (isBelowManaThreshold.erase(guid) > 0) + erased = true; + } + + if (!AI_VALUE2(Unit*, "find target", "azgalor")) + { + if (azgalorTankStep.erase(guid) > 0) + erased = true; + + if (rainOfFirePosition.erase(instanceId) > 0) + erased = true; + } + + return erased; + } + else + { + if (!AI_VALUE2(Unit*, "find target", "rage winterchill")) + { + if (hasReachedWinterchillPosition.erase(guid) > 0) + erased = true; + + if (deathAndDecayPosition.erase(instanceId) > 0) + erased = true; + } + + if (!AI_VALUE2(Unit*, "find target", "anetheron") && + hasReachedAnetheronPosition.erase(guid) > 0) + erased = true; + + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal") && + isBelowManaThreshold.erase(guid) > 0) + erased = true; + + if (!AI_VALUE2(Unit*, "find target", "archimonde") && + doomfireTrails.erase(instanceId) > 0) + erased = true; + + return erased; + } +} + +// Rage Winterchill + +bool RageWinterchillMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + if (!winterchill) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", winterchill)) + return botAI->CastSpell("steady shot", winterchill); + + return false; +} + +// Position is back towards the center of the base to give some more room to manuever +bool RageWinterchillMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + if (!winterchill) + return false; + + if (AI_VALUE(Unit*, "current target") != winterchill) + return Attack(winterchill); + + if (winterchill->GetVictim() == bot) + { + const Position& position = WINTERCHILL_TANK_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.0f) + { + float moveX, moveY, moveZ; + constexpr float moveDist = 5.0f; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + return false; +} + +// Spread ranged DPS in a circle initially--after the initial spread, movement is free +bool RageWinterchillSpreadRangedInCircleAction::Execute(Event /*event*/) +{ + RangedGroups groups = GetRangedGroups(botAI, bot); + + if (groups.healers.empty() && groups.rangedDps.empty()) + return false; + + const ObjectGuid guid = bot->GetGUID(); + + if (!hasReachedWinterchillPosition[guid]) + { + auto [botIndex, count] = GetBotCircleIndexAndCount(botAI, bot, groups); + const float radius = botAI->IsHeal(bot) ? 25.0f : 35.0f; + float angle = 0.0f; + + constexpr float arcSpan = 2.0f * M_PI; + constexpr float arcCenter = 0.0f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + const Position& position = WINTERCHILL_TANK_POSITION; + float targetX = position.GetPositionX() + radius * std::cos(angle); + float targetY = position.GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + float moveX, moveY, moveZ; + constexpr float moveDist = 10.0f; + if (GetGroundedStepPosition(bot, targetX, targetY, moveDist, + moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + else + { + hasReachedWinterchillPosition[guid] = true; + } + } + + return false; +} + +bool RageWinterchillMeleeGetOutOfDeathAndDecayAction::Execute(Event /*event*/) +{ + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + if (!winterchill) + return false; + + DeathAndDecayData* data = + GetActiveWinterchillDeathAndDecay(bot->GetMap()->GetInstanceId()); + if (!data) + return false; + + constexpr float moveDist = 10.0f; + + const float centerX = data->position.GetPositionX(); + const float centerY = data->position.GetPositionY(); + const float currentDistance = bot->GetExactDist2d(centerX, centerY); + float escapeAngle = + std::atan2(bot->GetPositionY() - centerY, bot->GetPositionX() - centerX); + + if (currentDistance <= 0.1f) + { + escapeAngle = std::atan2(centerY - winterchill->GetPositionY(), + centerX - winterchill->GetPositionX()); + } + + for (float delta = 0.0f; delta <= M_PI; delta += M_PI / 8.0f) + { + for (float angle : { escapeAngle + delta, escapeAngle - delta }) + { + if (delta == 0.0f && angle != escapeAngle) + continue; + + const float targetX = centerX + std::cos(angle) * DEATH_AND_DECAY_SAFE_RADIUS; + const float targetY = centerY + std::sin(angle) * DEATH_AND_DECAY_SAFE_RADIUS; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, targetX, targetY, moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + } + + return false; +} + +// Anetheron + +bool AnetheronMisdirectBossAndInfernalsToTanksAction::Execute(Event /*event*/) +{ + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (!anetheron) + return false; + + if (anetheron->GetHealthPct() > 95.0f) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", anetheron)) + return botAI->CastSpell("steady shot", anetheron); + } + + if (Unit* infernal = AI_VALUE2(Unit*, "find target", "towering infernal"); + infernal && infernal->GetHealthPct() > 50.0f) + { + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + if (!firstAssistTank) + return false; + + if (botAI->CanCastSpell("misdirection", firstAssistTank)) + return botAI->CastSpell("misdirection", firstAssistTank); + + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", infernal)) + return botAI->CastSpell("steady shot", infernal); + } + + return false; +} + +// Position is back towards the center of the base, near the crossroads +bool AnetheronMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (!anetheron) + return false; + + if (MarkTargetWithSquare(bot, anetheron)) + return true; + + SetRtiTarget(botAI, "square", anetheron); + + if (AI_VALUE(Unit*, "current target") != anetheron) + return Attack(anetheron); + + if (anetheron->GetVictim() == bot) + { + const Position& position = ANETHERON_TANK_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.0f) + { + float moveX, moveY, moveZ; + constexpr float moveDist = 5.0f; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + return false; +} + +bool AnetheronSpreadRangedInCircleAction::Execute(Event /*event*/) +{ + RangedGroups groups = GetRangedGroups(botAI, bot); + + if (groups.healers.empty() && groups.rangedDps.empty()) + return false; + + const ObjectGuid guid = bot->GetGUID(); + + if (!hasReachedAnetheronPosition[guid]) + { + auto [botIndex, count] = GetBotCircleIndexAndCount(botAI, bot, groups); + const float radius = botAI->IsHeal(bot) ? 27.0f : 34.0f; + float angle = 0.0f; + + constexpr float arcSpan = M_PI * 2.0f; + constexpr float arcCenter = 0.0f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + const Position& position = ANETHERON_TANK_POSITION; + + float targetX = position.GetPositionX() + radius * std::sin(angle); + float targetY = position.GetPositionY() + radius * std::cos(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, targetX, targetY, moveDist, + moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + else + { + hasReachedAnetheronPosition[guid] = true; + } + } + else + { + constexpr float safeDistFromPlayer = 6.0f; + constexpr uint32 minInterval = 2000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval); + } + + return false; +} + +// Run to the nearest of two Infernal tanking spots, East and West of Anetheron +bool AnetheronBringInfernalToInfernalTankAction::Execute(Event /*event*/) +{ + const Position& position = GetClosestInfernalTankPosition(bot); + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +// Pick up the Infernal and bring it to the closest Infernal tanking position +bool AnetheronFirstAssistTankPickUpInfernalsAction::Execute(Event /*event*/) +{ + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (!anetheron) + return false; + + Player* infernoTarget = GetInfernoTarget(anetheron); + if (infernoTarget && infernoTarget != bot) + { + float distToInfernoTarget = bot->GetExactDist2d(infernoTarget); + if (distToInfernoTarget > 5.0f) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, infernoTarget->GetPositionX(), + infernoTarget->GetPositionY(), moveDist, + moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + Unit* infernal = AI_VALUE2(Unit*, "find target", "towering infernal"); + if (!infernal) + return false; + + if (MarkTargetWithDiamond(bot, infernal)) + return true; + + SetRtiTarget(botAI, "diamond", infernal); + + if (AI_VALUE(Unit*, "current target") != infernal) + return Attack(infernal); + + if ((infernoTarget && infernoTarget == bot) || + (infernal->GetVictim() == bot && bot->IsWithinMeleeRange(infernal))) + { + const Position& position = GetClosestInfernalTankPosition(bot); + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + constexpr float moveDist = 5.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + return false; +} + +// Only nearbyish ranged DPS should attack Infernals +bool AnetheronAssignDpsPriorityAction::Execute(Event /*event*/) +{ + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (!anetheron) + return false; + + if (botAI->IsMelee(bot)) + { + SetRtiTarget(botAI, "square", anetheron); + + if (AI_VALUE(Unit*, "current target") != anetheron) + return Attack(anetheron); + + return false; + } + if (Unit* infernal = AI_VALUE2(Unit*, "find target", "towering infernal")) + { + constexpr float safeDistFromInfernal = 10.0f; + constexpr uint32 minInterval = 0; + if (infernal->GetVictim() != bot && + bot->GetDistance2d(infernal) < safeDistFromInfernal) + { + return FleePosition(infernal->GetPosition(), safeDistFromInfernal, minInterval); + } + + if (anetheron->GetHealthPct() > 10.0f && botAI->IsRangedDps(bot) && + bot->GetDistance2d(infernal) < 50.0f) + { + if (Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + !firstAssistTank || infernal->GetVictim() == firstAssistTank) + { + SetRtiTarget(botAI, "diamond", infernal); + + if (AI_VALUE(Unit*, "current target") != infernal) + return Attack(infernal); + } + } + } + else if (botAI->IsRangedDps(bot)) + { + SetRtiTarget(botAI, "square", anetheron); + + if (AI_VALUE(Unit*, "current target") != anetheron) + return Attack(anetheron); + } + + return false; +} + +// Kaz'rogal + +bool KazrogalMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal"); + if (!kazrogal) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", kazrogal)) + return botAI->CastSpell("steady shot", kazrogal); + + return false; +} + +// Position is near the gate so the raid can get start on DPS ASAP +bool KazrogalMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal"); + if (!kazrogal) + return false; + + if (AI_VALUE(Unit*, "current target") != kazrogal) + return Attack(kazrogal); + + if (kazrogal->GetVictim() == bot && bot->IsWithinMeleeRange(kazrogal)) + { + const ObjectGuid guid = bot->GetGUID(); + TankPositionState state = kazrogalTankStep.count(guid) ? + kazrogalTankStep[guid] : TankPositionState::MovingToTransition; + + constexpr float maxDistance = 2.0f; + const Position& position = state == TankPositionState::MovingToTransition ? + KAZROGAL_TANK_TRANSITION_POSITION : KAZROGAL_TANK_FINAL_POSITION; + const float distToPosition = bot->GetExactDist2d(position); + + if (distToPosition > maxDistance) + { + constexpr float moveDist = 5.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + if (state == TankPositionState::MovingToTransition && distToPosition <= maxDistance) + { + kazrogalTankStep[guid] = TankPositionState::MovingToFinal; + } + else if (state != TankPositionState::MovingToTransition && + distToPosition <= maxDistance) + { + const float orientation = atan2(kazrogal->GetPositionY() - bot->GetPositionY(), + kazrogal->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); + kazrogalTankStep[guid] = TankPositionState::Positioned; + } + } + + return false; +} + +// To spread cleave damage +bool KazrogalAssistTanksMoveInFrontOfBossAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (bot->GetExactDist2d(mainTank) > 4.0f) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, mainTank->GetPositionX(), mainTank->GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KazrogalSpreadRangedInArcAction::Execute(Event /*event*/) +{ + Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal"); + if (!kazrogal) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector rangedMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + + if (rangedMembers.empty()) + return false; + + size_t count = rangedMembers.size(); + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = (findIt != rangedMembers.end()) ? + std::distance(rangedMembers.begin(), findIt) : 0; + + constexpr float arcSpan = M_PI / 3.0f; + constexpr float arcCenter = 4.225f; + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + constexpr float radius = 20.0f; + float angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + float targetX = kazrogal->GetPositionX() + radius * std::cos(angle); + float targetY = kazrogal->GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 0.5f) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, targetX, targetY, moveDist, + moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KazrogalLowManaBotTakeDefensiveMeasuresAction::Execute(Event /*event*/) +{ + switch (bot->getClass()) + { + case CLASS_HUNTER: + if (!botAI->HasAura("aspect of the viper", bot) && + botAI->CanCastSpell("aspect of the viper", bot)) + { + return botAI->CastSpell("aspect of the viper", bot); + } + return false; + + case CLASS_WARLOCK: + if (botAI->CanCastSpell("life tap", bot) && + botAI->CastSpell("life tap", bot)) + { + return true; + } + break; + + case CLASS_MAGE: + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MARK_OF_KAZROGAL)) && + bot->GetPower(POWER_MANA) <= 1200 && botAI->CanCastSpell("ice block", bot) && + botAI->CastSpell("ice block", bot)) + { + return true; + } + break; + + case CLASS_PALADIN: + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MARK_OF_KAZROGAL)) && + bot->GetPower(POWER_MANA) <= 1200 && botAI->CanCastSpell("divine shield", bot) && + botAI->CastSpell("divine shield", bot)) + { + return true; + } + break; + + default: + break; + } + + if (bot->GetPower(POWER_MANA) <= 3200) + isBelowManaThreshold.try_emplace(bot->GetGUID(), true); + + constexpr float safeDistance = 16.0f; + + Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + if (!nearestPlayer) + return false; + + const float currentDistance = bot->GetDistance2d(nearestPlayer); + if (currentDistance < safeDistance) + { + Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal"); + if (!kazrogal) + return false; + + if (bot->GetExactDist2d(kazrogal) > 36.0f) + return MoveAway(nearestPlayer, safeDistance - currentDistance); + else + return MoveFromGroup(safeDistance); + } + + return false; +} + +// Warlocks: Use Shadow Ward if Mark is applied and mana is <= 3000 +// Paladins: Use Shadow Resistance Aura if Priest Shadow Protection is not up +bool KazrogalCastShadowProtectionSpellAction::Execute(Event /*event*/) +{ + if (bot->getClass() == CLASS_WARLOCK && bot->GetPower(POWER_MANA) <= 3000 && + botAI->CanCastSpell("shadow ward", bot)) + return botAI->CastSpell("shadow ward", bot); + + if (bot->getClass() == CLASS_PALADIN && + botAI->CanCastSpell("shadow resistance aura", bot)) + return botAI->CastSpell("shadow resistance aura", bot); + + return false; +} + +// Azgalor + +bool AzgalorMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", azgalor)) + return botAI->CastSpell("steady shot", azgalor); + + return false; +} + +// Two-step move: back up toward the base, then move back toward the base entrance +// to turn Azgalor away from the raid +bool AzgalorMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return false; + + if (MarkTargetWithStar(bot, azgalor)) + return true; + + SetRtiTarget(botAI, "star", azgalor); + + if (AI_VALUE(Unit*, "current target") != azgalor) + return Attack(azgalor); + + if (azgalor->GetVictim() == bot && bot->IsWithinMeleeRange(azgalor)) + { + const ObjectGuid guid = bot->GetGUID(); + auto it = azgalorTankStep.try_emplace( + guid, TankPositionState::MovingToTransition).first; + TankPositionState state = it->second; + + constexpr float maxDistance = 2.0f; + const Position& position = state == TankPositionState::MovingToTransition ? + AZGALOR_TANK_TRANSITION_POSITION : AZGALOR_TANK_FINAL_POSITION; + const float distToPosition = bot->GetExactDist2d(position); + + if (distToPosition > maxDistance) + { + constexpr float moveDist = 5.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + if (state == TankPositionState::MovingToTransition && distToPosition <= maxDistance) + { + azgalorTankStep[guid] = TankPositionState::MovingToFinal; + } + else if (state != TankPositionState::MovingToTransition && + distToPosition <= maxDistance) + { + const float orientation = atan2(azgalor->GetPositionY() - bot->GetPositionY(), + azgalor->GetPositionX() - bot->GetPositionX()); + bot->SetFacingTo(orientation); + azgalorTankStep[guid] = TankPositionState::Positioned; + } + } + + return false; +} + +bool AzgalorDisperseRangedAction::Execute(Event /*event*/) +{ + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return false; + + TankPositionState tankState = GetAzgalorTankPositionState(botAI, bot); + const float safeDistFromBoss = + (tankState == TankPositionState::MovingToTransition ? 35.0f : 29.0f); + constexpr uint32 minInterval = 0; + + if (bot->GetExactDist2d(azgalor) < safeDistFromBoss && + FleePosition(azgalor->GetPosition(), safeDistFromBoss, minInterval)) + return true; + + Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard"); + constexpr float safeDistFromDoomguard = 14.0f; + constexpr float safeDistFromPlayer = 5.0f; + + if (doomguard && bot->GetExactDist2d(doomguard) < safeDistFromDoomguard) + { + return FleePosition(doomguard->GetPosition(), safeDistFromDoomguard); + } + else if (!doomguard || AI_VALUE(Unit*, "current target") != doomguard) + { + Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer); + if (nearestPlayer) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer); + } + + return false; +} + +bool AzgalorMeleeGetOutOfFireAndSwapTargetsAction::Execute(Event /*event*/) +{ + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return false; + + constexpr float singleTickMoveAwayDist = 6.0f; + if (!IsInRainOfFire(bot, RAIN_OF_FIRE_RADIUS + singleTickMoveAwayDist)) + { + SetRtiTarget(botAI, "star", azgalor); + return false; + } + + Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard"); + Unit* desiredTarget = doomguard; + + if (!desiredTarget) + { + SetRtiTarget(botAI, "star", azgalor); + return MoveAway(azgalor, 5.0f); + } + + SetRtiTarget(botAI, "circle", desiredTarget); + + if (!bot->IsWithinMeleeRange(desiredTarget)) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, desiredTarget->GetPositionX(), + desiredTarget->GetPositionY(), moveDist, + moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + if (AI_VALUE(Unit*, "current target") != desiredTarget) + return Attack(desiredTarget); + + return false; +} + +// Wait for the tank to get to the transition position (i.e., move in to attack as +// Azgalor turns away from the raid) +bool AzgalorWaitAtSafePositionAction::Execute(Event /*event*/) +{ + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return false; + + SetRtiTarget(botAI, "star", azgalor); + + const Position& position = AZGALOR_DOOMGUARD_POSITION; + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + botAI->Reset(); + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +// The spot is between the paths leading from Thrall's keep +bool AzgalorMoveToDoomguardTankAction::Execute(Event /*event*/) +{ + const Position& position = AZGALOR_DOOMGUARD_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 5.0f) + { + constexpr float moveDist = 10.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool AzgalorFirstAssistTankPositionDoomguardAction::Execute(Event /*event*/) +{ + const Position& position = AZGALOR_DOOMGUARD_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + float moveDist = 0.0f; + bool shouldMove = false; + bool moveBackwards = false; + + if (Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard")) + { + if (MarkTargetWithCircle(bot, doomguard)) + return true; + + SetRtiTarget(botAI, "circle", doomguard); + + if (AI_VALUE(Unit*, "current target") != doomguard) + return Attack(doomguard); + + if (doomguard->GetVictim() == bot && bot->IsWithinMeleeRange(doomguard) && + distToPosition > 3.0f) + { + moveDist = std::min(5.0f, distToPosition); + shouldMove = true; + moveBackwards = true; + } + } + else if (distToPosition > 3.0f) + { + moveDist = std::min(10.0f, distToPosition); + shouldMove = true; + moveBackwards = false; + } + else + { + return true; + } + + if (shouldMove) + { + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, moveBackwards); + } + } + + return false; +} + +// Only nearbyish ranged DPS should attack Doomguards; 65 yards should get to the +// side of Azgalor but not bring in any ranged standing in front +bool AzgalorRangedDpsPrioritizeDoomguardsAction::Execute(Event /*event*/) +{ + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return false; + + if (azgalor->GetHealthPct() > 10.0f) + { + if (Unit* doomguard = AI_VALUE2(Unit*, "find target", "lesser doomguard"); + doomguard && bot->GetDistance2d(doomguard) < 65.0f) + { + SetRtiTarget(botAI, "circle", doomguard); + + if (AI_VALUE(Unit*, "current target") != doomguard) + return Attack(doomguard); + } + } + else + { + SetRtiTarget(botAI, "star", azgalor); + + if (AI_VALUE(Unit*, "current target") != azgalor) + return Attack(azgalor); + } + + return false; +} + +// Archimonde + +bool ArchimondeMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (!archimonde) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", archimonde)) + return botAI->CastSpell("steady shot", archimonde); + + return false; +} + +// Initially move Archimonde up the hill a bit to get space from the World Tree +bool ArchimondeMoveBossToInitialPositionAction::Execute(Event /*event*/) +{ + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (!archimonde) + return false; + + if (AI_VALUE(Unit*, "current target") != archimonde) + return Attack(archimonde); + + if (archimonde->GetVictim() == bot && bot->IsWithinMeleeRange(archimonde) && + bot->GetHealthPct() > 50.0f) + { + const Position& position = ARCHIMONDE_INITIAL_POSITION; + const float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 3.0f) + { + constexpr float moveDist = 5.0f; + float moveX, moveY, moveZ; + if (GetGroundedStepPosition(bot, position.GetPositionX(), position.GetPositionY(), + moveDist, moveX, moveY, moveZ)) + { + return MoveTo(HYJAL_SUMMIT_MAP_ID, moveX, moveY, moveZ, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + } + + return false; +} + +bool ArchimondeCastFearImmunitySpellAction::Execute(Event /*event*/) +{ + if (bot->getClass() == CLASS_PRIEST) + return CastFearWardOnMainTank(); + else + return UseTremorTotemStrategy(); +} + +bool ArchimondeCastFearImmunitySpellAction::CastFearWardOnMainTank() +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +bool ArchimondeCastFearImmunitySpellAction::UseTremorTotemStrategy() +{ + if (!botAI->HasStrategy("tremor", BOT_STATE_COMBAT)) + { + botAI->ChangeStrategy("+tremor", BOT_STATE_COMBAT); + return botAI->HasStrategy("tremor", BOT_STATE_COMBAT); + } + + return false; +} + +// (1) Try to run away from the Air Burst target +// (2) At the beginning of the fight, spread ranged in anticipation of Air Burst +bool ArchimondeSpreadToAvoidAirBurstAction::Execute(Event /*event*/) +{ + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (!archimonde) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && bot != mainTank) + { + const float distanceToMainTank = bot->GetDistance2d(mainTank); + bool shouldMoveFromMainTank = false; + if (AirBurstData* data = GetRecentArchimondeAirBurst(bot->GetMap()->GetInstanceId())) + { + bool isRelevantAirBurstTarget = + data->targetGuid == mainTank->GetGUID() || data->targetGuid == bot->GetGUID(); + shouldMoveFromMainTank = + isRelevantAirBurstTarget && distanceToMainTank < AIR_BURST_SAFE_DISTANCE; + } + + if (!shouldMoveFromMainTank && archimonde->HasUnitState(UNIT_STATE_CASTING)) + { + Spell* spell = archimonde->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (spell && spell->m_spellInfo->Id == + static_cast(HyjalSummitSpells::SPELL_AIR_BURST)) + { + Unit* spellTarget = spell->m_targets.GetUnitTarget(); + if ((spellTarget == mainTank || spellTarget == bot) && + distanceToMainTank < AIR_BURST_SAFE_DISTANCE) + { + shouldMoveFromMainTank = true; + } + } + } + + if (shouldMoveFromMainTank) + return MoveAway(mainTank, AIR_BURST_SAFE_DISTANCE - distanceToMainTank); + } + + if (archimonde->GetHealthPct() < 90.0f) + return false; + + constexpr float safeDistFromPlayer = 10.0f; + constexpr uint32 minInterval = 1000; + + if (botAI->IsRanged(bot)) + { + Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer); + if (nearestPlayer && + FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval)) + { + return true; + } + } + + return false; +} + +bool ArchimondeAvoidDoomfireAction::Execute(Event /*event*/) +{ + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (!archimonde) + return false; + + constexpr float dangerDist = 10.0f; + constexpr uint32 trailDuration = 18000; + + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + const uint32 now = getMSTime(); + + auto it = doomfireTrails.find(instanceId); + if (it == doomfireTrails.end() || it->second.empty()) + return false; + + it->second.erase(std::remove_if(it->second.begin(), it->second.end(), + [now](const DoomfireTrailData& d) + { + return getMSTimeDiff(d.recordTime, now) > trailDuration; + }), it->second.end()); + + float totalDx = 0.0f, totalDy = 0.0f; + for (auto const& data : it->second) + { + const float d = bot->GetExactDist2d(data.position.GetPositionX(), + data.position.GetPositionY()); + + if (d < dangerDist && d > 0.0f) + { + const float weight = (dangerDist - d) / dangerDist; + totalDx += (bot->GetPositionX() - data.position.GetPositionX()) / d * weight; + totalDy += (bot->GetPositionY() - data.position.GetPositionY()) / d * weight; + } + } + + if (totalDx != 0.0f || totalDy != 0.0f) + { + const float norm = std::sqrt(totalDx * totalDx + totalDy * totalDy); + const float moveDist = std::min(norm * dangerDist, dangerDist); + if (moveDist < 0.5f) + return false; + + const float targetX = bot->GetPositionX() + (totalDx / norm) * moveDist; + const float targetY = bot->GetPositionY() + (totalDy / norm) * moveDist; + + const MovementPriority priority = botAI->IsHeal(bot) ? + MovementPriority::MOVEMENT_COMBAT : MovementPriority::MOVEMENT_FORCED; + + const bool backwards = archimonde->GetVictim() == bot; + + return MoveTo(HYJAL_SUMMIT_MAP_ID, targetX, targetY, bot->GetPositionZ(), + false, false, false, false, priority, true, backwards); + } + + return false; +} + +bool ArchimondeRemoveDoomfireDotAction::Execute(Event /*event*/) +{ + switch (bot->getClass()) + { + case CLASS_MAGE: + return botAI->CanCastSpell("ice block", bot) && + botAI->CastSpell("ice block", bot); + + case CLASS_PALADIN: + return botAI->CanCastSpell("divine shield", bot) && + botAI->CastSpell("divine shield", bot); + + case CLASS_ROGUE: + return botAI->CanCastSpell("cloak of shadows", bot) && + botAI->CastSpell("cloak of shadows", bot); + + default: + return false; + } +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.h new file mode 100644 index 000000000..4ee51ccb2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalActions.h @@ -0,0 +1,278 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALACTIONS_H +#define PLAYERBOTS_HYJALACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +// General + +class HyjalSummitEraseTrackersAction : public Action +{ +public: + HyjalSummitEraseTrackersAction( + PlayerbotAI* botAI) : Action(botAI, "hyjal summit erase trackers") {} + bool Execute(Event event) override; +}; + +// Rage Winterchill + +class RageWinterchillMisdirectBossToMainTankAction : public AttackAction +{ +public: + RageWinterchillMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "rage winterchill misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class RageWinterchillMainTankPositionBossAction : public AttackAction +{ +public: + RageWinterchillMainTankPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "rage winterchill main tank position boss") {} + bool Execute(Event event) override; +}; + +class RageWinterchillSpreadRangedInCircleAction : public MovementAction +{ +public: + RageWinterchillSpreadRangedInCircleAction( + PlayerbotAI* botAI) : MovementAction(botAI, "rage winterchill spread ranged in circle") {} + bool Execute(Event event) override; +}; + +class RageWinterchillMeleeGetOutOfDeathAndDecayAction : public AttackAction +{ +public: + RageWinterchillMeleeGetOutOfDeathAndDecayAction( + PlayerbotAI* botAI) : AttackAction(botAI, "rage winterchill melee get out of death and decay") {} + bool Execute(Event event) override; +}; + +// Anetheron + +class AnetheronMisdirectBossAndInfernalsToTanksAction : public AttackAction +{ +public: + AnetheronMisdirectBossAndInfernalsToTanksAction( + PlayerbotAI* botAI) : AttackAction(botAI, "anetheron misdirect boss and infernals to tanks") {} + bool Execute(Event event) override; +}; + +class AnetheronMainTankPositionBossAction : public AttackAction +{ +public: + AnetheronMainTankPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "anetheron main tank position boss") {} + bool Execute(Event event) override; +}; + +class AnetheronSpreadRangedInCircleAction : public MovementAction +{ +public: + AnetheronSpreadRangedInCircleAction( + PlayerbotAI* botAI) : MovementAction(botAI, "anetheron spread ranged in circle") {} + bool Execute(Event event) override; +}; + +class AnetheronBringInfernalToInfernalTankAction : public MovementAction +{ +public: + AnetheronBringInfernalToInfernalTankAction( + PlayerbotAI* botAI) : MovementAction(botAI, "anetheron bring infernal to infernal tank") {} + bool Execute(Event event) override; +}; + +class AnetheronFirstAssistTankPickUpInfernalsAction : public AttackAction +{ +public: + AnetheronFirstAssistTankPickUpInfernalsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "anetheron first assist tank pick up infernals") {} + bool Execute(Event event) override; +}; + +class AnetheronAssignDpsPriorityAction : public AttackAction +{ +public: + AnetheronAssignDpsPriorityAction( + PlayerbotAI* botAI) : AttackAction(botAI, "anetheron assign dps priority") {} + bool Execute(Event event) override; +}; + +// Kaz'rogal + +class KazrogalMisdirectBossToMainTankAction : public AttackAction +{ +public: + KazrogalMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "kaz'rogal misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class KazrogalMainTankPositionBossAction : public AttackAction +{ +public: + KazrogalMainTankPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "kaz'rogal main tank position boss") {} + bool Execute(Event event) override; +}; + +class KazrogalAssistTanksMoveInFrontOfBossAction : public AttackAction +{ +public: + KazrogalAssistTanksMoveInFrontOfBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "kaz'rogal assist tanks move in front of boss") {} + bool Execute(Event event) override; +}; + +class KazrogalSpreadRangedInArcAction : public MovementAction +{ +public: + KazrogalSpreadRangedInArcAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kaz'rogal spread ranged in arc") {} + bool Execute(Event event) override; +}; + +class KazrogalLowManaBotTakeDefensiveMeasuresAction : public MovementAction +{ +public: +KazrogalLowManaBotTakeDefensiveMeasuresAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kaz'rogal low mana bot take defensive measures") {} + bool Execute(Event event) override; +}; + +class KazrogalCastShadowProtectionSpellAction : public Action +{ +public: + KazrogalCastShadowProtectionSpellAction( + PlayerbotAI* botAI) : Action(botAI, "kaz'rogal cast shadow protection spell") {} + bool Execute(Event event) override; +}; + +// Azgalor + +class AzgalorMisdirectBossToMainTankAction : public AttackAction +{ +public: + AzgalorMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "azgalor misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class AzgalorMainTankPositionBossAction : public AttackAction +{ +public: + AzgalorMainTankPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "azgalor main tank position boss") {} + bool Execute(Event event) override; +}; + +class AzgalorWaitAtSafePositionAction : public MovementAction +{ +public: + AzgalorWaitAtSafePositionAction( + PlayerbotAI* botAI) : MovementAction(botAI, "azgalor wait at safe position") {} + bool Execute(Event event) override; +}; + +class AzgalorDisperseRangedAction : public MovementAction +{ +public: + AzgalorDisperseRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "azgalor disperse ranged") {} + bool Execute(Event event) override; +}; + +class AzgalorMeleeGetOutOfFireAndSwapTargetsAction : public AttackAction +{ +public: + AzgalorMeleeGetOutOfFireAndSwapTargetsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "azgalor melee get out of fire and swap targets") {} + bool Execute(Event event) override; +}; + +class AzgalorMoveToDoomguardTankAction : public MovementAction +{ +public: + AzgalorMoveToDoomguardTankAction( + PlayerbotAI* botAI) : MovementAction(botAI, "azgalor move to doomguard tank") {} + bool Execute(Event event) override; +}; + +class AzgalorFirstAssistTankPositionDoomguardAction : public AttackAction +{ +public: + AzgalorFirstAssistTankPositionDoomguardAction( + PlayerbotAI* botAI) : AttackAction(botAI, "azgalor first assist tank position doomguard") {} + bool Execute(Event event) override; +}; + +class AzgalorRangedDpsPrioritizeDoomguardsAction : public AttackAction +{ +public: + AzgalorRangedDpsPrioritizeDoomguardsAction( + PlayerbotAI* botAI) : AttackAction(botAI, "azgalor ranged dps prioritize doomguards") {} + bool Execute(Event event) override; +}; + +// Archimonde + +class ArchimondeMisdirectBossToMainTankAction : public AttackAction +{ +public: + ArchimondeMisdirectBossToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "archimonde misdirect boss to main tank") {} + bool Execute(Event event) override; +}; + +class ArchimondeMoveBossToInitialPositionAction : public AttackAction +{ +public: + ArchimondeMoveBossToInitialPositionAction( + PlayerbotAI* botAI) : AttackAction(botAI, "archimonde move boss to initial position") {} + bool Execute(Event event) override; +}; + +class ArchimondeCastFearImmunitySpellAction : public Action +{ +public: + ArchimondeCastFearImmunitySpellAction( + PlayerbotAI* botAI) : Action(botAI, "archimonde cast fear immunity spell") {} + bool Execute(Event event) override; + +private: + bool CastFearWardOnMainTank(); + bool UseTremorTotemStrategy(); +}; + +class ArchimondeSpreadToAvoidAirBurstAction : public MovementAction +{ +public: + ArchimondeSpreadToAvoidAirBurstAction( + PlayerbotAI* botAI) : MovementAction(botAI, "archimonde spread to avoid air burst") {} + bool Execute(Event event) override; +}; + +class ArchimondeAvoidDoomfireAction : public MovementAction +{ +public: + ArchimondeAvoidDoomfireAction( + PlayerbotAI* botAI) : MovementAction(botAI, "archimonde avoid doomfire") {} + bool Execute(Event event) override; +}; + +class ArchimondeRemoveDoomfireDotAction : public Action +{ +public: + ArchimondeRemoveDoomfireDotAction( + PlayerbotAI* botAI) : Action(botAI, "archimonde remove doomfire dot") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.cpp new file mode 100644 index 000000000..77b305428 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.cpp @@ -0,0 +1,296 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HyjalMultipliers.h" +#include "HyjalActions.h" +#include "HyjalHelpers.h" +#include "AiFactory.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidBearActions.h" +#include "HunterActions.h" +#include "PaladinActions.h" +#include "RaidBossHelpers.h" +#include "ReachTargetActions.h" +#include "ShamanActions.h" +#include "WarriorActions.h" + +using namespace HyjalSummitHelpers; + +// Without this multiplier, Bloodlust/Heroism will not be available for +// bosses because it will be used on cooldown during trash waves +float HyjalSummitTimeBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (archimonde && archimonde->GetHealthPct() < 90.0f) + return 1.0f; + + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (azgalor && azgalor->GetHealthPct() < 90.0f) + return 1.0f; + + Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal"); + if (kazrogal && kazrogal->GetHealthPct() < 90.0f) + return 1.0f; + + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (anetheron && anetheron->GetHealthPct() < 85.0f) + return 1.0f; + + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + if (winterchill && winterchill->GetHealthPct() < 90.0f) + return 1.0f; + + return 0.0f; + } + + return 1.0f; +} + +// Rage Winterchill + +float RageWinterchillDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "rage winterchill")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RageWinterchillMeleeControlAvoidanceMultiplier::GetValue(Action* action) +{ + if (botAI->IsRanged(bot)) + return 1.0f; + + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + if (!winterchill) + return 1.0f; + + if (IsInDeathAndDecay(bot, DEATH_AND_DECAY_SAFE_RADIUS + 2.0f)) + { + if (dynamic_cast(action)) + return 0.0f; + + if (botAI->IsMainTank(bot) || winterchill->GetVictim() == bot) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Anetheron + +float AnetheronDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || !AI_VALUE2(Unit*, "find target", "anetheron")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() != nullptr && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AnetheronDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "anetheron")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AnetheronControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "anetheron")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Kaz'rogal + +float KazrogalLowManaBotStayAwayFromGroupMultiplier::GetValue(Action* action) +{ + if (bot->getClass() == CLASS_WARRIOR || bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_DEATH_KNIGHT || bot->getClass() == CLASS_HUNTER) + return 1.0f; + + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + if (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return 1.0f; + + if (!isBelowManaThreshold.count(bot->GetGUID())) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float KazrogalKeepAspectOfTheViperActiveMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || bot->GetPower(POWER_MANA) > 4000 || + !AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KazrogalControlMovementMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (botAI->IsRanged(bot) && dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Azgalor + +float AzgalorDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot) || !AI_VALUE2(Unit*, "find target", "azgalor")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + { + if (botAI->IsMainTank(bot)) + { + return 0.0f; + } + else if (botAI->IsAssistTank(bot) && (AnyGroupMemberHasDoom(bot) || + AI_VALUE2(Unit*, "find target", "lesser doomguard"))) + { + return 0.0f; + } + } + + return 1.0f; +} + +float AzgalorDoomedBotPrioritizePositioningMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM))) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AzgalorMeleeDpsControlAvoidanceMultiplier::GetValue(Action* action) +{ + if (botAI->IsRanged(bot) || botAI->IsTank(bot)) + return 1.0f; + + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor) + return 1.0f; + + constexpr float singleTickMoveAwayDist = 6.0f; + if (IsInRainOfFire(bot, RAIN_OF_FIRE_RADIUS + singleTickMoveAwayDist)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank || !GET_PLAYERBOT_AI(mainTank)) + return 1.0f; + + TankPositionState tankState = GetAzgalorTankPositionState(botAI, bot); + if ((tankState == TankPositionState::Unknown || + tankState == TankPositionState::MovingToTransition) && + dynamic_cast(action) && + !dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +// Archimonde + +float ArchimondeDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "archimonde")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.h new file mode 100644 index 000000000..a66e262bc --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalMultipliers.h @@ -0,0 +1,126 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALMULTIPLIERS_H +#define PLAYERBOTS_HYJALMULTIPLIERS_H + +#include "Multiplier.h" + +class HyjalSummitTimeBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + HyjalSummitTimeBloodlustAndHeroismMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "hyjal summit time bloodlust and heroism multiplier") {} + virtual float GetValue(Action* action); +}; + +// Rage Winterchill + +class RageWinterchillDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + RageWinterchillDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "rage winterchill disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + +class RageWinterchillMeleeControlAvoidanceMultiplier : public Multiplier +{ +public: + RageWinterchillMeleeControlAvoidanceMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "rage winterchill melee control avoidance multiplier") {} + virtual float GetValue(Action* action); +}; + +// Anetheron + +class AnetheronDisableTankActionsMultiplier : public Multiplier +{ +public: + AnetheronDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "anetheron disable tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class AnetheronDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + AnetheronDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "anetheron disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + +class AnetheronControlMisdirectionMultiplier : public Multiplier +{ +public: + AnetheronControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "anetheron control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +// Kaz'rogal + +class KazrogalLowManaBotStayAwayFromGroupMultiplier : public Multiplier +{ +public: + KazrogalLowManaBotStayAwayFromGroupMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kaz'rogal low mana bot stay away from group multiplier") {} + virtual float GetValue(Action* action); +}; + +class KazrogalKeepAspectOfTheViperActiveMultiplier : public Multiplier +{ +public: + KazrogalKeepAspectOfTheViperActiveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kaz'rogal keep aspect of the viper active multiplier") {} + virtual float GetValue(Action* action); +}; + +class KazrogalControlMovementMultiplier : public Multiplier +{ +public: + KazrogalControlMovementMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kaz'rogal control movement multiplier") {} + virtual float GetValue(Action* action); +}; + +// Azgalor + +class AzgalorDisableTankActionsMultiplier : public Multiplier +{ +public: + AzgalorDisableTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "azgalor disable tank actions multiplier") {} + virtual float GetValue(Action* action); +}; + +class AzgalorDoomedBotPrioritizePositioningMultiplier : public Multiplier +{ +public: + AzgalorDoomedBotPrioritizePositioningMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "azgalor doomed bot prioritize positioning multiplier") {} + virtual float GetValue(Action* action); +}; + +class AzgalorMeleeDpsControlAvoidanceMultiplier : public Multiplier +{ +public: + AzgalorMeleeDpsControlAvoidanceMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "azgalor melee dps control avoidance multiplier") {} + virtual float GetValue(Action* action); +}; + +// Archimonde + +class ArchimondeDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + ArchimondeDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "archimonde disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.cpp new file mode 100644 index 000000000..8db9b8559 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.cpp @@ -0,0 +1,138 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HyjalStrategy.h" +#include "HyjalMultipliers.h" + +void RaidHyjalSummitStrategy::InitTriggers(std::vector& triggers) +{ + // General + triggers.push_back(new TriggerNode("hyjal summit bot is not in combat", { + NextAction("hyjal summit erase trackers", ACTION_EMERGENCY + 11) })); + + // Rage Winterchill + triggers.push_back(new TriggerNode("rage winterchill pulling boss", { + NextAction("rage winterchill misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("rage winterchill boss engaged by main tank", { + NextAction("rage winterchill main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("rage winterchill boss casts death and decay on ranged", { + NextAction("rage winterchill spread ranged in circle", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("rage winterchill melee is standing in death and decay", { + NextAction("rage winterchill melee get out of death and decay", ACTION_EMERGENCY + 1) })); + + // Anetheron + triggers.push_back(new TriggerNode("anetheron pulling boss or infernal", { + NextAction("anetheron misdirect boss and infernals to tanks", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("anetheron boss engaged by main tank", { + NextAction("anetheron main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("anetheron boss casts carrion swarm", { + NextAction("anetheron spread ranged in circle", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("anetheron bot is targeted by infernal", { + NextAction("anetheron bring infernal to infernal tank", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("anetheron infernals need to be kept away from raid", { + NextAction("anetheron first assist tank pick up infernals", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("anetheron infernals continue to spawn", { + NextAction("anetheron assign dps priority", ACTION_RAID + 1) })); + + // Kaz'rogal + triggers.push_back(new TriggerNode("kaz'rogal pulling boss", { + NextAction("kaz'rogal misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kaz'rogal boss engaged by main tank", { + NextAction("kaz'rogal main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kaz'rogal boss engaged by assist tanks", { + NextAction("kaz'rogal assist tanks move in front of boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kaz'rogal low mana bots need escape path", { + NextAction("kaz'rogal spread ranged in arc", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kaz'rogal bot is low on mana", { + NextAction("kaz'rogal low mana bot take defensive measures", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kaz'rogal mark deals shadow damage", { + NextAction("kaz'rogal cast shadow protection spell", ACTION_EMERGENCY + 6) })); + + // Azgalor + triggers.push_back(new TriggerNode("azgalor pulling boss", { + NextAction("azgalor misdirect boss to main tank", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("azgalor boss engaged by main tank", { + NextAction("azgalor main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("azgalor main tank is positioning boss", { + NextAction("azgalor wait at safe position", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("azgalor boss engaged by ranged", { + NextAction("azgalor disperse ranged", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("azgalor boss casts rain of fire on melee", { + NextAction("azgalor melee get out of fire and swap targets", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("azgalor bot is doomed", { + NextAction("azgalor move to doomguard tank", ACTION_EMERGENCY + 3) })); + + triggers.push_back(new TriggerNode("azgalor doomguards must be controlled", { + NextAction("azgalor first assist tank position doomguard", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("azgalor doomguards must die", { + NextAction("azgalor ranged dps prioritize doomguards", ACTION_RAID + 1) })); + + // Archimonde + triggers.push_back(new TriggerNode("archimonde pulling boss", { + NextAction("archimonde misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("archimonde boss engaged by main tank", { + NextAction("archimonde move boss to initial position", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("archimonde boss casts fear", { + NextAction("archimonde cast fear immunity spell", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("archimonde boss casts air burst", { + NextAction("archimonde spread to avoid air burst", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("archimonde boss summoned doomfire", { + NextAction("archimonde avoid doomfire", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("archimonde bot stood in doomfire", { + NextAction("archimonde remove doomfire dot", ACTION_EMERGENCY + 7) })); +} + +void RaidHyjalSummitStrategy::InitMultipliers(std::vector& multipliers) +{ + // Trash + multipliers.push_back(new HyjalSummitTimeBloodlustAndHeroismMultiplier(botAI)); + + // Rage Winterchill + multipliers.push_back(new RageWinterchillDisableCombatFormationMoveMultiplier(botAI)); + multipliers.push_back(new RageWinterchillMeleeControlAvoidanceMultiplier(botAI)); + + // Anetheron + multipliers.push_back(new AnetheronDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new AnetheronDisableCombatFormationMoveMultiplier(botAI)); + multipliers.push_back(new AnetheronControlMisdirectionMultiplier(botAI)); + + // Kaz'rogal + multipliers.push_back(new KazrogalLowManaBotStayAwayFromGroupMultiplier(botAI)); + multipliers.push_back(new KazrogalKeepAspectOfTheViperActiveMultiplier(botAI)); + multipliers.push_back(new KazrogalControlMovementMultiplier(botAI)); + + // Azgalor + multipliers.push_back(new AzgalorDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new AzgalorDoomedBotPrioritizePositioningMultiplier(botAI)); + multipliers.push_back(new AzgalorMeleeDpsControlAvoidanceMultiplier(botAI)); + + // Archimonde + multipliers.push_back(new ArchimondeDisableCombatFormationMoveMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.h new file mode 100644 index 000000000..910c0df61 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalStrategy.h @@ -0,0 +1,23 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALSTRATEGY_H +#define PLAYERBOTS_HYJALSTRATEGY_H + +#include "Strategy.h" + +class RaidHyjalSummitStrategy : public Strategy +{ +public: + RaidHyjalSummitStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "hyjal"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggerContext.h new file mode 100644 index 000000000..6e1549ba0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggerContext.h @@ -0,0 +1,225 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALTRIGGERCONTEXT_H +#define PLAYERBOTS_HYJALTRIGGERCONTEXT_H + +#include "HyjalTriggers.h" +#include "NamedObjectContext.h" + +class RaidHyjalSummitTriggerContext : public NamedObjectContext +{ +public: + RaidHyjalSummitTriggerContext() + { + // General + creators["hyjal summit bot is not in combat"] = + &RaidHyjalSummitTriggerContext::hyjal_summit_bot_is_not_in_combat; + + // Rage Winterchill + creators["rage winterchill pulling boss"] = + &RaidHyjalSummitTriggerContext::rage_winterchill_pulling_boss; + + creators["rage winterchill boss engaged by main tank"] = + &RaidHyjalSummitTriggerContext::rage_winterchill_boss_engaged_by_main_tank; + + creators["rage winterchill boss casts death and decay on ranged"] = + &RaidHyjalSummitTriggerContext::rage_winterchill_boss_casts_death_and_decay_on_ranged; + + creators["rage winterchill melee is standing in death and decay"] = + &RaidHyjalSummitTriggerContext::rage_winterchill_melee_is_standing_in_death_and_decay; + + // Anetheron + creators["anetheron pulling boss or infernal"] = + &RaidHyjalSummitTriggerContext::anetheron_pulling_boss_or_infernal; + + creators["anetheron boss engaged by main tank"] = + &RaidHyjalSummitTriggerContext::anetheron_boss_engaged_by_main_tank; + + creators["anetheron boss casts carrion swarm"] = + &RaidHyjalSummitTriggerContext::anetheron_boss_casts_carrion_swarm; + + creators["anetheron bot is targeted by infernal"] = + &RaidHyjalSummitTriggerContext::anetheron_bot_is_targeted_by_infernal; + + creators["anetheron infernals need to be kept away from raid"] = + &RaidHyjalSummitTriggerContext::anetheron_infernals_need_to_be_kept_away_from_raid; + + creators["anetheron infernals continue to spawn"] = + &RaidHyjalSummitTriggerContext::anetheron_infernals_continue_to_spawn; + + // Kaz'rogal + creators["kaz'rogal pulling boss"] = + &RaidHyjalSummitTriggerContext::kazrogal_pulling_boss; + + creators["kaz'rogal boss engaged by main tank"] = + &RaidHyjalSummitTriggerContext::kazrogal_boss_engaged_by_main_tank; + + creators["kaz'rogal boss engaged by assist tanks"] = + &RaidHyjalSummitTriggerContext::kazrogal_boss_engaged_by_assist_tanks; + + creators["kaz'rogal bot is low on mana"] = + &RaidHyjalSummitTriggerContext::kazrogal_bot_is_low_on_mana; + + creators["kaz'rogal low mana bots need escape path"] = + &RaidHyjalSummitTriggerContext::kazrogal_low_mana_bots_need_escape_path; + + creators["kaz'rogal mark deals shadow damage"] = + &RaidHyjalSummitTriggerContext::kazrogal_mark_deals_shadow_damage; + + // Azgalor + creators["azgalor pulling boss"] = + &RaidHyjalSummitTriggerContext::azgalor_pulling_boss; + + creators["azgalor boss engaged by main tank"] = + &RaidHyjalSummitTriggerContext::azgalor_boss_engaged_by_main_tank; + + creators["azgalor main tank is positioning boss"] = + &RaidHyjalSummitTriggerContext::azgalor_main_tank_is_positioning_boss; + + creators["azgalor boss engaged by ranged"] = + &RaidHyjalSummitTriggerContext::azgalor_boss_engaged_by_ranged; + + creators["azgalor boss casts rain of fire on melee"] = + &RaidHyjalSummitTriggerContext::azgalor_boss_casts_rain_of_fire_on_melee; + + creators["azgalor bot is doomed"] = + &RaidHyjalSummitTriggerContext::azgalor_bot_is_doomed; + + creators["azgalor doomguards must be controlled"] = + &RaidHyjalSummitTriggerContext::azgalor_doomguards_must_be_controlled; + + creators["azgalor doomguards must die"] = + &RaidHyjalSummitTriggerContext::azgalor_doomguards_must_die; + + // Archimonde + creators["archimonde pulling boss"] = + &RaidHyjalSummitTriggerContext::archimonde_pulling_boss; + + creators["archimonde boss engaged by main tank"] = + &RaidHyjalSummitTriggerContext::archimonde_boss_engaged_by_main_tank; + + creators["archimonde boss casts fear"] = + &RaidHyjalSummitTriggerContext::archimonde_boss_casts_fear; + + creators["archimonde boss casts air burst"] = + &RaidHyjalSummitTriggerContext::archimonde_boss_casts_air_burst; + + creators["archimonde boss summoned doomfire"] = + &RaidHyjalSummitTriggerContext::archimonde_boss_summoned_doomfire; + + creators["archimonde bot stood in doomfire"] = + &RaidHyjalSummitTriggerContext::archimonde_bot_stood_in_doomfire; + } + +private: + // General + static Trigger* hyjal_summit_bot_is_not_in_combat(PlayerbotAI* botAI) { + return new HyjalSummitBotIsNotInCombatTrigger(botAI); + } + + // Rage Winterchill + static Trigger* rage_winterchill_pulling_boss(PlayerbotAI* botAI) { + return new RageWinterchillPullingBossTrigger(botAI); + } + static Trigger* rage_winterchill_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new RageWinterchillBossEngagedByMainTankTrigger(botAI); + } + static Trigger* rage_winterchill_boss_casts_death_and_decay_on_ranged(PlayerbotAI* botAI) { + return new RageWinterchillBossCastsDeathAndDecayOnRangedTrigger(botAI); + } + static Trigger* rage_winterchill_melee_is_standing_in_death_and_decay(PlayerbotAI* botAI) { + return new RageWinterchillMeleeIsStandingInDeathAndDecayTrigger(botAI); + } + + // Anetheron + static Trigger* anetheron_pulling_boss_or_infernal(PlayerbotAI* botAI) { + return new AnetheronPullingBossOrInfernalTrigger(botAI); + } + static Trigger* anetheron_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new AnetheronBossEngagedByMainTankTrigger(botAI); + } + static Trigger* anetheron_boss_casts_carrion_swarm(PlayerbotAI* botAI) { + return new AnetheronBossCastsCarrionSwarmTrigger(botAI); + } + static Trigger* anetheron_bot_is_targeted_by_infernal(PlayerbotAI* botAI) { + return new AnetheronBotIsTargetedByInfernalTrigger(botAI); + } + static Trigger* anetheron_infernals_need_to_be_kept_away_from_raid(PlayerbotAI* botAI) { + return new AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger(botAI); + } + static Trigger* anetheron_infernals_continue_to_spawn(PlayerbotAI* botAI) { + return new AnetheronInfernalsContinueToSpawnTrigger(botAI); + } + + // Kaz'rogal + static Trigger* kazrogal_pulling_boss(PlayerbotAI* botAI) { + return new KazrogalPullingBossTrigger(botAI); + } + static Trigger* kazrogal_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new KazrogalBossEngagedByMainTankTrigger(botAI); + } + static Trigger* kazrogal_boss_engaged_by_assist_tanks(PlayerbotAI* botAI) { + return new KazrogalBossEngagedByAssistTanksTrigger(botAI); + } + static Trigger* kazrogal_low_mana_bots_need_escape_path(PlayerbotAI* botAI) { + return new KazrogalLowManaBotsNeedEscapePathTrigger(botAI); + } + static Trigger* kazrogal_bot_is_low_on_mana(PlayerbotAI* botAI) { + return new KazrogalBotIsLowOnManaTrigger(botAI); + } + static Trigger* kazrogal_mark_deals_shadow_damage(PlayerbotAI* botAI) { + return new KazrogalMarkDealsShadowDamageTrigger(botAI); + } + + // Azgalor + static Trigger* azgalor_pulling_boss(PlayerbotAI* botAI) { + return new AzgalorPullingBossTrigger(botAI); + } + static Trigger* azgalor_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new AzgalorBossEngagedByMainTankTrigger(botAI); + } + static Trigger* azgalor_main_tank_is_positioning_boss(PlayerbotAI* botAI) { + return new AzgalorMainTankIsPositioningBossTrigger(botAI); + } + static Trigger* azgalor_boss_engaged_by_ranged(PlayerbotAI* botAI) { + return new AzgalorBossEngagedByRangedTrigger(botAI); + } + static Trigger* azgalor_boss_casts_rain_of_fire_on_melee(PlayerbotAI* botAI) { + return new AzgalorBossCastsRainOfFireOnMeleeTrigger(botAI); + } + static Trigger* azgalor_bot_is_doomed(PlayerbotAI* botAI) { + return new AzgalorBotIsDoomedTrigger(botAI); + } + static Trigger* azgalor_doomguards_must_be_controlled(PlayerbotAI* botAI) { + return new AzgalorDoomguardsMustBeControlledTrigger(botAI); + } + static Trigger* azgalor_doomguards_must_die(PlayerbotAI* botAI) { + return new AzgalorDoomguardsMustDieTrigger(botAI); + } + + // Archimonde + static Trigger* archimonde_pulling_boss(PlayerbotAI* botAI) { + return new ArchimondePullingBossTrigger(botAI); + } + static Trigger* archimonde_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new ArchimondeBossEngagedByMainTankTrigger(botAI); + } + static Trigger* archimonde_boss_casts_fear(PlayerbotAI* botAI) { + return new ArchimondeBossCastsFearTrigger(botAI); + } + static Trigger* archimonde_boss_casts_air_burst(PlayerbotAI* botAI) { + return new ArchimondeBossCastsAirBurstTrigger(botAI); + } + static Trigger* archimonde_boss_summoned_doomfire(PlayerbotAI* botAI) { + return new ArchimondeBossSummonedDoomfireTrigger(botAI); + } + static Trigger* archimonde_bot_stood_in_doomfire(PlayerbotAI* botAI) { + return new ArchimondeBotStoodInDoomfireTrigger(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.cpp new file mode 100644 index 000000000..f431a7c1e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.cpp @@ -0,0 +1,359 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HyjalTriggers.h" +#include "HyjalHelpers.h" +#include "HyjalActions.h" +#include "AiFactory.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace HyjalSummitHelpers; + +// General + +bool HyjalSummitBotIsNotInCombatTrigger::IsActive() +{ + return bot->GetMapId() == HYJAL_SUMMIT_MAP_ID && + !AI_VALUE2(bool, "combat", "self target"); +} + +// Rage Winterchill + +bool RageWinterchillPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + return winterchill && winterchill->GetHealthPct() > 95.0f; +} + +bool RageWinterchillBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "rage winterchill"); +} + +bool RageWinterchillBossCastsDeathAndDecayOnRangedTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "rage winterchill"); +} + +bool RageWinterchillMeleeIsStandingInDeathAndDecayTrigger::IsActive() +{ + if (botAI->IsRanged(bot)) + return false; + + Unit* winterchill = AI_VALUE2(Unit*, "find target", "rage winterchill"); + if (!winterchill || winterchill->GetVictim() == bot) + return false; + + if (botAI->IsMainTank(bot)) + return false; + + return IsInDeathAndDecay(bot, DEATH_AND_DECAY_SAFE_RADIUS); +} + +// Anetheron + +bool AnetheronPullingBossOrInfernalTrigger::IsActive() +{ + return bot->getClass() == CLASS_HUNTER && + AI_VALUE2(Unit*, "find target", "anetheron"); +} + +bool AnetheronBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "anetheron"); +} + +bool AnetheronBossCastsCarrionSwarmTrigger::IsActive() +{ + if (botAI->IsMelee(bot)) + return false; + + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (!anetheron) + return false; + + return GetInfernoTarget(anetheron) != bot; +} + +bool AnetheronBotIsTargetedByInfernalTrigger::IsActive() +{ + Unit* anetheron = AI_VALUE2(Unit*, "find target", "anetheron"); + if (!anetheron || botAI->IsMainTank(bot)) + return false; + + return GetInfernoTarget(anetheron) == bot; +} + +bool AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "towering infernal"); +} + +bool AnetheronInfernalsContinueToSpawnTrigger::IsActive() +{ + return !botAI->IsTank(bot) && AI_VALUE2(Unit*, "find target", "anetheron"); +} + +// Kaz'rogal + +bool KazrogalPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* kazrogal = AI_VALUE2(Unit*, "find target", "kaz'rogal"); + return kazrogal && kazrogal->GetHealthPct() > 95.0f; +} + +bool KazrogalBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "kaz'rogal"); +} + +bool KazrogalBossEngagedByAssistTanksTrigger::IsActive() +{ + if (!botAI->IsAssistTank(bot)) + return false; + + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return false; + + return bot->GetPower(POWER_MANA) > 3000; +} + +bool KazrogalLowManaBotsNeedEscapePathTrigger::IsActive() +{ + if (bot->getClass() == CLASS_WARRIOR || bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_DEATH_KNIGHT) + return false; + + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + if (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL) + return false; + + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return false; + + if (bot->getClass() == CLASS_HUNTER) + { + return true; + } + else if (bot->GetPower(POWER_MANA) > 4000) + { + isBelowManaThreshold.erase(bot->GetGUID()); + if (botAI->IsMelee(bot)) + return false; + else + return true; + } + + return false; +} + +bool KazrogalBotIsLowOnManaTrigger::IsActive() +{ + if (bot->getClass() == CLASS_WARRIOR || bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_DEATH_KNIGHT) + return false; + + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + if (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL) + return false; + + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return false; + + if (botAI->HasAnyAuraOf(bot, "ice block", "divine shield", nullptr)) + return false; + + if (isBelowManaThreshold.count(bot->GetGUID()) || + bot->GetPower(POWER_MANA) <= 3200) + return true; + + return false; +} + +bool KazrogalMarkDealsShadowDamageTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PALADIN && bot->getClass() != CLASS_WARLOCK) + return false; + + if (!AI_VALUE2(Unit*, "find target", "kaz'rogal")) + return false; + + if (bot->getClass() == CLASS_PALADIN && + (botAI->HasAura("shadow resistance aura", bot) || + botAI->HasAura("prayer of shadow protection", bot) || + botAI->HasAura("shadow protection", bot))) + return false; + + return bot->HasAura( + static_cast(HyjalSummitSpells::SPELL_MARK_OF_KAZROGAL)); +} + +// Azgalor + +bool AzgalorPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + return azgalor && azgalor->GetHealthPct() > 95.0f; +} + +bool AzgalorBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "azgalor"); +} + +bool AzgalorMainTankIsPositioningBossTrigger::IsActive() +{ + if (botAI->IsRanged(bot)) + return false; + + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor || azgalor->GetVictim() == bot) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank || !GET_PLAYERBOT_AI(mainTank) || botAI->IsMainTank(bot)) + return false; + + TankPositionState tankState = GetAzgalorTankPositionState(botAI, bot); + return tankState == TankPositionState::Unknown || + tankState == TankPositionState::MovingToTransition; +} + +bool AzgalorBossEngagedByRangedTrigger::IsActive() +{ + if (botAI->IsMelee(bot)) + return false; + + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + return azgalor && azgalor->GetVictim() != bot && + !bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM)); +} + +bool AzgalorBossCastsRainOfFireOnMeleeTrigger::IsActive() +{ + if (botAI->IsRanged(bot) || botAI->IsTank(bot)) + return false; + + Unit* azgalor = AI_VALUE2(Unit*, "find target", "azgalor"); + if (!azgalor || azgalor->GetVictim() == bot || + bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM))) + return false; + + return IsInRainOfFire(bot, RAIN_OF_FIRE_RADIUS); +} + +bool AzgalorBotIsDoomedTrigger::IsActive() +{ + return bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM)); +} + +bool AzgalorDoomguardsMustBeControlledTrigger::IsActive() +{ + if (!botAI->IsAssistTank(bot) || + !AI_VALUE2(Unit*, "find target", "azgalor")) + return false; + + if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + return AI_VALUE2(Unit*, "find target", "lesser doomguard") || + AnyGroupMemberHasDoom(bot); + } + + if (botAI->IsAssistTankOfIndex(bot, 1, true)) + { + // Trigger for second assist tank only if first assist tank has Doom + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + if (firstAssistTank && + !firstAssistTank->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM))) + return false; + + return AI_VALUE2(Unit*, "find target", "lesser doomguard") || + AnyGroupMemberHasDoom(bot); + } + + return false; +} + +bool AzgalorDoomguardsMustDieTrigger::IsActive() +{ + return botAI->IsRangedDps(bot) && AI_VALUE2(Unit*, "find target", "azgalor"); +} + +// Archimonde + +bool ArchimondePullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + return archimonde && archimonde->GetHealthPct() > 95.0f; +} + +bool ArchimondeBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + return archimonde && archimonde->GetHealthPct() > 95.0f; +} + +bool ArchimondeBossCastsFearTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST && + bot->getClass() != CLASS_SHAMAN) + return false; + + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + return archimonde && archimonde->GetHealthPct() > 10.0f; +} + +bool ArchimondeBossCastsAirBurstTrigger::IsActive() +{ + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (!archimonde || archimonde->GetHealthPct() <= 10.0f || + archimonde->GetVictim() == bot) + return false; + + return !botAI->IsMainTank(bot); +} + +bool ArchimondeBossSummonedDoomfireTrigger::IsActive() +{ + Unit* archimonde = AI_VALUE2(Unit*, "find target", "archimonde"); + if (!archimonde || archimonde->GetHealthPct() <= 10.0f) + return false; + + // If I don't make an exception, bots actually refuse to enter the + // Doomfire even when feared + return !bot->HasAura( + static_cast(HyjalSummitSpells::SPELL_ARCHIMONDE_FEAR)); +} + +bool ArchimondeBotStoodInDoomfireTrigger::IsActive() +{ + if (bot->getClass() != CLASS_MAGE && bot->getClass() != CLASS_ROGUE && + bot->getClass() != CLASS_PALADIN) + return false; + + return bot->GetHealthPct() < 40.0f && + (bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOMFIRE)) || + bot->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOMFIRE_DOT))); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.h new file mode 100644 index 000000000..5c5ec589c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/HyjalTriggers.h @@ -0,0 +1,272 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALTRIGGERS_H +#define PLAYERBOTS_HYJALTRIGGERS_H + +#include "Trigger.h" + +// General + +class HyjalSummitBotIsNotInCombatTrigger : public Trigger +{ +public: + HyjalSummitBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hyjal summit bot is not in combat") {} + bool IsActive() override; +}; + +// Rage Winterchill + +class RageWinterchillPullingBossTrigger : public Trigger +{ +public: + RageWinterchillPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill pulling boss") {} + bool IsActive() override; +}; + +class RageWinterchillBossEngagedByMainTankTrigger : public Trigger +{ +public: + RageWinterchillBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill boss engaged by main tank") {} + bool IsActive() override; +}; + +class RageWinterchillBossCastsDeathAndDecayOnRangedTrigger : public Trigger +{ +public: + RageWinterchillBossCastsDeathAndDecayOnRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill boss casts death and decay on ranged") {} + bool IsActive() override; +}; + +class RageWinterchillMeleeIsStandingInDeathAndDecayTrigger : public Trigger +{ +public: + RageWinterchillMeleeIsStandingInDeathAndDecayTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "rage winterchill melee is standing in death and decay") {} + bool IsActive() override; +}; + +// Anetheron + +class AnetheronPullingBossOrInfernalTrigger : public Trigger +{ +public: + AnetheronPullingBossOrInfernalTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "anetheron pulling boss or infernal") {} + bool IsActive() override; +}; + +class AnetheronBossEngagedByMainTankTrigger : public Trigger +{ +public: + AnetheronBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "anetheron boss engaged by main tank") {} + bool IsActive() override; +}; + +class AnetheronBossCastsCarrionSwarmTrigger : public Trigger +{ +public: + AnetheronBossCastsCarrionSwarmTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "anetheron boss casts carrion swarm") {} + bool IsActive() override; +}; + +class AnetheronBotIsTargetedByInfernalTrigger : public Trigger +{ +public: + AnetheronBotIsTargetedByInfernalTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "anetheron bot is targeted by infernal") {} + bool IsActive() override; +}; + +class AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger : public Trigger +{ +public: + AnetheronInfernalsNeedToBeKeptAwayFromRaidTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "anetheron infernals need to be kept away from raid") {} + bool IsActive() override; +}; + +class AnetheronInfernalsContinueToSpawnTrigger : public Trigger +{ +public: + AnetheronInfernalsContinueToSpawnTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "anetheron infernals continue to spawn") {} + bool IsActive() override; +}; + +// Kaz'rogal + +class KazrogalPullingBossTrigger : public Trigger +{ +public: + KazrogalPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal pulling boss") {} + bool IsActive() override; +}; + +class KazrogalBossEngagedByMainTankTrigger : public Trigger +{ +public: + KazrogalBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal boss engaged by main tank") {} + bool IsActive() override; +}; + +class KazrogalBossEngagedByAssistTanksTrigger : public Trigger +{ +public: + KazrogalBossEngagedByAssistTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal boss engaged by assist tanks") {} + bool IsActive() override; +}; + +class KazrogalLowManaBotsNeedEscapePathTrigger : public Trigger +{ +public: + KazrogalLowManaBotsNeedEscapePathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal low mana bots need escape path") {} + bool IsActive() override; +}; + +class KazrogalBotIsLowOnManaTrigger : public Trigger +{ +public: + KazrogalBotIsLowOnManaTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal bot is low on mana") {} + bool IsActive() override; +}; + +class KazrogalMarkDealsShadowDamageTrigger : public Trigger +{ +public: + KazrogalMarkDealsShadowDamageTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kaz'rogal mark deals shadow damage") {} + bool IsActive() override; +}; + +// Azgalor + +class AzgalorPullingBossTrigger : public Trigger +{ +public: + AzgalorPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor pulling boss") {} + bool IsActive() override; +}; + +class AzgalorBossEngagedByMainTankTrigger : public Trigger +{ +public: + AzgalorBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor boss engaged by main tank") {} + bool IsActive() override; +}; + +class AzgalorMainTankIsPositioningBossTrigger : public Trigger +{ +public: + AzgalorMainTankIsPositioningBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor main tank is positioning boss") {} + bool IsActive() override; +}; + +class AzgalorBossEngagedByRangedTrigger : public Trigger +{ +public: + AzgalorBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor boss engaged by ranged") {} + bool IsActive() override; +}; + +class AzgalorBossCastsRainOfFireOnMeleeTrigger : public Trigger +{ +public: + AzgalorBossCastsRainOfFireOnMeleeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor boss casts rain of fire on melee") {} + bool IsActive() override; +}; + +class AzgalorBotIsDoomedTrigger : public Trigger +{ +public: + AzgalorBotIsDoomedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor bot is doomed") {} + bool IsActive() override; +}; + +class AzgalorDoomguardsMustBeControlledTrigger : public Trigger +{ +public: + AzgalorDoomguardsMustBeControlledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor doomguards must be controlled") {} + bool IsActive() override; +}; + +class AzgalorDoomguardsMustDieTrigger : public Trigger +{ +public: + AzgalorDoomguardsMustDieTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "azgalor doomguards must die") {} + bool IsActive() override; +}; + +// Archimonde + +class ArchimondePullingBossTrigger : public Trigger +{ +public: + ArchimondePullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "archimonde pulling boss") {} + bool IsActive() override; +}; + +class ArchimondeBossEngagedByMainTankTrigger : public Trigger +{ +public: + ArchimondeBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss engaged by main tank") {} + bool IsActive() override; +}; + +class ArchimondeBossCastsFearTrigger : public Trigger +{ +public: + ArchimondeBossCastsFearTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss casts fear") {} + bool IsActive() override; +}; + +class ArchimondeBossCastsAirBurstTrigger : public Trigger +{ +public: + ArchimondeBossCastsAirBurstTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss casts air burst") {} + bool IsActive() override; +}; + +class ArchimondeBossSummonedDoomfireTrigger : public Trigger +{ +public: + ArchimondeBossSummonedDoomfireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "archimonde boss summoned doomfire") {} + bool IsActive() override; +}; + +class ArchimondeBotStoodInDoomfireTrigger : public Trigger +{ +public: + ArchimondeBotStoodInDoomfireTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "archimonde bot stood in doomfire") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.cpp new file mode 100644 index 000000000..b823f16d9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.cpp @@ -0,0 +1,269 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HyjalHelpers.h" + +#include + +#include "Playerbots.h" +#include "RaidBossHelpers.h" +#include "Timer.h" + +namespace HyjalSummitHelpers +{ + // General + + bool GetGroundedStepPosition( + Player* bot, float destinationX, float destinationY, float moveDist, + float& stepX, float& stepY, float& stepZ) + { + const float distance = bot->GetExactDist2d(destinationX, destinationY); + if (distance <= 0.0f) + return false; + + const float stepDistance = std::min(moveDist, distance); + const float deltaX = destinationX - bot->GetPositionX(); + const float deltaY = destinationY - bot->GetPositionY(); + stepX = bot->GetPositionX() + (deltaX / distance) * stepDistance; + stepY = bot->GetPositionY() + (deltaY / distance) * stepDistance; + stepZ = bot->GetMapWaterOrGroundLevel(stepX, stepY, bot->GetPositionZ()); + if (stepZ <= INVALID_HEIGHT) + stepZ = bot->GetPositionZ(); + + bot->GetMap()->CheckCollisionAndGetValidCoords( + bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + stepX, stepY, stepZ, false); + + return true; + } + + RangedGroups GetRangedGroups(PlayerbotAI* botAI, Player* bot) + { + RangedGroups result; + Group* group = bot->GetGroup(); + if (!group) + return result; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + if (botAI->IsHeal(member)) + result.healers.push_back(member); + else + result.rangedDps.push_back(member); + } + + return result; + } + + std::pair GetBotCircleIndexAndCount(PlayerbotAI* botAI, Player* bot, + const RangedGroups& groups) + { + const std::vector& vec = botAI->IsHeal(bot) ? groups.healers : groups.rangedDps; + auto it = std::find(vec.begin(), vec.end(), bot); + size_t index = (it != vec.end()) ? std::distance(vec.begin(), it) : 0; + + return {index, vec.size()}; + } + + // Rage Winterchill + + const Position WINTERCHILL_TANK_POSITION = { 5031.061f, -1784.521f, 1321.626f }; + std::unordered_map hasReachedWinterchillPosition; + std::unordered_map deathAndDecayPosition; + + DeathAndDecayData* GetActiveWinterchillDeathAndDecay(uint32 instanceId) + { + auto instanceIt = deathAndDecayPosition.find(instanceId); + if (instanceIt == deathAndDecayPosition.end()) + return nullptr; + + const uint32 now = getMSTime(); + const uint32 elapsed = getMSTimeDiff(instanceIt->second.spawnTime, now); + if (elapsed >= DEATH_AND_DECAY_REACQUIRE_DELAY) + { + deathAndDecayPosition.erase(instanceIt); + return nullptr; + } + + if (elapsed >= DEATH_AND_DECAY_DURATION) + return nullptr; + + return &instanceIt->second; + } + + bool IsInDeathAndDecay(Player* bot, float radius) + { + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + Aura* aura = bot->GetAura(static_cast(HyjalSummitSpells::SPELL_DEATH_AND_DECAY)); + if (aura) + { + DynamicObject* dynObj = aura->GetDynobjOwner(); + if (dynObj && dynObj->IsInWorld()) + { + const uint32 now = getMSTime(); + auto instanceIt = deathAndDecayPosition.find(instanceId); + if (instanceIt == deathAndDecayPosition.end() || + getMSTimeDiff(instanceIt->second.spawnTime, now) >= DEATH_AND_DECAY_REACQUIRE_DELAY) + { + deathAndDecayPosition[instanceId] = + DeathAndDecayData{ dynObj->GetPosition(), now }; + } + } + } + + DeathAndDecayData* data = GetActiveWinterchillDeathAndDecay(instanceId); + if (!data) + return false; + + return bot->GetExactDist2d(data->position) < radius; + } + + // Anetheron + + const Position ANETHERON_TANK_POSITION = { 5033.177f, -1765.996f, 1324.195f }; + const Position ANETHERON_E_INFERNAL_POSITION = { 5016.578f, -1800.233f, 1323.070f }; + const Position ANETHERON_W_INFERNAL_POSITION = { 5048.911f, -1722.164f, 1321.408f }; + std::unordered_map hasReachedAnetheronPosition; + + Player* GetInfernoTarget(Unit* anetheron) + { + if (!anetheron) + return nullptr; + + Spell* spell = anetheron->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (spell && spell->m_spellInfo->Id == + static_cast(HyjalSummitSpells::SPELL_INFERNO)) + { + Unit* spellTarget = spell->m_targets.GetUnitTarget(); + if (spellTarget && spellTarget->IsPlayer()) + return spellTarget->ToPlayer(); + } + + return nullptr; + } + + const Position& GetClosestInfernalTankPosition(Player* bot) + { + const Position& east = ANETHERON_E_INFERNAL_POSITION; + const Position& west = ANETHERON_W_INFERNAL_POSITION; + return (bot->GetExactDist2d(east.GetPositionX(), east.GetPositionY()) <= + bot->GetExactDist2d(west.GetPositionX(), west.GetPositionY())) ? east : west; + } + + // Kaz'rogal + + const Position KAZROGAL_TANK_TRANSITION_POSITION = { 5528.792f, -2636.486f, 1481.293f }; + const Position KAZROGAL_TANK_FINAL_POSITION = { 5511.514f, -2662.466f, 1480.288f }; + std::unordered_map kazrogalTankStep; + std::unordered_map isBelowManaThreshold; + + TankPositionState GetKazrogalTankPositionState(PlayerbotAI* botAI, Player* bot) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return TankPositionState::Unknown; + + auto it = kazrogalTankStep.find(mainTank->GetGUID()); + if (it != kazrogalTankStep.end()) + return it->second; + + return TankPositionState::Unknown; + } + + // Azgalor + + const Position AZGALOR_TANK_TRANSITION_POSITION = { 5486.787f, -2696.215f, 1482.007f }; + const Position AZGALOR_TANK_FINAL_POSITION = { 5496.379f, -2675.265f, 1481.053f }; + const Position AZGALOR_DOOMGUARD_POSITION = { 5485.555f, -2731.659f, 1485.555f }; + std::unordered_map azgalorTankStep; + std::unordered_map rainOfFirePosition; + + RainOfFireData* GetActiveAzgalorRainOfFire(uint32 instanceId) + { + auto instanceIt = rainOfFirePosition.find(instanceId); + if (instanceIt == rainOfFirePosition.end()) + return nullptr; + + const uint32 now = getMSTime(); + const uint32 elapsed = getMSTimeDiff(instanceIt->second.spawnTime, now); + if (elapsed >= RAIN_OF_FIRE_REACQUIRE_DELAY) + { + rainOfFirePosition.erase(instanceIt); + return nullptr; + } + + if (elapsed >= RAIN_OF_FIRE_DURATION) + return nullptr; + + return &instanceIt->second; + } + + TankPositionState GetAzgalorTankPositionState(PlayerbotAI* botAI, Player* bot) + { + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return TankPositionState::Unknown; + + auto it = azgalorTankStep.find(mainTank->GetGUID()); + if (it != azgalorTankStep.end()) + return it->second; + + return TankPositionState::Unknown; + } + + bool IsInRainOfFire(Player* bot, float radius) + { + RainOfFireData* data = GetActiveAzgalorRainOfFire(bot->GetMap()->GetInstanceId()); + if (!data) + return false; + + return bot->GetExactDist2d(data->position) < radius; + } + + bool AnyGroupMemberHasDoom(Player* bot) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && + member->HasAura(static_cast(HyjalSummitSpells::SPELL_DOOM))) + return true; + } + } + + return false; + } + + // Archimonde + + const Position ARCHIMONDE_INITIAL_POSITION = { 5640.502f, -3421.238f, 1587.453f }; + std::unordered_map archimondeAirBurstTargets; + std::unordered_map> doomfireTrails; + std::unordered_map doomfireLastSampleTime; + + AirBurstData* GetRecentArchimondeAirBurst(uint32 instanceId) + { + auto instanceIt = archimondeAirBurstTargets.find(instanceId); + if (instanceIt == archimondeAirBurstTargets.end()) + return nullptr; + + constexpr uint32 airBurstReactionWindow = 2000; + const uint32 now = getMSTime(); + if (getMSTimeDiff(instanceIt->second.castTime, now) >= airBurstReactionWindow) + { + archimondeAirBurstTargets.erase(instanceIt); + return nullptr; + } + + return &instanceIt->second; + } +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.h b/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.h new file mode 100644 index 000000000..79d86f0d9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalHelpers.h @@ -0,0 +1,144 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_HYJALHELPERS_H +#define PLAYERBOTS_HYJALHELPERS_H + +#include +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace HyjalSummitHelpers +{ + enum class HyjalSummitSpells : uint32 + { + // Rage Winterchill + SPELL_DEATH_AND_DECAY = 31258, + + // Anetheron + SPELL_INFERNO = 31299, + + // Kaz'rogal + SPELL_MARK_OF_KAZROGAL = 31447, + + // Azgalor + SPELL_RAIN_OF_FIRE = 31340, + SPELL_DOOM = 31347, + + // Archimonde + SPELL_DOOMFIRE = 31944, // Damaging part of trail + SPELL_DOOMFIRE_DOT = 31969, // DoT after exiting trail + SPELL_ARCHIMONDE_FEAR = 31970, + SPELL_AIR_BURST = 32014, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum class HyjalSummitNpcs : uint32 + { + // Archimonde + NPC_DOOMFIRE = 18095, + }; + + enum class TankPositionState : uint8 + { + MovingToTransition = 0, + MovingToFinal = 1, + Positioned = 2, + Unknown = 255, + }; + + // General + constexpr uint32 HYJAL_SUMMIT_MAP_ID = 534; + struct RangedGroups + { + std::vector healers; + std::vector rangedDps; + }; + bool GetGroundedStepPosition( + Player* bot, float destinationX, float destinationY, float moveDist, + float& stepX, float& stepY, float& stepZ); + RangedGroups GetRangedGroups(PlayerbotAI* botAI, Player* bot); + std::pair GetBotCircleIndexAndCount(PlayerbotAI* botAI, Player* bot, + const RangedGroups& groups); + + // Rage Winterchill + extern const Position WINTERCHILL_TANK_POSITION; + extern std::unordered_map hasReachedWinterchillPosition; + constexpr uint32 DEATH_AND_DECAY_DURATION = 15000; + constexpr uint32 DEATH_AND_DECAY_REACQUIRE_DELAY = 20000; + constexpr float DEATH_AND_DECAY_SAFE_RADIUS = 22.0f; // 20y radius + 1.5y player hitbox + 0.5y buffer + struct DeathAndDecayData + { + Position position; + uint32 spawnTime; + }; + extern std::unordered_map deathAndDecayPosition; + DeathAndDecayData* GetActiveWinterchillDeathAndDecay(uint32 instanceId); + bool IsInDeathAndDecay(Player* bot, float radius); + + // Anetheron + extern const Position ANETHERON_TANK_POSITION; + extern const Position ANETHERON_E_INFERNAL_POSITION; + extern const Position ANETHERON_W_INFERNAL_POSITION; + extern std::unordered_map hasReachedAnetheronPosition; + Player* GetInfernoTarget(Unit* anetheron); + const Position& GetClosestInfernalTankPosition(Player* bot); + + // Kaz'rogal + extern const Position KAZROGAL_TANK_TRANSITION_POSITION; + extern const Position KAZROGAL_TANK_FINAL_POSITION; + extern std::unordered_map kazrogalTankStep; + extern std::unordered_map isBelowManaThreshold; + TankPositionState GetKazrogalTankPositionState(PlayerbotAI* botAI, Player* bot); + + // Azgalor + extern const Position AZGALOR_TANK_TRANSITION_POSITION; + extern const Position AZGALOR_TANK_FINAL_POSITION; + extern const Position AZGALOR_DOOMGUARD_POSITION; + extern std::unordered_map azgalorTankStep; + constexpr uint32 RAIN_OF_FIRE_DURATION = 10000; + constexpr uint32 RAIN_OF_FIRE_REACQUIRE_DELAY = 15000; + constexpr float RAIN_OF_FIRE_RADIUS = 17.0f; // 15y radius + 1.5y player hitbox + 0.5y buffer + struct RainOfFireData + { + Position position; + uint32 spawnTime; + }; + extern std::unordered_map rainOfFirePosition; + TankPositionState GetAzgalorTankPositionState(PlayerbotAI* botAI, Player* bot); + RainOfFireData* GetActiveAzgalorRainOfFire(uint32 instanceId); + bool IsInRainOfFire(Player* bot, float radius); + bool AnyGroupMemberHasDoom(Player* bot); + + // Archimonde + constexpr float AIR_BURST_SAFE_DISTANCE = 15.0f; + struct AirBurstData + { + ObjectGuid targetGuid; + uint32 castTime; + }; + struct DoomfireTrailData + { + Position position; + uint32 recordTime; + }; + extern const Position ARCHIMONDE_INITIAL_POSITION; + extern std::unordered_map archimondeAirBurstTargets; + extern std::unordered_map> doomfireTrails; + extern std::unordered_map doomfireLastSampleTime; + AirBurstData* GetRecentArchimondeAirBurst(uint32 instanceId); +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalScripts.cpp b/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalScripts.cpp new file mode 100644 index 000000000..d6acd90e2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Hyjal/Util/HyjalScripts.cpp @@ -0,0 +1,212 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "HyjalHelpers.h" +#include "AllCreatureScript.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "RaidBossHelpers.h" +#include "DynamicObjectScript.h" +#include "Playerbots.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "Timer.h" + +using namespace HyjalSummitHelpers; + +static Player* GetFirstPlayerSpellTarget(Spell* spell, Unit* caster) +{ + if (!spell || !caster) + return nullptr; + + if (Unit* unitTarget = spell->m_targets.GetUnitTarget()) + return unitTarget->ToPlayer(); + + std::list const& targets = *spell->GetUniqueTargetInfo(); + for (TargetInfo const& targetInfo : targets) + { + if (Player* target = ObjectAccessor::GetPlayer(*caster, targetInfo.targetGUID)) + return target; + } + + return nullptr; +} + +static bool ShouldInterruptForArchimondeAirBurst(PlayerbotAI* botAI, Player* bot, Player* target) +{ + if (!target) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank || bot == mainTank) + return false; + + float distanceToMainTank = bot->GetExactDist2d(mainTank); + + return (target == mainTank || target == bot) && + distanceToMainTank < AIR_BURST_SAFE_DISTANCE; +} + +// Records the active Rain of Fire dynamic object so that melee bots can avoid it by running +// away from Azgalor or swapping to a Doomguard; the standard FleePosition() logic to avoid aoe +// can take melee in front of Azgalor, resulting in them getting cleaved +class AzgalorRainOfFireScript : public DynamicObjectScript +{ +public: + AzgalorRainOfFireScript() : DynamicObjectScript("AzgalorRainOfFireScript") {} + + void OnUpdate(DynamicObject* dynobj, uint32 /*diff*/) override + { + if (dynobj->GetSpellId() != static_cast(HyjalSummitSpells::SPELL_RAIN_OF_FIRE)) + return; + + uint32 instanceId = dynobj->GetMap()->GetInstanceId(); + if (GetActiveAzgalorRainOfFire(instanceId)) + return; + + uint32 now = getMSTime(); + auto instanceIt = rainOfFirePosition.find(instanceId); + if (instanceIt != rainOfFirePosition.end() && + getMSTimeDiff(instanceIt->second.spawnTime, now) < RAIN_OF_FIRE_REACQUIRE_DELAY) + { + return; + } + + bool shouldTrackRainOfFire = false; + Map::PlayerList const& players = dynobj->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive()) + continue; + + PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); + if (!botAI || !botAI->HasStrategy("hyjal", BOT_STATE_COMBAT)) + continue; + + shouldTrackRainOfFire = true; + break; + } + + if (!shouldTrackRainOfFire) + return; + + rainOfFirePosition[instanceId] = RainOfFireData{ dynobj->GetPosition(), now }; + } +}; + +// Records the position of each Doomfire NPC at regular intervals so that bots can avoid +// the persistent fire trail it leaves behind. Each sample is tagged with a timestamp and +// expires after TRAIL_DURATION ms, matching the lifetime of a Doomfire DynamicObject (18s) +class ArchimondeDoomfireTrailScript : public AllCreatureScript +{ +public: + ArchimondeDoomfireTrailScript() : AllCreatureScript("ArchimondeDoomfireTrailScript") {} + + void OnAllCreatureUpdate(Creature* creature, uint32 /*diff*/) override + { + if (creature->GetEntry() != static_cast(HyjalSummitNpcs::NPC_DOOMFIRE)) + return; + + uint32 now = getMSTime(); + ObjectGuid guid = creature->GetGUID(); + + auto& lastSample = doomfireLastSampleTime[guid]; + if (getMSTimeDiff(lastSample, now) < 500) + return; + + lastSample = now; + + uint32 instanceId = creature->GetMap()->GetInstanceId(); + auto& trail = doomfireTrails[instanceId]; + + DoomfireTrailData data; + data.position = creature->GetPosition(); + data.recordTime = now; + trail.push_back(data); + + constexpr uint32 TRAIL_DURATION = 18000; + trail.erase(std::remove_if(trail.begin(), trail.end(), + [now](const DoomfireTrailData& d) + { + return getMSTimeDiff(d.recordTime, now) > TRAIL_DURATION; + }), trail.end()); + + constexpr float DOOMFIRE_DANGER_RANGE = 10.0f; + Map::PlayerList const& players = creature->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive()) + continue; + + PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); + if (!botAI || !botAI->HasStrategy("hyjal", BOT_STATE_COMBAT) || + creature->GetDistance(player) > DOOMFIRE_DANGER_RANGE) + { + continue; + } + + botAI->RequestSpellInterrupt(); + } + } + + void OnCreatureRemoveWorld(Creature* creature) override + { + if (creature->GetEntry() != static_cast(HyjalSummitNpcs::NPC_DOOMFIRE)) + return; + + doomfireLastSampleTime.erase(creature->GetGUID()); + } +}; + +class ArchimondeAirBurstSpellListenerScript : public AllSpellScript +{ +public: + ArchimondeAirBurstSpellListenerScript() : + AllSpellScript("ArchimondeAirBurstSpellListenerScript") {} + + void OnSpellCast( + Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (!spell || !caster || !spellInfo) + return; + + if (spellInfo->Id != static_cast(HyjalSummitSpells::SPELL_AIR_BURST)) + return; + + Player* target = GetFirstPlayerSpellTarget(spell, caster); + if (!target) + return; + + archimondeAirBurstTargets[caster->GetMap()->GetInstanceId()] = + AirBurstData{ target->GetGUID(), getMSTime() }; + + Map::PlayerList const& players = caster->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive()) + continue; + + PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); + if (!botAI || !botAI->HasStrategy("hyjal", BOT_STATE_COMBAT) || + !ShouldInterruptForArchimondeAirBurst(botAI, player, target)) + { + continue; + } + + botAI->RequestSpellInterrupt(); + } + } +}; + +void AddSC_HyjalSummitBotScripts() +{ + new AzgalorRainOfFireScript(); + new ArchimondeDoomfireTrailScript(); + new ArchimondeAirBurstSpellListenerScript(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions.h b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions.h new file mode 100644 index 000000000..5b434f1a1 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions.h @@ -0,0 +1,849 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCACTIONS_H +#define PLAYERBOTS_ICCACTIONS_H + +#include + +#include "Action.h" +#include "MovementActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "AttackAction.h" +#include "LastMovementValue.h" +#include "ObjectGuid.h" +#include "PlayerbotAIConfig.h" +#include "ICCStrategy.h" +#include "ScriptedCreature.h" +#include "SharedDefines.h" +#include "Trigger.h" +#include "CellImpl.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "Vehicle.h" +#include "ICCTriggers.h" +#include "ICCScripts.h" +#include "ICCShared.h" + +inline const Position ICC_LM_TANK_POSITION = Position(-391.0f, 2259.0f, 42.0f); +inline const Position ICC_LM_BONE_STORM_AT_POSITION = Position(-390.02332f, 2179.3481f, 41.96729f); +inline const Position ICC_LM_MID_POSITION = Position(-393.61722f, 2216.335f, 41.99396f); +inline const Position ICC_DARK_RECKONING_SAFE_POSITION = Position(-523.33386f, 2211.2031f, 62.823116f); +inline const Position ICC_LDW_TANK_POSTION = Position(-593.7436f, 2211.298f, 49.476673f); //old closer to stairs -570.1f, 2211.2456f, 49.476616f +inline const Position ICC_LDW_RANGED_POSITION = Position(-653.11096f, 2211.9568f,51.551437f); //old in frint of the boss (-607.0631f, 2212.2488f, 49.47009f) +inline const Position ICC_ROTTING_FROST_GIANT_TANK_POSITION = Position(-328.5085f, 2225.5142f, 199.97298f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_ALLY = Position (-370.04645f, 1993.3536f, 466.65656f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_ALLY2 = Position (-392.66208f, 2064.893f, 466.5672f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_ALLY_MIDDLE_POINT = Position(-383.01166f, 2022.8046f, 467.38193f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_ALLY_FRIENDLY_POINT = Position(-389.26788f, 2045.9136f, 467.56168f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_HORDE = Position (-449.5343f, 2477.2024f, 471.31906f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_HORDE2 = Position (-429.81586f, 2400.6804f, 471.56537f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_HORDE_MIDDLE_POINT = Position(-449.17645f, 2449.2705f, 470.9257f); +inline const Position ICC_GUNSHIP_ROCKET_JUMP_HORDE_FRIENDLY_POINT = Position(-444.74533f, 2420.1208f, 470.78748f); +inline const Position ICC_DBS_TANK_POSITION = Position(-494.26517f, 2211.549f, 541.11414f); +inline const Position ICC_FESTERGUT_TANK_POSITION = Position(4269.1772f, 3144.7673f, 360.38577f); +inline const Position ICC_FESTERGUT_RANGED_SPORE = Position(4264.3623f, 3120.889f, 360.38565f); //old closer to gates 4261.143f, 3109.4146f, 360.38605f +inline const Position ICC_FESTERGUT_RANGED_SPORE_2 = Position(4288.7974f, 3115.6274f, 360.38577f); //new position +inline const Position ICC_FESTERGUT_MELEE_SPORE = Position(4269.1772f, 3144.7673f, 360.38577f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_1 = Position(4453.2085f, 3108.7488f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_2 = Position(4459.4390f, 3118.0210f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_3 = Position(4473.4062f, 3126.2861f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_4 = Position(4469.9370f, 3137.0173f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_5 = Position(4476.7583f, 3145.7160f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_6 = Position(4466.2100f, 3150.0630f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_7 = Position(4464.4297f, 3161.2317f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_8 = Position(4447.2900f, 3163.0322f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_9 = Position(4437.1250f, 3168.4062f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_10 = Position(4417.2217f, 3148.8909f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_11 = Position(4420.9727f, 3137.7700f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_12 = Position(4414.8716f, 3127.9534f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_13 = Position(4425.1553f, 3123.4717f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_14 = Position(4426.3760f, 3113.4153f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_15 = Position(4443.8027f, 3113.9207f, 360.38626f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_16 = Position(4432.477f, 3156.7651f, 360.38568f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_17 = Position(4458.083f, 3132.5842f, 360.38565f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_18 = Position(4457.4565f, 3144.8442f, 360.38565f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_19 = Position(4422.5460f, 3158.0435f, 360.38565f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_20 = Position(4432.1646f, 3142.3418f, 360.38565f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_21 = Position(4436.649f, 3126.1245f, 360.38565f); +inline const Position ICC_ROTFACE_RANGED_POSITION_HC_22 = Position(4450.2363f, 3122.4033f, 360.38565f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_1 = Position(4481.433f, 3137.0117f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_2 = Position(4474.4565f, 3156.9617f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_3 = Position(4452.0586f, 3170.2231f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_4 = Position(4421.2437f, 3161.2275f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_5 = Position(4410.904f, 3136.9976f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_6 = Position(4417.603f, 3119.4712f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_7 = Position(4442.514f, 3105.2234f, 360.38522f); +inline const Position ICC_ROTFACE_EXPLOSION_POSITION_8 = Position(4465.7173f, 3113.6284f, 360.38522f); +inline const Position ICC_ROTFACE_TANK_POSITION = Position(4447.061f, 3150.9758f, 360.38568f); +inline const Position ICC_ROTFACE_BIG_OOZE_POSITION = Position(4432.687f, 3142.3035f, 360.38568f); +inline const Position ICC_ROTFACE_SAFE_POSITION = Position(4446.557f, 3065.6594f, 360.38568f); +inline const Position ICC_ROTFACE_CENTER_POSITION = Position(4446.0547f, 3144.8677f, 360.38568f); //actual center 4.74089 4445.6616f, 3137.1526f, 360.38608 +inline const Position ICC_ROTFACE_CENTER_POSITION_BOSS = Position(4445.656f, 3137.1663f, 360.38565f); +inline const Position ICC_PUTRICIDE_TANK_POSITION = Position(4373.227f, 3222.058f, 389.4029f); +inline const Position ICC_PUTRICIDE_GREEN_POSITION = Position(4423.4126f, 3194.2715f, 389.37683f); +inline const Position ICC_PUTRICIDE_GATE_POSITION = Position(4356.3345f, 3167.9407f, 389.39825f); +inline const Position ICC_BPC_OT_POSITION = Position(4649.2236f, 2796.0972f, 361.1815f); +inline const Position ICC_BPC_MT_POSITION = Position(4648.5674f, 2744.847f, 361.18222f); +inline const Position ICC_BPC_CENTER_POSITION = Position(4638.7056f, 2769.3713f, 361.17108f); +inline const Position ICC_BQL_CENTER_POSITION = Position(4595.0f, 2769.0f, 400.0f); +inline const Position ICC_BQL_LWALL1_POSITION = Position(4624.685f, 2789.4895f, 400.13834f); +inline const Position ICC_BQL_LWALL2_POSITION = Position(4600.749f, 2805.7568f, 400.1374f); +inline const Position ICC_BQL_LWALL3_POSITION = Position(4572.857f, 2797.3872f, 400.1374f); +inline const Position ICC_BQL_RWALL1_POSITION = Position(4625.724f, 2748.9917f, 400.13693f); +inline const Position ICC_BQL_RWALL2_POSITION = Position(4608.3774f, 2735.7466f, 400.13693f); +inline const Position ICC_BQL_RWALL3_POSITION = Position(4576.813f, 2739.6067f, 400.13693f); +inline const Position ICC_BQL_LRWALL4_POSITION = Position(4539.345f, 2769.3853f, 403.7267f); +inline const Position ICC_BQL_TANK_POSITION = Position(4633.7964f, 2769.2515f, 401.74777f); //old 4629.746f, 2769.6396f, 401.7479f +inline const Position ICC_VDW_HEAL_POSITION = Position(4203.752f, 2483.4343f, 364.87274f); +inline const Position ICC_VDW_GROUP1_POSITION = Position(4203.585f, 2464.422f, 364.87323f); +inline const Position ICC_VDW_GROUP2_POSITION = Position(4203.5806f, 2505.2383f, 364.87323f); +inline const Position ICC_VDW_PORTALSTART_POSITION = Position(4202.637f, 2488.171f, 375.00256f); +inline const Position ICC_SINDRAGOSA_TANK_POSITION = Position(4408.016f, 2508.0647f, 203.37955f); //X: 4183.136 Y: 2492.9358 Z: 364.87595 Orientation: 5.3975997 +inline const Position ICC_SINDRAGOSA_FLYING_POSITION = Position(4525.6f, 2485.15f, 245.082f); +inline const Position ICC_SINDRAGOSA_RANGED_POSITION = Position(4441.572f, 2484.482f, 203.37836f); +inline const Position ICC_SINDRAGOSA_MELEE_POSITION = Position(4423.1646f, 2486.6792f, 203.3749f); //old 4423.4546f, 2491.7175f, 203.37686f +inline const Position ICC_SINDRAGOSA_BLISTERING_COLD_POSITION = Position(4473.6616f, 2484.8489f, 203.38258f); +inline const Position ICC_SINDRAGOSA_THOMB1_POSITION = Position(4433.6484f, 2469.4133f, 203.3806f); +inline const Position ICC_SINDRAGOSA_THOMB2_POSITION = Position(4434.143f, 2486.201f, 203.37473f); +inline const Position ICC_SINDRAGOSA_THOMB3_POSITION = Position(4436.1147f, 2501.464f, 203.38266f); +inline const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC1_POSITION = Position(4444.9707f, 2455.7322f, 203.38701f); +inline const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC2_POSITION = Position(4461.3945f, 2463.5513f, 203.38727f); +inline const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC3_POSITION = Position(4473.6616f, 2484.8489f, 203.38258f); +inline const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC4_POSITION = Position(4459.9336f, 2507.409f, 203.38606f); +inline const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC5_POSITION = Position(4442.3096f, 2512.4688f, 203.38647f); +inline const Position ICC_SINDRAGOSA_CENTER_POSITION = Position(4408.0464f, 2484.478f, 203.37529f); +inline const Position ICC_SINDRAGOSA_THOMBMB2_POSITION = Position(4436.895f, 2498.1401f, 203.38133f); +inline const Position ICC_SINDRAGOSA_FBOMB_POSITION = Position(4453.861f, 2485.524f, 203.3787f); +inline const Position ICC_SINDRAGOSA_FBOMB10_POSITION = Position(4453.861f, 2485.524f, 203.3787f); +inline const Position ICC_SINDRAGOSA_LOS2_POSITION = Position(4441.8286f, 2501.946f, 203.38435f); +inline const Position ICC_LICH_KING_ADDS_POSITION = Position(476.7332f, -2095.3894f, 840.857f); // old 486.63647f, -2095.7915f, 840.857f +inline const Position ICC_LICH_KING_MELEE_POSITION = Position(503.5546f, -2106.8213f, 840.857f); +inline const Position ICC_LICH_KING_RANGED_POSITION = Position(501.3563f, -2085.1816f, 840.857f); +inline const Position ICC_LICH_KING_ASSISTHC_POSITION = Position(517.2145f, -2125.0674f, 840.857f); +inline const Position ICC_LICH_KING_CENTER_POSITION = Position(503.62036f, -2124.7336f, 840.857f); +inline const Position ICC_LK_FROST1_POSITION = Position(503.96548f, -2183.216f, 840.857f); +inline const Position ICC_LK_FROST2_POSITION = Position(563.07166f, -2125.7578f, 840.857f); +inline const Position ICC_LK_FROST3_POSITION = Position(503.40182f, -2067.3435f, 840.857f); +inline const Position ICC_LK_FROSTR1_POSITION = Position(481.168f, -2177.8723f, 840.857f); +inline const Position ICC_LK_FROSTR2_POSITION = Position(562.20807f, -2100.2393f, 840.857f); +inline const Position ICC_LK_FROSTR3_POSITION = Position(526.35297f, -2071.0317f, 840.857f); +inline const Position ICC_LK_VILE_SPIRIT1_POSITION = Position(505.24002f, -2086.7778f, 840.857f); +inline const Position ICC_LK_VILE_SPIRIT2_POSITION = Position(532.668f, -2122.603f, 840.857f); +inline const Position ICC_LK_VILE_SPIRIT3_POSITION = Position(502.8796f, -2159.7466f, 840.857f); + +//Lord Marrogwar +class IccLmTankPositionAction : public AttackAction +{ +public: + IccLmTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc lm tank position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool MoveTowardPosition(Position const& position, float incrementSize); +}; + +class IccSpikeAction : public AttackAction +{ +public: + IccSpikeAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc spike") {} + bool Execute(Event event) override; + + std::vector FindAliveSpikes(); + bool HandleSpikeMarking(std::vector const& spikes, Unit* boss); + bool HandleNoSpikesMarking(Unit* boss); + bool HandleSpikeAssignment(std::vector const& spikes, Unit* boss); + bool MoveTowardPosition(Position const& position, float incrementSize); + bool IsSpikeInColdFlame(Unit* spike); + static Player* GetSpikeVictim(Unit* spike); +}; + +//Lady Deathwhisper +class IccDarkReckoningAction : public MovementAction +{ +public: + IccDarkReckoningAction(PlayerbotAI* botAI, std::string const name = "icc dark reckoning") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class IccRangedPositionLadyDeathwhisperAction : public AttackAction +{ +public: + IccRangedPositionLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc ranged position lady deathwhisper") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool MaintainRangedSpacing(); +}; + +class IccAddsLadyDeathwhisperAction : public AttackAction +{ +public: + IccAddsLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc adds lady deathwhisper") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool IsTargetedByShade(uint32 shadeEntry); + bool HandleAddTargeting(Unit* boss); + bool UpdateRaidTargetIcon(Unit* target); + bool HandleNonTankAddEvasion(); + bool IsAdd(Unit* unit); + bool IsAssistTankAlive(); + bool ApplyNearbyAddCC(); + bool ApplyCCToAdd(Unit* add); + bool IsAddsAlive(); + bool EngageBoss(); + Unit* FindAndCollectAdd(Unit* boss); + Unit* FindAddNearBoss(Unit* boss, float maxDist); + +}; + +class IccShadeLadyDeathwhisperAction : public MovementAction +{ +public: + IccShadeLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc shade lady deathwhisper") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +//Gunship Battle +class IccRottingFrostGiantTankPositionAction : public AttackAction +{ +public: + IccRottingFrostGiantTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotting frost giant tank position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Gunship Battle +class IccCannonFireAction : public Action +{ +public: + IccCannonFireAction(PlayerbotAI* botAI, std::string const name = "icc cannon fire") : Action(botAI, name) {} + bool Execute(Event event) override; + +private: + Unit* FindValidCannonTarget(); + bool TryCastCannonSpell(uint32 spellId, Unit* target, Unit* vehicleBase); +}; + +class IccGunshipEnterCannonAction : public MovementAction +{ +public: + IccGunshipEnterCannonAction(PlayerbotAI* botAI, std::string const name = "icc gunship enter cannon") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + Unit* FindBestAvailableCannon(); + bool IsValidCannon(Unit* vehicle); + bool EnterVehicle(Unit* vehicleBase, bool moveIfFar); +}; + +class IccGunshipRocketJumpAction : public AttackAction +{ +public: + IccGunshipRocketJumpAction(PlayerbotAI* botAI, std::string const name = "icc gunship rocket jump") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + enum class GunshipSide + { + NONE, + ALLY, + HORDE + }; + + GunshipSide DetectShip() const; + Item* FindRocketPack() const; + bool UseRocketPack(Position const& destination, bool walkIfOutOfRange); + bool RocketPackJumpToward(Position const& target); + bool ExitCannonIfSeated(); + bool CleanupSkullIcon(uint8 skullIconIndex); + bool UpdateBossSkullIcon(Unit* boss, uint8 skullIconIndex); + bool IsMainTankOnEnemyShip(GunshipSide side) const; + bool AnyNonTankAwayFromFriendly(GunshipSide side) const; + Unit* FindNearestFriendlyCannon(GunshipSide side) const; +}; + +class IccGunshipRocketPackSetupAction : public MovementAction +{ +public: + IccGunshipRocketPackSetupAction(PlayerbotAI* botAI, std::string const name = "icc gunship rocket pack setup") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + Item* FindRocketPack() const; + bool AcquireRocketPack(); + bool EquipRocketPack(); +}; + +//DBS +class IccDbsTankPositionAction : public AttackAction +{ +public: + IccDbsTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc dbs tank position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool CrowdControlBloodBeasts(); + bool EvadeBloodBeasts(); + bool PositionInRangedFormation(); +}; + +class IccAddsDbsAction : public AttackAction +{ +public: + IccAddsDbsAction(PlayerbotAI* botAI, std::string const name = "icc adds dbs") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + Unit* FindPriorityTarget(Unit* boss); + bool UpdateSkullMarker(Unit* priorityTarget); +}; + +class IccDogsTankPositionAction : public AttackAction +{ +public: + IccDogsTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc dogs tank position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +//FESTERGUT +class IccFestergutGroupPositionAction : public AttackAction +{ +public: + IccFestergutGroupPositionAction(PlayerbotAI* botAI, std::string const name = "icc festergut group position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool HasSporesInGroup(); + bool PositionNonTankMembers(); + int CalculatePositionIndex(Group* group); +}; + +class IccFestergutSporeAction : public AttackAction +{ +public: + IccFestergutSporeAction(PlayerbotAI* botAI, std::string const name = "icc festergut spore") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + Position CalculateSpreadPosition(); + struct SporeInfo + { + std::vector sporedPlayers; + ObjectGuid lowestGuid; + bool hasLowestGuid = false; + }; + SporeInfo FindSporedPlayers(); + Position DetermineTargetPosition(bool hasSpore, const SporeInfo& sporeInfo, const Position& spreadRangedPos); + bool CheckMainTankSpore(); + bool GooNear(Position const& pos); +}; + +class IccFestergutAvoidMalleableGooAction : public MovementAction +{ +public: + IccFestergutAvoidMalleableGooAction(PlayerbotAI* botAI, + std::string const name = "icc festergut avoid malleable goo") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +//Rotface +class IccRotfaceTankPositionAction : public AttackAction +{ +public: + IccRotfaceTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotface tank position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool MarkBossWithSkull(Unit* boss); + bool PositionMainTankAndMelee(Unit* boss, Unit* smallOoze = nullptr); + bool HandleAssistTankPositioning(Unit* boss); + Unit* FindAssignedBigOoze(Unit* boss, std::vector& bigOozes); + bool HandleBigOozeKiting(Unit* bigOoze); +}; + +class IccRotfaceGroupPositionAction : public AttackAction +{ +public: + IccRotfaceGroupPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotface group position") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + //bool MoveAwayFromBigOoze(Unit* bigOoze); + bool HandlePuddleAvoidance(Unit* boss); + bool MoveAwayFromPuddle(Unit* boss, Unit* puddle, float puddleDistance); + bool HandleOozeTargeting(); + bool HandleOozeMemberPositioning(Unit* mySmallOoze); + bool PositionRangedAndHealers(Unit* boss,Unit* smallOoze); + bool PositionHeroicGrid(Unit* boss); +}; + +class IccRotfaceMoveAwayFromExplosionAction : public MovementAction +{ +public: + IccRotfaceMoveAwayFromExplosionAction(PlayerbotAI* botAI, std::string const name = "icc rotface move away from explosion") + : MovementAction(botAI, name), + _escapePosition(0.0f, 0.0f, 0.0f), + _hasEscape(false), + _holdUntil(0) {} + bool Execute(Event event) override; + +private: + Position _escapePosition; + bool _hasEscape; + uint32 _holdUntil; +}; + +class IccRotfaceAvoidVileGasAction : public MovementAction +{ +public: + IccRotfaceAvoidVileGasAction(PlayerbotAI* botAI, std::string const name = "icc rotface avoid vile gas") + : MovementAction(botAI, name), + _safeSpot(0.0f, 0.0f, 0.0f), + _hasSafeSpot(false) {} + bool Execute(Event event) override; + +private: + Position _safeSpot; + bool _hasSafeSpot; +}; + +//PP +class IccPutricideMutatedPlagueAction : public AttackAction +{ +public: + IccPutricideMutatedPlagueAction(PlayerbotAI* botAI, std::string const name = "icc putricide mutated plague") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class IccPutricideGrowingOozePuddleAction : public AttackAction +{ +public: + IccPutricideGrowingOozePuddleAction(PlayerbotAI* botAI, std::string const name = "icc putricide growing ooze puddle") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + Unit* FindClosestThreateningPuddle(); + Position CalculateSafeMovePosition(Unit* closestPuddle); + bool IsPositionTooCloseToOtherPuddles(float x, float y, Unit* ignorePuddle); + bool PathCrossesAnyPuddle(float fromX, float fromY, float toX, float toY, Unit* ignorePuddle); +}; + +class IccPutricideVolatileOozeAction : public AttackAction +{ +public: + IccPutricideVolatileOozeAction(PlayerbotAI* botAI, std::string const name = "icc putricide volatile ooze") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool MarkOozeWithSkull(Unit* ooze); + Unit* FindAuraTarget(); +}; + +class IccPutricideGasCloudAction : public AttackAction +{ +public: + IccPutricideGasCloudAction(PlayerbotAI* botAI, std::string const name = "icc putricide gas cloud") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + + bool HandleGaseousBloatMovement(Unit* gasCloud); + bool HandleGroupAuraSituation(Unit* gasCloud); + bool GroupHasGaseousBloat(Group* group); +}; + +class IccPutricideAvoidMalleableGooAction : public MovementAction +{ +public: + IccPutricideAvoidMalleableGooAction(PlayerbotAI* botAI, std::string const name = "icc putricide avoid malleable goo") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; + + bool HandleTankPositioning(Unit* boss); + bool HandleUnboundPlague(Unit* boss); + bool HandleBossPositioning(Unit* boss); + bool HasObstacleBetween(Position const& from, Position const& to); + bool IsOnPath(Position const& from, Position const& to, Position const& point, float threshold); +}; + +class IccPutricideAbominationAction : public AttackAction +{ +public: + IccPutricideAbominationAction(PlayerbotAI* botAI, std::string const name = "icc putricide abomination") + : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool BecomeAbomination(); + bool IsSomeoneAlreadyPiloting(); + Unit* FindClosestPuddle(float maxRange); + Unit* PickSlashTarget(Unit* boss); + bool TryRegurgitate(Unit* abo, Unit* target); + bool TryEatOoze(Unit* abo, Unit* puddle); +}; + +//BPC +class IccBpcKelesethTankAction : public AttackAction +{ +public: + IccBpcKelesethTankAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc bpc keleseth tank") {} + bool Execute(Event event) override; +}; + +class IccBpcMainTankAction : public AttackAction +{ +public: + IccBpcMainTankAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc bpc main tank") {} + bool Execute(Event event) override; + + bool MarkEmpoweredPrince(); +}; + +class IccBpcEmpoweredVortexAction : public MovementAction +{ +public: + IccBpcEmpoweredVortexAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc bpc empowered vortex") {} + bool Execute(Event event) override; + + bool MaintainRangedSpacing(); + bool HandleEmpoweredVortexSpread(); +}; + +class IccBpcKineticBombAction : public AttackAction +{ +public: + IccBpcKineticBombAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc bpc kinetic bomb") {} + bool Execute(Event event) override; + + Unit* FindNearestBomb(); +}; + +class IccBpcBallOfFlameAction : public MovementAction +{ +public: + IccBpcBallOfFlameAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc bpc ball of flame") {} + bool Execute(Event event) override; +}; + +//Blood Queen Lana'thel +class IccBqlGroupPositionAction : public AttackAction +{ +public: + IccBqlGroupPositionAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc bql group position") {} + bool Execute(Event event) override; + + bool HandleTankPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura); + bool HandleShadowsMovement(); + Position AdjustControlPoint(const Position& wall, const Position& center, float factor); + Position CalculateBezierPoint(float t, const Position path[4]); + bool HandleGroupPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura); + +private: + // Evaluate curves + struct CurveInfo + { + Position moveTarget; + int curveIdx = 0; + bool foundSafe = false; + float minDist = 0.0f; + float score = 0.0f; + Position closestPoint; + float t_closest = 0.0f; + }; +}; + +class IccBqlPactOfDarkfallenAction : public MovementAction +{ +public: + IccBqlPactOfDarkfallenAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc bql pact of darkfallen") {} + bool Execute(Event event) override; + + bool CalculateCenterPosition(Position& targetPos, const std::vector& playersWithAura); + bool MoveToTargetPosition(const Position& targetPos, int auraCount); +}; + +class IccBqlVampiricBiteAction : public AttackAction +{ +public: + IccBqlVampiricBiteAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc bql vampiric bite") {} + bool Execute(Event event) override; + + Player* FindBestBiteTarget(Group* group); + bool IsInvalidTarget(Player* player); + bool MoveTowardsTarget(Player* target); + bool CastVampiricBite(Player* target); +}; + +// Sister Svalna +class IccValkyreSpearAction : public AttackAction +{ +public: + IccValkyreSpearAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc valkyre spear") {} + bool Execute(Event event) override; +}; + +class IccSisterSvalnaAction : public AttackAction +{ +public: + IccSisterSvalnaAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc sister svalna") {} + bool Execute(Event event) override; +}; + +// Valithria Dreamwalker + +class IccValithriaGroupAction : public AttackAction +{ +public: + IccValithriaGroupAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc valithria group") {} + bool Execute(Event event) override; + + bool Handle25ManGroupLogic(); + bool HandleMarkingLogic(bool inGroup1, bool inGroup2, bool singleMarkMode); + bool Handle10ManGroupLogic(); + +private: + bool ApplyCrowdControl(Unit* zombie); +}; + +class IccValithriaZombieKiteAction : public MovementAction +{ +public: + IccValithriaZombieKiteAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc valithria zombie kite") {} + bool Execute(Event event) override; +}; + +class IccValithriaPortalAction : public MovementAction +{ +public: + IccValithriaPortalAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc valithria portal") {} + bool Execute(Event event) override; +}; + +class IccValithriaHealAction : public AttackAction +{ +public: + IccValithriaHealAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc valithria heal") {} + bool Execute(Event event) override; +}; + +class IccValithriaDreamCloudAction : public MovementAction +{ +public: + IccValithriaDreamCloudAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc valithria dream cloud") {} + bool Execute(Event event) override; + +private: + std::vector CollectClouds(uint32 entry, Unit* reference); +}; + +//Sindragosa +class IccSindragosaGroupPositionAction : public AttackAction +{ +public: + IccSindragosaGroupPositionAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc sindragosa group position") {} + bool Execute(Event event) override; + + bool HandleTankPositioning(Unit* boss); + bool HandleNonTankPositioning(); + bool MoveIncrementallyToPosition(const Position& targetPos, float maxStep); +}; + +class IccSindragosaFrostBeaconAction : public MovementAction +{ +public: + IccSindragosaFrostBeaconAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc sindragosa frost beacon") {} + bool Execute(Event event) override; + + bool HandleSupportActions(); + bool HandleBeaconedPlayer(const Unit* boss); + bool HandleNonBeaconedPlayer(const Unit* boss); + bool MoveToPositionIfNeeded(const Position& position, float tolerance); + bool MoveToPosition(const Position& position); + bool IsBossFlying(const Unit* boss); + bool TryDropTombFlares(Unit const* boss); + + private: + static constexpr float POSITION_TOLERANCE = 1.0f; + static constexpr float TOMB_POSITION_TOLERANCE = 0.5f; + static constexpr float MIN_SAFE_DISTANCE = 13.0f; + static constexpr float MOVE_TOLERANCE = 2.0f; + static constexpr uint32 FLARE_ITEM_COOLDOWN_MS = 1000; +}; + +class IccSindragosaHotAction : public Action +{ +public: + IccSindragosaHotAction(PlayerbotAI* botAI) : Action(botAI, "icc sindragosa hot") {} + bool Execute(Event event) override; +}; + +class IccSindragosaBlisteringColdAction : public MovementAction +{ +public: + IccSindragosaBlisteringColdAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc sindragosa blistering cold") {} + bool Execute(Event event) override; +}; + +class IccSindragosaUnchainedMagicAction : public AttackAction +{ +public: + IccSindragosaUnchainedMagicAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc sindragosa unchained magic") {} + bool Execute(Event event) override; +}; + +class IccSindragosaChilledToTheBoneAction : public AttackAction +{ +public: + IccSindragosaChilledToTheBoneAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc sindragosa chilled to the bone") {} + bool Execute(Event event) override; +}; + +class IccSindragosaMysticBuffetAction : public MovementAction +{ +public: + IccSindragosaMysticBuffetAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc sindragosa mystic buffet") {} + bool Execute(Event event) override; +}; + +class IccSindragosaFrostBombAction : public MovementAction +{ +public: + IccSindragosaFrostBombAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc sindragosa frost bomb") {} + bool Execute(Event event) override; + +private: + struct FrostBombContext + { + Unit* marker = nullptr; + std::vector tombs; + }; + bool CollectContext(FrostBombContext& ctx) const; + int ResolveGroupIndex(Group* group) const; + void PinGroupToCurrentZone(); + std::vector SelectTombs(std::vector const& tombs, int groupIndex, int groupCount) const; + Unit* ResolveStickyTomb(std::vector const& myTombs); + bool HandleRtiMarking(Group* group, int groupIndex, std::vector const& myTombs, Unit* losTomb); +}; + +//LK +class IccLichKingShadowTrapAction : public MovementAction +{ +public: + IccLichKingShadowTrapAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc lich king shadow trap") {} + bool Execute(Event event) override; +}; + +class IccLichKingNecroticPlagueAction : public MovementAction +{ +public: + IccLichKingNecroticPlagueAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc lich king necrotic plague") {} + bool Execute(Event event) override; +}; + +class IccLichKingWinterAction : public AttackAction +{ +public: + IccLichKingWinterAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc lich king winter") {} + bool Execute(Event event) override; + + bool IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance) const; + bool IsPositionSafeFromShadowTraps(float x, float y) const; + bool IsValidCollectibleAdd(Unit* unit) const; + bool HandleTankPositioning(); + bool HandleMeleePositioning(); + bool HandleRangedPositioning(); + bool HandleMainTankAddManagement(Unit* boss, Position const* tankPos); + bool HandleAssistTankAddManagement(Unit* boss, Position const* tankPos); + bool HandlePetManagement(); + +private: + static constexpr float PLATFORM_Z = 840.857f; + static constexpr float BEHIND_DISTANCE = 4.0f; + bool FixPlatformPosition(); + bool ClearInvalidTarget(); + Position const* GetMainTankPosition(); + Position const* GetMainTankRangedPosition(); + bool TryMoveToPosition(float x, float y, float z, bool forced = true); +}; + +class IccLichKingAddsAction : public AttackAction +{ +public: + IccLichKingAddsAction(PlayerbotAI* botAI) + : AttackAction(botAI, "icc lich king adds") {} + bool Execute(Event event) override; + + bool HandleTeleportationFixes(Difficulty diff, Unit* terenas); + bool HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenas); + bool HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenas); + bool HandleQuakeMechanics(Unit* boss); + bool HandleShamblingHorrors(Unit* boss, bool hasPlague); + bool HandleRagingSpiritFlanking(Unit* boss, bool hasPlague, Difficulty diff); + bool HandleAssistTankAddManagement(Unit* boss, Difficulty diff); + bool HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff); + bool HandleMainTankTargeting(Unit* boss, Difficulty diff); + bool HandleNonTankHeroicPositioning(Unit* boss, Difficulty diff, bool hasPlague); + bool HandleRangedPositioning(Unit* boss, bool hasPlague, Difficulty diff); + bool HandleDefileMechanics(Unit* boss, Difficulty diff); + bool HandleCenterStacking(Unit* boss, Difficulty diff); + bool HandleValkyrMechanics(Difficulty diff); + bool HandleValkyrMarking(std::vector const& valkyrs, Difficulty diff); + bool HandleValkyrAssignment(std::vector const& valkyrs); + bool HandleVileSpiritMechanics(); + bool HandleIceSphereMechanics(); + bool ApplyCCToValkyr(Unit* valkyr); + bool IsValkyr(Unit* unit); + std::pair DefileAwareStep(float tx, float ty, + std::vector const& defiles, + Difficulty diff); +}; + +class IccLichKingSpiritBombAction : public MovementAction +{ +public: + IccLichKingSpiritBombAction(PlayerbotAI* botAI) + : MovementAction(botAI, "icc lich king spirit bomb") {} + bool Execute(Event event) override; + + static bool IsBombThreatActive(PlayerbotAI* botAI, Player* bot); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BPC.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BPC.cpp new file mode 100644 index 000000000..1adccb5f8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BPC.cpp @@ -0,0 +1,764 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCActions.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "Vehicle.h" +#include "RtiValue.h" +#include "GenericSpellActions.h" +#include "GenericActions.h" +#include "ICCTriggers.h" +#include "Multiplier.h" + +static float const BPC_FLOOR_Z = 361.18222f; + +bool IccBpcKelesethTankAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); + if (!boss) + return false; + + static float const Z_TELEPORT_THRESHOLD = 6.0f; + + // Teleport Z-axis back to center-level if the bot is significantly off in Z + float centerZ = ICC_BPC_CENTER_POSITION.GetPositionZ(); + if (std::abs(bot->GetPositionZ() - centerZ) > Z_TELEPORT_THRESHOLD) + { + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), centerZ, bot->GetOrientation()); + return false; + } + + if (!botAI->IsAssistTank(bot)) + { + // Non-assist-tank: pull toward center if too far + static float const CENTER_MOVE_THRESHOLD = 30.0f; + static float const MOVE_INCREMENT = 15.0f; + float distToCenter = bot->GetExactDist2d(ICC_BPC_CENTER_POSITION); + if (distToCenter > CENTER_MOVE_THRESHOLD) + { + float dirX = ICC_BPC_CENTER_POSITION.GetPositionX() - bot->GetPositionX(); + float dirY = ICC_BPC_CENTER_POSITION.GetPositionY() - bot->GetPositionY(); + float length = std::sqrt(dirX * dirX + dirY * dirY); + if (length > 0.001f) + { + dirX /= length; + dirY /= length; + float needed = distToCenter - CENTER_MOVE_THRESHOLD; + float step = std::min(MOVE_INCREMENT, needed); + float moveX = bot->GetPositionX() + dirX * step; + float moveY = bot->GetPositionY() + dirY * step; + MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + return false; + } + } + } + + if (!botAI->IsAssistTank(bot)) + return false; + + bool const isBossVictim = boss->GetVictim() == bot; + + if (!isBossVictim) + { + IccCastClassTaunt(bot, botAI, boss); + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + } + else + { + // Collect stray Dark Nuclei via taunt, move toward if out of range + static float const TAUNT_RANGE = 30.0f; + std::list nuclei; + bot->GetCreatureListWithEntryInGrid(nuclei, NPC_DARK_NUCLEUS, 100.0f); + + Unit* strayNucleus = nullptr; + for (Creature* nucleus : nuclei) + { + if (nucleus && nucleus->IsAlive() && nucleus->GetVictim() != bot) + { + strayNucleus = nucleus; + break; + } + } + + if (strayNucleus) + { + float dist = bot->GetExactDist2d(strayNucleus); + if (dist <= TAUNT_RANGE) + IccCastClassTaunt(bot, botAI, strayNucleus); + else + { + float dirX = strayNucleus->GetPositionX() - bot->GetPositionX(); + float dirY = strayNucleus->GetPositionY() - bot->GetPositionY(); + float length = std::sqrt(dirX * dirX + dirY * dirY); + if (length > 0.001f) + { + dirX /= length; + dirY /= length; + static float const NUCLEUS_MOVE_STEP = 10.0f; + static float const NUCLEUS_APPROACH_BUFFER = 5.0f; + float step = std::min(NUCLEUS_MOVE_STEP, dist - TAUNT_RANGE + NUCLEUS_APPROACH_BUFFER); + float moveX = bot->GetPositionX() + dirX * step; + float moveY = bot->GetPositionY() + dirY * step; + MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; + } + } + } + + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + Attack(boss); + + return false; +} + +bool IccBpcMainTankAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot)) + { + if (botAI->IsTank(bot)) + MarkEmpoweredPrince(); + return false; + } + + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); + + bool const isVictimOfValanar = valanar && valanar->GetVictim() == bot; + bool const isVictimOfTaldaram = taldaram && taldaram->GetVictim() == bot; + + // Move to MT position if targeted by both princes and not already close + static float const MT_POSITION_THRESHOLD = 15.0f; + if (isVictimOfValanar && isVictimOfTaldaram && bot->GetExactDist2d(ICC_BPC_MT_POSITION) > MT_POSITION_THRESHOLD) + { + float dirX = ICC_BPC_MT_POSITION.GetPositionX() - bot->GetPositionX(); + float dirY = ICC_BPC_MT_POSITION.GetPositionY() - bot->GetPositionY(); + float length = std::sqrt(dirX * dirX + dirY * dirY); + if (length > 0.001f) + { + dirX /= length; + dirY /= length; + float moveDist = std::min(3.0f, length); + float moveX = bot->GetPositionX() + dirX * moveDist; + float moveY = bot->GetPositionY() + dirY * moveDist; + MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // Taunt princes not targeting us + if (valanar && !isVictimOfValanar) + { + IccCastClassTaunt(bot, botAI, valanar); + bot->SetTarget(valanar->GetGUID()); + bot->SetFacingToObject(valanar); + Attack(valanar); + } + + if (taldaram && !isVictimOfTaldaram) + { + IccCastClassTaunt(bot, botAI, taldaram); + bot->SetTarget(taldaram->GetGUID()); + bot->SetFacingToObject(taldaram); + Attack(taldaram); + } + + // Only the princes are tanked here (Dark Nuclei are handled by the Keleseth tank). + // Never taunt other hostile NPCs: threat/fixate bypasses taunt immunity and would + // lock a Ball of Flame / Kinetic Bomb onto the tank, breaking that mechanic. + + // Target marking for all tanks, called after main tank priority actions + if (botAI->IsTank(bot)) + MarkEmpoweredPrince(); + + return false; +} + +bool IccBpcMainTankAction::MarkEmpoweredPrince() +{ + static constexpr uint8 SKULL_RAID_ICON = RtiTargetValue::skullIndex; + + // Find empowered prince (Invocation of Blood) + Unit* empoweredPrince = nullptr; + GuidVector const& targets = AI_VALUE(GuidVector, "possible targets"); + + for (auto const& targetGuid : targets) + { + Unit* unit = botAI->GetUnit(targetGuid); + if (!unit || !unit->IsAlive()) + continue; + + if (botAI->HasAura("Invocation of Blood", unit)) + { + uint32 const entry = unit->GetEntry(); + if (entry == NPC_PRINCE_KELESETH || entry == NPC_PRINCE_VALANAR || entry == NPC_PRINCE_TALDARAM) + { + empoweredPrince = unit; + break; + } + } + } + + // Handle marking if we found an empowered prince + if (empoweredPrince && empoweredPrince->IsAlive()) + { + Group* group = bot->GetGroup(); + if (group) + { + ObjectGuid const currentSkullGuid = group->GetTargetIcon(SKULL_RAID_ICON); + Unit* markedUnit = botAI->GetUnit(currentSkullGuid); + + // Clear dead marks or marks that are not on empowered prince. Null out + // markedUnit here too - otherwise the check below still sees the stale + // guid/unit from before the clear and skips re-marking for a full tick. + if (markedUnit && (!markedUnit->IsAlive() || markedUnit != empoweredPrince)) + { + group->SetTargetIcon(SKULL_RAID_ICON, bot->GetGUID(), ObjectGuid::Empty); + markedUnit = nullptr; + } + + // Mark alive empowered prince if needed + if (!markedUnit) + group->SetTargetIcon(SKULL_RAID_ICON, bot->GetGUID(), empoweredPrince->GetGUID()); + } + } + + return false; +} + +bool IccBpcEmpoweredVortexAction::Execute(Event /*event*/) +{ + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + if (!valanar) + return false; + + // Check if boss is casting empowered vortex + bool const isCastingVortex = valanar->HasUnitState(UNIT_STATE_CASTING) && + (valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4)); + + // Use complex positioning system for empowered vortex + if (isCastingVortex) + return HandleEmpoweredVortexSpread(); + + // Use simple ranged spreading for non-vortex situations + return MaintainRangedSpacing(); +} + +bool IccBpcEmpoweredVortexAction::MaintainRangedSpacing() +{ + static float const IDEAL_RADIUS = 25.0f; + static float const RADIUS_TOLERANCE = 3.0f; + static float const MOVE_INCREMENT = 3.0f; + + bool const isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); + if (!isRanged) + return false; + + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + if (!valanar) + return false; + + // Collect ranged/healer bots sorted by GUID for consistent slot assignment + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector rangedBots; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (botAI->IsTank(member)) + continue; + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI) + continue; + if (memberAI->IsRanged(member) || memberAI->IsHeal(member)) + rangedBots.push_back(member); + } + + if (rangedBots.empty()) + return false; + + std::sort(rangedBots.begin(), rangedBots.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + // Find this bot's slot index + int slotIndex = -1; + for (size_t i = 0; i < rangedBots.size(); ++i) + { + if (rangedBots[i] == bot) + { + slotIndex = static_cast(i); + break; + } + } + + if (slotIndex < 0) + return false; + + // Calculate angular position around Valanar + float angleStep = 2.0f * static_cast(M_PI) / static_cast(rangedBots.size()); + float targetAngle = angleStep * static_cast(slotIndex); + + // Collect shock vortex positions to avoid + static float const VORTEX_AVOID_DIST = 13.0f; + std::list vortexList; + bot->GetCreatureListWithEntryInGrid(vortexList, NPC_SHOCK_VORTEX, 100.0f); + + // Find safe angle — try assigned slot first, then nudge away from vortices + static float const ANGLE_NUDGE = static_cast(M_PI) / 18.0f; + static int const MAX_NUDGE_STEPS = 18; + float bestAngle = targetAngle; + + auto isAngleSafe = [&](float angle) -> bool + { + float testX = valanar->GetPositionX() + IDEAL_RADIUS * std::cos(angle); + float testY = valanar->GetPositionY() + IDEAL_RADIUS * std::sin(angle); + for (Creature* vortex : vortexList) + { + if (!vortex || !vortex->IsAlive()) + continue; + float vdx = testX - vortex->GetPositionX(); + float vdy = testY - vortex->GetPositionY(); + if (std::sqrt(vdx * vdx + vdy * vdy) < VORTEX_AVOID_DIST) + return false; + } + return true; + }; + + if (!isAngleSafe(bestAngle)) + { + for (int i = 1; i <= MAX_NUDGE_STEPS; ++i) + { + float plusAngle = targetAngle + ANGLE_NUDGE * static_cast(i); + if (isAngleSafe(plusAngle)) + { + bestAngle = plusAngle; + break; + } + float minusAngle = targetAngle - ANGLE_NUDGE * static_cast(i); + if (isAngleSafe(minusAngle)) + { + bestAngle = minusAngle; + break; + } + } + } + + float targetX = valanar->GetPositionX() + IDEAL_RADIUS * std::cos(bestAngle); + float targetY = valanar->GetPositionY() + IDEAL_RADIUS * std::sin(bestAngle); + float targetZ = bot->GetPositionZ(); + + float distToSlot = bot->GetExactDist2d(targetX, targetY); + if (distToSlot <= RADIUS_TOLERANCE) + return false; + + // Move toward assigned slot + float dx = targetX - bot->GetPositionX(); + float dy = targetY - bot->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) + return false; + + dx /= len; + dy /= len; + float step = std::min(MOVE_INCREMENT, distToSlot); + float moveX = bot->GetPositionX() + dx * step; + float moveY = bot->GetPositionY() + dy * step; + + if (bot->IsWithinLOS(moveX, moveY, targetZ)) + { + MoveTo(bot->GetMapId(), moveX, moveY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + + return false; +} + +bool IccBpcEmpoweredVortexAction::HandleEmpoweredVortexSpread() +{ + auto& spreadLockTimers = IcecrownHelpers::IccState(bot->GetInstanceId()).bpcSpreadLockTimers; + static uint32 const SPREAD_LOCK_DURATION_MS = 250; + static float const MOVE_INCREMENT = 4.0f; + static float const SLOT_TOLERANCE = 2.0f; + static float const MIN_SPACING = 13.0f; + + if (botAI->IsTank(bot)) + return false; + + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + if (!valanar) + return false; + + // If locked in position, don't move — allow combat/healing + uint32 now = getMSTime(); + + // Prune expired entries to prevent unbounded growth + for (auto it = spreadLockTimers.begin(); it != spreadLockTimers.end(); ) + { + if (getMSTimeDiff(it->second, now) >= SPREAD_LOCK_DURATION_MS) + it = spreadLockTimers.erase(it); + else + ++it; + } + + auto it = spreadLockTimers.find(bot->GetGUID()); + if (it != spreadLockTimers.end()) + return false; + + // Collect ALL non-tank bots for spreading (melee must scatter too) + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector spreadBots; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (botAI->IsTank(member)) + continue; + spreadBots.push_back(member); + } + + if (spreadBots.empty()) + return false; + + std::sort(spreadBots.begin(), spreadBots.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + int slotIndex = -1; + for (size_t i = 0; i < spreadBots.size(); ++i) + { + if (spreadBots[i] == bot) + { + slotIndex = static_cast(i); + break; + } + } + + if (slotIndex < 0) + return false; + + // Calculate radius so angular spacing >= MIN_SPACING + // Arc length between slots = 2*pi*R / N, need >= MIN_SPACING + float const botCount = static_cast(spreadBots.size()); + float idealRadius = (botCount * MIN_SPACING) / (2.0f * static_cast(M_PI)); + // Clamp radius to room bounds (room ~40y radius from center) + if (idealRadius < 15.0f) + idealRadius = 15.0f; + if (idealRadius > 38.0f) + idealRadius = 38.0f; + + float angleStep = 2.0f * static_cast(M_PI) / botCount; + float targetAngle = angleStep * static_cast(slotIndex); + + // Collect shock vortex positions to avoid + static float const VORTEX_AVOID_DIST = 13.0f; + std::list vortexList; + bot->GetCreatureListWithEntryInGrid(vortexList, NPC_SHOCK_VORTEX, 100.0f); + + // Find safe angle — try assigned slot first, then nudge away from vortices + static float const ANGLE_NUDGE = static_cast(M_PI) / 18.0f; + static int const MAX_NUDGE_STEPS = 18; + float bestAngle = targetAngle; + + auto isAngleSafe = [&](float angle) -> bool + { + float testX = valanar->GetPositionX() + idealRadius * std::cos(angle); + float testY = valanar->GetPositionY() + idealRadius * std::sin(angle); + for (Creature* vortex : vortexList) + { + if (!vortex || !vortex->IsAlive()) + continue; + float vdx = testX - vortex->GetPositionX(); + float vdy = testY - vortex->GetPositionY(); + if (std::sqrt(vdx * vdx + vdy * vdy) < VORTEX_AVOID_DIST) + return false; + } + return true; + }; + + if (!isAngleSafe(bestAngle)) + { + for (int i = 1; i <= MAX_NUDGE_STEPS; ++i) + { + float plusAngle = targetAngle + ANGLE_NUDGE * static_cast(i); + if (isAngleSafe(plusAngle)) + { + bestAngle = plusAngle; + break; + } + float minusAngle = targetAngle - ANGLE_NUDGE * static_cast(i); + if (isAngleSafe(minusAngle)) + { + bestAngle = minusAngle; + break; + } + } + } + + float targetX = valanar->GetPositionX() + idealRadius * std::cos(bestAngle); + float targetY = valanar->GetPositionY() + idealRadius * std::sin(bestAngle); + float targetZ = bot->GetPositionZ(); + + float distToSlot = bot->GetExactDist2d(targetX, targetY); + + // Close enough to slot — lock position + if (distToSlot <= SLOT_TOLERANCE) + { + spreadLockTimers[bot->GetGUID()] = now; + return false; + } + + // Move toward assigned slot + float dx = targetX - bot->GetPositionX(); + float dy = targetY - bot->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) + return false; + + dx /= len; + dy /= len; + float step = std::min(MOVE_INCREMENT, distToSlot); + float moveX = bot->GetPositionX() + dx * step; + float moveY = bot->GetPositionY() + dy * step; + + if (bot->IsWithinLOS(moveX, moveY, targetZ)) + { + MoveTo(bot->GetMapId(), moveX, moveY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool IccBpcKineticBombAction::Execute(Event /*event*/) +{ + if (!botAI->IsRangedDps(bot)) + return false; + + static float const MAX_HEIGHT_DIFF = 30.0f; + static float const SAFE_HEIGHT_Z = 371.16473f; + static float const TELEPORT_DOWN_Z = 367.16473f; + + // Fix bot stuck above arena + if (bot->GetPositionZ() > SAFE_HEIGHT_Z) + { + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TELEPORT_DOWN_Z, + bot->GetOrientation()); + } + + // Assign bombs 1-to-1 to nearest available ranged DPS + Unit* assignedBomb = FindNearestBomb(); + if (!assignedBomb) + return false; + + // Already attacking a valid bomb that's still low enough, keep going + if (Unit* currentTarget = AI_VALUE(Unit*, "current target")) + { + if (currentTarget == assignedBomb && currentTarget->IsAlive()) + { + float const heightDiff = currentTarget->GetPositionZ() - BPC_FLOOR_Z; + if (heightDiff < MAX_HEIGHT_DIFF) + return false; + } + } + + bot->SetTarget(assignedBomb->GetGUID()); + bot->SetFacingToObject(assignedBomb); + Attack(assignedBomb); + return false; +} + +Unit* IccBpcKineticBombAction::FindNearestBomb() +{ + static constexpr std::array BOMB_ENTRIES = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, + NPC_KINETIC_BOMB4}; + static float const MAX_HEIGHT_DIFF = 35.0f; + + GuidVector const targets = AI_VALUE(GuidVector, "possible targets no los"); + if (targets.empty()) + return nullptr; + + // Gather all valid bombs, sorted by Z ascending (lowest = most urgent) + std::vector kineticBombs; + for (auto const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (std::find(BOMB_ENTRIES.begin(), BOMB_ENTRIES.end(), unit->GetEntry()) == BOMB_ENTRIES.end()) + continue; + + if (unit->GetPositionZ() - BPC_FLOOR_Z > MAX_HEIGHT_DIFF) + continue; + + kineticBombs.push_back(unit); + } + + if (kineticBombs.empty()) + return nullptr; + + std::sort(kineticBombs.begin(), kineticBombs.end(), + [](Unit* unitA, Unit* unitB) { return unitA->GetPositionZ() < unitB->GetPositionZ(); }); + + // Gather alive ranged DPS bots only (no real players) + std::vector rangedDps; + Group* group = bot->GetGroup(); + if (group) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && botAI->IsRangedDps(member)) + rangedDps.push_back(member); + } + } + else + { + rangedDps.push_back(bot); + } + + // Greedy 1-to-1 assignment: hunters first, druids second, then any ranged DPS + static float const MAX_ASSIGN_RANGE = 80.0f; + std::set assigned; + for (Unit* bomb : kineticBombs) + { + Player* nearest = nullptr; + float nearestDist = std::numeric_limits::max(); + + // Priority classes: hunter > druid > any + static constexpr std::array classPriority = {CLASS_HUNTER, CLASS_DRUID, 0}; + for (uint8 priorityClass : classPriority) + { + nearest = nullptr; + nearestDist = std::numeric_limits::max(); + + for (Player* dps : rangedDps) + { + if (assigned.count(dps)) + continue; + + if (priorityClass != 0 && dps->getClass() != priorityClass) + continue; + + float dist = dps->GetDistance(bomb); + if (dist < nearestDist && dist < MAX_ASSIGN_RANGE) + { + nearestDist = dist; + nearest = dps; + } + } + + if (nearest) + break; + } + + if (nearest) + { + assigned.insert(nearest); + if (nearest == bot) + return bomb; + } + } + + return nullptr; +} + +bool IccBpcBallOfFlameAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "prince taldaram"); + if (!boss) + return false; + + Unit* ballOfFlameUnit = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f); + Unit* infernoFlameUnit = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f); + + bool const hasBallOfFlame = ballOfFlameUnit && (ballOfFlameUnit->GetVictim() == bot); + bool const hasInfernoFlame = infernoFlameUnit && (infernoFlameUnit->GetVictim() == bot); + + float infernoDist = infernoFlameUnit ? infernoFlameUnit->GetDistance2d(boss) : 0.0f; + // Hunters excluded — they can DPS from range without needing to soak + if (infernoFlameUnit && infernoDist > 2.0f && infernoDist <= 10.0f && !hasInfernoFlame && + bot->getClass() != CLASS_HUNTER) + { + if (!botAI->IsTank(bot) && infernoFlameUnit->GetVictim() != bot) + { + float flameX = infernoFlameUnit->GetPositionX(); + float flameY = infernoFlameUnit->GetPositionY(); + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + + // Calculate direction vector + float dx = flameX - botX; + float dy = flameY - botY; + float distance = std::sqrt(dx * dx + dy * dy); + + // Normalize and scale to 5 units (or remaining distance if less than 5) + float step = std::min(5.0f, distance); + if (distance > 0.001f) + { + dx = dx / distance * step; + dy = dy / distance * step; + } + + // Calculate intermediate position + float newX = botX + dx; + float newY = botY + dy; + + MoveTo(infernoFlameUnit->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // If victim of ball of flame, keep at least 15f from other party members + if (hasBallOfFlame || hasInfernoFlame) + { + static float const SAFE_DIST = 15.0f; + GuidVector members = AI_VALUE(GuidVector, "group members"); + for (auto const& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + float dist = bot->GetExactDist2d(member); + if (dist < SAFE_DIST) + { + // Move away from this member + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) + continue; + dx /= len; + dy /= len; + float moveX = bot->GetPositionX() + dx * (SAFE_DIST - dist + 1.0f); + float moveY = bot->GetPositionY() + dy * (SAFE_DIST - dist + 1.0f); + float moveZ = bot->GetPositionZ(); + if (bot->IsWithinLOS(moveX, moveY, moveZ)) + { + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + } + } + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BQL.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BQL.cpp new file mode 100644 index 000000000..350366879 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_BQL.cpp @@ -0,0 +1,1294 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCActions.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "Vehicle.h" +#include "RtiValue.h" +#include "GenericSpellActions.h" +#include "GenericActions.h" +#include "ICCTriggers.h" +#include "Multiplier.h" + +bool IccBqlGroupPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); + if (!boss) + return false; + + Aura* frenzyAura = botAI->GetAura("Frenzied Bloodthirst", bot); + Aura* shadowAura = botAI->GetAura("Swarming Shadows", bot); + bool isTank = botAI->IsTank(bot); + if (isTank && shadowAura) + { + bot->RemoveAurasDueToSpell(shadowAura->GetId()); + return true; + } + + // Handle tank positioning + if (isTank && HandleTankPosition(boss, frenzyAura, shadowAura)) + return true; + + // Handle swarming shadows movement + if (shadowAura && HandleShadowsMovement()) + return true; + + // Handle group positioning + if (!frenzyAura && !shadowAura && HandleGroupPosition(boss, frenzyAura, shadowAura)) + return true; + + return false; +} + +bool IccBqlGroupPositionAction::HandleTankPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura) +{ + if (frenzyAura || shadowAura) + return false; + + // Main tank positioning + if (botAI->IsMainTank(bot) && botAI->HasAggro(boss)) + { + if (bot->GetExactDist2d(ICC_BQL_TANK_POSITION) > 3.0f) + { + MoveTo(bot->GetMapId(), ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY(), + ICC_BQL_TANK_POSITION.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // Assist tank positioning + if (botAI->IsAssistTank(bot) && !botAI->GetAura("Blood Mirror", bot)) + { + if (Unit* mainTank = AI_VALUE(Unit*, "main tank")) + { + MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + } + + if (botAI->IsAssistTank(bot) && botAI->GetAura("Blood Mirror", bot) && boss && boss->HealthAbovePct(90)) + return true; // don't do anything to avoid taking bite + + return false; +} + +bool IccBqlGroupPositionAction::HandleShadowsMovement() +{ + float const SAFE_SHADOW_DIST = 4.0f; + float const ARC_STEP = 0.05f; + float const LANE_STEP = 0.15f; + int const MAX_CURVES = 3; + float const maxClosestDist = botAI->IsMelee(bot) ? 25.0f : 20.0f; + Position const& center = ICC_BQL_CENTER_POSITION; + float const OUTER_CURVE_PREFERENCE = 200.0f; // Strong preference for outer curves + float const CURVE_SWITCH_PENALTY = 50.0f; // Penalty for switching curves + float const DISTANCE_PENALTY_FACTOR = 100.0f; // Penalty per yard moved from current position + float const MAX_CURVE_JUMP_DIST = 12.0f; // Maximum distance for jumping between curves + + // Track current curve to avoid unnecessary switching (keyed per-instance to avoid + // cross-instance pollution when multiple ICCs run simultaneously) + auto& botCurrentCurve = IcecrownHelpers::IccState(bot->GetInstanceId()).bqlBotCurrentCurve; + ObjectGuid curveKey = bot->GetGUID(); + bool hasLane = botCurrentCurve.count(curveKey) != 0; + int currentCurve = hasLane ? botCurrentCurve[curveKey] : 0; + + // Find closest wall path + Position lwall[4] = {ICC_BQL_LWALL1_POSITION, AdjustControlPoint(ICC_BQL_LWALL2_POSITION, center, 1.30f), + AdjustControlPoint(ICC_BQL_LWALL3_POSITION, center, 1.30f), ICC_BQL_LRWALL4_POSITION}; + Position rwall[4] = {ICC_BQL_RWALL1_POSITION, AdjustControlPoint(ICC_BQL_RWALL2_POSITION, center, 1.30f), + AdjustControlPoint(ICC_BQL_RWALL3_POSITION, center, 1.30f), ICC_BQL_LRWALL4_POSITION}; + Position* basePath = (bot->GetExactDist2d(lwall[0]) < bot->GetExactDist2d(rwall[0])) ? lwall : rwall; + + // Find all swarming shadows + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + constexpr int MAX_SHADOW_NPCS = 100; + Unit* shadows[MAX_SHADOW_NPCS]{}; // Reasonable max estimate + int shadowCount = 0; + for (uint32 i = 0; i < npcs.size() && shadowCount < MAX_SHADOW_NPCS; i++) + { + Unit* unit = botAI->GetUnit(npcs[i]); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) + shadows[shadowCount++] = unit; + } + + // Helper lambda to check if a position is inside a shadow + auto IsPositionInShadow = [&](Position const& pos) -> bool + { + for (int i = 0; i < shadowCount; ++i) + { + if (pos.GetExactDist2d(shadows[i]) < SAFE_SHADOW_DIST) + return true; + } + return false; + }; + + CurveInfo bestCurve; + bestCurve.foundSafe = false; + bestCurve.score = FLT_MAX; + bool foundCurve = false; + + // Keep track of information about all curves for possible fallback + CurveInfo curveInfos[MAX_CURVES]; + for (int i = 0; i < MAX_CURVES; i++) + { + curveInfos[i].foundSafe = false; + curveInfos[i].score = FLT_MAX; + } + + // Evaluate all curves starting from outermost (lowest index) + for (int curveIdx = 0; curveIdx < MAX_CURVES; curveIdx++) + { + float laneScale = 1.0f - float(curveIdx) * LANE_STEP; + Position path[4] = {AdjustControlPoint(basePath[0], center, laneScale), + AdjustControlPoint(basePath[1], center, laneScale), + AdjustControlPoint(basePath[2], center, laneScale), + AdjustControlPoint(basePath[3], center, laneScale)}; + + // Find closest point on curve + float minDist = 9999.0f; + float t_closest = 0.0f; + Position closestPoint = path[0]; + + for (float t = 0.0f; t <= 1.0f; t += ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + float dist = bot->GetExactDist2d(pt); + if (dist < minDist) + { + minDist = dist; + t_closest = t; + closestPoint = pt; + } + } + + // Check if the closest point is safe + bool closestIsSafe = !IsPositionInShadow(closestPoint); + + // Find closest safe point by searching in both directions from closest point + Position safeMoveTarget = closestPoint; + bool foundSafe = closestIsSafe; + + // Only search for safe spots if the closest point isn't already safe + if (!closestIsSafe) + { + // Find the nearest safe point along the curve, not by direct distance + // but by distance along the curve from the closest point + + // Search forward on curve from closest point + float forwardT = -1.0f; + Position forwardPt; + for (float t = t_closest + ARC_STEP; t <= 1.0f; t += ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + if (!IsPositionInShadow(pt)) + { + forwardT = t; + forwardPt = pt; + break; + } + } + + // Search backward on curve from closest point + float backwardT = -1.0f; + Position backwardPt; + for (float t = t_closest - ARC_STEP; t >= 0.0f; t -= ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + if (!IsPositionInShadow(pt)) + { + backwardT = t; + backwardPt = pt; + break; + } + } + + // Choose the closest safe point based on curve distance, not direct distance + if (forwardT >= 0 && backwardT >= 0) + { + // Both directions have safe points, choose the closer one by curve distance + if (std::abs(forwardT - t_closest) < std::abs(backwardT - t_closest)) + { + safeMoveTarget = forwardPt; + foundSafe = true; + } + else + { + safeMoveTarget = backwardPt; + foundSafe = true; + } + } + else if (forwardT >= 0) + { + safeMoveTarget = forwardPt; + foundSafe = true; + } + else if (backwardT >= 0) + { + safeMoveTarget = backwardPt; + foundSafe = true; + } + } + + // Score this curve + float distancePenalty = 0.0f; + float score = 0.0f; + + if (foundSafe) + { + // If we found a safe point, penalize based on travel distance along the curve to reach it + float safeDist = bot->GetExactDist2d(safeMoveTarget); + + // Add distance penalty based on how far we need to move along the curve + distancePenalty = safeDist * (1.0f / DISTANCE_PENALTY_FACTOR); + score = safeDist + distancePenalty; + } + else + { + // No safe point found, assign a high score + distancePenalty = minDist * (1.0f / DISTANCE_PENALTY_FACTOR); + score = minDist + distancePenalty + 1000.0f; // Penalty for unsafe position + } + + // Apply strong penalty for curves that are too far + if (minDist > maxClosestDist) + score += 500.0f; + + // Apply penalty for unsafe curves + if (!foundSafe) + score += 1000.0f; + + // Apply curve index preference (strongly prefer outer curves) + score += curveIdx * OUTER_CURVE_PREFERENCE; + + // Apply curve switching penalty + if (hasLane && curveIdx != currentCurve) + score += CURVE_SWITCH_PENALTY; + + // MORE IMPORTANT: Apply additional curve switching penalty if the bot is far away + // from the target curve (prevent jumping between curves when far away) + if (curveIdx != currentCurve && minDist > MAX_CURVE_JUMP_DIST) + score += 2000.0f; // Strong penalty to prevent jumping between curves + + // Store this curve's info + curveInfos[curveIdx].moveTarget = foundSafe ? safeMoveTarget : closestPoint; + curveInfos[curveIdx].foundSafe = foundSafe; + curveInfos[curveIdx].minDist = minDist; + curveInfos[curveIdx].curveIdx = curveIdx; + curveInfos[curveIdx].score = score; + curveInfos[curveIdx].closestPoint = closestPoint; + curveInfos[curveIdx].t_closest = t_closest; + + // Only update if this curve is better than our current best + if (!foundCurve || score < bestCurve.score) + { + bestCurve = curveInfos[curveIdx]; + foundCurve = true; + } + } + + // Fallback: If we're trying to switch to a far curve and we're not near any curve, + // find and use the closest curve instead of making a direct beeline + if (foundCurve && bestCurve.minDist > MAX_CURVE_JUMP_DIST && bestCurve.curveIdx != currentCurve) + { + // Look for the closest curve first + float closestDist = FLT_MAX; + int closestCurveIdx = -1; + + for (int i = 0; i < MAX_CURVES; i++) + { + if (curveInfos[i].minDist < closestDist) + { + closestDist = curveInfos[i].minDist; + closestCurveIdx = i; + } + } + + // If we found a closer curve, use that instead + if (closestCurveIdx >= 0 && closestCurveIdx != bestCurve.curveIdx) + { + bestCurve = curveInfos[closestCurveIdx]; + } + } + + // Remember the selected curve for next time + if (foundCurve) + { + botCurrentCurve[curveKey] = bestCurve.curveIdx; + } + + // Every lane blocked and standing in a shadow - escape to room center + if (foundCurve && !bestCurve.foundSafe && IsPositionInShadow(bot->GetPosition())) + { + MoveTo(bot->GetMapId(), center.GetPositionX(), center.GetPositionY(), center.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + return false; + } + + // Create a move plan to guide the bot along the curve if necessary + if (foundCurve && bot->GetExactDist2d(bestCurve.moveTarget) > 1.0f) + { + // Final check: ensure we're not moving into a shadow + if (!IsPositionInShadow(bestCurve.moveTarget)) + { + float laneScale = 1.0f - float(bestCurve.curveIdx) * LANE_STEP; + Position path[4] = {AdjustControlPoint(basePath[0], center, laneScale), + AdjustControlPoint(basePath[1], center, laneScale), + AdjustControlPoint(basePath[2], center, laneScale), + AdjustControlPoint(basePath[3], center, laneScale)}; + + // CRITICAL CHANGE: First check if we need to move to the curve + float distToClosestPoint = bot->GetExactDist2d(bestCurve.closestPoint); + + // If we're not on the curve yet, first move to the closest point on the curve + if (distToClosestPoint > 2.0f) + { + botAI->Reset(); + return MoveTo(bot->GetMapId(), bestCurve.closestPoint.GetPositionX(), + bestCurve.closestPoint.GetPositionY(), bestCurve.closestPoint.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + // Now we know we're on or very close to the curve, so we'll follow it properly + + // Find target point on curve (t_target parameter) + float t_target = 0.0f; + float targetMinDist = 9999.0f; + + for (float t = 0.0f; t <= 1.0f; t += ARC_STEP) + { + Position pt = CalculateBezierPoint(t, path); + float dist = bestCurve.moveTarget.GetExactDist2d(pt); + if (dist < targetMinDist) + { + targetMinDist = dist; + t_target = t; + } + } + + // Find an intermediate point along the curve between closest and target + float t_step = (t_target > bestCurve.t_closest) ? ARC_STEP : -ARC_STEP; + float t_intermediate = bestCurve.t_closest + t_step; + Position intermediateTarget; + bool foundValidIntermediate = false; + + // Limit the distance we move along the curve in one step + float const MAX_CURVE_MOVEMENT = 7.0f; // Max yards to move along curve + float curveDistanceMoved = 0.0f; + Position lastPos = bestCurve.closestPoint; + + while ((t_step > 0 && t_intermediate <= t_target) || (t_step < 0 && t_intermediate >= t_target)) + { + Position pt = CalculateBezierPoint(t_intermediate, path); + + // Check if this point is safe + if (!IsPositionInShadow(pt)) + { + // Calculate distance moved along curve so far + curveDistanceMoved += lastPos.GetExactDist2d(pt); + lastPos = pt; + + // If we've moved the maximum allowed distance, use this position + if (curveDistanceMoved >= MAX_CURVE_MOVEMENT) + { + intermediateTarget = pt; + foundValidIntermediate = true; + break; + } + + // Otherwise, continue moving along the curve + intermediateTarget = pt; + foundValidIntermediate = true; + } + else + { + // We've hit a shadow, stop here + break; + } + + t_intermediate += t_step; + } + + // If we found a valid intermediate point, use it + if (foundValidIntermediate) + { + botAI->Reset(); + MoveTo(bot->GetMapId(), intermediateTarget.GetPositionX(), intermediateTarget.GetPositionY(), + intermediateTarget.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + else + { + botAI->Reset(); + // Fallback to direct movement to the target point on the curve + MoveTo(bot->GetMapId(), bestCurve.moveTarget.GetPositionX(), bestCurve.moveTarget.GetPositionY(), + bestCurve.moveTarget.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + return false; +} + +Position IccBqlGroupPositionAction::AdjustControlPoint(Position const& wall, Position const& center, float factor) +{ + float dx = wall.GetPositionX() - center.GetPositionX(); + float dy = wall.GetPositionY() - center.GetPositionY(); + float dz = wall.GetPositionZ() - center.GetPositionZ(); + return Position(center.GetPositionX() + dx * factor, center.GetPositionY() + dy * factor, + center.GetPositionZ() + dz * factor); +} + +Position IccBqlGroupPositionAction::CalculateBezierPoint(float t, Position const path[4]) +{ + float omt = 1 - t; + float omt2 = omt * omt; + float omt3 = omt2 * omt; + float t2 = t * t; + float t3 = t2 * t; + + float x = omt3 * path[0].GetPositionX() + 3 * omt2 * t * path[1].GetPositionX() + + 3 * omt * t2 * path[2].GetPositionX() + t3 * path[3].GetPositionX(); + + float y = omt3 * path[0].GetPositionY() + 3 * omt2 * t * path[1].GetPositionY() + + 3 * omt * t2 * path[2].GetPositionY() + t3 * path[3].GetPositionY(); + + float z = omt3 * path[0].GetPositionZ() + 3 * omt2 * t * path[1].GetPositionZ() + + 3 * omt * t2 * path[2].GetPositionZ() + t3 * path[3].GetPositionZ(); + + return Position(x, y, z); +} + +bool IccBqlGroupPositionAction::HandleGroupPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura) +{ + if (frenzyAura || shadowAura) + return false; + + GuidVector members = AI_VALUE(GuidVector, "group members"); + bool isRanged = botAI->IsRanged(bot); + bool isTank = botAI->IsTank(bot); + bool isMeleeDps = botAI->IsMelee(bot) && !isTank; + + // Air-phase latch: only arm once boss has been anchored at tank pos (ground phase + // established). Prevents false-trigger at pull when boss comes near center. + // Disarm when boss returns to tank pos (ground phase resumed after landing). + // Keyed per-instance so concurrent ICC raids don't share the latch. + // Armed when boss lands from air, disarmed when boss returns to tank pos. + // While armed, bots skip the pre-air center stack so they don't bunch up at center + // during the post-air walk-back and die to lingering AoE. + uint32 instanceId = bot->GetInstanceId(); + IcecrownHelpers::IccInstanceState& bqlState = IcecrownHelpers::IccState(instanceId); + bool& groundPhaseEstablished = bqlState.bqlGroundPhaseEstablished; + bool& wasAirborne = bqlState.bqlBossWasAirborne; + bool& postAirLanding = bqlState.bqlPostAirLanding; + float bossFromTank = boss->GetExactDist2d(ICC_BQL_TANK_POSITION); + float bossFromCenter = boss->GetExactDist2d(ICC_BQL_CENTER_POSITION); + bool bossAirborne = (boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f; + + // Landing edge: arm post-air latch + if (wasAirborne && !bossAirborne) + postAirLanding = true; + wasAirborne = bossAirborne; + + if (!bossAirborne && bossFromTank < 10.0f) + { + groundPhaseEstablished = true; + postAirLanding = false; + } + + bool bossMovingToCenter = groundPhaseEstablished && !bossAirborne && !postAirLanding && + bossFromCenter < 20.0f && bossFromTank > 10.0f; + bool isAirPhase = bossAirborne || bossMovingToCenter; + + // Pre-airborne: nitro boost + move all bots to center (not ring slots yet) + if (bossMovingToCenter) + { + float cx = ICC_BQL_CENTER_POSITION.GetPositionX(); + float cy = ICC_BQL_CENTER_POSITION.GetPositionY(); + float cz = ICC_BQL_CENTER_POSITION.GetPositionZ(); + if (bot->GetExactDist2d(cx, cy) > 2.0f && bot->IsWithinLOS(cx, cy, cz)) + { + MoveTo(bot->GetMapId(), cx, cy, cz, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + return true; // block until at center + } + return false; // at center, let combat rotation run + } + + // Air phase: every bot spreads around room center on concentric rings, hunters outer + if (bossAirborne) + { + // Bloodbolt Whirl is heavy raid-wide damage — pop personal defensive CD to survive + if (boss->FindCurrentSpellBySpellId(SPELL_BLOODBOLT_WHIRL)) + { + static char const* defensives[] = { + "shield wall", "last stand", "icebound fortitude", "survival instincts", + "barkskin", "dispersion", "ice block", "divine shield", "divine protection", + "evasion", "cloak of shadows", "deterrence", "shamanistic rage" + }; + for (char const* spell : defensives) + { + if (botAI->CanCastSpell(spell, bot)) + { + botAI->CastSpell(spell, bot); + break; + } + } + } + + float const SHADOW_AVOID_DIST = 7.0f; + + std::vector hunters; + std::vector nonHunters; + std::vector tanks; + for (auto const& guid : members) + { + Unit* member = botAI->GetUnit(guid); + if (!member || !member->IsAlive()) + continue; + Player* player = member->ToPlayer(); + if (!player) + continue; + if (!sPlayerbotsMgr.GetPlayerbotAI(player)) + continue; + // Frenzied biters roam freely to reach their bite target — exclude from slot pool + // (and tank stack) so we don't reserve a slot for them and so other bots can use + // any slot they left. + if (botAI->GetAura("Frenzied Bloodthirst", player)) + continue; + if (botAI->IsTank(player)) + { + tanks.push_back(player); + continue; + } + if (player->getClass() == CLASS_HUNTER) + hunters.push_back(player); + else + nonHunters.push_back(player); + } + + auto guidSort = [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }; + std::sort(hunters.begin(), hunters.end(), guidSort); + std::sort(nonHunters.begin(), nonHunters.end(), guidSort); + std::sort(tanks.begin(), tanks.end(), guidSort); + + // Tanks stack together at lowest-GUID tank. Anchor tank sits at fixed center position. + if (isTank) + { + Player* anchorTank = tanks.empty() ? nullptr : tanks.front(); + float tx, ty, tz; + if (anchorTank == bot || !anchorTank) + { + tx = ICC_BQL_CENTER_POSITION.GetPositionX(); + ty = ICC_BQL_CENTER_POSITION.GetPositionY(); + tz = ICC_BQL_CENTER_POSITION.GetPositionZ(); + } + else + { + tx = anchorTank->GetPositionX(); + ty = anchorTank->GetPositionY(); + tz = anchorTank->GetPositionZ(); + } + if (bot->GetExactDist2d(tx, ty) > 2.0f && bot->IsWithinLOS(tx, ty, tz)) + { + MoveTo(bot->GetMapId(), tx, ty, tz, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + return true; + } + return false; + } + + // Roster order: hunters first (they get outer slots), then others (inner slots) + std::vector roster; + roster.insert(roster.end(), hunters.begin(), hunters.end()); + roster.insert(roster.end(), nonHunters.begin(), nonHunters.end()); + + int myIndex = -1; + for (int i = 0; i < (int)roster.size(); i++) + { + if (roster[i] == bot) + { + myIndex = i; + break; + } + } + if (myIndex < 0) + return false; + + // 4 rings around boss center, 73 slots total, all pairs >=7f apart, ring gap 8f + struct AirSlot { float radius; float angle; }; + float const D2R = float(M_PI) / 180.0f; + static AirSlot const allSlots[] = { + // Inner ring r=10f, 8 slots at 45° — chord 7.65f + {10.0f, 0.0f * D2R}, {10.0f, 45.0f * D2R}, {10.0f, 90.0f * D2R}, {10.0f, 135.0f * D2R}, + {10.0f, 180.0f * D2R}, {10.0f, 225.0f * D2R}, {10.0f, 270.0f * D2R}, {10.0f, 315.0f * D2R}, + // Ring 2 r=18f, 15 slots at 24° — chord 7.5f + {18.0f, 0.0f * D2R}, {18.0f, 24.0f * D2R}, {18.0f, 48.0f * D2R}, {18.0f, 72.0f * D2R}, + {18.0f, 96.0f * D2R}, {18.0f, 120.0f * D2R}, {18.0f, 144.0f * D2R}, {18.0f, 168.0f * D2R}, + {18.0f, 192.0f * D2R}, {18.0f, 216.0f * D2R}, {18.0f, 240.0f * D2R}, {18.0f, 264.0f * D2R}, + {18.0f, 288.0f * D2R}, {18.0f, 312.0f * D2R}, {18.0f, 336.0f * D2R}, + // Ring 3 r=26f, 20 slots at 18° — chord 8.1f + {26.0f, 0.0f * D2R}, {26.0f, 18.0f * D2R}, {26.0f, 36.0f * D2R}, {26.0f, 54.0f * D2R}, + {26.0f, 72.0f * D2R}, {26.0f, 90.0f * D2R}, {26.0f, 108.0f * D2R}, {26.0f, 126.0f * D2R}, + {26.0f, 144.0f * D2R}, {26.0f, 162.0f * D2R}, {26.0f, 180.0f * D2R}, {26.0f, 198.0f * D2R}, + {26.0f, 216.0f * D2R}, {26.0f, 234.0f * D2R}, {26.0f, 252.0f * D2R}, {26.0f, 270.0f * D2R}, + {26.0f, 288.0f * D2R}, {26.0f, 306.0f * D2R}, {26.0f, 324.0f * D2R}, {26.0f, 342.0f * D2R}, + // Outer ring r=34f, 30 slots at 12° — chord 7.1f + {34.0f, 0.0f * D2R}, {34.0f, 12.0f * D2R}, {34.0f, 24.0f * D2R}, {34.0f, 36.0f * D2R}, + {34.0f, 48.0f * D2R}, {34.0f, 60.0f * D2R}, {34.0f, 72.0f * D2R}, {34.0f, 84.0f * D2R}, + {34.0f, 96.0f * D2R}, {34.0f, 108.0f * D2R}, {34.0f, 120.0f * D2R}, {34.0f, 132.0f * D2R}, + {34.0f, 144.0f * D2R}, {34.0f, 156.0f * D2R}, {34.0f, 168.0f * D2R}, {34.0f, 180.0f * D2R}, + {34.0f, 192.0f * D2R}, {34.0f, 204.0f * D2R}, {34.0f, 216.0f * D2R}, {34.0f, 228.0f * D2R}, + {34.0f, 240.0f * D2R}, {34.0f, 252.0f * D2R}, {34.0f, 264.0f * D2R}, {34.0f, 276.0f * D2R}, + {34.0f, 288.0f * D2R}, {34.0f, 300.0f * D2R}, {34.0f, 312.0f * D2R}, {34.0f, 324.0f * D2R}, + {34.0f, 336.0f * D2R}, {34.0f, 348.0f * D2R}, + }; + int const totalSlots = sizeof(allSlots) / sizeof(allSlots[0]); + + float const cx = ICC_BQL_CENTER_POSITION.GetPositionX(); + float const cy = ICC_BQL_CENTER_POSITION.GetPositionY(); + float const cz = ICC_BQL_CENTER_POSITION.GetPositionZ(); + + // Shadow safety (rare during air phase but possible at transition) + std::list shadowList; + bot->GetCreatureListWithEntryInGrid(shadowList, NPC_SWARMING_SHADOWS, 100.0f); + auto IsInShadow = [&](float x, float y) -> bool + { + for (Creature* shadow : shadowList) + { + if (!shadow->IsAlive()) + continue; + float sdx = x - shadow->GetPositionX(); + float sdy = y - shadow->GetPositionY(); + if ((sdx * sdx + sdy * sdy) < SHADOW_AVOID_DIST * SHADOW_AVOID_DIST) + return true; + } + return false; + }; + auto AirSlotPos = [&](int idx, float& x, float& y) + { + float r = allSlots[idx].radius; + float a = allSlots[idx].angle; + x = cx + r * std::cos(a); + y = cy + r * std::sin(a); + }; + auto IsAirSlotSafe = [&](int idx) -> bool + { + float sx, sy; + AirSlotPos(idx, sx, sy); + return !IsInShadow(sx, sy); + }; + + // Persistent memory separate from ground phase (different slot sets). + // Keyed per-instance to avoid cross-instance pollution. + uint32 const airInstanceId = bot->GetInstanceId(); + auto& airSlotMemory = IcecrownHelpers::IccState(airInstanceId).bqlAirSlotMemory; + + std::vector reservedSlots; + for (Player* rp : roster) + { + if (rp == bot) + continue; + auto it = airSlotMemory.find(rp->GetGUID()); + if (it != airSlotMemory.end() && it->second >= 0 && it->second < totalSlots) + reservedSlots.push_back(it->second); + } + auto IsReserved = [&](int s) -> bool + { + return std::find(reservedSlots.begin(), reservedSlots.end(), s) != reservedSlots.end(); + }; + + bool botInShadow = IsInShadow(bot->GetPositionX(), bot->GetPositionY()); + + int myAssignedSlot = -1; + ObjectGuid myAirKey = bot->GetGUID(); + + auto myMemIt = airSlotMemory.find(myAirKey); + if (myMemIt != airSlotMemory.end()) + { + int prev = myMemIt->second; + if (prev >= 0 && prev < totalSlots && !IsReserved(prev) && IsAirSlotSafe(prev)) + myAssignedSlot = prev; + } + + // Pick a new slot — hunters start from outer ring going in, others from inner going out + if (myAssignedSlot < 0) + { + bool isHunter = (bot->getClass() == CLASS_HUNTER); + for (int attempt = 0; attempt < totalSlots; attempt++) + { + int s; + if (isHunter) + s = (totalSlots - 1) - ((myIndex + attempt) % totalSlots); + else + s = (myIndex + attempt) % totalSlots; + if (IsReserved(s)) + continue; + if (IsAirSlotSafe(s)) + { + myAssignedSlot = s; + break; + } + } + } + + if (myAssignedSlot < 0) + { + airSlotMemory.erase(myAirKey); + + // No safe slot available — if standing in shadow, flee away from nearest shadow + if (botInShadow) + { + Creature* nearest = nullptr; + float bestDist = 1e9f; + for (Creature* shadow : shadowList) + { + if (!shadow->IsAlive()) + continue; + float d = bot->GetExactDist2d(shadow); + if (d < bestDist) + { + bestDist = d; + nearest = shadow; + } + } + if (nearest) + { + float dx = bot->GetPositionX() - nearest->GetPositionX(); + float dy = bot->GetPositionY() - nearest->GetPositionY(); + float mag = std::sqrt(dx * dx + dy * dy); + if (mag > 0.001f) + { + float fleeX = bot->GetPositionX() + (dx / mag) * 10.0f; + float fleeY = bot->GetPositionY() + (dy / mag) * 10.0f; + float fleeZ = bot->GetPositionZ(); + bot->UpdateAllowedPositionZ(fleeX, fleeY, fleeZ); + MoveTo(bot->GetMapId(), fleeX, fleeY, fleeZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + } + else + { + airSlotMemory[myAirKey] = myAssignedSlot; + + float candidateX, candidateY; + AirSlotPos(myAssignedSlot, candidateX, candidateY); + float candidateZ = cz; + bot->UpdateAllowedPositionZ(candidateX, candidateY, candidateZ); + + MovementPriority prio = botInShadow ? MovementPriority::MOVEMENT_FORCED : MovementPriority::MOVEMENT_COMBAT; + float moveGate = botInShadow ? 0.0f : 1.0f; + + if (bot->IsWithinLOS(candidateX, candidateY, candidateZ) && + bot->GetExactDist2d(candidateX, candidateY) > moveGate) + { + MoveTo(bot->GetMapId(), candidateX, candidateY, candidateZ, false, false, false, true, + prio, true, false); + // Still moving to slot — block combat so we actually get there + return true; + } + } + // At slot (or fleeing shadow) — let combat rotation attack/heal + return false; + } + + if (isMeleeDps && !isAirPhase) + { + if (bot->GetDistance2d(boss) > 2.0f) + { + MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), boss->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + } + return false; + } + + // Ground phase ranged positioning — persistent slot assignment, only affected bots reassign + if (isRanged && !isAirPhase) + { + float const SHADOW_AVOID_DIST = 7.0f; + + // Gather ranged bots + melee bots (skip real players) + std::vector hunters; + std::vector otherRanged; + std::vector meleeBots; + for (auto const& guid : members) + { + Unit* member = botAI->GetUnit(guid); + if (!member || !member->IsAlive()) + continue; + Player* player = member->ToPlayer(); + if (!player) + continue; + if (!sPlayerbotsMgr.GetPlayerbotAI(player)) + continue; + if (botAI->IsRanged(player)) + { + if (player->getClass() == CLASS_HUNTER) + hunters.push_back(player); + else + otherRanged.push_back(player); + } + else if (botAI->IsMelee(player)) + { + meleeBots.push_back(player); + } + } + + auto guidSort = [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }; + std::sort(hunters.begin(), hunters.end(), guidSort); + std::sort(otherRanged.begin(), otherRanged.end(), guidSort); + std::sort(meleeBots.begin(), meleeBots.end(), guidSort); + + std::vector roster; + roster.insert(roster.end(), hunters.begin(), hunters.end()); + roster.insert(roster.end(), otherRanged.begin(), otherRanged.end()); + + int myIndex = -1; + for (int i = 0; i < (int)roster.size(); i++) + { + if (roster[i] == bot) + { + myIndex = i; + break; + } + } + if (myIndex < 0) + return false; + + // Fixed world-space anchor and direction + float const anchorX = ICC_BQL_TANK_POSITION.GetPositionX(); + float const anchorY = ICC_BQL_TANK_POSITION.GetPositionY(); + float const anchorZ = ICC_BQL_TANK_POSITION.GetPositionZ(); + float baseDx = ICC_BQL_CENTER_POSITION.GetPositionX() - anchorX; + float baseDy = ICC_BQL_CENTER_POSITION.GetPositionY() - anchorY; + float const anchorToCenter = std::atan2(baseDy, baseDx); + + // 19 fixed slots: 6 inner (20f) + 7 middle (27f) + 6 outer (35f) + struct Slot { float radius; float angleOffset; }; + float const D2R = float(M_PI) / 180.0f; + static Slot const allSlots[] = { + {20.0f, -60.0f * D2R}, {20.0f, -36.0f * D2R}, {20.0f, -12.0f * D2R}, + {20.0f, 12.0f * D2R}, {20.0f, 36.0f * D2R}, {20.0f, 60.0f * D2R}, + {27.0f, -45.0f * D2R}, {27.0f, -30.0f * D2R}, {27.0f, -15.0f * D2R}, + {27.0f, 0.0f}, {27.0f, 15.0f * D2R}, {27.0f, 30.0f * D2R}, + {27.0f, 45.0f * D2R}, + {35.0f, -60.0f * D2R}, {35.0f, -36.0f * D2R}, {35.0f, -12.0f * D2R}, + {35.0f, 12.0f * D2R}, {35.0f, 36.0f * D2R}, {35.0f, 60.0f * D2R}, + }; + int const totalSlots = sizeof(allSlots) / sizeof(allSlots[0]); + + // Shadow list + safety check + std::list shadowList; + bot->GetCreatureListWithEntryInGrid(shadowList, NPC_SWARMING_SHADOWS, 100.0f); + auto IsInShadow = [&](float x, float y) -> bool + { + for (Creature* shadow : shadowList) + { + if (!shadow->IsAlive()) + continue; + float sdx = x - shadow->GetPositionX(); + float sdy = y - shadow->GetPositionY(); + if ((sdx * sdx + sdy * sdy) < SHADOW_AVOID_DIST * SHADOW_AVOID_DIST) + return true; + } + return false; + }; + auto SlotPos = [&](int idx, float& x, float& y) + { + float angle = anchorToCenter + allSlots[idx].angleOffset; + float r = allSlots[idx].radius; + x = anchorX + r * std::cos(angle); + y = anchorY + r * std::sin(angle); + }; + auto IsSlotSafe = [&](int idx) -> bool + { + float sx, sy; + SlotPos(idx, sx, sy); + return !IsInShadow(sx, sy); + }; + + // Persistent per-bot slot memory shared across all bots. + // Keyed per-instance to avoid cross-instance pollution. + uint32 const groundInstanceId = bot->GetInstanceId(); + auto& botSlotMemory = IcecrownHelpers::IccState(groundInstanceId).bqlBotSlotMemory; + + // Collect every OTHER bot's remembered slot as "reserved" — each bot owns its own + // memory and we must respect their claim, even if we can't see the same shadows. + // This prevents cascading reassignments when one bot moves. + std::vector reservedSlots; + for (Player* rp : roster) + { + if (rp == bot) + continue; + auto it = botSlotMemory.find(rp->GetGUID()); + if (it != botSlotMemory.end() && it->second >= 0 && it->second < totalSlots) + reservedSlots.push_back(it->second); + } + + auto IsReserved = [&](int s) -> bool + { + return std::find(reservedSlots.begin(), reservedSlots.end(), s) != reservedSlots.end(); + }; + + int myAssignedSlot = -1; + bool myFellBack = false; + ObjectGuid myGroundKey = bot->GetGUID(); + + // Step 1: keep my remembered slot if still safe and not reserved by someone else + auto myMemIt = botSlotMemory.find(myGroundKey); + if (myMemIt != botSlotMemory.end()) + { + int prev = myMemIt->second; + if (prev >= 0 && prev < totalSlots && !IsReserved(prev) && IsSlotSafe(prev)) + myAssignedSlot = prev; + } + + // Step 2: need a new slot — pick first safe slot not reserved by others + if (myAssignedSlot < 0) + { + // Prefer slots by roster seniority (hunters/lower-GUID first get inner slots) + // Start from index myIndex to fall into "my natural zone", wrap around + for (int attempt = 0; attempt < totalSlots; attempt++) + { + int s = (myIndex + attempt) % totalSlots; + if (IsReserved(s)) + continue; + if (IsSlotSafe(s)) + { + myAssignedSlot = s; + break; + } + } + } + + if (myAssignedSlot < 0) + { + // No safe unreserved slot — fall back to melee. Forget my slot so others can use it. + botSlotMemory.erase(myGroundKey); + myFellBack = true; + } + else + { + botSlotMemory[myGroundKey] = myAssignedSlot; + } + + if (myAssignedSlot >= 0) + { + float candidateX, candidateY; + SlotPos(myAssignedSlot, candidateX, candidateY); + float candidateZ = anchorZ; + bot->UpdateAllowedPositionZ(candidateX, candidateY, candidateZ); + + if (bot->IsWithinLOS(candidateX, candidateY, candidateZ) && + bot->GetExactDist2d(candidateX, candidateY) > 1.0f) + { + MoveTo(bot->GetMapId(), candidateX, candidateY, candidateZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + else if (myFellBack && !meleeBots.empty()) + { + // No safe slot — flee to lowest-GUID melee bot until a safe slot frees up + Player* anchor = meleeBots.front(); + if (anchor != bot) + { + float ax = anchor->GetPositionX(); + float ay = anchor->GetPositionY(); + float az = anchor->GetPositionZ(); + if (bot->GetExactDist2d(ax, ay) > 3.0f && bot->IsWithinLOS(ax, ay, az)) + { + MoveTo(bot->GetMapId(), ax, ay, az, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + } + + return false; +} + +bool IccBqlPactOfDarkfallenAction::Execute(Event /*event*/) +{ + // Check if bot has Pact of the Darkfallen + if (!botAI->GetAura("Pact of the Darkfallen", bot)) + return false; + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Find other players with Pact of the Darkfallen + Player* tankWithAura = nullptr; + std::vector playersWithAura; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot) + continue; + if (botAI->GetAura("Pact of the Darkfallen", member)) + { + playersWithAura.push_back(member); + if (botAI->IsTank(member)) + tankWithAura = member; + } + } + + if (playersWithAura.empty()) + return false; + + // Determine target position + Position targetPos; + if (tankWithAura) + { + // If there's a tank with aura, everyone moves to the tank (including the tank itself for center positioning) + if (botAI->IsTank(bot)) + { + // If current bot is the tank, stay put or move slightly for better positioning + targetPos.Relocate(bot); + } + else + { + // Non-tank bots move to the tank + targetPos.Relocate(tankWithAura); + } + } + else if (playersWithAura.size() >= 2) + { + // Calculate center position of all players with aura (including bot) + CalculateCenterPosition(targetPos, playersWithAura); + } + else if (playersWithAura.size() == 1) + { + // Move to the single other player with aura + targetPos.Relocate(playersWithAura[0]); + } + else + { + // No valid movement case found + return true; + } + + // Move to target position if needed + return MoveToTargetPosition(targetPos, playersWithAura.size() + 1); // +1 to include the bot itself +} + +bool IccBqlPactOfDarkfallenAction::CalculateCenterPosition(Position& targetPos, const std::vector& playersWithAura) +{ + float sumX = bot->GetPositionX(); + float sumY = bot->GetPositionY(); + float sumZ = bot->GetPositionZ(); + + // Add positions of all other players with aura + for (Player* player : playersWithAura) + { + sumX += player->GetPositionX(); + sumY += player->GetPositionY(); + sumZ += player->GetPositionZ(); + } + + // Calculate average position (center) + int totalPlayers = playersWithAura.size() + 1; // +1 for the bot itself + targetPos.Relocate(sumX / totalPlayers, sumY / totalPlayers, sumZ / totalPlayers); + + return false; +} + +bool IccBqlPactOfDarkfallenAction::MoveToTargetPosition(Position const& targetPos, int auraCount) +{ + float const POSITION_TOLERANCE = 0.1f; + float distance = bot->GetDistance(targetPos); + if (distance <= POSITION_TOLERANCE) + return true; + + // Calculate movement increment + float dx = targetPos.GetPositionX() - bot->GetPositionX(); + float dy = targetPos.GetPositionY() - bot->GetPositionY(); + float dz = targetPos.GetPositionZ() - bot->GetPositionZ(); + float len = sqrt(dx * dx + dy * dy); + + float moveX, moveY, moveZ; + if (len > 5.0f && auraCount <= 2) + { + dx /= len; + dy /= len; + moveX = bot->GetPositionX() + dx * 5.0f; + moveY = bot->GetPositionY() + dy * 5.0f; + moveZ = bot->GetPositionZ() + (dz / distance) * 5.0f; + } + else + { + moveX = targetPos.GetPositionX(); + moveY = targetPos.GetPositionY(); + moveZ = targetPos.GetPositionZ(); + } + + botAI->Reset(); + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED); + return false; +} + +bool IccBqlVampiricBiteAction::Execute(Event /*event*/) +{ + // Only act when bot has Frenzied Bloodthirst + if (!botAI->GetAura("Frenzied Bloodthirst", bot)) + return false; + + // Bloodbolt Whirl defensive CDs (divine shield, ice block, cloak) make bot untargetable + // and block the bite cast. Strip them now so the bite can land. + botAI->RemoveAura("divine shield"); + botAI->RemoveAura("ice block"); + botAI->RemoveAura("cloak of shadows"); + botAI->RemoveAura("deterrence"); + + float const BITE_RANGE = 4.0f; + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Find best target + Player* target = FindBestBiteTarget(group); + if (!target) + return false; + + // Handle movement or casting + if (bot->GetExactDist2d(target) > BITE_RANGE) + return MoveTowardsTarget(target); + + return CastVampiricBite(target); +} + +Player* IccBqlVampiricBiteAction::FindBestBiteTarget(Group* group) +{ + // Collect frenzied biters (sorted by GUID) and all candidate targets. + // Lower-GUID biters claim their nearest valid target first; higher-GUID + // biters skip claimed targets. Deterministic across all bots so two biters + // never converge on the same victim. + std::vector biters; + std::vector dpsHealCandidates; + std::vector tankCandidates; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (botAI->GetAura("Frenzied Bloodthirst", member)) + { + biters.push_back(member); + continue; + } + + // Skip already-bitten / shadowed / mind-controlled + if (botAI->GetAura("Essence of the Blood Queen", member) || + botAI->GetAura("Uncontrollable Frenzy", member) || + botAI->GetAura("Swarming Shadows", member)) + continue; + + if (botAI->IsTank(member)) + tankCandidates.push_back(member); + else if (botAI->IsDps(member) || botAI->IsHeal(member)) + dpsHealCandidates.push_back(member); + } + + std::sort(biters.begin(), biters.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + auto pickFromPool = [&](std::vector& pool) -> Player* + { + std::set claimed; + Player* myPick = nullptr; + for (Player* biter : biters) + { + float bestDist = FLT_MAX; + Player* bestTarget = nullptr; + for (Player* cand : pool) + { + if (claimed.count(cand->GetGUID())) + continue; + float d = biter->GetDistance(cand); + if (d < bestDist) + { + bestDist = d; + bestTarget = cand; + } + } + if (!bestTarget) + continue; + claimed.insert(bestTarget->GetGUID()); + if (biter == bot) + { + myPick = bestTarget; + break; + } + } + return myPick; + }; + + if (Player* pick = pickFromPool(dpsHealCandidates)) + return pick; + + // Fallback: tanks only when no DPS/heal target available + return pickFromPool(tankCandidates); +} + +bool IccBqlVampiricBiteAction::IsInvalidTarget(Player* player) +{ + return botAI->GetAura("Frenzied Bloodthirst", player) || botAI->GetAura("Essence of the Blood Queen", player) || + botAI->GetAura("Uncontrollable Frenzy", player) || botAI->GetAura("Swarming Shadows", player); +} + +bool IccBqlVampiricBiteAction::MoveTowardsTarget(Player* target) +{ + if (IsInvalidTarget(target) || !target->IsAlive()) + return false; + + float x = target->GetPositionX(); + float y = target->GetPositionY(); + float z = target->GetPositionZ(); + + // Don't gate movement on LOS — if blocked, take a step toward target so LOS + // can clear next tick. Multiplier zeroes all non-bite actions for biters, + // so silently aborting here would idle the bot until the aura expires. + + float dx = x - bot->GetPositionX(); + float dy = y - bot->GetPositionY(); + float dz = z - bot->GetPositionZ(); + float len = sqrt(dx * dx + dy * dy); + + float moveX, moveY, moveZ; + if (len > 5.0f) + { + dx /= len; + dy /= len; + moveX = bot->GetPositionX() + dx * 5.0f; + moveY = bot->GetPositionY() + dy * 5.0f; + moveZ = bot->GetPositionZ() + (dz / len) * 5.0f; + } + else + { + moveX = x; + moveY = y; + moveZ = z; + } + + MoveTo(target->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + + return false; +} + +bool IccBqlVampiricBiteAction::CastVampiricBite(Player* target) +{ + if (IsInvalidTarget(target) || !target->IsAlive()) + return false; + + return botAI->CanCastSpell("Vampiric Bite", target) && botAI->CastSpell("Vampiric Bite", target); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_DBS.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_DBS.cpp new file mode 100644 index 000000000..f6e70835a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_DBS.cpp @@ -0,0 +1,381 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "RtiValue.h" +#include "Vehicle.h" + +bool IccDbsTankPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); + if (!boss) + return false; + + if (botAI->IsTank(bot)) + { + bool const hasRuneOfBlood = botAI->GetAura("Rune of Blood", bot) != nullptr; + + if (hasRuneOfBlood) + { + // Stop attacking boss but still taunt loose blood beasts + if (bot->GetVictim() == boss) + bot->AttackStop(); + + std::array const beastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, + NPC_BLOOD_BEAST4}; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + bool const isBloodBeast = + std::find(beastEntries.begin(), beastEntries.end(), unit->GetEntry()) != beastEntries.end(); + if (!isBloodBeast) + continue; + + Unit* victim = unit->GetVictim(); + Player* victimPlayer = victim ? victim->ToPlayer() : nullptr; + if (!victimPlayer || !botAI->IsTank(victimPlayer)) + { + IccCastClassTaunt(bot, botAI,unit); + break; + } + } + + if (bot->GetExactDist2d(ICC_DBS_TANK_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_DBS_TANK_POSITION.GetPositionX(), + ICC_DBS_TANK_POSITION.GetPositionY(), ICC_DBS_TANK_POSITION.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_NORMAL); + return true; + } + + // Tank without Rune of Blood: taunt boss if current tank has the debuff + Unit* currentTarget = boss->GetVictim(); + if (currentTarget && currentTarget != bot && botAI->GetAura("Rune of Blood", currentTarget)) + IccCastClassTaunt(bot, botAI,boss); + + // Taunt any blood beasts not targeting a tank + std::array const bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, + NPC_BLOOD_BEAST4}; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + bool const isBloodBeast = std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != + bloodBeastEntries.end(); + if (!isBloodBeast) + continue; + + Unit* victim = unit->GetVictim(); + Player* victimPlayer = victim ? victim->ToPlayer() : nullptr; + if (!victimPlayer || !botAI->IsTank(victimPlayer)) + { + IccCastClassTaunt(bot, botAI,unit); + break; + } + } + + if (bot->GetExactDist2d(ICC_DBS_TANK_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_DBS_TANK_POSITION.GetPositionX(), ICC_DBS_TANK_POSITION.GetPositionY(), + ICC_DBS_TANK_POSITION.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + + return false; + } + + if (!botAI->IsTank(bot)) + { + if (CrowdControlBloodBeasts()) + return true; + } + + // Handle ranged and healer positioning + if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) + { + // Handle evasion from blood beasts + if (EvadeBloodBeasts()) + return true; + + // Position in formation + return PositionInRangedFormation(); + } + + return false; +} + +bool IccDbsTankPositionAction::CrowdControlBloodBeasts() +{ + std::array const bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, + NPC_BLOOD_BEAST4}; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + // Check if this is a blood beast + bool const isBloodBeast = + std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != bloodBeastEntries.end(); + + if (!isBloodBeast) + continue; + + if (auto cast = IccTryClassCC(bot, botAI, unit)) + return *cast; + } + + return false; +} + +bool IccDbsTankPositionAction::EvadeBloodBeasts() +{ + float const evasionDistance = 12.0f; + std::array const bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, + NPC_BLOOD_BEAST4}; + + // Get the nearest hostile NPCs + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit) + continue; + + // Check if this is a blood beast + bool const isBloodBeast = + std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != bloodBeastEntries.end(); + + // Only evade if it's a blood beast targeting us + if (isBloodBeast && unit->GetVictim() == bot) + { + float const currentDistance = bot->GetDistance2d(unit); + + // Move away if too close + if (currentDistance < evasionDistance) + return MoveAway(unit, evasionDistance - currentDistance); + } + } + + return false; +} + +bool IccDbsTankPositionAction::PositionInRangedFormation() +{ + // Get group + Group* group = bot->GetGroup(); + if (!group) + return false; + + int32 const totalSlots = 15; // 3 rows x 5 cols + uint32 const dbsInstanceId = bot->GetInstanceId(); + + // Persistent per-bot slot memory shared across all bots. + // Keyed per-instance to avoid cross-instance pollution. + auto& botSlotMemory = IcecrownHelpers::IccState(dbsInstanceId).dbsBotSlotMemory; + ObjectGuid myKey = bot->GetGUID(); + + // Single pass: collect natural index (alive ranged/healer non-tank order) + // and other bots' reserved slots. + int32 myIndex = -1; + int32 currentIndex = 0; + std::vector reservedSlots; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member) + continue; + + if (member != bot) + { + auto it = botSlotMemory.find(member->GetGUID()); + if (it != botSlotMemory.end() && it->second >= 0 && it->second < totalSlots) + reservedSlots.push_back(it->second); + } + + if (!member->IsAlive()) + continue; + + if ((botAI->IsRanged(member) || botAI->IsHeal(member)) && !botAI->IsTank(member)) + { + if (member == bot) + { + myIndex = currentIndex; + } + currentIndex++; + } + } + + if (myIndex == -1) + return false; + + auto IsReserved = [&](int s) -> bool + { + return std::find(reservedSlots.begin(), reservedSlots.end(), s) != reservedSlots.end(); + }; + + int myAssignedSlot = -1; + + // Step 1: keep my remembered slot if still in range and not reserved by someone else. + auto myMemIt = botSlotMemory.find(myKey); + if (myMemIt != botSlotMemory.end()) + { + int prev = myMemIt->second; + if (prev >= 0 && prev < totalSlots && !IsReserved(prev)) + myAssignedSlot = prev; + } + + // Step 2: pick first unreserved slot, seeded from my natural index. + if (myAssignedSlot < 0) + { + for (int attempt = 0; attempt < totalSlots; attempt++) + { + int s = (myIndex + attempt) % totalSlots; + if (!IsReserved(s)) + { + myAssignedSlot = s; + break; + } + } + } + + // Step 3: overflow (16th+ ranged) - stack with closest non-tank melee bot, + // lowest GUID on tie. Forget my slot so others can take it. + if (myAssignedSlot < 0) + { + botSlotMemory.erase(myKey); + + Player* anchor = nullptr; + float anchorDist = 0.0f; + uint64 anchorGuidRaw = 0; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + if (!botAI->IsMelee(member) || botAI->IsTank(member)) + continue; + + float d = bot->GetExactDist2d(member); + uint64 g = member->GetGUID().GetRawValue(); + if (!anchor || d < anchorDist || (d == anchorDist && g < anchorGuidRaw)) + { + anchor = member; + anchorDist = d; + anchorGuidRaw = g; + } + } + + if (!anchor) + return false; + + float ax = anchor->GetPositionX(); + float ay = anchor->GetPositionY(); + float az = anchor->GetPositionZ(); + if (bot->GetExactDist2d(ax, ay) > 3.0f) + return MoveTo(bot->GetMapId(), ax, ay, az, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + return false; + } + + botSlotMemory[myKey] = myAssignedSlot; + + // Fixed positions calculation + float const tankToBossAngle = 3.14f; + float const minBossDistance = 11.0f; + float const spreadDistance = 10.0f; + int32 const columnsPerRow = 5; + + // Calculate position in a fixed grid (3 rows x 5 columns) + int32 const row = myAssignedSlot / columnsPerRow; + int32 const col = myAssignedSlot % columnsPerRow; + + // Calculate base position + float xOffset = (col - 2) * spreadDistance; // Center around tank position + float yOffset = minBossDistance + (row * spreadDistance); // Each row further back + + // Add zigzag offset for odd rows + if (row % 2 == 1) + xOffset += spreadDistance / 2.0f; + + // Rotate position based on tank-to-boss angle + float finalX = + ICC_DBS_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); + float finalY = + ICC_DBS_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); + float finalZ = ICC_DBS_TANK_POSITION.GetPositionZ(); + + // Update Z coordinate + bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); + + // Move if not in position + if (bot->GetExactDist2d(finalX, finalY) > 3.0f) + return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + + return false; +} + +bool IccAddsDbsAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); + if (!boss) + return false; + + // This action is only for melee + if (!botAI->IsMelee(bot)) + return false; + + Unit* priorityTarget = FindPriorityTarget(boss); + + // Update raid target icons if needed + UpdateSkullMarker(priorityTarget); + + return false; +} + +Unit* IccAddsDbsAction::FindPriorityTarget(Unit* boss) +{ + GuidVector const targets = AI_VALUE(GuidVector, "possible targets no los"); + + // Blood beast entry IDs + std::array const addEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, NPC_BLOOD_BEAST4}; + + // First check for alive adds + for (uint32 const entry : addEntries) + { + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + return unit; + } + } + + // Only fallback to boss if it's alive + return boss->IsAlive() ? const_cast(boss) : nullptr; +} + +bool IccAddsDbsAction::UpdateSkullMarker(Unit* priorityTarget) +{ + IccEnsureIconOn(bot, botAI, RtiTargetValue::skullIndex, priorityTarget); + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_Dogs.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_Dogs.cpp new file mode 100644 index 000000000..388472d39 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_Dogs.cpp @@ -0,0 +1,71 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" + +bool IccDogsTankPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "stinky"); + if (!boss) + boss = AI_VALUE2(Unit*, "find target", "precious"); + if (!boss) + return false; + + if (botAI->IsTank(bot)) + { + Aura* aura = botAI->GetAura("mortal wound", bot, false, true); + bool const hasMortalWound = aura && aura->GetStackAmount() >= 8; + + if (hasMortalWound) + { + if (bot->GetVictim() == boss) + bot->AttackStop(); + + return true; + } + + // Tank without high mortal wound stacks: taunt boss if current tank has the debuff + Unit* currentTarget = boss->GetVictim(); + if (currentTarget && currentTarget != bot) + { + Aura* victimAura = botAI->GetAura("mortal wound", currentTarget, false, true); + if (victimAura && victimAura->GetStackAmount() >= 8) + IccCastClassTaunt(bot, botAI,boss); + } + + // Taunt nearby hostile adds not targeting a tank + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + if (unit == boss) + continue; + + if (bot->GetDistance2d(unit) > 20.0f) + continue; + + Unit* victim = unit->GetVictim(); + Player* victimPlayer = victim ? victim->ToPlayer() : nullptr; + if (!victimPlayer || !botAI->IsTank(victimPlayer)) + { + IccCastClassTaunt(bot, botAI,unit); + break; + } + } + + return false; + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_FG.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_FG.cpp new file mode 100644 index 000000000..5cb08a662 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_FG.cpp @@ -0,0 +1,526 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "ICCScripts.h" +#include "RtiValue.h" +#include "Vehicle.h" +#include + +// Festergut +bool IccFestergutGroupPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss) + return false; + + bot->SetTarget(boss->GetGUID()); + + if (botAI->IsTank(bot)) + { + Aura* aura = botAI->GetAura("gastric bloat", bot, false, true); + bool const hasGastricBloat = aura && aura->GetStackAmount() >= 6; + + if (hasGastricBloat) + { + if (bot->GetVictim() == boss) + bot->AttackStop(); + + if (bot->GetExactDist2d(ICC_FESTERGUT_TANK_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_FESTERGUT_TANK_POSITION.GetPositionX(), + ICC_FESTERGUT_TANK_POSITION.GetPositionY(), ICC_FESTERGUT_TANK_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + return true; + } + + Unit* currentTarget = boss->GetVictim(); + if (currentTarget && currentTarget != bot) + { + Aura* victimAura = botAI->GetAura("gastric bloat", currentTarget, false, true); + if (victimAura && victimAura->GetStackAmount() >= 6) + IccCastClassTaunt(bot, botAI,boss); + } + + if (bot->GetExactDist2d(ICC_FESTERGUT_TANK_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_FESTERGUT_TANK_POSITION.GetPositionX(), + ICC_FESTERGUT_TANK_POSITION.GetPositionY(), ICC_FESTERGUT_TANK_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_NORMAL); + + return false; + } + + // Check for spores in the group + if (HasSporesInGroup()) + return false; + + // No spore, no goo dodge - melee stack on main tank. + if (botAI->IsMelee(bot)) + { + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (mainTank && bot->GetExactDist2d(mainTank) > 3.0f) + return MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), + mainTank->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + return false; + } + + // Position non-tank ranged and healers + return PositionNonTankMembers(); +} + +bool IccFestergutGroupPositionAction::HasSporesInGroup() +{ + return IccAnyGroupMemberHasAura(bot, SPELL_GAS_SPORE); +} + +bool IccFestergutGroupPositionAction::PositionNonTankMembers() +{ + // Only position ranged and healers without spores + if (!(botAI->IsRanged(bot) || botAI->IsHeal(bot))) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + int32 positionIndex = CalculatePositionIndex(group); + if (positionIndex == -1) + return false; + + // Position calculation parameters + constexpr float tankToBossAngle = 4.58f; + constexpr float minBossDistance = 15.0f; + constexpr float spreadDistance = 10.0f; + constexpr int32 columnsPerRow = 6; + + // Calculate grid position + int32 row = positionIndex / columnsPerRow; + int32 col = positionIndex % columnsPerRow; + + // Calculate base position + float xOffset = (col - 2) * spreadDistance; // Center around tank position + float yOffset = minBossDistance + (row * spreadDistance); // Each row further back + + // Add zigzag offset for odd rows + if (row % 2 == 1) + xOffset += spreadDistance / 2.0f; + + // Rotate position based on tank-to-boss angle + float finalX = + ICC_FESTERGUT_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); + float finalY = + ICC_FESTERGUT_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); + float finalZ = ICC_FESTERGUT_TANK_POSITION.GetPositionZ(); + + // Update Z coordinate + bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); + + // Move if not in position + if (bot->GetExactDist2d(finalX, finalY) > 3.0f) + return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + + return false; +} + +int32 IccFestergutGroupPositionAction::CalculatePositionIndex(Group* group) +{ + std::vector healerGuids; + std::vector rangedDpsGuids; + std::vector hunterGuids; + + // Collect all eligible members with their GUIDs + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || botAI->IsTank(member)) + continue; + + ObjectGuid memberGuid = member->GetGUID(); + + if (botAI->IsHeal(member)) + healerGuids.push_back(memberGuid); + else if (botAI->IsRanged(member)) + { + if (member->getClass() == CLASS_HUNTER) + hunterGuids.push_back(memberGuid); + else + rangedDpsGuids.push_back(memberGuid); + } + } + + // Sort GUIDs for consistent ordering across all bots + std::sort(healerGuids.begin(), healerGuids.end()); + std::sort(rangedDpsGuids.begin(), rangedDpsGuids.end()); + std::sort(hunterGuids.begin(), hunterGuids.end()); + + ObjectGuid botGuid = bot->GetGUID(); + + // Find which group this bot belongs to + auto healerIt = std::find(healerGuids.begin(), healerGuids.end(), botGuid); + auto rangedIt = std::find(rangedDpsGuids.begin(), rangedDpsGuids.end(), botGuid); + auto hunterIt = std::find(hunterGuids.begin(), hunterGuids.end(), botGuid); + + // Calculate global position index considering group constraints + int32 const healerRows = 2; + int32 const columnsPerRow = 6; + + // Healers: rows 0-1 (first two rows) + if (healerIt != healerGuids.end()) + { + int32 healerIndex = static_cast(std::distance(healerGuids.begin(), healerIt)); + + // Ensure healers only occupy first two rows + if (healerIndex < healerRows * columnsPerRow) + return healerIndex; + + // If too many healers, overflow to later rows but keep them early + return healerIndex; // Will be in row = index / 6, col = index % 6 + } + + // Non-hunter ranged DPS: can be any row (no strict restriction) + if (rangedIt != rangedDpsGuids.end()) + { + int32 rangedIndex = static_cast(std::distance(rangedDpsGuids.begin(), rangedIt)); + + // Start after all healers, then fill remaining spots + return static_cast(healerGuids.size()) + rangedIndex; + } + + // Hunters: never in 1st row (row 0) + if (hunterIt != hunterGuids.end()) + { + int32 hunterIndex = static_cast(std::distance(hunterGuids.begin(), hunterIt)); + + // Calculate how many non-healer positions are before this hunter position + int32 baseOffset = static_cast(healerGuids.size()) + static_cast(rangedDpsGuids.size()); + + // Each row of hunters starts at positions that are multiples of columnsPerRow + // To avoid row 0, skip first column slots reserved for healers/non-hunters + return baseOffset + hunterIndex; + } + + return -1; +} + +bool IccFestergutSporeAction::Execute(Event /*event*/) +{ + constexpr float positionTolerance = 4.0f; + + bool hasSpore = bot->HasAura(SPELL_GAS_SPORE); + + Position spreadRangedPos = CalculateSpreadPosition(); + SporeInfo sporeInfo = FindSporedPlayers(); + Position targetPos = DetermineTargetPosition(hasSpore, sporeInfo, spreadRangedPos); + + if (bot->GetExactDist2d(targetPos) > positionTolerance) + { + botAI->Reset(); + return MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + true, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + + // In position — let combat rotation run (multiplier blocks movement so bot stays put) + return false; +} + +Position IccFestergutSporeAction::CalculateSpreadPosition() +{ + constexpr float spreadRadius = 2.0f; + constexpr float gooNearSporeRadius = 12.0f; + constexpr uint32 impactLifetimeMs = 8000; + constexpr uint32 cycleIdleResetMs = 2000; + + // Group-wide sticky slot decision. Spore + malleable goo can overlap: if + // an active goo lands near the current spread spot we flip to the other + // spot, and stay there for the rest of this spore cycle so bots aren't + // pulled back into the danger zone when the goo expires. The action only + // runs while the spore trigger is active, so a gap >2s between calls + // means the cycle ended - reset to the primary slot for the next cycle. + // State is keyed by instance ID so concurrent ICC raids don't share slots. + IcecrownHelpers::SpreadSlotState& state = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()).fgSlotState; + + uint32 now = getMSTime(); + if (state.lastCallMs == 0 || now - state.lastCallMs > cycleIdleResetMs) + state.currentSlot = 1; + state.lastCallMs = now; + + Position currentSpot = (state.currentSlot == 2) ? ICC_FESTERGUT_RANGED_SPORE_2 + : ICC_FESTERGUT_RANGED_SPORE; + + for (Position const& goo : IcecrownHelpers::ActiveGooPositions(bot->GetMap()->GetInstanceId(), impactLifetimeMs)) + { + float dx = goo.GetPositionX() - currentSpot.GetPositionX(); + float dy = goo.GetPositionY() - currentSpot.GetPositionY(); + if (dx * dx + dy * dy < gooNearSporeRadius * gooNearSporeRadius) + { + state.currentSlot = (state.currentSlot == 1) ? 2 : 1; + currentSpot = (state.currentSlot == 2) ? ICC_FESTERGUT_RANGED_SPORE_2 + : ICC_FESTERGUT_RANGED_SPORE; + break; + } + } + + // Unique angle based on bot's GUID + float angle = (bot->GetGUID().GetCounter() % 16) * (M_PI / 8); + + Position spreadRangedPos = currentSpot; + spreadRangedPos.Relocate(spreadRangedPos.GetPositionX() + cos(angle) * spreadRadius, + spreadRangedPos.GetPositionY() + sin(angle) * spreadRadius, + spreadRangedPos.GetPositionZ(), spreadRangedPos.GetOrientation()); + + return spreadRangedPos; +} + +IccFestergutSporeAction::SporeInfo IccFestergutSporeAction::FindSporedPlayers() +{ + SporeInfo info; + GuidVector const members = AI_VALUE(GuidVector, "group members"); + + for (auto const& memberGuid : members) + { + Unit* unit = botAI->GetUnit(memberGuid); + if (!unit) + continue; + + if (unit->HasAura(SPELL_GAS_SPORE)) + { + info.sporedPlayers.push_back(unit); + + if (!info.hasLowestGuid || unit->GetGUID() < info.lowestGuid) + { + info.lowestGuid = unit->GetGUID(); + info.hasLowestGuid = true; + } + } + } + + return info; +} + +bool IccFestergutSporeAction::GooNear(Position const& pos) +{ + constexpr float gooDangerRadius = 12.0f; + + for (Position const& goo : IcecrownHelpers::ActiveGooPositions(bot->GetMap()->GetInstanceId(), 8000)) + { + float dx = pos.GetPositionX() - goo.GetPositionX(); + float dy = pos.GetPositionY() - goo.GetPositionY(); + if (dx * dx + dy * dy < gooDangerRadius * gooDangerRadius) + return true; + } + return false; +} + +Position IccFestergutSporeAction::DetermineTargetPosition(bool hasSpore, SporeInfo const& sporeInfo, + Position const& spreadRangedPos) +{ + // No spores at all + if (sporeInfo.sporedPlayers.empty()) + return botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; + + bool mainTankHasSpore = CheckMainTankSpore(); + + // Goo overlap override — checked before hasSpore so non-spored bots also redirect. + bool gooAtMelee = GooNear(ICC_FESTERGUT_MELEE_SPORE); + bool gooAtRanged = GooNear(ICC_FESTERGUT_RANGED_SPORE) || GooNear(ICC_FESTERGUT_RANGED_SPORE_2); + + if (gooAtMelee && !gooAtRanged) + { + // Goo at melee: tank + melee-spore bot hold, other melee flee to ranged slot 1. + bool isMeleeSporeBot = (hasSpore && bot->GetGUID() == sporeInfo.lowestGuid && !botAI->IsTank(bot) && !mainTankHasSpore); + if (botAI->IsMainTank(bot) || isMeleeSporeBot) + return ICC_FESTERGUT_MELEE_SPORE; + if (botAI->IsMelee(bot)) + return ICC_FESTERGUT_RANGED_SPORE; + return spreadRangedPos; + } + + if (gooAtRanged && !gooAtMelee) + { + // Goo at ranged: all ranged collapse to melee spot. + if (!botAI->IsMelee(bot)) + return ICC_FESTERGUT_MELEE_SPORE; + return ICC_FESTERGUT_MELEE_SPORE; + } + + // Normal spore logic (no overlap or both spots hit). + if (!hasSpore) + return botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; + + if (botAI->IsMainTank(bot)) + return ICC_FESTERGUT_MELEE_SPORE; + + if (bot->GetGUID() == sporeInfo.lowestGuid && !botAI->IsTank(bot) && !mainTankHasSpore) + return ICC_FESTERGUT_MELEE_SPORE; + + return spreadRangedPos; +} + +bool IccFestergutSporeAction::CheckMainTankSpore() +{ + GuidVector const members = AI_VALUE(GuidVector, "group members"); + + for (auto const& memberGuid : members) + { + Unit* unit = botAI->GetUnit(memberGuid); + if (!unit) + continue; + + if (botAI->IsMainTank(unit->ToPlayer()) && unit->HasAura(SPELL_GAS_SPORE)) + return true; + } + + return false; +} + +bool IccFestergutAvoidMalleableGooAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss) + return false; + + // Tanks hold aggro at the fixed tank spot - never dodge. + if (botAI->IsTank(bot)) + return false; + + // Festergut heroic - Putricide throws Malleable Goo from the balcony at + // random non-tank players. The impact is a triggered cast with no cast bar + // and no DynamicObject, so the IccPutricideListenerScript stamps the target's + // position into IcecrownHelpers::malleableGooImpacts at OnSpellCast time. + // Any bot within 12yd of an active impact must flee; danger persists 8s. + // Once a bot dodges, we return true (blocking group-position) until the + // impact expires so the bot doesn't immediately re-enter the danger zone. + constexpr float gooDangerRadius = 12.0f; + + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + float botZ = bot->GetPositionZ(); + ObjectGuid botGuid = bot->GetGUID(); + + std::vector goos = IcecrownHelpers::ActiveGooPositions(bot->GetMap()->GetInstanceId(), 8000); + bool botInDanger = false; + for (Position const& goo : goos) + { + float dx = botX - goo.GetPositionX(); + float dy = botY - goo.GetPositionY(); + if (dx * dx + dy * dy < gooDangerRadius * gooDangerRadius) + botInDanger = true; + } + + if (!botInDanger) + { + // Already safe. Return false so DPS/heal rotations can still fire - + // the multiplier blocks repositioning actions during the wait window + // via festergutGooWaitUntil so the bot stays put without idling. + return false; + } + + // Keep 10yd between fleeing bots. During spore phase melee stack at the + // tank spot, so we ignore melee allies (the tank/melee pile would reject + // every nearby candidate). When no spore is active, melee also spread. + constexpr float botSpacing = 10.0f; + bool const sporeActive = IccAnyGroupMemberHasAura(bot, SPELL_GAS_SPORE); + + std::vector alliesToSpace; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member->GetGUID() == botGuid) + continue; + if (sporeActive && botAI->IsMelee(member)) + continue; + alliesToSpace.push_back(member->GetPosition()); + } + } + + constexpr int angleSteps = 24; + float const radii[] = {13.0f, 16.0f, 20.0f}; + float bestScore = -1.0f; + float bestX = botX; + float bestY = botY; + bool found = false; + + // Per-bot preferred flee angle - stable across ticks, distinct per GUID - + // so stacked bots fan out into different sectors instead of converging. + float preferredAngle = (botGuid.GetCounter() % angleSteps) * (2.0f * float(M_PI) / angleSteps); + constexpr float angleBias = 3.0f; + + for (float r : radii) + { + for (int i = 0; i < angleSteps; ++i) + { + float a = (2.0f * float(M_PI) * i) / angleSteps; + float cx = botX + std::cos(a) * r; + float cy = botY + std::sin(a) * r; + + float minGooDistSq = std::numeric_limits::max(); + bool safe = true; + for (Position const& g : goos) + { + float gdx = cx - g.GetPositionX(); + float gdy = cy - g.GetPositionY(); + float d2 = gdx * gdx + gdy * gdy; + if (d2 < gooDangerRadius * gooDangerRadius) + { + safe = false; + break; + } + if (d2 < minGooDistSq) + minGooDistSq = d2; + } + if (!safe) + continue; + + bool tooCloseToAlly = false; + for (Position const& a2 : alliesToSpace) + { + float adx = cx - a2.GetPositionX(); + float ady = cy - a2.GetPositionY(); + if (adx * adx + ady * ady < botSpacing * botSpacing) + { + tooCloseToAlly = true; + break; + } + } + if (tooCloseToAlly) + continue; + + if (!bot->IsWithinLOS(cx, cy, botZ)) + continue; + + float travel = std::sqrt((cx - botX) * (cx - botX) + (cy - botY) * (cy - botY)); + float score = std::sqrt(minGooDistSq) - travel * 0.1f + std::cos(a - preferredAngle) * angleBias; + + if (score > bestScore) + { + bestScore = score; + bestX = cx; + bestY = cy; + found = true; + } + } + if (found) + break; + } + + if (found) + { + return MoveTo(bot->GetMapId(), bestX, bestY, botZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_GSB.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_GSB.cpp new file mode 100644 index 000000000..befbe5b11 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_GSB.cpp @@ -0,0 +1,1030 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "EquipAction.h" +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "RtiValue.h" +#include "Vehicle.h" + +bool IccCannonFireAction::Execute(Event /*event*/) +{ + Unit* vehicleBase = bot->GetVehicleBase(); + Vehicle* vehicle = bot->GetVehicle(); + + if (!vehicleBase || !vehicle) + return false; + + Unit* target = FindValidCannonTarget(); + if (!target) + return false; + + // Use Incinerating Blast when we have enough energy, otherwise Cannon Blast + static constexpr float ENERGY_THRESHOLD = 90.0f; + if (vehicleBase->GetPower(POWER_ENERGY) >= ENERGY_THRESHOLD) + { + uint32 const blastSpellId = AI_VALUE2(uint32, "vehicle spell id", "incinerating blast"); + if (TryCastCannonSpell(blastSpellId, target, vehicleBase)) + return true; + } + + uint32 const cannonSpellId = AI_VALUE2(uint32, "vehicle spell id", "cannon blast"); + return TryCastCannonSpell(cannonSpellId, target, vehicleBase); +} + +Unit* IccCannonFireAction::FindValidCannonTarget() +{ + // availableTargetsGS is ordered per-faction (earliest = highest priority). + // For each entry, enumerate all creatures and pick the nearest hostile + // alive one. FindNearestCreature returns the single closest match which + // may be a friendly of the same entry on our own ship, causing the entry + // to be skipped even when a hostile of that entry exists on the enemy + // ship. + static constexpr float CANNON_TARGET_RANGE = 150.0f; + + for (size_t i = 0; i < availableTargetsGS.size(); ++i) + { + std::list candidates; + bot->GetCreatureListWithEntryInGrid(candidates, availableTargetsGS[i], CANNON_TARGET_RANGE); + + Unit* best = nullptr; + float bestDist = FLT_MAX; + for (Creature* c : candidates) + { + if (!c || !c->IsAlive() || !c->IsHostileTo(bot)) + continue; + float const d = bot->GetExactDist(c); + if (d < bestDist) + { + bestDist = d; + best = c; + } + } + + if (best) + return best; + } + + return nullptr; +} + +bool IccCannonFireAction::TryCastCannonSpell(uint32 spellId, Unit* target, Unit* vehicleBase) +{ + static constexpr uint32 COOLDOWN_MS = 1000; + + if (botAI->CanCastVehicleSpell(spellId, target) && botAI->CastVehicleSpell(spellId, target)) + { + vehicleBase->AddSpellCooldown(spellId, 0, COOLDOWN_MS); + return true; + } + + return false; +} + +bool IccGunshipEnterCannonAction::Execute(Event /*event*/) +{ + // Never switch vehicles while already in one + if (bot->GetVehicle()) + return false; + + // Require rocket pack acquired and equipped before boarding a cannon + Item* rocketPack = bot->GetItemByEntry(ITEM_GOBLIN_ROCKET_PACK); + if (!rocketPack || !rocketPack->IsEquipped()) + return false; + + Unit* bestVehicle = FindBestAvailableCannon(); + if (!bestVehicle) + return false; + + return EnterVehicle(bestVehicle, true); +} + +Unit* IccGunshipEnterCannonAction::FindBestAvailableCannon() +{ + Unit* bestVehicle = nullptr; + + GuidVector const npcs = AI_VALUE(GuidVector, "nearest vehicles"); + for (ObjectGuid const& npcGuid : npcs) + { + Unit* vehicleBase = botAI->GetUnit(npcGuid); + if (!IsValidCannon(vehicleBase)) + continue; + + // Prefer the closest valid cannon + if (!bestVehicle || bot->GetExactDist(vehicleBase) < bot->GetExactDist(bestVehicle)) + bestVehicle = vehicleBase; + } + + return bestVehicle; +} + +bool IccGunshipEnterCannonAction::IsValidCannon(Unit* vehicle) +{ + if (!vehicle) + return false; + + if (vehicle->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return false; + + if (!vehicle->IsFriendlyTo(bot)) + return false; + + if (!vehicle->GetVehicleKit() || !vehicle->GetVehicleKit()->GetAvailableSeatCount()) + return false; + + uint32 const entry = vehicle->GetEntry(); + if (entry != NPC_CANNONA && entry != NPC_CANNONH) + return false; + + // Frozen or disabled cannon - skip + if (vehicle->HasAura(SPELL_FROZEN_CANNON) || vehicle->HasAura(SPELL_BELOW_ZERO)) + return false; + + return true; +} + +bool IccGunshipEnterCannonAction::EnterVehicle(Unit* vehicleBase, bool moveIfFar) +{ + float const dist = bot->GetDistance(vehicleBase); + + if (dist > INTERACTION_DISTANCE && !moveIfFar) + return false; + + if (dist > INTERACTION_DISTANCE) + return MoveTo(vehicleBase); + + botAI->RemoveShapeshift(); + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + + vehicleBase->HandleSpellClick(bot); + + if (!bot->IsOnVehicle(vehicleBase)) + return false; + + // Dismount - bots can enter a vehicle while still mounted + WorldPacket emptyPacket; + bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); + + return true; +} + +IccGunshipRocketJumpAction::GunshipSide IccGunshipRocketJumpAction::DetectShip() const +{ + Unit* cannonA = bot->FindNearestCreature(NPC_CANNONA, 100.0f); + if (cannonA && cannonA->IsFriendlyTo(bot)) + return GunshipSide::ALLY; + + Unit* cannonH = bot->FindNearestCreature(NPC_CANNONH, 100.0f); + if (cannonH && cannonH->IsFriendlyTo(bot)) + return GunshipSide::HORDE; + + return GunshipSide::NONE; +} + +bool IccGunshipRocketJumpAction::Execute(Event /*event*/) +{ + GunshipSide const side = DetectShip(); + if (side == GunshipSide::NONE) + return false; + + // Z-axis correction: teleport bot back if fallen below deck + if (!bot->GetVehicle()) + { + Position const& zRef = (side == GunshipSide::ALLY) ? ICC_GUNSHIP_ROCKET_JUMP_ALLY2 : ICC_GUNSHIP_ROCKET_JUMP_HORDE2; + Position const& zDest = (side == GunshipSide::ALLY) ? ICC_GUNSHIP_ROCKET_JUMP_ALLY2 : ICC_GUNSHIP_ROCKET_JUMP_HORDE2; + float const Z_THRESHOLD = zRef.GetPositionZ() - 5.0f; + if (bot->GetPositionZ() < Z_THRESHOLD) + { + bot->TeleportTo(bot->GetMapId(), zDest.GetPositionX(), zDest.GetPositionY(), + zDest.GetPositionZ(), bot->GetOrientation()); + botAI->Reset(); + return false; + } + } + + float const maxWaitingDistance = (side == GunshipSide::ALLY) ? 30.0f : 25.0f; + static constexpr float MAX_ATTACK_DISTANCE = 20.0f; + static constexpr float HOLD_RADIUS = 20.0f; + static constexpr uint8 SKULL_ICON_INDEX = RtiTargetValue::skullIndex; + + uint32 const mageEntry = (side == GunshipSide::ALLY) ? NPC_KOR_KRON_BATTLE_MAGE : NPC_SKYBREAKER_SORCERER; + Position const& waitPos = (side == GunshipSide::ALLY) ? ICC_GUNSHIP_ROCKET_JUMP_ALLY2 : ICC_GUNSHIP_ROCKET_JUMP_HORDE_FRIENDLY_POINT; + Position const& attackPos = (side == GunshipSide::ALLY) ? ICC_GUNSHIP_ROCKET_JUMP_ALLY : ICC_GUNSHIP_ROCKET_JUMP_HORDE; + + Unit* boss = nullptr; + { + std::list mages; + bot->GetCreatureListWithEntryInGrid(mages, mageEntry, 200.0f); + for (Creature* m : mages) + { + if (m && m->IsAlive() && m->HasUnitState(UNIT_STATE_CASTING)) + { + boss = m; + break; + } + } + if (!boss) + boss = bot->FindNearestCreature(mageEntry, 200.0f); + } + + CleanupSkullIcon(SKULL_ICON_INDEX); + + uint32 const cannonEntry = (side == GunshipSide::ALLY) ? NPC_CANNONA : NPC_CANNONH; + bool cannonsHaveBelowZero = false; + { + std::list cannons; + bot->GetCreatureListWithEntryInGrid(cannons, cannonEntry, 100.0f); + for (Creature* c : cannons) + { + if (c && c->IsFriendlyTo(bot) && c->HasAura(SPELL_BELOW_ZERO)) + { + cannonsHaveBelowZero = true; + break; + } + } + } + + bool const botOnEnemyShip = bot->GetExactDist2d(attackPos) <= MAX_ATTACK_DISTANCE; + bool const mageAlive = boss && boss->IsAlive() && boss->HasUnitState(UNIT_STATE_CASTING); + + // Assist tank stays on friendly ship to collect and tank adds + if (botAI->IsAssistTank(bot)) + { + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + // Find nearest uncollected add near hold position + Unit* targetAdd = nullptr; + float closestDist = FLT_MAX; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (unit->GetExactDist2d(waitPos) > HOLD_RADIUS) + continue; + + Unit* victim = unit->GetVictim(); + bool alreadyTanked = victim && victim->IsPlayer() && botAI->IsTank(victim->ToPlayer()); + if (alreadyTanked) + continue; + + float dist = bot->GetExactDist2d(unit); + if (dist < closestDist) + { + closestDist = dist; + targetAdd = unit; + } + } + + if (targetAdd) + { + IccCastClassTaunt(bot, botAI, targetAdd); + bot->SetTarget(targetAdd->GetGUID()); + bot->SetFacingToObject(targetAdd); + Attack(targetAdd); + return false; + } + + // No uncollected adds - keep attacking adds targeting us + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (unit->GetExactDist2d(waitPos) > HOLD_RADIUS) + continue; + + if (unit->GetVictim() == bot) + { + bot->SetTarget(unit->GetGUID()); + bot->SetFacingToObject(unit); + Attack(unit); + return false; + } + } + + // No adds — return to hold position if drifted + if (bot->GetExactDist2d(waitPos) > HOLD_RADIUS) + { + return MoveTo(bot->GetMapId(), waitPos.GetPositionX(), waitPos.GetPositionY(), waitPos.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_NORMAL); + } + + return false; + } + + bool const isMainTank = botAI->IsMainTank(bot); + bool const isHorde = (side == GunshipSide::HORDE); + Position const& middlePoint = ICC_GUNSHIP_ROCKET_JUMP_HORDE_MIDDLE_POINT; + static constexpr float JUMP_GATE = 30.0f; + + // Main tank: tank captain during below-zero OR while an ally is on enemy ship. + // Otherwise return to friendly ship — captain one-shots if tanked too long. + if (isMainTank) + { + uint32 const captainEntry = (side == GunshipSide::ALLY) ? NPC_HIGH_OVERLORD_SAURFANG : NPC_MURADIN_BRONZEBEARD; + Unit* captain = bot->FindNearestCreature(captainEntry, 1000.0f); + + // Battle Fury buff bugged on captain - strip all difficulty variants every tick. todo: remove when boss agro fixed + if (captain) + { + static constexpr uint32 BATTLE_FURY_IDS[] = {SPELL_BATTLE_FURY1, SPELL_BATTLE_FURY2, + SPELL_BATTLE_FURY3, SPELL_BATTLE_FURY4, + SPELL_BATTLE_FURY5}; + for (uint32 const id : BATTLE_FURY_IDS) + if (captain->HasAura(id)) + captain->RemoveAurasDueToSpell(id); + } + + bool allyAtMage = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + if (botAI->IsMainTank(member) || botAI->IsAssistTank(member)) + continue; + if (member->GetExactDist2d(attackPos) <= 20.0f) + { + allyAtMage = true; + break; + } + } + } + bool const shouldTankCaptain = cannonsHaveBelowZero || allyAtMage; + + // Captain gone or outside tanking window: return to friendly ship if stranded, else hold. + // Drop target/threat so combat rotation doesn't chase adds on friendly ship. + if (!captain || !captain->IsAlive() || !shouldTankCaptain) + { + if (bot->GetVictim()) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->SetTarget(ObjectGuid::Empty); + } + if (botOnEnemyShip) + { + // Main tank jumps back last - wait for all non-tanks to leave enemy ship + if (AnyNonTankAwayFromFriendly(side)) + return false; + + if (ExitCannonIfSeated()) + return true; + + if (isHorde) + { + float const distFriendly = bot->GetExactDist2d(waitPos); + float const distMiddle = bot->GetExactDist2d(middlePoint); + if (distMiddle <= 10.0f && distFriendly > 5.0f) + { + if (UseRocketPack(waitPos, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), waitPos.GetPositionX(), waitPos.GetPositionY(), + waitPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (distMiddle <= JUMP_GATE) + { + if (UseRocketPack(middlePoint, /*walkIfOutOfRange=*/false)) + return true; + return MoveTo(bot->GetMapId(), middlePoint.GetPositionX(), middlePoint.GetPositionY(), + middlePoint.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (UseRocketPack(middlePoint, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), middlePoint.GetPositionX(), middlePoint.GetPositionY(), + middlePoint.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + + // Alliance MT return: ALLY_MIDDLE_POINT -> ALLY_FRIENDLY_POINT + Position const& allyFriendly = ICC_GUNSHIP_ROCKET_JUMP_ALLY_FRIENDLY_POINT; + Position const& allyMiddle = ICC_GUNSHIP_ROCKET_JUMP_ALLY_MIDDLE_POINT; + float const distAllyMid = bot->GetExactDist2d(allyMiddle); + float const distAllyFriendly = bot->GetExactDist2d(allyFriendly); + + if (distAllyMid <= 10.0f && distAllyFriendly > 5.0f) + { + if (UseRocketPack(allyFriendly, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), allyFriendly.GetPositionX(), allyFriendly.GetPositionY(), + allyFriendly.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (distAllyMid <= JUMP_GATE) + { + if (UseRocketPack(allyMiddle, /*walkIfOutOfRange=*/false)) + return true; + return MoveTo(bot->GetMapId(), allyMiddle.GetPositionX(), allyMiddle.GetPositionY(), + allyMiddle.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (UseRocketPack(allyMiddle, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), allyMiddle.GetPositionX(), allyMiddle.GetPositionY(), + allyMiddle.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (bot->GetExactDist2d(waitPos) > maxWaitingDistance) + { + if (ExitCannonIfSeated()) + return true; + return UseRocketPack(waitPos, /*walkIfOutOfRange=*/true); + } + return false; + } + + // Not on enemy ship: rocket directly to captain + if (!botOnEnemyShip) + { + if (ExitCannonIfSeated()) + return true; + + Position const captainPos(captain->GetPositionX(), captain->GetPositionY(), captain->GetPositionZ()); + + if (isHorde) + { + Position const& friendlyPoint = ICC_GUNSHIP_ROCKET_JUMP_HORDE_FRIENDLY_POINT; + float const distFriendly = bot->GetExactDist2d(friendlyPoint); + float const distMiddle = bot->GetExactDist2d(middlePoint); + float const distCaptain = bot->GetExactDist2d(&captainPos); + + if (distCaptain <= JUMP_GATE || distMiddle <= 5.0f) + return UseRocketPack(captainPos, /*walkIfOutOfRange=*/false); + if (distFriendly <= 5.0f) + return RocketPackJumpToward(middlePoint); + if (distMiddle > JUMP_GATE && distCaptain > JUMP_GATE) + return UseRocketPack(friendlyPoint, /*walkIfOutOfRange=*/true); + + Position const* nearest = &friendlyPoint; + float nearestDist = distFriendly; + if (distMiddle < nearestDist) { nearest = &middlePoint; nearestDist = distMiddle; } + if (distCaptain < nearestDist) { nearest = &captainPos; nearestDist = distCaptain; } + return UseRocketPack(*nearest, /*walkIfOutOfRange=*/true); + } + + // Alliance: ALLY_FRIENDLY_POINT -> ALLY_MIDDLE_POINT -> captain + Position const& allyFriendly = ICC_GUNSHIP_ROCKET_JUMP_ALLY_FRIENDLY_POINT; + Position const& allyMiddle = ICC_GUNSHIP_ROCKET_JUMP_ALLY_MIDDLE_POINT; + float const distAllyFriendly = bot->GetExactDist2d(allyFriendly); + float const distAllyMid = bot->GetExactDist2d(allyMiddle); + float const distCaptainA = bot->GetExactDist2d(&captainPos); + + if (distCaptainA <= JUMP_GATE || distAllyMid <= 5.0f) + return UseRocketPack(captainPos, /*walkIfOutOfRange=*/false); + if (distAllyFriendly <= 5.0f) + return RocketPackJumpToward(allyMiddle); + if (distAllyMid > JUMP_GATE && distCaptainA > JUMP_GATE) + return UseRocketPack(allyFriendly, /*walkIfOutOfRange=*/true); + + Position const* nearestA = &allyFriendly; + float nearestDistA = distAllyFriendly; + if (distAllyMid < nearestDistA) { nearestA = &allyMiddle; nearestDistA = distAllyMid; } + if (distCaptainA < nearestDistA) { nearestA = &captainPos; nearestDistA = distCaptainA; } + return UseRocketPack(*nearestA, /*walkIfOutOfRange=*/true); + } + + // On enemy ship. If not captain's victim, force aggro. + if (captain->GetVictim() != bot) + { + IccCastClassTaunt(bot, botAI, captain); + bot->SetTarget(captain->GetGUID()); + bot->SetFacingToObject(captain); + Attack(captain); + return false; + } + + // Captain's victim: drag boss to 40f-from-friendly-cannon spot on enemy ship + static constexpr float TANK_OFFSET_FROM_CANNON = 40.0f; + static constexpr float TANK_POS_TOLERANCE = 3.0f; + Unit* friendlyCannon = FindNearestFriendlyCannon(side); + if (friendlyCannon) + { + float const cx = friendlyCannon->GetPositionX(); + float const cy = friendlyCannon->GetPositionY(); + float const dirX = attackPos.GetPositionX() - cx; + float const dirY = attackPos.GetPositionY() - cy; + float const dirLen = std::sqrt(dirX * dirX + dirY * dirY); + if (dirLen > 0.1f) + { + float const tx = cx + (dirX / dirLen) * TANK_OFFSET_FROM_CANNON; + float const ty = cy + (dirY / dirLen) * TANK_OFFSET_FROM_CANNON; + float const tz = bot->GetPositionZ(); + float const dx = bot->GetPositionX() - tx; + float const dy = bot->GetPositionY() - ty; + if (std::sqrt(dx * dx + dy * dy) > TANK_POS_TOLERANCE) + { + bot->SetTarget(captain->GetGUID()); + return MoveTo(bot->GetMapId(), tx, ty, tz, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + } + + bot->SetTarget(captain->GetGUID()); + bot->SetFacingToObject(captain); + Attack(captain); + return false; + } + + // Non-tank rocket jump logic + + if (cannonsHaveBelowZero && mageAlive) + { + UpdateBossSkullIcon(boss, SKULL_ICON_INDEX); + if (!botOnEnemyShip) + { + if (ExitCannonIfSeated()) + return true; + + if (isHorde) + { + Position const& friendlyPoint = ICC_GUNSHIP_ROCKET_JUMP_HORDE_FRIENDLY_POINT; + float const distFriendly = bot->GetExactDist2d(friendlyPoint); + float const distMiddle = bot->GetExactDist2d(middlePoint); + float const distMage = bot->GetExactDist2d(attackPos); + + // Mage in range or already at middle: jump mage + if (distMage <= JUMP_GATE || distMiddle <= 5.0f) + return UseRocketPack(attackPos, /*walkIfOutOfRange=*/false); + // At friendly: jump middle + if (distFriendly <= 5.0f) + return RocketPackJumpToward(middlePoint); + // Both middle and mage out of range: detour through friendly + if (distMiddle > JUMP_GATE && distMage > JUMP_GATE) + return UseRocketPack(friendlyPoint, /*walkIfOutOfRange=*/true); + + // Fallback: jump nearest of the three + Position const* nearest = &friendlyPoint; + float nearestDist = distFriendly; + if (distMiddle < nearestDist) { nearest = &middlePoint; nearestDist = distMiddle; } + if (distMage < nearestDist) { nearest = &attackPos; nearestDist = distMage; } + return UseRocketPack(*nearest, /*walkIfOutOfRange=*/true); + } + + // Alliance non-tank: ALLY_FRIENDLY_POINT -> ALLY_MIDDLE_POINT -> mage (attackPos) + Position const& allyFriendly = ICC_GUNSHIP_ROCKET_JUMP_ALLY_FRIENDLY_POINT; + Position const& allyMiddle = ICC_GUNSHIP_ROCKET_JUMP_ALLY_MIDDLE_POINT; + float const distAllyFriendly = bot->GetExactDist2d(allyFriendly); + float const distAllyMid = bot->GetExactDist2d(allyMiddle); + float const distMageA = bot->GetExactDist2d(attackPos); + + if (distMageA <= JUMP_GATE || distAllyMid <= 5.0f) + return UseRocketPack(attackPos, /*walkIfOutOfRange=*/false); + if (distAllyFriendly <= 5.0f) + return RocketPackJumpToward(allyMiddle); + if (distAllyMid > JUMP_GATE && distMageA > JUMP_GATE) + return UseRocketPack(allyFriendly, /*walkIfOutOfRange=*/true); + + Position const* nearestA = &allyFriendly; + float nearestDistA = distAllyFriendly; + if (distAllyMid < nearestDistA) { nearestA = &allyMiddle; nearestDistA = distAllyMid; } + if (distMageA < nearestDistA) { nearestA = &attackPos; nearestDistA = distMageA; } + return UseRocketPack(*nearestA, /*walkIfOutOfRange=*/true); + } + + // On enemy ship: attack mage + context->GetValue("rti")->Set("skull"); + if (!bot->GetVehicle()) + Attack(boss); + return false; + } + else + { + // Stranded at middle point: force jump back to friendly point + if (isHorde) + { + float const distMiddle = bot->GetExactDist2d(middlePoint); + float const distFriendly = bot->GetExactDist2d(waitPos); + if (distMiddle <= 10.0f && distFriendly > 5.0f) + { + if (ExitCannonIfSeated()) + return true; + if (UseRocketPack(waitPos, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), waitPos.GetPositionX(), waitPos.GetPositionY(), + waitPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + } + + // Return to friendly point if on enemy ship or drifted too far + if (botOnEnemyShip || bot->GetExactDist2d(waitPos) > maxWaitingDistance) + { + if (ExitCannonIfSeated()) + return true; + + if (isHorde) + { + float const distMiddle = bot->GetExactDist2d(middlePoint); + // Middle in range: jump middle (next tick stranded override jumps friendly) + if (distMiddle <= JUMP_GATE) + { + if (UseRocketPack(middlePoint, /*walkIfOutOfRange=*/false)) + return true; + return MoveTo(bot->GetMapId(), middlePoint.GetPositionX(), middlePoint.GetPositionY(), + middlePoint.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + // Middle out of range: walk 10f toward middle + if (UseRocketPack(middlePoint, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), middlePoint.GetPositionX(), middlePoint.GetPositionY(), + middlePoint.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + + // Alliance return: ALLY_MIDDLE_POINT -> ALLY_FRIENDLY_POINT + Position const& allyFriendly = ICC_GUNSHIP_ROCKET_JUMP_ALLY_FRIENDLY_POINT; + Position const& allyMiddle = ICC_GUNSHIP_ROCKET_JUMP_ALLY_MIDDLE_POINT; + float const distAllyMid = bot->GetExactDist2d(allyMiddle); + float const distAllyFriendly = bot->GetExactDist2d(allyFriendly); + + if (distAllyMid <= 10.0f && distAllyFriendly > 5.0f) + { + if (UseRocketPack(allyFriendly, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), allyFriendly.GetPositionX(), allyFriendly.GetPositionY(), + allyFriendly.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (distAllyMid <= JUMP_GATE) + { + if (UseRocketPack(allyMiddle, /*walkIfOutOfRange=*/false)) + return true; + return MoveTo(bot->GetMapId(), allyMiddle.GetPositionX(), allyMiddle.GetPositionY(), + allyMiddle.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + if (UseRocketPack(allyMiddle, /*walkIfOutOfRange=*/true)) + return true; + return MoveTo(bot->GetMapId(), allyMiddle.GetPositionX(), allyMiddle.GetPositionY(), + allyMiddle.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + + // Melee DPS on friendly ship: assist the assist tank on adds it's tanking + if (botAI->IsMelee(bot) && !botAI->IsTank(bot)) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (!botAI->IsAssistTank(member)) + continue; + Unit* assistTarget = member->GetVictim(); + if (!assistTarget || !assistTarget->IsAlive()) + break; + if (assistTarget->GetExactDist2d(waitPos) > HOLD_RADIUS) + break; + bot->SetTarget(assistTarget->GetGUID()); + bot->SetFacingToObject(assistTarget); + Attack(assistTarget); + return false; + } + } + } + + // Ranged DPS star-mark axethrower/rifleman for weapon focus fire. + // Handoff uses the previous star's spot as anchor so the marker + // doesn't jump to whichever add is closest to the picking bot. + if (botAI->IsRangedDps(bot)) + { + static constexpr uint8 STAR_ICON_INDEX = RtiTargetValue::starIndex; + static constexpr float ADD_SEARCH_RANGE = 200.0f; + uint32 const addEntry = (side == GunshipSide::ALLY) + ? NPC_KOR_KRON_AXETHROWER : NPC_SKYBREAKER_RIFLEMAN; + + Position& lastStarPos = IcecrownHelpers::IccState(bot->GetInstanceId()).gsbLastStarPos; + + if (Group* group = bot->GetGroup()) + { + Unit* starTarget = botAI->GetUnit(group->GetTargetIcon(STAR_ICON_INDEX)); + bool const validStar = starTarget && starTarget->IsAlive() + && starTarget->GetEntry() == addEntry; + + if (validStar) + { + lastStarPos = Position(starTarget->GetPositionX(), + starTarget->GetPositionY(), + starTarget->GetPositionZ()); + } + else + { + Position const refPos = (lastStarPos.GetPositionX() != 0.0f || lastStarPos.GetPositionY() != 0.0f) + ? lastStarPos + : Position(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + + std::list adds; + bot->GetCreatureListWithEntryInGrid(adds, addEntry, ADD_SEARCH_RANGE); + + Unit* nextAdd = nullptr; + float bestDist = FLT_MAX; + for (Creature* c : adds) + { + if (!c || !c->IsAlive() || !c->IsHostileTo(bot)) + continue; + float const d = c->GetExactDist(&refPos); + if (d < bestDist) + { + bestDist = d; + nextAdd = c; + } + } + + if (nextAdd) + { + group->SetTargetIcon(STAR_ICON_INDEX, bot->GetGUID(), nextAdd->GetGUID()); + lastStarPos = Position(nextAdd->GetPositionX(), + nextAdd->GetPositionY(), + nextAdd->GetPositionZ()); + starTarget = nextAdd; + } + else + { + starTarget = nullptr; + } + } + + if (starTarget) + { + context->GetValue("rti")->Set("star"); + if (!bot->GetVehicle()) + Attack(starTarget); + } + } + } + } + + return false; +} + +bool IccGunshipRocketJumpAction::IsMainTankOnEnemyShip(GunshipSide side) const +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + uint32 const captainEntry = (side == GunshipSide::ALLY) ? NPC_HIGH_OVERLORD_SAURFANG : NPC_MURADIN_BRONZEBEARD; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (!botAI->IsMainTank(member)) + continue; + Unit* captain = member->FindNearestCreature(captainEntry, 15.0f); + return captain != nullptr; + } + return false; +} + +bool IccGunshipRocketJumpAction::AnyNonTankAwayFromFriendly(GunshipSide side) const +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Position const& waitPos = (side == GunshipSide::ALLY) ? ICC_GUNSHIP_ROCKET_JUMP_ALLY2 : ICC_GUNSHIP_ROCKET_JUMP_HORDE2; + float const threshold = (side == GunshipSide::ALLY) ? 40.0f : 45.0f; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (botAI->IsMainTank(member) || botAI->IsAssistTank(member)) + continue; + if (member->GetExactDist2d(waitPos) > threshold) + return true; + } + return false; +} + +bool IccGunshipRocketJumpAction::ExitCannonIfSeated() +{ + Vehicle* vehicle = bot->GetVehicle(); + if (!vehicle) + return false; + + VehicleSeatEntry const* seat = vehicle->GetSeatForPassenger(bot); + if (!seat || !seat->CanEnterOrExit()) + return false; + + WorldPacket packet; + bot->GetSession()->HandleRequestVehicleExit(packet); + return true; +} + +Item* IccGunshipRocketJumpAction::FindRocketPack() const +{ + return bot->GetItemByEntry(ITEM_GOBLIN_ROCKET_PACK); +} + +bool IccGunshipRocketJumpAction::UseRocketPack(Position const& destination, bool walkIfOutOfRange) +{ + Item* rocketPack = FindRocketPack(); + if (!rocketPack) + return false; + + if (!bot->HasAura(SPELL_ROCKET_PACK_USEABLE)) + return false; + + if (bot->IsNonMeleeSpellCast(true)) + return false; + + if (bot->CanUseItem(rocketPack) != EQUIP_ERR_OK) + return false; + + // Throttle rocket pack use to prevent mid-air re-jump spam + static constexpr uint32 ROCKET_PACK_COOLDOWN_MS = 2500; + auto& lastRocketPackUse = IcecrownHelpers::IccState(bot->GetInstanceId()).gsbLastRocketPackUse; + uint32 const now = getMSTime(); + auto const it = lastRocketPackUse.find(bot->GetGUID()); + bool const onCooldown = it != lastRocketPackUse.end() && + getMSTimeDiff(it->second, now) < ROCKET_PACK_COOLDOWN_MS; + + static constexpr float MAX_JUMP_RANGE = 30.0f; + float const dist = bot->GetExactDist(&destination); + + // On cooldown: consume tick to block MoveTo fallback when target is reachable + if (onCooldown) + { + if (dist <= MAX_JUMP_RANGE) + return true; + return false; + } + if (dist > MAX_JUMP_RANGE) + { + if (!walkIfOutOfRange) + return false; + + // Walk 10f toward destination, retry jump next tick + static constexpr float STEP_DISTANCE = 10.0f; + float const ratio = STEP_DISTANCE / dist; + float const stepX = bot->GetPositionX() + (destination.GetPositionX() - bot->GetPositionX()) * ratio; + float const stepY = bot->GetPositionY() + (destination.GetPositionY() - bot->GetPositionY()) * ratio; + float const stepZ = bot->GetPositionZ() + (destination.GetPositionZ() - bot->GetPositionZ()) * ratio; + return MoveTo(bot->GetMapId(), stepX, stepY, stepZ, false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + + // Settle bot before firing rocket pack - moving bots tend to misfire mid-stride + if (bot->isMoving()) + { + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + return true; + } + + uint8 bagIndex = rocketPack->GetBagSlot(); + uint8 slot = rocketPack->GetSlot(); + uint8 castCount = 1; + uint32 spellId = SPELL_ROCKET_PACK_USE; + ObjectGuid itemGuid = rocketPack->GetGUID(); + uint32 glyphIndex = 0; + uint8 castFlags = 0; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << castCount << spellId << itemGuid << glyphIndex << castFlags; + packet << uint32(TARGET_FLAG_DEST_LOCATION); + packet.appendPackGUID(0); + packet << destination.GetPositionX() << destination.GetPositionY() << destination.GetPositionZ(); + + bot->GetSession()->HandleUseItemOpcode(packet); + lastRocketPackUse[bot->GetGUID()] = now; + return true; +} + +bool IccGunshipRocketJumpAction::RocketPackJumpToward(Position const& target) +{ + static constexpr float JUMP_DISTANCE = 30.0f; + float const dist = bot->GetExactDist(&target); + if (dist <= JUMP_DISTANCE) + return UseRocketPack(target, /*walkIfOutOfRange=*/false); + + float const ratio = JUMP_DISTANCE / dist; + Position jumpTo( + bot->GetPositionX() + (target.GetPositionX() - bot->GetPositionX()) * ratio, + bot->GetPositionY() + (target.GetPositionY() - bot->GetPositionY()) * ratio, + bot->GetPositionZ() + (target.GetPositionZ() - bot->GetPositionZ()) * ratio); + return UseRocketPack(jumpTo, /*walkIfOutOfRange=*/false); +} + +Unit* IccGunshipRocketJumpAction::FindNearestFriendlyCannon(GunshipSide side) const +{ + if (side == GunshipSide::NONE) + return nullptr; + + uint32 const cannonEntry = (side == GunshipSide::ALLY) ? NPC_CANNONA : NPC_CANNONH; + Unit* cannon = bot->FindNearestCreature(cannonEntry, 300.0f); + if (cannon && cannon->IsFriendlyTo(bot)) + return cannon; + return nullptr; +} + +bool IccGunshipRocketJumpAction::CleanupSkullIcon(uint8 skullIconIndex) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid const currentSkullTarget = group->GetTargetIcon(skullIconIndex); + if (currentSkullTarget.IsEmpty()) + return false; + + Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); + if (!skullTarget || !skullTarget->IsAlive()) + group->SetTargetIcon(skullIconIndex, bot->GetGUID(), ObjectGuid::Empty); + + return false; +} + +bool IccGunshipRocketJumpAction::UpdateBossSkullIcon(Unit* boss, uint8 skullIconIndex) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (group->GetTargetIcon(skullIconIndex) != boss->GetGUID()) + group->SetTargetIcon(skullIconIndex, bot->GetGUID(), boss->GetGUID()); + + return false; +} + +bool IccGunshipRocketPackSetupAction::Execute(Event /*event*/) +{ + Item* rocketPack = FindRocketPack(); + if (!rocketPack) + return AcquireRocketPack(); + if (!rocketPack->IsEquipped()) + return EquipRocketPack(); + return false; +} + +Item* IccGunshipRocketPackSetupAction::FindRocketPack() const +{ + return bot->GetItemByEntry(ITEM_GOBLIN_ROCKET_PACK); +} + +bool IccGunshipRocketPackSetupAction::AcquireRocketPack() +{ + static constexpr float ZAFOD_SEARCH_RADIUS = 150.0f; + static constexpr uint32 ZAFOD_GOSSIP_MENU_ID = 10885; + static constexpr uint32 ZAFOD_GOSSIP_OPTION = 0; + + Creature* zafod = bot->FindNearestCreature(NPC_ZAFOD_BOOMBOX, ZAFOD_SEARCH_RADIUS); + if (!zafod) + return false; + + if (bot->GetDistance(zafod) > INTERACTION_DISTANCE) + return MoveTo(bot->GetMapId(), zafod->GetPositionX(), zafod->GetPositionY(), zafod->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_NORMAL); + + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + bot->SetFacingToObject(zafod); + + WorldPacket gossipHello(CMSG_GOSSIP_HELLO); + gossipHello << zafod->GetGUID(); + bot->GetSession()->HandleGossipHelloOpcode(gossipHello); + + WorldPacket gossipSelect(CMSG_GOSSIP_SELECT_OPTION); + gossipSelect << zafod->GetGUID(); + gossipSelect << uint32(ZAFOD_GOSSIP_MENU_ID) << uint32(ZAFOD_GOSSIP_OPTION); + bot->GetSession()->HandleGossipSelectOptionOpcode(gossipSelect); + return true; +} + +bool IccGunshipRocketPackSetupAction::EquipRocketPack() +{ + EquipAction* equipAction = dynamic_cast(botAI->GetAiObjectContext()->GetAction("equip")); + if (!equipAction) + return false; + + ItemIds ids; + ids.insert(ITEM_GOBLIN_ROCKET_PACK); + equipAction->EquipItems(ids); + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LDW.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LDW.cpp new file mode 100644 index 000000000..505412725 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LDW.cpp @@ -0,0 +1,983 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "RtiValue.h" +#include "Vehicle.h" + +// Lady Deathwhisper +bool IccDarkReckoningAction::Execute(Event /*event*/) +{ + constexpr float SAFE_DISTANCE_THRESHOLD = 2.0f; + + if (bot->HasAura(SPELL_DARK_RECKONING) && + bot->GetExactDist2d(ICC_DARK_RECKONING_SAFE_POSITION) > SAFE_DISTANCE_THRESHOLD) + { + return MoveTo(bot->GetMapId(), ICC_DARK_RECKONING_SAFE_POSITION.GetPositionX(), + ICC_DARK_RECKONING_SAFE_POSITION.GetPositionY(), ICC_DARK_RECKONING_SAFE_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + + return false; +} + +bool IccRangedPositionLadyDeathwhisperAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); + if (!boss) + return false; + + float const currentDistance = bot->GetDistance2d(boss); + float const minDistance = 7.0f; + float const maxDistance = 30.0f; + + if (currentDistance < minDistance || currentDistance > maxDistance) + return false; + + if (!botAI->IsRanged(bot) && !botAI->IsHeal(bot)) + return false; + + return MaintainRangedSpacing(); +} + +bool IccRangedPositionLadyDeathwhisperAction::MaintainRangedSpacing() +{ + float const safeSpacingRadius = 3.0f; + float const moveIncrement = 2.0f; + float const maxMoveDistance = 5.0f; + bool const isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); + + if (!isRanged) + return false; + + GuidVector const members = AI_VALUE(GuidVector, "group members"); + + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (auto const& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + float const distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + ++nearbyCount; + } + } + + if (nearbyCount > 0) + { + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) + { + totalX /= magnitude; + totalY /= magnitude; + + float const moveDistance = std::min(moveIncrement, maxMoveDistance); + + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); + + if (bot->IsWithinLOS(targetX, targetY, targetZ)) + { + Position targetPos(targetX, targetY, targetZ); + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + else + { + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + Position targetPos(targetX, targetY, targetZ); + MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + } + } + + return false; +} + +bool IccAddsLadyDeathwhisperAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); + if (!boss) + return false; + + Difficulty diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig.EnableICCBuffs && boss->IsInCombat() && diff && + (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + if (!bot->HasAura(SPELL_NO_THREAT) && botAI->HasAggro(boss) && !botAI->IsTank(bot)) + bot->AddAura(SPELL_NO_THREAT, bot); + + if (botAI->IsMainTank(bot) && !bot->HasAura(SPELL_SPITEFULL_FURY) && boss->GetVictim() != bot) + bot->AddAura(SPELL_SPITEFULL_FURY, bot); + } + + if (botAI->HasAura("Dominate Mind", bot, false, false) && !bot->HasAura(SPELL_CYCLONE)) + bot->AddAura(SPELL_CYCLONE, bot); + else if (bot->HasAura(SPELL_CYCLONE) && !botAI->HasAura("Dominate Mind", bot, false, false)) + bot->RemoveAura(SPELL_CYCLONE); + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); + if (memberBotAI && !memberBotAI->IsRealPlayer()) + continue; + + if (botAI->HasAura("Dominate Mind", member, false, false) && !member->HasAura(SPELL_CYCLONE)) + member->AddAura(SPELL_CYCLONE, member); + else if (member->HasAura(SPELL_CYCLONE) && !botAI->HasAura("Dominate Mind", member, false, false)) + member->RemoveAura(SPELL_CYCLONE); + } + } + + constexpr uint32 shadeEntryId = NPC_SHADE; + + if (botAI->IsTank(bot)) + { + if (bot->HasAura(SPELL_TOUCH_OF_INSIGNIFICANCE)) + bot->RemoveAura(SPELL_TOUCH_OF_INSIGNIFICANCE); + + if (IsTargetedByShade(shadeEntryId)) + return false; + + Unit* currentTarget = bot->GetVictim(); + bool const isAttackingAdd = currentTarget && IsAdd(currentTarget); + if (isAttackingAdd && currentTarget->IsAlive()) + { + if (botAI->IsAssistTank(bot) || !IsAssistTankAlive()) + { + constexpr float MARK_RANGE = 25.0f; + Unit* nearAdd = FindAddNearBoss(boss, MARK_RANGE); + UpdateRaidTargetIcon(nearAdd ? nearAdd : boss); + } + + if (currentTarget->GetVictim() == bot) + { + constexpr float KITE_THRESHOLD = 25.0f; + if (!boss->HealthAbovePct(95)) + { + Position const& tankPos = ICC_LDW_TANK_POSTION; + if (bot->GetExactDist2d(tankPos.GetPositionX(), tankPos.GetPositionY()) > KITE_THRESHOLD) + MoveTo(bot->GetMapId(), tankPos.GetPositionX(), tankPos.GetPositionY(), tankPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + else + { + constexpr float CONSOLIDATE_THRESHOLD = 5.0f; + if (currentTarget->GetExactDist2d(boss) > CONSOLIDATE_THRESHOLD) + MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), boss->GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + } + + return false; + } + + Unit* targetAdd = FindAndCollectAdd(boss); + if (targetAdd) + { + if (botAI->IsAssistTank(bot) || !IsAssistTankAlive()) + { + constexpr float MARK_RANGE = 25.0f; + Unit* nearAdd = FindAddNearBoss(boss, MARK_RANGE); + UpdateRaidTargetIcon(nearAdd ? nearAdd : boss); + } + } + + if (boss->IsAlive()) + { + { + constexpr float MARK_RANGE = 25.0f; + Unit* nearAdd = FindAddNearBoss(boss, MARK_RANGE); + UpdateRaidTargetIcon(nearAdd ? nearAdd : boss); + } + + if (!boss->HealthAbovePct(95)) + { + bool anyTankHasAggro = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsTank(member) && boss->GetVictim() == member) + { + anyTankHasAggro = true; + break; + } + } + } + + if (anyTankHasAggro) + { + Position const& tankPos = ICC_LDW_TANK_POSTION; + if (bot->GetExactDist2d(tankPos.GetPositionX(), tankPos.GetPositionY()) > 5.0f) + MoveTo(bot->GetMapId(), tankPos.GetPositionX(), tankPos.GetPositionY(), tankPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + } + } + + return false; + } + + // Phase 1 mana drain: every hunter keeps its own Viper Sting up on boss. + // Per-hunter aura tracking via caster-GUID so multiple hunters stack cleanly. + if (bot->getClass() == CLASS_HUNTER && boss && boss->HealthAbovePct(95) && boss->IsAlive() && !boss->HasAura(SPELL_VIPER_STING, bot->GetGUID()) && botAI->CanCastSpell(SPELL_VIPER_STING, boss)) + botAI->CastSpell("viper sting", boss); + + if (HandleNonTankAddEvasion()) + return false; + + ApplyNearbyAddCC(); + + if (!boss->HealthAbovePct(95) && (botAI->IsRanged(bot) || botAI->IsHeal(bot))) + { + bool threatened = false; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && unit->GetVictim() == bot && (IsAdd(unit) || unit->GetEntry() == NPC_SHADE)) + { + threatened = true; + break; + } + } + + if (!threatened) + { + constexpr float REGROUP_DISTANCE = 18.0f; + Position const& tankPos = ICC_LDW_TANK_POSTION; + if (bot->GetExactDist2d(tankPos.GetPositionX(), tankPos.GetPositionY()) > REGROUP_DISTANCE) + MoveTo(bot->GetMapId(), tankPos.GetPositionX(), tankPos.GetPositionY(), tankPos.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + } + + if (IsAddsAlive()) + return false; + + if (boss->HealthAbovePct(95)) + return EngageBoss(); + + return false; +} + +Unit* IccAddsLadyDeathwhisperAction::FindAndCollectAdd(Unit* boss) +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* bestAdd = nullptr; + float bestScore = FLT_MAX; + + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || !unit->IsAlive()) + continue; + + if (!IsAdd(unit)) + continue; + + Unit* victim = unit->GetVictim(); + if (victim && victim->IsPlayer() && botAI->IsTank(victim->ToPlayer())) + continue; + + float distance = bot->GetExactDist(unit); + float score = distance; + if (victim && victim != bot) + score += 5.0f; + + if (score < bestScore) + { + bestScore = score; + bestAdd = unit; + } + } + + if (!bestAdd) + return nullptr; + + float const tauntRange = 30.0f; + float const currentDist = bot->GetExactDist(bestAdd); + + if (currentDist > tauntRange) + { + float moveX = bot->GetPositionX(); + float moveY = bot->GetPositionY(); + float moveZ = bot->GetPositionZ(); + float step = 5.0f; + float dx = bestAdd->GetPositionX() - bot->GetPositionX(); + float dy = bestAdd->GetPositionY() - bot->GetPositionY(); + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > 0.1f) + { + dx /= dist; + dy /= dist; + float moveDist = std::min(step, dist - (tauntRange - 5.0f)); + moveX = bot->GetPositionX() + dx * moveDist; + moveY = bot->GetPositionY() + dy * moveDist; + } + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + return nullptr; + } + + bool taunted = false; + if (botAI->CastSpell("taunt", bestAdd)) + taunted = true; + else + { + switch (bot->getClass()) + { + case CLASS_PALADIN: + taunted = botAI->CastSpell("hand of reckoning", bestAdd); + break; + case CLASS_DEATH_KNIGHT: + taunted = botAI->CastSpell("dark command", bestAdd); + break; + case CLASS_DRUID: + taunted = botAI->CastSpell("growl", bestAdd); + break; + default: + break; + } + } + + if (!taunted) + { + if (currentDist > 5.0f) + { + float dx = bestAdd->GetPositionX() - bot->GetPositionX(); + float dy = bestAdd->GetPositionY() - bot->GetPositionY(); + float dist = std::sqrt(dx * dx + dy * dy); + dx /= dist; + dy /= dist; + float step = std::max(0.5f, std::min(3.0f, dist - 3.0f)); + float moveX = bot->GetPositionX() + dx * step; + float moveY = bot->GetPositionY() + dy * step; + MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + return nullptr; + } + } + + if (boss && boss->HealthAbovePct(95)) + { + constexpr float CONSOLIDATE_THRESHOLD = 5.0f; + if (bestAdd->GetExactDist2d(boss) > CONSOLIDATE_THRESHOLD) + MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), boss->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + + return bestAdd; +} + +bool IccAddsLadyDeathwhisperAction::HandleNonTankAddEvasion() +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + bool targetedByAdd = false; + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || !unit->IsAlive() || unit->GetVictim() != bot) + continue; + + if (IsAdd(unit)) + { + targetedByAdd = true; + ApplyCCToAdd(unit); + } + } + + if (!targetedByAdd) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); + if (!boss) + return false; + + if (boss->HealthAbovePct(95)) + { + Unit* closestAdd = nullptr; + float closestDist = FLT_MAX; + GuidVector const npcs2 = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npcGuid : npcs2) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || !unit->IsAlive() || unit->GetVictim() != bot || !IsAdd(unit)) + continue; + + float d = bot->GetExactDist2d(unit); + if (d < closestDist) + { + closestDist = d; + closestAdd = unit; + } + } + + if (!closestAdd) + return false; + + constexpr float KEEP_DISTANCE = 10.0f; + if (closestDist >= KEEP_DISTANCE) + return false; + + float dx = bot->GetPositionX() - closestAdd->GetPositionX(); + float dy = bot->GetPositionY() - closestAdd->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) + return false; + + dx /= len; + dy /= len; + float moveX = bot->GetPositionX() + dx * (KEEP_DISTANCE - closestDist + 2.0f); + float moveY = bot->GetPositionY() + dy * (KEEP_DISTANCE - closestDist + 2.0f); + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + else + { + constexpr float ARRIVAL_THRESHOLD = 5.0f; + if (bot->GetExactDist2d(boss) <= ARRIVAL_THRESHOLD) + return false; + + return MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), boss->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT); + } +} + +bool IccAddsLadyDeathwhisperAction::ApplyNearbyAddCC() +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + constexpr float CC_RANGE = 10.0f; + + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || !unit->IsAlive()) + continue; + + if (!IsAdd(unit)) + continue; + + if (bot->GetDistance(unit) <= CC_RANGE) + ApplyCCToAdd(unit); + } + + return false; +} + +bool IccAddsLadyDeathwhisperAction::ApplyCCToAdd(Unit* add) +{ + IccTryClassCC(bot, botAI, add); + return false; +} + +bool IccAddsLadyDeathwhisperAction::IsAddsAlive() +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->IsAlive() && IsAdd(unit)) + return true; + } + + return false; +} + +bool IccAddsLadyDeathwhisperAction::EngageBoss() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); + if (!boss || !boss->IsAlive()) + return false; + + bool const isMelee = botAI->IsMelee(bot); + bool const isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); + + if (isMelee) + { + float dist = bot->GetDistance(boss); + if (dist > 25.0f) + { + float dx = boss->GetPositionX() - bot->GetPositionX(); + float dy = boss->GetPositionY() - bot->GetPositionY(); + float len = std::sqrt(dx * dx + dy * dy); + if (len > 0.1f) + { + dx /= len; + dy /= len; + float step = std::min(3.0f, len); + float moveX = bot->GetPositionX() + dx * step; + float moveY = bot->GetPositionY() + dy * step; + MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + return false; + } + } + } + + if (isRanged) + { + Position const rangedPos = ICC_LDW_RANGED_POSITION; + float const distToPos = bot->GetDistance2d(rangedPos.GetPositionX(), rangedPos.GetPositionY()); + if (distToPos > 30.0f) + { + MoveTo(bot->GetMapId(), rangedPos.GetPositionX(), rangedPos.GetPositionY(), rangedPos.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_COMBAT); + return false; + } + } + + return false; +} + +Unit* IccAddsLadyDeathwhisperAction::FindAddNearBoss(Unit* boss, float maxDist) +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* best = nullptr; + float bestDist = maxDist; + + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (!unit || !unit->IsAlive() || !IsAdd(unit)) + continue; + + float dist = unit->GetExactDist2d(boss); + if (dist < bestDist) + { + bestDist = dist; + best = unit; + } + } + + return best; +} + +bool IccAddsLadyDeathwhisperAction::IsAdd(Unit* unit) +{ + if (!unit) + return false; + + uint32 entry = unit->GetEntry(); + return std::find(addEntriesLady.begin(), addEntriesLady.end(), entry) != addEntriesLady.end(); +} + +bool IccAddsLadyDeathwhisperAction::IsAssistTankAlive() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + if (botAI->IsTank(member) && !botAI->IsMainTank(member)) + return true; + } + + return false; +} + +bool IccAddsLadyDeathwhisperAction::IsTargetedByShade(uint32 shadeEntry) +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == shadeEntry && unit->GetVictim() == bot) + return true; + } + + return false; +} + +bool IccAddsLadyDeathwhisperAction::HandleAddTargeting(Unit* boss) +{ + GuidVector const targets = AI_VALUE(GuidVector, "possible targets no los"); + + Unit* priorityTarget = nullptr; + bool hasValidAdds = false; + + for (auto const& entry : addEntriesLady) + { + for (auto const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + { + priorityTarget = unit; + hasValidAdds = true; + break; + } + } + + if (priorityTarget) + break; + } + + if (!hasValidAdds && boss->IsAlive()) + priorityTarget = boss; + + if (priorityTarget) + UpdateRaidTargetIcon(priorityTarget); + + return false; +} + +bool IccAddsLadyDeathwhisperAction::UpdateRaidTargetIcon(Unit* target) +{ + static constexpr uint8 SKULL_ICON_INDEX = RtiTargetValue::skullIndex; + + if (!target || !target->IsAlive()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); + Unit* currentSkullUnit = botAI->GetUnit(currentSkull); + + bool const currentIsAdd = currentSkullUnit && currentSkullUnit->IsAlive() && IsAdd(currentSkullUnit); + bool const targetIsAdd = IsAdd(target); + + bool const shouldUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || (targetIsAdd && !currentIsAdd); + + if (shouldUpdate) + group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), target->GetGUID()); + + return false; +} + +bool IccShadeLadyDeathwhisperAction::Execute(Event /*event*/) +{ + static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE; + static constexpr float SAFE_DISTANCE = 12.0f; + + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + float fleeX = 0.0f; + float fleeY = 0.0f; + float closestDist = SAFE_DISTANCE; + int shadeCount = 0; + + for (auto const& npcGuid : npcs) + { + Unit* shade = botAI->GetUnit(npcGuid); + + if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID) + continue; + + if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID()) + continue; + + float const currentDistance = bot->GetDistance2d(shade); + + if (currentDistance >= SAFE_DISTANCE) + continue; + + float dx = bot->GetPositionX() - shade->GetPositionX(); + float dy = bot->GetPositionY() - shade->GetPositionY(); + float dist = std::sqrt(dx * dx + dy * dy); + + if (dist < 0.001f) + continue; + + float weight = (SAFE_DISTANCE - currentDistance) / dist; + fleeX += dx * weight; + fleeY += dy * weight; + + if (currentDistance < closestDist) + closestDist = currentDistance; + + shadeCount++; + } + + if (shadeCount == 0) + return false; + + float fleeDist = std::sqrt(fleeX * fleeX + fleeY * fleeY); + if (fleeDist < 0.001f) + return false; + + fleeX /= fleeDist; + fleeY /= fleeDist; + + float moveDistance = SAFE_DISTANCE - closestDist + 2.0f; + float targetX = bot->GetPositionX() + fleeX * moveDistance; + float targetY = bot->GetPositionY() + fleeY * moveDistance; + float targetZ = bot->GetPositionZ(); + + if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + { + moveDistance *= 0.5f; + targetX = bot->GetPositionX() + fleeX * moveDistance; + targetY = bot->GetPositionY() + fleeY * moveDistance; + } + + botAI->Reset(); + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +// Rotting Giant WiP (waiting for core fix) +bool IccRottingFrostGiantTankPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotting frost giant"); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("death plague", bot, false, false); + if (aura) + bot->RemoveAura(aura->GetId()); + + /* TODO: code works for handling plague, but atm script is bugged and one bot can have 2 plagues at the same time or + when cured, which should not happen, and it is immpossible to handle plague atm the legit way. const bool hasCure + = botAI->GetAura("recently infected", bot) != nullptr; + + // Tank behavior - unchanged + if (botAI->IsTank(bot) && botAI->HasAggro(boss) && !isInfected) + if (bot->GetExactDist2d(ICC_ROTTING_FROST_GIANT_TANK_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionX(), + ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionY(), + ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + + if (botAI->IsTank(bot)) + return false; + + // Handle infected bot behavior - move near a non-infected, non-cured bot + if (isInfected) + { + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Count how many bots are targeting each potential target + std::map targetCounts; + + // First, identify all infected bots and their current targets (approximate) + for (auto const& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; + + if (memberIsInfected) + { + // Find the nearest non-infected bot to this infected bot (as a guess of its target) + float minDist = 5.0f; // Only count if they're close enough to likely be targeting + Unit* likelyTarget = nullptr; + + for (auto const& targetGuid : members) + { + Unit* potentialTarget = botAI->GetUnit(targetGuid); + if (!potentialTarget || !potentialTarget->IsAlive() || potentialTarget == member) + continue; + + const bool targetIsInfected = botAI->GetAura("death plague", potentialTarget) != nullptr; + const bool targetHasCure = botAI->GetAura("recently infected", potentialTarget) != nullptr; + + if (!targetIsInfected && !targetHasCure) + { + float dist = member->GetExactDist2d(potentialTarget); + if (dist < minDist) + { + minDist = dist; + likelyTarget = potentialTarget; + } + } + } + + if (likelyTarget) + { + targetCounts[likelyTarget->GetGUID()]++; + } + } + } + + // Find viable targets and score them based on various factors + std::vector> viableTargets; + + // First try to find ranged, non-infected, non-cured bots + for (auto const& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + const bool memberHasCure = botAI->GetAura("recently infected", member) != nullptr; + const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; + + if (!memberIsInfected && !memberHasCure) + { + // Base score is distance (lower is better) + float score = bot->GetExactDist2d(member); + + // Prefer ranged targets + if (botAI->IsRanged(bot)) + { + score *= 0.7f; // Bonus for ranged targets + } + + // Apply penalty based on how many other infected bots are targeting this one + int targetingCount = targetCounts[member->GetGUID()]; + score *= (1.0f + targetingCount * 0.5f); // Increase score (worse) for heavily targeted bots + + viableTargets.push_back(std::make_pair(member, score)); + } + } + + // Sort targets by score (lowest/best first) + std::sort(viableTargets.begin(), viableTargets.end(), + [](const std::pair& a, const std::pair& b) + { return a.second < b.second; }); + + // Choose the best target + Unit* targetBot = nullptr; + if (!viableTargets.empty()) + { + targetBot = viableTargets[0].first; + } + + // Move to target bot if found + if (targetBot) + { + // If we're already close enough (1 yard), no need to move + if (bot->GetExactDist2d(targetBot) > 1.0f) + { + // Calculate a unique angle based on bot's GUID to ensure different approach angles + // This helps spread infected bots around the target + uint32 guidLow = bot->GetGUID().GetCounter(); + float angleOffset = float(guidLow % 628) / 100.0f; // Random angle between 0 and 2π + + // Calculate position 1 yard away from target at our unique angle + float angle = targetBot->GetOrientation() + angleOffset; + float targetX = targetBot->GetPositionX() + cos(angle) * 1.0f; + float targetY = targetBot->GetPositionY() + sin(angle) * 1.0f; + float targetZ = targetBot->GetPositionZ(); + + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + return true; // Already in position + } + // No suitable target found, continue with normal behavior + } + + // For ranged bots, only spread from non-infected bots + if (botAI->IsRanged(bot)) + { + const float safeSpacingRadius = 11.0f; + const float moveIncrement = 2.0f; + const float maxMoveDistance = 15.0f; + + const GuidVector members = AI_VALUE(GuidVector, "group members"); + + // Calculate a combined vector representing all nearby NON-INFECTED members' positions + float totalX = 0.0f; + float totalY = 0.0f; + int nearbyCount = 0; + + for (auto const& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || member == bot) + continue; + + // Only spread from non-infected bots (can stay near infected or cured bots) + const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; + if (memberIsInfected) + continue; + + const float distance = bot->GetExactDist2d(member); + if (distance < safeSpacingRadius) + { + // Calculate vector from member to bot + float dx = bot->GetPositionX() - member->GetPositionX(); + float dy = bot->GetPositionY() - member->GetPositionY(); + + // Weight by inverse distance (closer members have more influence) + float weight = (safeSpacingRadius - distance) / safeSpacingRadius; + totalX += dx * weight; + totalY += dy * weight; + nearbyCount++; + } + } + + // If we have nearby non-infected members, move away in the combined direction + if (nearbyCount > 0) + { + // Normalize the combined vector + float magnitude = std::sqrt(totalX * totalX + totalY * totalY); + if (magnitude > 0.001f) // Avoid division by zero + { + totalX /= magnitude; + totalY /= magnitude; + + // Calculate move distance based on nearest member + float moveDistance = std::min(moveIncrement, maxMoveDistance); + + // Create target position in the combined direction + float targetX = bot->GetPositionX() + totalX * moveDistance; + float targetY = bot->GetPositionY() + totalY * moveDistance; + float targetZ = bot->GetPositionZ(); // Maintain current Z + + // Check if the target position is valid and move there + if (bot->IsWithinLOS(targetX, targetY, targetZ)) + { + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + else + { + // If los check fails, try shorter distance + targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); + targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, + MovementPriority::MOVEMENT_NORMAL); + } + } + } + } + */ + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LK.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LK.cpp new file mode 100644 index 000000000..ab84976d4 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LK.cpp @@ -0,0 +1,4406 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCActions.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "Vehicle.h" +#include "RtiValue.h" +#include "GenericSpellActions.h" +#include "GenericActions.h" +#include "ICCTriggers.h" +#include "ICCScripts.h" +#include "Multiplier.h" + +static bool IsLkShambling(uint32 entry) +{ + return entry == NPC_SHAMBLING_HORROR1 || entry == NPC_SHAMBLING_HORROR2 || + entry == NPC_SHAMBLING_HORROR3 || entry == NPC_SHAMBLING_HORROR4; +} + +static bool IsLkRagingSpirit(uint32 entry) +{ + return entry == NPC_RAGING_SPIRIT1 || entry == NPC_RAGING_SPIRIT2 || + entry == NPC_RAGING_SPIRIT3 || entry == NPC_RAGING_SPIRIT4; +} + +static bool IsLkValkyr(Unit* unit) +{ + if (!unit) + return false; + + uint32 const entry = unit->GetEntry(); + return entry == NPC_VALKYR_SHADOWGUARD1 || entry == NPC_VALKYR_SHADOWGUARD2 || + entry == NPC_VALKYR_SHADOWGUARD3 || entry == NPC_VALKYR_SHADOWGUARD4; +} + +static bool IsLkVileSpirit(Unit* unit) +{ + if (!unit) + return false; + + uint32 const entry = unit->GetEntry(); + return entry == NPC_VILE_SPIRIT1 || entry == NPC_VILE_SPIRIT2 || + entry == NPC_VILE_SPIRIT3 || entry == NPC_VILE_SPIRIT4; +} + +static bool IsLkWickedSpirit(uint32 entry) +{ + return entry == NPC_WICKED_SPIRIT1 || entry == NPC_WICKED_SPIRIT2 || + entry == NPC_WICKED_SPIRIT3 || entry == NPC_WICKED_SPIRIT4; +} + +static bool IsIceSphere(uint32 entry) +{ + return entry == NPC_ICE_SPHERE1 || entry == NPC_ICE_SPHERE2 || + entry == NPC_ICE_SPHERE3 || entry == NPC_ICE_SPHERE4; +} + +static bool IsLkCollectibleAdd(Unit* unit) +{ + if (!unit || !unit->IsAlive()) + return false; + + uint32 const entry = unit->GetEntry(); + return IsLkShambling(entry) || IsLkRagingSpirit(entry) || + entry == NPC_DRUDGE_GHOUL1 || entry == NPC_DRUDGE_GHOUL2 || + entry == NPC_DRUDGE_GHOUL3 || entry == NPC_DRUDGE_GHOUL4; +} + +static bool IsHeroicLk(Difficulty diff) +{ + return diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC; +} + +static float GetDefileEffectiveRadius(Unit const* defile, Difficulty diff) +{ + static constexpr float BASE_RADIUS = 6.0f; + + for (uint32 const auraId : DEFILE_AURAS) + { + Aura const* grow = defile->GetAura(auraId); + if (!grow) + continue; + + float const multiplier = + (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) ? 1.4f : 0.95f; + + return BASE_RADIUS + grow->GetStackAmount() * multiplier; + } + + return BASE_RADIUS; +} + +// Compute the centroid of alive group members for a stable reference point. +// Unlike a single reference player, the centroid barely shifts when one +// player dies, preventing the raid from oscillating between positions. +static Position ComputeGroupCentroid(Player* bot) +{ + Group* group = bot->GetGroup(); + if (!group) + return Position(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + + float sumX = 0.0f; + float sumY = 0.0f; + int count = 0; + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !member->IsInWorld()) + continue; + + sumX += member->GetPositionX(); + sumY += member->GetPositionY(); + ++count; + } + + if (count == 0) + return Position(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); + + return Position(sumX / count, sumY / count, bot->GetPositionZ()); +} + +static Position const& SelectClosestOf3(Position const& ref, Position const& p1, Position const& p2, Position const& p3) +{ + float const dx1 = ref.GetPositionX() - p1.GetPositionX(); + float const dy1 = ref.GetPositionY() - p1.GetPositionY(); + float const d1 = dx1 * dx1 + dy1 * dy1; + + float const dx2 = ref.GetPositionX() - p2.GetPositionX(); + float const dy2 = ref.GetPositionY() - p2.GetPositionY(); + float const d2 = dx2 * dx2 + dy2 * dy2; + + float const dx3 = ref.GetPositionX() - p3.GetPositionX(); + float const dy3 = ref.GetPositionY() - p3.GetPositionY(); + float const d3 = dx3 * dx3 + dy3 * dy3; + + if (d2 < d1 && d2 < d3) + return p2; + if (d3 < d1 && d3 < d2) + return p3; + return p1; +} + +// AoE taunt — returns true if a spell was cast +static bool CastAoeTaunt(PlayerbotAI* botAI, Player* bot) +{ + switch (bot->getClass()) + { + case CLASS_WARRIOR: + if (botAI->CastSpell("challenging shout", bot)) + return true; + break; + case CLASS_DRUID: + if (botAI->CastSpell("challenging roar", bot)) + return true; + break; + default: + break; + } + return false; +} + +bool IccLichKingShadowTrapAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss || !botAI->IsTank(bot)) + return false; + + Difficulty const diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig.EnableICCBuffs && boss->IsInCombat() && IsHeroicLk(diff)) + IccApplyHeroicBuffToMember(botAI, bot, true, false); + + static constexpr float CIRCLE_RADIUS = 20.0f; + static constexpr float SAFE_DISTANCE = 12.0f; + static constexpr float SEARCH_DISTANCE = SAFE_DISTANCE + 5.0f; + static constexpr int TEST_POSITIONS = 16; + static constexpr float ANGLE_STEP = 2.0f * float(M_PI) / TEST_POSITIONS; + + float const centerX = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(); + float const centerY = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY(); + float const centerZ = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionZ(); + + // Collect shadow traps within search range + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector nearbyTraps; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != NPC_SHADOW_TRAP) + continue; + + if (bot->GetDistance(unit) < SEARCH_DISTANCE) + nearbyTraps.push_back(unit); + } + + if (nearbyTraps.empty()) + return false; + + // Check whether the current position is already safe + auto isSafePosition = [&](float x, float y) -> bool + { + for (Unit const* trap : nearbyTraps) + { + if (trap->GetDistance2d(x, y) < SAFE_DISTANCE) + return false; + } + return true; + }; + + if (isSafePosition(bot->GetPositionX(), bot->GetPositionY())) + return false; + + // Walk clockwise around the centre circle to find the nearest safe spot + float const currentAngle = std::atan2(bot->GetPositionY() - centerY, bot->GetPositionX() - centerX); + + for (int i = 1; i <= TEST_POSITIONS + 1; ++i) + { + // Last iteration tests the diametrically opposite point as a fallback + float const testAngle = (i <= TEST_POSITIONS) + ? currentAngle - ANGLE_STEP * i + : currentAngle + float(M_PI); + + float testX = centerX + std::cos(testAngle) * CIRCLE_RADIUS; + float testY = centerY + std::sin(testAngle) * CIRCLE_RADIUS; + float testZ = centerZ; + + bot->UpdateAllowedPositionZ(testX, testY, testZ); + + if (!bot->IsWithinLOS(testX, testY, testZ)) + continue; + + if (!isSafePosition(testX, testY)) + continue; + + return MoveTo(bot->GetMapId(), testX, testY, testZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IccLichKingNecroticPlagueAction::Execute(Event /*event*/) +{ + if (!botAI->HasAura("Necrotic Plague", bot)) + return false; + + static constexpr float DELIVER_RANGE = 3.0f; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + // Prefer shamblings tanked by the assist tank so plague is delivered + // to the correct add pile. Fall back to nearest if none qualify. + Unit* closestHorror = nullptr; + float minDist = 150.0f; + Unit* fallbackHorror = nullptr; + float fallbackDist = 150.0f; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (!IsLkShambling(unit->GetEntry())) + continue; + + float const dist = bot->GetDistance(unit); + + Unit* victim = unit->GetVictim(); + if (victim && botAI->IsAssistTank(victim->ToPlayer())) + { + if (dist < minDist) + { + minDist = dist; + closestHorror = unit; + } + } + else if (dist < fallbackDist) + { + fallbackDist = dist; + fallbackHorror = unit; + } + } + + if (!closestHorror) + { + closestHorror = fallbackHorror; + minDist = fallbackDist; + } + + // No alive Horror — stop moving so healers can dispel immediately. + // Keeping the plague active with no valid target risks a raid wipe. + if (!closestHorror) + { + bot->StopMoving(); + bot->AttackStop(); + bot->SetTarget(ObjectGuid::Empty); + return true; + } + + // Already in delivery range — hold position for dispel + if (minDist <= DELIVER_RANGE) + { + bot->StopMoving(); + return true; + } + + // Move toward the Horror, suppressing combat so we don't get sidetracked + bot->AttackStop(); + bot->SetTarget(ObjectGuid::Empty); + + // During winter (boss <= 71% HP) approach from behind the shambling to + // avoid its frontal Shockwave. + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + bool const duringWinter = boss && !boss->HealthAbovePct(71); + + if (duringWinter) + { + Unit* victim = closestHorror->GetVictim(); + if (victim) + { + static constexpr float BEHIND_OFFSET = 2.0f; + float const vecX = closestHorror->GetPositionX() - victim->GetPositionX(); + float const vecY = closestHorror->GetPositionY() - victim->GetPositionY(); + float const vecLen = std::hypot(vecX, vecY); + + if (vecLen > 0.1f) + { + float const dirX = vecX / vecLen; + float const dirY = vecY / vecLen; + + // Try directly behind, then slight left/right arcs + static constexpr std::array Arcs = {0.0f, 0.35f, -0.35f, 0.70f, -0.70f}; + + for (float const arc : Arcs) + { + float const cosA = std::cos(arc); + float const sinA = std::sin(arc); + float const rotX = dirX * cosA - dirY * sinA; + float const rotY = dirX * sinA + dirY * cosA; + + float const destX = closestHorror->GetPositionX() + rotX * BEHIND_OFFSET; + float const destY = closestHorror->GetPositionY() + rotY * BEHIND_OFFSET; + + if (bot->IsWithinLOS(destX, destY, closestHorror->GetPositionZ())) + { + return MoveTo(bot->GetMapId(), destX, destY, closestHorror->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + } + } + + return MoveTo(bot->GetMapId(), closestHorror->GetPositionX(), closestHorror->GetPositionY(), + closestHorror->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool IccLichKingWinterAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return false; + + Difficulty const diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig.EnableICCBuffs && boss->IsInCombat() && IsHeroicLk(diff)) + IccApplyHeroicBuffToMember(botAI, bot, true, true); + + // Speed boost to help escape the inward push + if (bot->GetDistance2d(boss) < 35.0f && !bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + // Heroic: if more than 2 Raging Spirits are alive, kill the one with the + // highest HP. Only the main tank does this to avoid simultaneous actions. + if (botAI->IsMainTank(bot) && IsHeroicLk(diff)) + { + GuidVector const& heroicNpcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector spirits; + + for (ObjectGuid const& guid : heroicNpcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (IsLkRagingSpirit(unit->GetEntry())) + spirits.push_back(unit); + } + + if (spirits.size() > 2) + { + Unit* killTarget = *std::max_element(spirits.begin(), spirits.end(), + [](Unit const* a, Unit const* b) { return a->GetHealth() < b->GetHealth(); }); + bot->Kill(bot, killTarget); + } + } + + // Skull marking during Remorseless Winter. + // Priority: Shambling Horror > Raging Spirit (both on MT within 10 yd). + // Only re-mark when the current skull target is dead/gone. + if (Group* group = bot->GetGroup()) + { + static constexpr float MARK_RANGE = 10.0f; + + Unit* currentSkull = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + bool const currentValid = currentSkull && currentSkull->IsAlive() && (IsLkShambling(currentSkull->GetEntry()) || IsLkRagingSpirit(currentSkull->GetEntry())); + + if (!currentValid) + { + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + GuidVector const& winterNpcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* bestShambling = nullptr; + Unit* bestSpirit = nullptr; + + for (ObjectGuid const& guid : winterNpcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (!mainTank || unit->GetVictim() != mainTank) + continue; + + if (mainTank->GetDistance(unit) > MARK_RANGE) + continue; + + uint32 const entry = unit->GetEntry(); + if (!bestShambling && IsLkShambling(entry)) + bestShambling = unit; + else if (!bestSpirit && IsLkRagingSpirit(entry)) + bestSpirit = unit; + + if (bestShambling) + break; + } + + Unit* markTarget = bestShambling ? bestShambling : bestSpirit; + + if (markTarget) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), markTarget->GetGUID()); + else if (!group->GetTargetIcon(RtiTargetValue::skullIndex).IsEmpty()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), ObjectGuid::Empty); + } + } + + // Teleport back onto the platform if the engine pushed us through + FixPlatformPosition(); + + // Staging during Remorseless Winter cast: non-tanks converge on ONE shared + // safe vile spirit anchor and hold for 4s before falling through to the + // final-spot logic. The chosen anchor is locked-in on first detection of + // the cast so all bots agree even if defile state shifts mid-cast. + // Among safe slots VILE_SPIRIT1 and VILE_SPIRIT3, picks + // the one closest to the group centroid; VILE_SPIRIT2 is fallback only. + auto& s_winterStage = IcecrownHelpers::IccState(boss->GetInstanceId()).winterStage; + ObjectGuid const winterKey = boss->GetGUID(); + + bool const bossCastingWinter = IccBossCastingRemorselessWinter(boss); + + static constexpr uint32 STAGE_DURATION_MS = 4000; + + // Enter staging block if boss is casting Winter OR a staging entry exists + // that hasn't yet expired. The cast ends before the aura applies, so we + // can't rely on bossCastingWinter staying true for the full window. + uint32 const now = getMSTime(); + auto winterIt = s_winterStage.find(winterKey); + bool const stageActive = winterIt != s_winterStage.end() && + getMSTimeDiff(winterIt->second.startMs, now) < STAGE_DURATION_MS; + + if (!botAI->IsTank(bot) && (bossCastingWinter || stageActive)) + { + if (winterIt == s_winterStage.end()) + { + Position const* candidates[2] = {&ICC_LK_VILE_SPIRIT1_POSITION, + &ICC_LK_VILE_SPIRIT3_POSITION}; + Position const centroid = ComputeGroupCentroid(bot); + + Position const* chosen = nullptr; + float bestDist = std::numeric_limits::max(); + for (Position const* slot : candidates) + { + if (!IsPositionSafeFromDefile(slot->GetPositionX(), slot->GetPositionY(), + PLATFORM_Z, 3.0f)) + continue; + + if (!IsPositionSafeFromShadowTraps(slot->GetPositionX(), slot->GetPositionY())) + continue; + + float const d = std::hypot(centroid.GetPositionX() - slot->GetPositionX(), + centroid.GetPositionY() - slot->GetPositionY()); + if (d < bestDist) + { + bestDist = d; + chosen = slot; + } + } + + if (!chosen && + IsPositionSafeFromDefile(ICC_LK_VILE_SPIRIT2_POSITION.GetPositionX(), + ICC_LK_VILE_SPIRIT2_POSITION.GetPositionY(), + PLATFORM_Z, 3.0f) && + IsPositionSafeFromShadowTraps(ICC_LK_VILE_SPIRIT2_POSITION.GetPositionX(), + ICC_LK_VILE_SPIRIT2_POSITION.GetPositionY())) + chosen = &ICC_LK_VILE_SPIRIT2_POSITION; + + winterIt = s_winterStage.emplace(winterKey, IcecrownHelpers::WinterStageState{now, chosen}).first; + } + + uint32 const elapsed = getMSTimeDiff(winterIt->second.startMs, now); + Position const* stagePos = winterIt->second.pos; + + if (stagePos && elapsed < STAGE_DURATION_MS) + { + static constexpr float STAGE_TOLERANCE = 3.0f; + auto& s_stageInbound = IcecrownHelpers::IccState(bot->GetInstanceId()).winterStageInbound; + ObjectGuid const stageKey = bot->GetGUID(); + float const dist = bot->GetDistance2d(stagePos->GetPositionX(), stagePos->GetPositionY()); + if (dist > STAGE_TOLERANCE) + { + if (!s_stageInbound[stageKey]) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->SetTarget(ObjectGuid::Empty); + context->GetValue("current target")->Set(nullptr); + botAI->Reset(); + s_stageInbound[stageKey] = true; + } + TryMoveToPosition(stagePos->GetPositionX(), stagePos->GetPositionY(), PLATFORM_Z, true); + } + else + { + s_stageInbound[stageKey] = false; + bot->StopMoving(); + } + return true; + } + } + else if (!bossCastingWinter && !stageActive) + { + // Only clear when no staging window is active (timer expired AND not + // re-casting). This keeps the staging entry alive across the brief + // gap between cast end and aura application, and prevents tanks from + // wiping the entry mid-staging (tanks always hit this branch). + s_winterStage.erase(winterKey); + } + + // Defile evacuation has absolute priority over everything else + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) + { + Position const& safePos = botAI->IsTank(bot) + ? *GetMainTankPosition() + : *GetMainTankRangedPosition(); + + TryMoveToPosition(safePos.GetPositionX(), safePos.GetPositionY(), PLATFORM_Z, true); + return true; + } + + // If any ice sphere is targeting this bot, move to the midpoint between the + // melee and ranged frost positions + if (!botAI->IsTank(bot)) + { + GuidVector const& sphereNpcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (ObjectGuid const& guid : sphereNpcs) + { + Unit* sphere = botAI->GetUnit(guid); + if (!sphere || !sphere->IsAlive() || !IsIceSphere(sphere->GetEntry())) + continue; + + if (sphere->GetVictim() != bot) + continue; + + Position const& meleePos = *GetMainTankPosition(); + Position const& rangedPos = *GetMainTankRangedPosition(); + float const midX = (meleePos.GetPositionX() + rangedPos.GetPositionX()) * 0.5f; + float const midY = (meleePos.GetPositionY() + rangedPos.GetPositionY()) * 0.5f; + + static constexpr float SPHERE_HOLD_TOLERANCE = 2.0f; + if (bot->GetDistance2d(midX, midY) > SPHERE_HOLD_TOLERANCE) + TryMoveToPosition(midX, midY, PLATFORM_Z, true); + else + bot->StopMoving(); + return false; + } + } + + ClearInvalidTarget(); + HandlePetManagement(); + + if (botAI->IsTank(bot)) + HandleTankPositioning(); + else if (!botAI->IsRanged(bot)) + { + // Non-tank melee: + // - MT at the frost anchor with an add (shambling/spirit) within + // 5y of him: adds are in place, flank them. + // - Otherwise: wait at midpoint between melee and ranged anchors + // (the ice sphere hold spot), close enough to engage once adds + // settle, far enough not to eat frontals while MT drags them. + // Never pulled to MT/melee anchor. + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + bool addNearTank = false; + bool tankInPlace = false; + + if (mainTank && mainTank->IsAlive()) + { + static constexpr float TANK_IN_PLACE_RANGE = 10.0f; + Position const& frostPos = *GetMainTankPosition(); + tankInPlace = mainTank->GetDistance2d(frostPos.GetPositionX(), + frostPos.GetPositionY()) <= TANK_IN_PLACE_RANGE; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (ObjectGuid const& guid : npcs) + { + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsAlive()) + continue; + if (!IsLkShambling(add->GetEntry()) && !IsLkRagingSpirit(add->GetEntry())) + continue; + if (add->GetDistance2d(mainTank) <= 5.0f) + { + addNearTank = true; + break; + } + } + } + + if (addNearTank && tankInPlace) + HandleMeleePositioning(); + else + { + Position const& meleePos = *GetMainTankPosition(); + Position const& rangedPos = *GetMainTankRangedPosition(); + float const midX = (meleePos.GetPositionX() + rangedPos.GetPositionX()) * 0.5f; + float const midY = (meleePos.GetPositionY() + rangedPos.GetPositionY()) * 0.5f; + + static constexpr float MID_TOLERANCE = 2.0f; + if (bot->GetDistance2d(midX, midY) > MID_TOLERANCE) + TryMoveToPosition(midX, midY, PLATFORM_Z, true); + else + bot->StopMoving(); + return false; + } + } + else + HandleRangedPositioning(); + + return false; +} + +bool IccLichKingWinterAction::FixPlatformPosition() +{ + if (std::abs(bot->GetPositionZ() - PLATFORM_Z) > 1.0f) + { + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), + PLATFORM_Z, bot->GetOrientation()); + return true; + } + + return false; +} + +bool IccLichKingWinterAction::ClearInvalidTarget() +{ + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + if (!currentTarget) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + bool doClear = false; + + // Bots must not fight the boss during Remorseless Winter + if (boss && currentTarget == boss) + doClear = true; + + // Tanks must not chase ice spheres + if (botAI->IsTank(bot) && IsIceSphere(currentTarget->GetEntry())) + doClear = true; + + if (doClear) + { + context->GetValue("current target")->Set(nullptr); + bot->AttackStop(); + bot->SetTarget(ObjectGuid::Empty); + return true; + } + + return false; +} + +Position const* IccLichKingWinterAction::GetMainTankPosition() +{ + // Use group centroid so the chosen frost slot follows the majority of the + // raid, not the tanks. Non-tanks can't survive Remorseless Winter ticks + // while traversing the platform, so dragging them to a far slot just + // because tanks parked there gets them killed. Tanks tolerate a few + // ticks and will reroute to the majority slot too. + Position ref = ComputeGroupCentroid(bot); + + return &SelectClosestOf3(ref, ICC_LK_FROST1_POSITION, ICC_LK_FROST2_POSITION, ICC_LK_FROST3_POSITION); +} + +Position const* IccLichKingWinterAction::GetMainTankRangedPosition() +{ + Position const* melee = GetMainTankPosition(); + + if (melee == &ICC_LK_FROST1_POSITION) + return &ICC_LK_FROSTR1_POSITION; + if (melee == &ICC_LK_FROST2_POSITION) + return &ICC_LK_FROSTR2_POSITION; + + return &ICC_LK_FROSTR3_POSITION; +} + +bool IccLichKingWinterAction::IsPositionSafeFromDefile(float x, float y, float /*z*/, float minSafeDistance) const +{ + static constexpr float BASE_RADIUS = 6.0f; + static constexpr float SAFETY_MARGIN = 3.0f; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != DEFILE_NPC_ID) + continue; + + float currentRadius = BASE_RADIUS; + for (uint32 const auraId : DEFILE_AURAS) + { + Aura* grow = unit->GetAura(auraId); + if (!grow) + continue; + + float const growthMultiplier = + (bot->GetRaidDifficulty() == RAID_DIFFICULTY_10MAN_HEROIC || + bot->GetRaidDifficulty() == RAID_DIFFICULTY_10MAN_NORMAL) + ? 1.4f + : 0.95f; + + currentRadius = BASE_RADIUS + grow->GetStackAmount() * growthMultiplier; + break; + } + + float const dx = x - unit->GetPositionX(); + float const dy = y - unit->GetPositionY(); + + if (std::sqrt(dx * dx + dy * dy) < currentRadius + SAFETY_MARGIN + minSafeDistance) + return false; + } + + return true; +} + +bool IccLichKingWinterAction::IsPositionSafeFromShadowTraps(float x, float y) const +{ + // Shadow trap has a fixed 12 yd lethal radius (matches the SAFE_DISTANCE + // used by IccLichKingShadowTrapAction). + static constexpr float TRAP_SAFE_RADIUS = 12.0f; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != NPC_SHADOW_TRAP) + continue; + + float const dx = x - unit->GetPositionX(); + float const dy = y - unit->GetPositionY(); + + if (std::sqrt(dx * dx + dy * dy) < TRAP_SAFE_RADIUS) + return false; + } + + return true; +} + +bool IccLichKingWinterAction::TryMoveToPosition(float targetX, float targetY, float targetZ, bool forced) +{ + float const dx = targetX - bot->GetPositionX(); + float const dy = targetY - bot->GetPositionY(); + float const dist = std::hypot(dx, dy); + + if (dist < 0.1f) + return true; + + MovementPriority const priority = forced + ? MovementPriority::MOVEMENT_FORCED + : MovementPriority::MOVEMENT_COMBAT; + + if (IsPositionSafeFromDefile(targetX, targetY, targetZ, 3.0f) && + IsPositionSafeFromShadowTraps(targetX, targetY) && + bot->IsWithinLOS(targetX, targetY, targetZ)) + { + // Sample the straight-line path for defile and shadow trap crossings + bool pathSafe = true; + static constexpr float PATH_CHECK_INTERVAL = 3.0f; + int const numChecks = std::max(1, static_cast(dist / PATH_CHECK_INTERVAL)); + + for (int i = 1; i < numChecks; ++i) + { + float const t = static_cast(i) / static_cast(numChecks); + float const checkX = bot->GetPositionX() + dx * t; + float const checkY = bot->GetPositionY() + dy * t; + + if (!IsPositionSafeFromDefile(checkX, checkY, targetZ, 2.0f) || + !IsPositionSafeFromShadowTraps(checkX, checkY)) + { + pathSafe = false; + break; + } + } + + if (pathSafe) + { + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, priority, true, false); + return true; + } + // Path crosses defile or shadow trap — fall through to arc-stepping logic + } + + // Arc-stepping: rotate around the movement direction to find a way around defile/traps + static constexpr int STEPS = 8; + static constexpr float STEP_DIST = 8.0f; + + float const dirX = dx / dist; + float const dirY = dy / dist; + + for (int i = 1; i <= STEPS; ++i) + { + for (int sign : {1, -1}) + { + float const arc = float(M_PI) / STEPS * i * sign; + float const cosA = std::cos(arc); + float const sinA = std::sin(arc); + + float const testX = bot->GetPositionX() + (dirX * cosA - dirY * sinA) * STEP_DIST; + float const testY = bot->GetPositionY() + (dirX * sinA + dirY * cosA) * STEP_DIST; + + if (!IsPositionSafeFromDefile(testX, testY, targetZ, 3.0f)) + continue; + if (!IsPositionSafeFromShadowTraps(testX, testY)) + continue; + if (!bot->IsWithinLOS(testX, testY, targetZ)) + continue; + + MoveTo(bot->GetMapId(), testX, testY, targetZ, false, false, false, true, priority, true, false); + return false; + } + } + + // Probe along the direct path, shortening the distance until safe + static constexpr float PROBE_STEP = 5.0f; + + for (float probeLen = dist - PROBE_STEP; probeLen >= PROBE_STEP; probeLen -= PROBE_STEP) + { + float probeX = bot->GetPositionX() + dirX * probeLen; + float probeY = bot->GetPositionY() + dirY * probeLen; + float probeZ = targetZ; + bot->UpdateAllowedPositionZ(probeX, probeY, probeZ); + + if (!IsPositionSafeFromDefile(probeX, probeY, probeZ, 3.0f)) + continue; + if (!IsPositionSafeFromShadowTraps(probeX, probeY)) + continue; + if (!bot->IsWithinLOS(probeX, probeY, probeZ)) + continue; + + MoveTo(bot->GetMapId(), probeX, probeY, probeZ, + false, false, false, true, priority, true, false); + return false; + } + + // Small nudge forward + { + float nudgeX = bot->GetPositionX() + dirX * 2.0f; + float nudgeY = bot->GetPositionY() + dirY * 2.0f; + float nudgeZ = targetZ; + bot->UpdateAllowedPositionZ(nudgeX, nudgeY, nudgeZ); + + if (bot->IsWithinLOS(nudgeX, nudgeY, nudgeZ)) + { + MoveTo(bot->GetMapId(), nudgeX, nudgeY, nudgeZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + return false; + } + } + + // Last resort: move away from the nearest defile to get to safety first + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* nearestDefile = nullptr; + float nearestDefileDist = FLT_MAX; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != DEFILE_NPC_ID) + continue; + + float const d = bot->GetDistance2d(unit); + if (d < nearestDefileDist) + { + nearestDefileDist = d; + nearestDefile = unit; + } + } + + if (nearestDefile) + { + float const awayX = bot->GetPositionX() - nearestDefile->GetPositionX(); + float const awayY = bot->GetPositionY() - nearestDefile->GetPositionY(); + float const awayLen = std::hypot(awayX, awayY); + + if (awayLen > 0.1f) + { + float const escapeX = bot->GetPositionX() + (awayX / awayLen) * 5.0f; + float const escapeY = bot->GetPositionY() + (awayY / awayLen) * 5.0f; + + MoveTo(bot->GetMapId(), escapeX, escapeY, targetZ, + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return false; + } + } + } + + // No defile nearby — safe to move directly to the target. + + if (bot->IsWithinLOS(targetX, targetY, targetZ)) + { + botAI->Reset(); + MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool IccLichKingWinterAction::IsValidCollectibleAdd(Unit* unit) const +{ + return IsLkCollectibleAdd(unit); +} + +bool IccLichKingWinterAction::HandleTankPositioning() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return false; + + // Both tanks converge on the same frost position + Position const& frostPos = *GetMainTankPosition(); + static constexpr float FROST_AT_POS_TOLERANCE = 3.0f; + + if (botAI->IsMainTank(bot)) + { + float const dist = bot->GetDistance2d(frostPos.GetPositionX(), frostPos.GetPositionY()); + auto& s_mtInbound = IcecrownHelpers::IccState(bot->GetInstanceId()).mtAddInbound; + ObjectGuid const mtKey = bot->GetGUID(); + + // Hysteresis: converge to 3y once, then add management owns movement + // within a 12y leash. A single threshold flip-flops: orienting steps + // push past 3y and the walk-back (with its reset) yanks him straight + // back every other tick. + static constexpr float FROST_LEASH = 12.0f; + + if (dist > (s_mtInbound[mtKey] ? FROST_AT_POS_TOLERANCE : FROST_LEASH)) + { + if (!s_mtInbound[mtKey]) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->SetTarget(ObjectGuid::Empty); + context->GetValue("current target")->Set(nullptr); + botAI->Reset(); + s_mtInbound[mtKey] = true; + } + TryMoveToPosition(frostPos.GetPositionX(), frostPos.GetPositionY(), PLATFORM_Z, true); + return false; + } + + s_mtInbound[mtKey] = false; + return HandleMainTankAddManagement(boss, &frostPos); + } + + if (botAI->IsAssistTank(bot)) + return HandleAssistTankAddManagement(boss, &frostPos); + + return false; +} + +bool IccLichKingWinterAction::HandleMeleePositioning() +{ + // Frost engagement zone: bots within MELEE_FLANK_RADIUS of the frost anchor + // are considered "engaged" and trusted to flank. Outside, they're inbound + // and may need to wait at the ranged anchor for MT to gain control. + static constexpr float MELEE_FLANK_RADIUS = 10.0f; + Position const& tankPos = *GetMainTankPosition(); + float const distToPos = bot->GetDistance2d(tankPos.GetPositionX(), tankPos.GetPositionY()); + + // Transition guard: only retreat to ranged frost position if the bot is + // still inbound (outside the engagement zone) AND a nearby Shambling is + // not yet under MT control. Once engaged, trust flank logic — no retreats. + if (distToPos > MELEE_FLANK_RADIUS) + { + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (mainTank && mainTank->IsAlive()) + { + static constexpr float NEARBY_SHAMBLING_RANGE = 10.0f; + GuidVector const& transNpcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + bool nearbyShamblingLoose = false; + + for (ObjectGuid const& guid : transNpcs) + { + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsAlive()) + continue; + + if (!IsLkShambling(add->GetEntry())) + continue; + + if (bot->GetExactDist2d(add) > NEARBY_SHAMBLING_RANGE) + continue; + + Unit* victim = add->GetVictim(); + if (!victim || victim->GetGUID() != mainTank->GetGUID()) + { + nearbyShamblingLoose = true; + break; + } + } + + if (nearbyShamblingLoose) + { + Position const& rangedPos = *GetMainTankRangedPosition(); + TryMoveToPosition(rangedPos.GetPositionX(), rangedPos.GetPositionY(), PLATFORM_Z, true); + return false; + } + } + } + + // No re-anchor to tank position: flanking owns positioning. Pulling melee + // toward MT during winter puts bots in front of shamblings (frontal cone). + + // Only adds within MT_ADD_RANGE of the main tank are valid melee targets. + // Adds further out are not under tank control — engaging them puts bots + // in front of unrooted shamblings. + static constexpr float MT_ADD_RANGE = 5.0f; + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + + auto addNearMT = [&](Unit* add) -> bool + { + return mainTank && mainTank->IsAlive() && add && + add->GetDistance2d(mainTank) <= MT_ADD_RANGE; + }; + + // Acquire a valid add target + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + if (!currentTarget || !currentTarget->IsAlive() || + !IsLkCollectibleAdd(currentTarget) || IsIceSphere(currentTarget->GetEntry()) || + !addNearMT(currentTarget)) + { + Unit* newTarget = nullptr; + + // Priority: skull-marked target (only if near MT) + if (Group* group = bot->GetGroup()) + { + Unit* skull = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + if (skull && skull->IsAlive() && IsLkCollectibleAdd(skull) && addNearMT(skull)) + newTarget = skull; + } + + // Fallback: nearest valid add within MT range + if (!newTarget) + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + float closestDist = FLT_MAX; + + for (ObjectGuid const& guid : npcs) + { + Unit* add = botAI->GetUnit(guid); + if (!IsLkCollectibleAdd(add)) + continue; + if (!addNearMT(add)) + continue; + + float const d = bot->GetDistance(add); + if (d < closestDist) + { + closestDist = d; + newTarget = add; + } + } + } + + if (!newTarget) + return false; + + context->GetValue("current target")->Set(newTarget); + bot->SetTarget(newTarget->GetGUID()); + currentTarget = newTarget; + } + + // A flank slot must be out of the frontal cone of EVERY nearby shambling + // and spirit, not just the current target: their frontals hit whoever + // stands in them regardless of who the bot is attacking. + GuidVector const& coneNpcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + auto inAddFrontalCone = [&](float x, float y) -> bool + { + static constexpr float ADD_CONE_RANGE = 15.0f; + static constexpr float ADD_FRONTAL_ARC = 2.0f * float(M_PI) / 3.0f; + Position const slot(x, y); + + for (ObjectGuid const& guid : coneNpcs) + { + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsAlive()) + continue; + + uint32 const entry = add->GetEntry(); + if (!IsLkShambling(entry) && !IsLkRagingSpirit(entry)) + continue; + + if (add->GetExactDist2d(x, y) > ADD_CONE_RANGE) + continue; + + if (add->HasInArc(ADD_FRONTAL_ARC, &slot)) + return true; + } + + return false; + }; + + // Settle band: if a candidate flank slot is safe AND the bot is already + // within FLANK_SETTLE_DIST of it, stay put and attack — no per-tick step + // (prevents back-and-forth jitter near the slot). + static constexpr float FLANK_SETTLE_DIST = 2.0f; + + // Position behind the current melee target using mainTank→add vector + if (mainTank && mainTank->IsAlive()) + { + float const vecX = currentTarget->GetPositionX() - mainTank->GetPositionX(); + float const vecY = currentTarget->GetPositionY() - mainTank->GetPositionY(); + float const vecLen = std::hypot(vecX, vecY); + + if (vecLen > 0.1f) + { + float const dirX = vecX / vecLen; + float const dirY = vecY / vecLen; + + static constexpr std::array Arcs = {0.0f, 0.35f, -0.35f, 0.70f, -0.70f}; + + for (float const arc : Arcs) + { + float const cosA = std::cos(arc); + float const sinA = std::sin(arc); + float const rotX = dirX * cosA - dirY * sinA; + float const rotY = dirX * sinA + dirY * cosA; + + float const destX = currentTarget->GetPositionX() + rotX * BEHIND_DISTANCE; + float const destY = currentTarget->GetPositionY() + rotY * BEHIND_DISTANCE; + + float const bDx = destX - bot->GetPositionX(); + float const bDy = destY - bot->GetPositionY(); + float const bDist = std::hypot(bDx, bDy); + + if (!IsPositionSafeFromDefile(destX, destY, bot->GetPositionZ(), 2.0f)) + continue; + if (!bot->IsWithinLOS(destX, destY, bot->GetPositionZ())) + continue; + if (inAddFrontalCone(destX, destY)) + continue; + + if (bDist < FLANK_SETTLE_DIST) + { + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + return false; + } + + float const step = std::min(1.25f, bDist); + TryMoveToPosition(bot->GetPositionX() + (bDx / bDist) * step, bot->GetPositionY() + (bDy / bDist) * step, bot->GetPositionZ(), true); + return false; + } + } + } + + // Fallback: stored orientation-based angles + static constexpr std::array BehindAngles = + { + float(M_PI), // 180° directly behind + float(M_PI) * 0.75f, // 135° left-rear flank + float(M_PI) * 1.25f, // 225° right-rear flank + float(M_PI) * 0.5f, // 90° left flank + float(M_PI) * 1.5f, // 270° right flank + float(M_PI) * 0.875f, // 157° closer left-rear + }; + + for (float const angleOffset : BehindAngles) + { + float const angle = currentTarget->GetOrientation() + angleOffset; + float const destX = currentTarget->GetPositionX() + std::cos(angle) * BEHIND_DISTANCE; + float const destY = currentTarget->GetPositionY() + std::sin(angle) * BEHIND_DISTANCE; + + float const bDx = destX - bot->GetPositionX(); + float const bDy = destY - bot->GetPositionY(); + float const bDist = std::hypot(bDx, bDy); + + if (!IsPositionSafeFromDefile(destX, destY, bot->GetPositionZ(), 2.0f)) + continue; + if (!bot->IsWithinLOS(destX, destY, bot->GetPositionZ())) + continue; + if (inAddFrontalCone(destX, destY)) + continue; + + if (bDist < FLANK_SETTLE_DIST) + { + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + return false; + } + + float const step = std::min(1.25f, bDist); + TryMoveToPosition(bot->GetPositionX() + (bDx / bDist) * step, + bot->GetPositionY() + (bDy / bDist) * step, + bot->GetPositionZ(), true); + return false; + } + + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + return false; +} + +bool IccLichKingWinterAction::HandleRangedPositioning() +{ + Position const& targetPos = *GetMainTankRangedPosition(); + + // Evacuate defile first + if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), 3.0f)) + { + TryMoveToPosition(targetPos.GetPositionX(), targetPos.GetPositionY(), PLATFORM_Z, true); + return false; + } + + // Move to ranged frost position. Clear target + reset only on the FIRST + // tick of the inbound phase — otherwise we cancel the bot's own movement + // every tick and it ends up walking 1y per cycle. + auto& s_rangedInbound = IcecrownHelpers::IccState(bot->GetInstanceId()).rangedInbound; + ObjectGuid const rangedKey = bot->GetGUID(); + bool const farFromAnchor = + bot->GetDistance2d(targetPos.GetPositionX(), targetPos.GetPositionY()) > 2.0f; + + if (farFromAnchor) + { + bool const wasInbound = s_rangedInbound[rangedKey]; + if (!wasInbound) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->SetTarget(ObjectGuid::Empty); + context->GetValue("current target")->Set(nullptr); + botAI->Reset(); + s_rangedInbound[rangedKey] = true; + } + TryMoveToPosition(targetPos.GetPositionX(), targetPos.GetPositionY(), PLATFORM_Z, true); + // Skip target acquisition while inbound — Attack() would cancel movement. + return false; + } + + s_rangedInbound[rangedKey] = false; + + if (!botAI->IsRangedDps(bot)) + return false; + + static constexpr float SPHERE_RANGE = 30.0f; + + // Keep attacking current sphere if still alive and in range + Unit* cur = bot->GetVictim(); + if (cur && cur->IsAlive() && IsIceSphere(cur->GetEntry()) && + bot->GetDistance(cur) <= SPHERE_RANGE) + { + bot->SetFacingToObject(cur); + Attack(cur); + return false; + } + + // Drop stale sphere target + if (cur && IsIceSphere(cur->GetEntry())) + { + bot->AttackStop(); + bot->SetTarget(ObjectGuid::Empty); + } + + // Find the nearest sphere within range + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* nearestSphere = nullptr; + float nearestDist = SPHERE_RANGE; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || !IsIceSphere(unit->GetEntry())) + continue; + + float const d = bot->GetDistance(unit); + if (d < nearestDist) + { + nearestDist = d; + nearestSphere = unit; + } + } + + if (nearestSphere) + { + bot->SetTarget(nearestSphere->GetGUID()); + bot->SetFacingToObject(nearestSphere); + Attack(nearestSphere); + return true; + } + + // No spheres — engage skull-marked add or nearest valid add + Unit* addTarget = nullptr; + if (Group* group = bot->GetGroup()) + { + Unit* skull = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + if (skull && skull->IsAlive() && IsLkCollectibleAdd(skull)) + addTarget = skull; + } + + if (!addTarget) + { + float closestDist = SPHERE_RANGE; + for (ObjectGuid const& guid : npcs) + { + Unit* add = botAI->GetUnit(guid); + if (!IsLkCollectibleAdd(add)) + continue; + + float const d = bot->GetDistance(add); + if (d < closestDist) + { + closestDist = d; + addTarget = add; + } + } + } + + if (addTarget) + { + context->GetValue("current target")->Set(addTarget); + bot->SetTarget(addTarget->GetGUID()); + bot->SetFacingToObject(addTarget); + Attack(addTarget); + return true; + } + + return false; +} + +bool IccLichKingWinterAction::HandleMainTankAddManagement(Unit*, Position const* frostPos) +{ + static constexpr float ENGAGE_RADIUS = 12.0f; + static constexpr float TAUNT_RADIUS = 30.0f; + static constexpr int AOE_TAUNT_MIN = 2; + + bool hasAliveAssistTank = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member != bot && botAI->IsAssistTank(member)) + { + hasAliveAssistTank = true; + break; + } + } + } + + GuidVector const& targets = AI_VALUE(GuidVector, "possible targets"); + + Unit* targetAdd = nullptr; + int nearbyCount = 0; + + auto typeRank = [](Unit* u) -> int + { + uint32 const e = u->GetEntry(); + if (IsLkShambling(e)) + return 2; + if (IsLkRagingSpirit(e)) + return 1; + return 0; + }; + + auto rescueRank = [&](Unit* u) -> int + { + Unit* v = u->GetVictim(); + return v && v->IsPlayer() && !botAI->IsTank(v->ToPlayer()) ? 1 : 0; + }; + + auto better = [&](Unit* cand, Unit* cur) -> bool + { + if (!cur) + return true; + int const tc = typeRank(cand); + int const tr = typeRank(cur); + if (tc != tr) + return tc > tr; + int const rc = rescueRank(cand); + int const rr = rescueRank(cur); + if (rc != rr) + return rc > rr; + return bot->GetDistance(cand) < bot->GetDistance(cur); + }; + + for (ObjectGuid const& guid : targets) + { + Unit* add = botAI->GetUnit(guid); + if (!IsLkCollectibleAdd(add)) + continue; + + float const addDist = bot->GetDistance(add); + float const maxEngage = hasAliveAssistTank ? ENGAGE_RADIUS : 25.0f; + + if (addDist <= TAUNT_RADIUS) + ++nearbyCount; + + Unit* victim = add->GetVictim(); + + // Taunt pass: all adds in range that are NOT already on MT + bool const onMT = victim && victim->IsPlayer() && botAI->IsMainTank(victim->ToPlayer()); + if (!onMT && addDist <= TAUNT_RADIUS) + IccCastClassTaunt(bot, botAI, add); + + // Priority taunt: adds on the assist tank within 10 yd + bool const onAT = victim && victim->IsPlayer() && botAI->IsAssistTank(victim->ToPlayer()); + if (onAT && addDist <= 10.0f) + IccCastClassTaunt(bot, botAI, add); + + if (nearbyCount >= AOE_TAUNT_MIN) + CastAoeTaunt(botAI, bot); + + if (addDist > maxEngage) + continue; + + if (better(add, targetAdd)) + targetAdd = add; + } + + if (!targetAdd) + { + Unit* cur = bot->GetVictim(); + if (cur && !IsLkCollectibleAdd(cur)) + bot->SetTarget(ObjectGuid::Empty); + return false; + } + + { + static constexpr float HOLD_REACH = 3.0f; + static constexpr float HOLD_TOL = 1.5f; + + Position const& rangedPos = *GetMainTankRangedPosition(); + + // Only step when some add on us is deeper (farther from the ranged + // anchor) than we are. Chasing centroid + away unconditionally is a + // treadmill: adds follow every step, dragging the centroid along. + float const botRadial = bot->GetDistance2d(rangedPos.GetPositionX(), rangedPos.GetPositionY()); + bool behindAll = true; + + float sumX = 0.0f; + float sumY = 0.0f; + int count = 0; + for (ObjectGuid const& guid : targets) + { + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsAlive()) + continue; + + uint32 const entry = add->GetEntry(); + if (!IsLkShambling(entry) && !IsLkRagingSpirit(entry)) + continue; + + if (add->GetVictim() != bot) + continue; + + sumX += add->GetPositionX(); + sumY += add->GetPositionY(); + ++count; + + if (add->GetDistance2d(rangedPos.GetPositionX(), rangedPos.GetPositionY()) > botRadial + 1.0f) + behindAll = false; + } + + float cx = count > 0 ? sumX / count : targetAdd->GetPositionX(); + float cy = count > 0 ? sumY / count : targetAdd->GetPositionY(); + + float awayNx = cx - rangedPos.GetPositionX(); + float awayNy = cy - rangedPos.GetPositionY(); + float const awayLen = std::hypot(awayNx, awayNy); + + if (awayLen > 0.5f) + { + awayNx /= awayLen; + awayNy /= awayLen; + } + else + { + float const fdx = frostPos->GetPositionX() - rangedPos.GetPositionX(); + float const fdy = frostPos->GetPositionY() - rangedPos.GetPositionY(); + float const flen = std::hypot(fdx, fdy); + awayNx = flen > 0.01f ? fdx / flen : 1.0f; + awayNy = flen > 0.01f ? fdy / flen : 0.0f; + } + + float const holdX = cx + awayNx * HOLD_REACH; + float const holdY = cy + awayNy * HOLD_REACH; + + if (!behindAll && bot->GetDistance2d(holdX, holdY) > HOLD_TOL) + TryMoveToPosition(holdX, holdY, PLATFORM_Z, false); + } + + bot->SetTarget(targetAdd->GetGUID()); + bot->SetFacingToObject(targetAdd); + Attack(targetAdd); + return false; +} + +bool IccLichKingWinterAction::HandleAssistTankAddManagement(Unit*, Position const* frostPos) +{ + static constexpr float FROST_TOL = 3.0f; + static constexpr float MELE_RANGE = 5.0f; + static constexpr float TAUNT_RADIUS = 30.0f; + + GuidVector const& targets = AI_VALUE(GuidVector, "possible targets"); + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + + std::vector addsOnUs; + std::vector addsLoose; + + for (ObjectGuid const& guid : targets) + { + Unit* add = botAI->GetUnit(guid); + if (!IsLkCollectibleAdd(add)) + continue; + + Unit* victim = add->GetVictim(); + + // Skip adds already securely held by the main tank + if (mainTank && victim && victim->GetGUID() == mainTank->GetGUID()) + continue; + + if (victim == bot) + addsOnUs.push_back(add); + else + addsLoose.push_back(add); + } + + // adds are on us — walk back to frost position + if (!addsOnUs.empty()) + { + float const distToFrost = bot->GetExactDist2d(frostPos->GetPositionX(), + frostPos->GetPositionY()); + if (distToFrost > FROST_TOL) + TryMoveToPosition(frostPos->GetPositionX(), frostPos->GetPositionY(), PLATFORM_Z, true); + + // Maintain combat target on the way back + Unit* currentTarget = bot->GetVictim(); + bool keepCurrent = false; + + if (currentTarget && currentTarget->IsAlive()) + { + for (Unit* add : addsOnUs) + { + if (add->GetGUID() == currentTarget->GetGUID()) + { + keepCurrent = true; + break; + } + } + } + + if (!keepCurrent) + { + currentTarget = nullptr; + + // Prefer shambling as combat target + for (Unit* add : addsOnUs) + { + if (IsLkShambling(add->GetEntry())) + { + currentTarget = add; + break; + } + } + + if (!currentTarget) + currentTarget = addsOnUs.front(); + } + + if (currentTarget) + { + bot->SetTarget(currentTarget->GetGUID()); + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + } + + return false; + } + + // return to frost position if we drifted + float const distToFrost = bot->GetExactDist2d(frostPos->GetPositionX(), + frostPos->GetPositionY()); + if (distToFrost > FROST_TOL) + { + botAI->Reset(); + TryMoveToPosition(frostPos->GetPositionX(), frostPos->GetPositionY(), PLATFORM_Z, true); + + Unit* cur = bot->GetVictim(); + if (cur && !IsLkCollectibleAdd(cur)) + bot->SetTarget(ObjectGuid::Empty); + return false; + } + + // at frost position, collect the nearest loose add + Unit* targetAdd = nullptr; + float closestDist = FLT_MAX; + + // Priority 1: rescue add attacking a non-tank + for (Unit* add : addsLoose) + { + Unit* victim = add->GetVictim(); + if (victim && victim->IsPlayer() && !botAI->IsTank(victim->ToPlayer())) + { + float const dist = bot->GetDistance(add); + if (dist < closestDist) + { + closestDist = dist; + targetAdd = add; + } + } + } + + // Priority 2: any loose add + if (!targetAdd) + { + for (Unit* add : addsLoose) + { + float const dist = bot->GetDistance(add); + if (dist < closestDist) + { + closestDist = dist; + targetAdd = add; + } + } + } + + if (!targetAdd) + { + Unit* cur = bot->GetVictim(); + if (cur && !IsLkCollectibleAdd(cur)) + bot->SetTarget(ObjectGuid::Empty); + + // No loose adds — idle at frost pos but allow lower-priority actions to run + return false; + } + + IccCastClassTaunt(bot, botAI, targetAdd); + + // Also taunt other loose adds in range + for (Unit* add : addsLoose) + { + if (add == targetAdd) + continue; + if (bot->GetExactDist2d(add) <= TAUNT_RADIUS) + IccCastClassTaunt(bot, botAI, add); + } + + // AoE taunt if 2+ loose adds are close enough + { + int nearbyLoose = 0; + for (Unit* add : addsLoose) + { + if (bot->GetExactDist2d(add) <= TAUNT_RADIUS) + ++nearbyLoose; + } + if (nearbyLoose >= 2) + CastAoeTaunt(botAI, bot); + } + + bot->SetTarget(targetAdd->GetGUID()); + + if (closestDist > MELE_RANGE) + { + botAI->Reset(); + TryMoveToPosition(targetAdd->GetPositionX(), targetAdd->GetPositionY(), PLATFORM_Z, false); + } + else + { + bot->SetFacingToObject(targetAdd); + Attack(targetAdd); + } + + return false; +} + +bool IccLichKingWinterAction::HandlePetManagement() +{ + Pet* pet = bot->GetPet(); + if (!pet || !pet->IsAlive()) + return false; + + CharmInfo* ci = pet->GetCharmInfo(); + if (!ci) + return false; + + if (botAI->IsHeal(bot)) + { + if (ci->GetCommandState() != COMMAND_FOLLOW) + { + pet->AttackStop(); + pet->InterruptNonMeleeSpells(false); + pet->GetMotionMaster()->MoveFollow(bot, PET_FOLLOW_DIST, pet->GetFollowAngle()); + ci->SetCommandState(COMMAND_FOLLOW); + ci->SetIsCommandAttack(false); + ci->SetIsAtStay(false); + ci->SetIsReturning(true); + ci->SetIsCommandFollow(true); + ci->SetIsFollowing(false); + ci->RemoveStayPosition(); + } + return false; + } + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* bestTarget = nullptr; + int bestPriority = -1; + float bestHpPct = 101.0f; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + uint32 const entry = unit->GetEntry(); + int priority = -1; + + if (IsLkShambling(entry)) + priority = 3; + else if (IsLkRagingSpirit(entry)) + priority = 2; + else if (IsIceSphere(entry)) + priority = 1; + + if (priority < 0) + continue; + + float const hp = unit->GetHealthPct(); + if (priority > bestPriority || (priority == bestPriority && hp < bestHpPct)) + { + bestPriority = priority; + bestHpPct = hp; + bestTarget = unit; + } + } + + if (!bestTarget) + return false; + + // Skip if the pet is already attacking the right unit + if (pet->GetVictim() == bestTarget) + return false; + + ci->SetIsCommandAttack(true); + ci->SetIsAtStay(false); + ci->SetIsReturning(false); + ci->SetIsCommandFollow(false); + ci->SetIsFollowing(false); + ci->SetCommandState(COMMAND_ATTACK); + + if (pet->IsAIEnabled) + { + pet->AI()->AttackStart(bestTarget); + pet->GetMotionMaster()->MoveChase(bestTarget); + } + + return true; +} + +bool IccLichKingAddsAction::Execute(Event /*event*/) +{ + // Being carried by a Val'kyr — no actions possible + if (bot->HasAura(SPELL_HARVEST_SOUL_VALKYR)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + Difficulty const diff = bot->GetRaidDifficulty(); + bool const hasPlague = botAI->HasAura("Necrotic Plague", bot); + Unit* const terenas = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); + + // Heroic cheat buffs — apply to all group members + Creature* lichKing = bot->FindNearestCreature(NPC_THE_LICH_KING, 100.0f); + if (lichKing && lichKing->IsInCombat() && sPlayerbotAIConfig.EnableICCBuffs && IsHeroicLk(diff)) + { + Group* buffGroup = bot->GetGroup(); + if (buffGroup) + { + for (GroupReference* itr = buffGroup->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !member->IsInWorld()) + continue; + + IccApplyHeroicBuffToMember(botAI, member, true, true); + + if (boss && boss->HealthBelowPct(60) && boss->HealthAbovePct(40) && + !member->HasAura(SPELL_EMPOWERED_BLOOD)) + member->AddAura(SPELL_EMPOWERED_BLOOD, member); + } + } + } + + // Skull marking: phase 1 marks the boss; phase 2+ prioritises Raging Spirits. + // Val'kyr marking owns skull while any Val'kyr is actively grabbing — skip here. + if (Group* group = bot->GetGroup()) + { + bool anyValkyrGrabbing = false; + { + GuidVector const& nearbyNpcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (ObjectGuid const& guid : nearbyNpcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && IsLkValkyr(unit) && + unit->HasAura(SPELL_HARVEST_SOUL_VALKYR) && + (!IsHeroicLk(diff) || unit->HealthAbovePct(49))) + { + anyValkyrGrabbing = true; + break; + } + } + } + + if (!anyValkyrGrabbing) + { + // Phase 3 (boss < 40%, non-winter): skull priority is Raging Spirit > Vile Spirit > boss. + // During Vile Spirit windows, melee can't reach the spirits — mark boss on cross + // and route melee DPS there. + bool phase3 = false; + if (boss && boss->HealthBelowPct(40) && !IccBossHasRemorselessWinter(boss)) + { + phase3 = true; + + Unit* currentSkull = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + bool const ragingMarked = currentSkull && currentSkull->IsAlive() && + IsLkRagingSpirit(currentSkull->GetEntry()); + + GuidVector const& p3Npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* nearestRaging = nullptr; + float nearestRagingDist = std::numeric_limits::max(); + + for (ObjectGuid const& guid : p3Npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (!IsLkRagingSpirit(unit->GetEntry())) + continue; + + float const dist = bot->GetDistance(unit); + if (dist < nearestRagingDist) + { + nearestRagingDist = dist; + nearestRaging = unit; + } + } + + static constexpr uint8 CROSS_ICON = RtiTargetValue::crossIndex; + + // Priority: Raging Spirit > boss. Vile Spirits are not marked + // (handled positionally by HandleVileSpiritMechanics + AT chase + // + hunter frost trap). + if (nearestRaging) + { + if (!ragingMarked && group->GetTargetIcon(RtiTargetValue::skullIndex) != nearestRaging->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), nearestRaging->GetGUID()); + } + else if (group->GetTargetIcon(RtiTargetValue::skullIndex) != boss->GetGUID()) + { + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + } + + // Cross marker is no longer used for vile windows — clear if set + if (!group->GetTargetIcon(CROSS_ICON).IsEmpty()) + group->SetTargetIcon(CROSS_ICON, bot->GetGUID(), ObjectGuid::Empty); + + // Per-bot RTI: melee non-tanks always target skull (boss or raging) + if (botAI->IsMelee(bot) && !botAI->IsTank(bot)) + context->GetValue("rti")->Set("skull"); + } + + if (phase3) + { + // skip default skull logic + } + else if (boss && boss->HealthAbovePct(71)) + { + if (group->GetTargetIcon(RtiTargetValue::skullIndex) != boss->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + } + else if (boss) + { + Unit* currentSkull = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + bool const spiritMarked = currentSkull && currentSkull->IsAlive() && + IsLkRagingSpirit(currentSkull->GetEntry()); + + if (!spiritMarked) + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* nearestSpirit = nullptr; + float nearestDist = std::numeric_limits::max(); + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (!IsLkRagingSpirit(unit->GetEntry())) + continue; + + float const dist = bot->GetDistance(unit); + if (dist < nearestDist) + { + nearestDist = dist; + nearestSpirit = unit; + } + } + + if (nearestSpirit) + { + if (group->GetTargetIcon(RtiTargetValue::skullIndex) != nearestSpirit->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), nearestSpirit->GetGUID()); + } + else if (group->GetTargetIcon(RtiTargetValue::skullIndex) != boss->GetGUID()) + { + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + } + } + } + } + } + + // Val'kyr edge-dive (ongoing) + if (bot->HasAura(SPELL_VALKYR_CARRY)) + { + if (bot->GetPositionZ() > 779.0f) + return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 740.01f); + + bot->Kill(bot, bot); + return true; + } + + // Detect bots fallen off edge and initiate dive + if (boss && boss->GetHealthPct() < 70.0f && boss->GetHealthPct() > 40.0f && + !IccBossHasRemorselessWinter(boss)) + { + static constexpr float PLATFORM_CENTER_X = 503.0f; + static constexpr float PLATFORM_CENTER_Y = -2124.0f; + static constexpr float PLATFORM_EDGE_MIN = 52.0f; + static constexpr float PLATFORM_EDGE_MAX = 70.0f; + static constexpr float PLATFORM_MIN_Z = 844.0f; + + float const dx = bot->GetPositionX() - PLATFORM_CENTER_X; + float const dy = bot->GetPositionY() - PLATFORM_CENTER_Y; + float const platDist = std::hypot(dx, dy); + + if (platDist > PLATFORM_EDGE_MIN && platDist < PLATFORM_EDGE_MAX && + bot->GetPositionZ() > PLATFORM_MIN_Z) + { + bot->AddAura(SPELL_VALKYR_CARRY, bot); + return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 740.01f); + } + } + + if (HandleTeleportationFixes(diff, terenas)) + return true; + + HandleHeroicNonTankPositioning(diff, terenas); + HandleSpiritMarkingAndTargeting(diff, terenas); + + if (terenas) + return false; + + // Normal encounter flow + if (HandleQuakeMechanics(boss)) + return true; + + HandleShamblingHorrors(boss, hasPlague); + + if (HandleAssistTankAddManagement(boss, diff)) + return true; + + if (HandleRagingSpiritFlanking(boss, hasPlague, diff)) + return true; + + HandleMeleePositioning(boss, hasPlague, diff); + HandleMainTankTargeting(boss, diff); + HandleNonTankHeroicPositioning(boss, diff, hasPlague); + HandleRangedPositioning(boss, hasPlague, diff); + if (HandleDefileMechanics(boss, diff)) + return true; + if (HandleCenterStacking(boss, diff)) + return true; + HandleValkyrMechanics(diff); + HandleVileSpiritMechanics(); + HandleIceSphereMechanics(); + + return false; +} + +bool IccLichKingAddsAction::HandleTeleportationFixes(Difficulty diff, Unit* terenas) +{ + static constexpr float PLATFORM_Z = 840.857f; + static constexpr float SPIRIT_Z = 1049.865f; + static constexpr float MAX_Y_DRIFT = 200.0f; + static constexpr float MAX_Z_DIFF = 1.0f; + static constexpr float SPIRIT_Z_TOLERANCE = 5.0f; + + // Normal mode: snap back if teleported far outside the encounter area + // (Harvest Soul victim exits Frostmourne room). Land on the main tank, + // falling back to assist tank, then the fixed adds anchor. Iterate group + // directly so distance/LOS gating in PartyMemberValue::Check doesn't hide + // a far-away main tank. + if (!IsHeroicLk(diff) && std::abs(bot->GetPositionY() - (-2095.7915f)) > MAX_Y_DRIFT) + { + Player* mainTank = nullptr; + Player* assistTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (!mainTank && botAI->IsMainTank(member)) + mainTank = member; + else if (!assistTank && botAI->IsAssistTank(member)) + assistTank = member; + } + } + Unit* anchor = mainTank ? static_cast(mainTank) : static_cast(assistTank); + + if (anchor) + bot->TeleportTo(bot->GetMapId(), anchor->GetPositionX(), anchor->GetPositionY(), + anchor->GetPositionZ(), bot->GetOrientation()); + else + bot->TeleportTo(bot->GetMapId(), + ICC_LICH_KING_ADDS_POSITION.GetPositionX(), + ICC_LICH_KING_ADDS_POSITION.GetPositionY(), + ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), + bot->GetOrientation()); + return true; + } + + // Fix bots going underground (buggy ice-platform collisions) + if (!botAI->GetAura("Harvest Soul", bot, false, false) && + !botAI->GetAura("Harvest Souls", bot, false, false) && + std::abs(bot->GetPositionZ() - PLATFORM_Z) > MAX_Z_DIFF) + { + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), + PLATFORM_Z, bot->GetOrientation()); + return true; + } + + // Heroic: keep bot near main tank during Harvest Soul (fall back to Terenas). + // Resolve main tank via group iteration so distance/LOS gating in + // PartyMemberValue::Check doesn't hide a far-away main tank. + if (terenas && botAI->GetAura("Harvest Soul", bot, false, false) && + std::abs(bot->GetPositionZ() - SPIRIT_Z) > SPIRIT_Z_TOLERANCE) + { + Player* mainTank = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && botAI->IsMainTank(member)) + { + mainTank = member; + break; + } + } + } + Unit* anchor = mainTank ? static_cast(mainTank) : terenas; + bot->TeleportTo(bot->GetMapId(), anchor->GetPositionX(), anchor->GetPositionY(), + SPIRIT_Z, bot->GetOrientation()); + return true; + } + + return false; +} + +bool IccLichKingSpiritBombAction::IsBombThreatActive(PlayerbotAI* botAI, Player* bot) +{ + if (!botAI || !bot || !botAI->IsMainTank(bot)) + return false; + + Difficulty const diff = bot->GetMap() ? bot->GetMap()->GetDifficulty() : RAID_DIFFICULTY_10MAN_NORMAL; + if (!IsHeroicLk(diff)) + return false; + + if (!bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f)) + return false; + + GuidVector const& npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SPIRIT_BOMB) + return true; + } + + return false; +} + +bool IccLichKingSpiritBombAction::Execute(Event) +{ + Difficulty const diff = bot->GetMap() ? bot->GetMap()->GetDifficulty() : RAID_DIFFICULTY_10MAN_NORMAL; + Unit* terenas = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); + + if (!botAI->IsMainTank(bot) || !terenas || !IsHeroicLk(diff)) + return false; + + // Snap back to spirit-room Z if glitched through the floor + static constexpr float SPIRIT_Z = 1049.865f; + static constexpr float SPIRIT_Z_TOLERANCE = 5.0f; + if (std::abs(bot->GetPositionZ() - SPIRIT_Z) > SPIRIT_Z_TOLERANCE) + { + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), + SPIRIT_Z, bot->GetOrientation()); + return true; + } + + static constexpr float SAFE_DIST = 14.0f; + static constexpr float SAFE_HEIGHT = 12.0f; + static constexpr float DENSITY_RADIUS = 10.0f; + static constexpr float DENSITY_PENALTY = 10.0f; + static constexpr float MAX_HEIGHT_DIFF = 8.0f; + static constexpr uint32 UNSAFE_MEM_MS = 15000; + + IcecrownHelpers::IccInstanceState& st = IcecrownHelpers::IccState(bot->GetInstanceId()); + float& lastUnsafeX = st.defileLastUnsafeX; + float& lastUnsafeY = st.defileLastUnsafeY; + uint32& lastUnsafeTime = st.defileLastUnsafeTime; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + std::vector bombs; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SPIRIT_BOMB) + bombs.push_back(unit); + } + + if (bombs.empty()) + return false; + + // Check current position safety + bool currentlySafe = true; + for (Unit const* bomb : bombs) + { + float const hDist = std::hypot(bot->GetPositionX() - bomb->GetPositionX(), + bot->GetPositionY() - bomb->GetPositionY()); + float const vDist = std::abs(bot->GetPositionZ() - bomb->GetPositionZ()); + + if (hDist < SAFE_DIST && vDist <= SAFE_HEIGHT) + { + currentlySafe = false; + break; + } + } + + if (currentlySafe) + return true; + + lastUnsafeX = bot->GetPositionX(); + lastUnsafeY = bot->GetPositionY(); + lastUnsafeTime = getMSTime(); + + // Search for the best nearby safe position + static constexpr std::array SearchDistances = {6.0f, 10.0f, 15.0f, 20.0f, 25.0f}; + static constexpr int ANGLE_COUNT = 36; + + float bestScore = -std::numeric_limits::max(); + float bestX = 0.0f; + float bestY = 0.0f; + float bestZ = 0.0f; + bool found = false; + + for (float const radius : SearchDistances) + { + for (int i = 0; i < ANGLE_COUNT; ++i) + { + float const angle = i * 2.0f * float(M_PI) / ANGLE_COUNT; + float const testX = bot->GetPositionX() + radius * std::cos(angle); + float const testY = bot->GetPositionY() + radius * std::sin(angle); + float testZ = bot->GetPositionZ(); + + bot->UpdateAllowedPositionZ(testX, testY, testZ); + + if (std::abs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) + continue; + if (!bot->IsWithinLOS(testX, testY, testZ)) + continue; + + bool posSafe = true; + float minHDist = std::numeric_limits::max(); + int nearbyCount = 0; + + for (Unit const* bomb : bombs) + { + float const hDist = std::hypot(testX - bomb->GetPositionX(), + testY - bomb->GetPositionY()); + float const vDist = std::abs(testZ - bomb->GetPositionZ()); + + minHDist = std::min(minHDist, hDist); + + if (hDist < DENSITY_RADIUS) + ++nearbyCount; + + if (hDist < SAFE_DIST && vDist <= SAFE_HEIGHT) + { + posSafe = false; + break; + } + } + + if (!posSafe) + continue; + + if (lastUnsafeTime != 0 && (getMSTime() - lastUnsafeTime) < UNSAFE_MEM_MS && + std::hypot(testX - lastUnsafeX, testY - lastUnsafeY) < SAFE_DIST) + continue; + + float const distBonus = std::max(0.0f, 30.0f - radius); + float const score = minHDist - nearbyCount * DENSITY_PENALTY + distBonus; + + if (score > bestScore) + { + bestScore = score; + bestX = testX; + bestY = testY; + bestZ = testZ; + found = true; + } + } + + if (found && radius <= 15.0f) + break; + } + + // Fallback: surrounded with no clean path. Pick direction where the + // blocking bomb is highest on Z axis and move thru there + if (!found) + { + float bestEscapeZ = std::numeric_limits::max(); + for (float const radius : SearchDistances) + { + for (int i = 0; i < ANGLE_COUNT; ++i) + { + float const angle = i * 2.0f * float(M_PI) / ANGLE_COUNT; + float const testX = bot->GetPositionX() + radius * std::cos(angle); + float const testY = bot->GetPositionY() + radius * std::sin(angle); + float testZ = bot->GetPositionZ(); + + bot->UpdateAllowedPositionZ(testX, testY, testZ); + + if (std::abs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) + continue; + if (!bot->IsWithinLOS(testX, testY, testZ)) + continue; + + // Highest blocking bomb on the line from bot to test pos + float worstBombZ = -std::numeric_limits::max(); + bool blocked = false; + for (Unit const* bomb : bombs) + { + float const hDist = std::hypot(testX - bomb->GetPositionX(), + testY - bomb->GetPositionY()); + if (hDist < SAFE_DIST) + { + blocked = true; + worstBombZ = std::max(worstBombZ, bomb->GetPositionZ()); + } + } + + if (!blocked) + continue; + + if (worstBombZ < bestEscapeZ) + { + bestEscapeZ = worstBombZ; + bestX = testX; + bestY = testY; + bestZ = testZ; + found = true; + } + } + + if (found) + break; + } + } + + if (found && bot->IsWithinLOS(bestX, bestY, bestZ) && + std::abs(bestZ - bot->GetPositionZ()) <= MAX_HEIGHT_DIFF) + { + MoveTo(bot->GetMapId(), bestX, bestY, bestZ, + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + + return true; +} + +bool IccLichKingAddsAction::HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenas) +{ + if (!terenas || botAI->IsMainTank(bot) || !IsHeroicLk(diff)) + return false; + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) + return false; + + // Stack on main tank — 3-yard threshold avoids micro-jitter + if (bot->GetExactDist2d(mainTank) > 3.0f) + { + MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), + mainTank->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + + return false; +} + +bool IccLichKingAddsAction::HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenas) +{ + if (!terenas || botAI->IsMainTank(bot) || !IsHeroicLk(diff)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + static constexpr uint8 STAR_ICON = RtiTargetValue::starIndex; + static constexpr float MAX_Z_DIFF = 20.0f; + + auto const spiritTargetsGroup = [&](Unit* spirit) -> bool + { + Unit* victim = spirit->GetVictim(); + if (!victim || !victim->IsPlayer()) + return false; + + Group* g = victim->ToPlayer()->GetGroup(); + return g && g->GetGUID() == group->GetGUID(); + }; + + Unit* currentMark = botAI->GetUnit(group->GetTargetIcon(STAR_ICON)); + bool const needNewMark = !currentMark || !currentMark->IsAlive(); + bool const markOnTarget = currentMark && currentMark->IsAlive() && spiritTargetsGroup(currentMark); + + if (needNewMark || !markOnTarget) + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* prioritySpirit = nullptr; + Unit* nearestSpirit = nullptr; + float priorityDist = 100.0f; + float nearestDist = 100.0f; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || !unit->isTargetableForAttack()) + continue; + + if (!IsLkWickedSpirit(unit->GetEntry())) + continue; + + if (std::abs(unit->GetPositionZ() - bot->GetPositionZ()) > MAX_Z_DIFF) + continue; + + float const dist = bot->GetDistance(unit); + if (spiritTargetsGroup(unit) && dist < priorityDist) + { + prioritySpirit = unit; + priorityDist = dist; + } + if (dist < nearestDist) + { + nearestSpirit = unit; + nearestDist = dist; + } + } + + Unit* toMark = prioritySpirit ? prioritySpirit : nearestSpirit; + if (toMark && (needNewMark || (prioritySpirit && !markOnTarget))) + group->SetTargetIcon(STAR_ICON, bot->GetGUID(), toMark->GetGUID()); + } + + // Ranged DPS focus the star target + if (botAI->IsRangedDps(bot)) + { + context->GetValue("rti")->Set("star"); + + Unit* starTarget = botAI->GetUnit(group->GetTargetIcon(STAR_ICON)); + if (starTarget && starTarget->IsAlive()) + { + bot->SetTarget(starTarget->GetGUID()); + bot->SetFacingToObject(starTarget); + Attack(starTarget); + bot->Kill(bot, starTarget); + return true; + } + } + + return false; +} + +bool IccLichKingAddsAction::HandleQuakeMechanics(Unit* boss) +{ + if (!boss || !boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_QUAKE)) + return false; + + static constexpr float QUAKE_MIN = 35.0f; + static constexpr float QUAKE_MAX = 45.0f; + static constexpr float QUAKE_TARGET = 40.0f; + + float const dist = bot->GetExactDist2d(boss); + if (dist >= QUAKE_MIN && dist <= QUAKE_MAX) + return false; + + // Interrupt active spell cast before repositioning + if (bot->HasUnitState(UNIT_STATE_CASTING)) + bot->InterruptNonMeleeSpells(false); + + float const dx = bot->GetPositionX() - boss->GetPositionX(); + float const dy = bot->GetPositionY() - boss->GetPositionY(); + float const len = std::hypot(dx, dy); + if (len < 0.01f) + return false; + + float const targetX = boss->GetPositionX() + (dx / len) * QUAKE_TARGET; + float const targetY = boss->GetPositionY() + (dy / len) * QUAKE_TARGET; + + return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +bool IccLichKingAddsAction::HandleRagingSpiritFlanking(Unit* boss, bool hasPlague, Difficulty diff) +{ + if (!boss || botAI->IsTank(bot) || hasPlague) + return false; + if (IccBossHasRemorselessWinter(boss)) + return false; + if (bot->GetVehicle()) + return false; + if (bot->HasAura(SPELL_HARVEST_SOUL_VALKYR) || bot->HasAura(SPELL_VALKYR_CARRY)) + return false; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + // Designated trap hunter (lowest-GUID alive bot hunter) skips flanking when + // vile spirits are active so HandleVileSpiritMechanics can place the hunter + // at center for Frost Trap duty. On heroic, raging + vile coexist during P3 + // and would otherwise keep the hunter stuck in flanking. + if (bot->getClass() == CLASS_HUNTER) + { + bool vileAlive = false; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && IsLkVileSpirit(unit)) + { + vileAlive = true; + break; + } + } + + if (vileAlive) + { + if (Group* hunterGroup = bot->GetGroup()) + { + ObjectGuid bestGuid; + for (GroupReference* ref = hunterGroup->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (member->getClass() != CLASS_HUNTER) + continue; + if (!GET_PLAYERBOT_AI(member)) + continue; + + if (bestGuid.IsEmpty() || member->GetGUID() < bestGuid) + bestGuid = member->GetGUID(); + } + if (!bestGuid.IsEmpty() && bot->GetGUID() == bestGuid) + return false; + } + } + } + + std::vector spirits; + Unit* nearestSpirit = nullptr; + float nearestDist = std::numeric_limits::max(); + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + if (!IsLkRagingSpirit(unit->GetEntry())) + continue; + + float const dist = bot->GetExactDist2d(unit); + if (dist > 40.0f) + continue; + + spirits.push_back(unit); + if (dist < nearestDist) + { + nearestDist = dist; + nearestSpirit = unit; + } + } + + if (spirits.empty()) + return false; + + bool const isRanged = botAI->IsRanged(bot); + static constexpr float RANGED_SAFE_DIST = 15.0f; + + auto IsSafeFromAllSpirits = [&](float x, float y) -> bool + { + for (Unit* spirit : spirits) + { + float const dx = x - spirit->GetPositionX(); + float const dy = y - spirit->GetPositionY(); + float const d = std::hypot(dx, dy); + + if (isRanged && d < RANGED_SAFE_DIST) + return false; + + if (d < 0.1f) + return false; + + // Cone test: dot of spirit's facing vs vector spirit->point. + // dot > 0 means point is in front half of spirit. + float const dot = (std::cos(spirit->GetOrientation()) * dx + + std::sin(spirit->GetOrientation()) * dy) / d; + if (dot > 0.0f) + return false; + } + return true; + }; + + // Already safe from every spirit -- nothing to do + if (IsSafeFromAllSpirits(bot->GetPositionX(), bot->GetPositionY())) + return false; + + std::vector defiles; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) + defiles.push_back(unit); + } + + auto IsDefileSafe = [&](float x, float y) -> bool + { + static constexpr float SAFETY_MARGIN = 2.0f; + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const d = std::hypot(x - defile->GetPositionX(), y - defile->GetPositionY()); + if (d < radius + SAFETY_MARGIN) + return false; + } + return true; + }; + + static constexpr float MELEE_STEP = 7.0f; + static constexpr float RANGED_STEP = 5.0f; + float const stepSize = isRanged ? RANGED_STEP : MELEE_STEP; + + auto TryDest = [&](float destX, float destY) -> bool + { + float destZ = nearestSpirit->GetPositionZ(); + bot->UpdateAllowedPositionZ(destX, destY, destZ); + + if (!IsSafeFromAllSpirits(destX, destY)) + return false; + if (!IsDefileSafe(destX, destY)) + return false; + if (!bot->IsWithinLOS(destX, destY, destZ)) + return false; + + float const bDx = destX - bot->GetPositionX(); + float const bDy = destY - bot->GetPositionY(); + float const bDist = std::hypot(bDx, bDy); + if (bDist < 0.5f) + return false; + + // Ranged: don't chase spirits across the room. Reject destinations that + // require more than one short hop. + if (isRanged && bDist > RANGED_STEP * 1.5f) + return false; + + float const step = std::min(stepSize, bDist); + float const stepX = bot->GetPositionX() + (bDx / bDist) * step; + float const stepY = bot->GetPositionY() + (bDy / bDist) * step; + float stepZ = bot->GetPositionZ(); + bot->UpdateAllowedPositionZ(stepX, stepY, stepZ); + + MoveTo(bot->GetMapId(), stepX, stepY, stepZ, + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; + }; + + static constexpr int ANGLE_STEPS = 24; + + if (isRanged) + { + // Step away from current bot position. Prefer the direction directly away + // from the nearest spirit, then sweep outward in alternating angles. + float const awayX = bot->GetPositionX() - nearestSpirit->GetPositionX(); + float const awayY = bot->GetPositionY() - nearestSpirit->GetPositionY(); + float const awayLen = std::hypot(awayX, awayY); + float const baseAngle = (awayLen > 0.01f) ? std::atan2(awayY, awayX) : 0.0f; + + for (int i = 0; i < ANGLE_STEPS; ++i) + { + int const sign = (i % 2 == 0) ? 1 : -1; + int const offset = (i + 1) / 2; + float const angle = baseAngle + sign * offset * (2.0f * float(M_PI) / ANGLE_STEPS); + + float const destX = bot->GetPositionX() + std::cos(angle) * RANGED_STEP; + float const destY = bot->GetPositionY() + std::sin(angle) * RANGED_STEP; + + if (TryDest(destX, destY)) + return true; + } + + return false; + } + + // Melee: search rings around the nearest spirit + static constexpr std::array MeleeRings = {4.0f, 6.0f, 9.0f, 13.0f}; + + for (float const radius : MeleeRings) + { + for (int i = 0; i < ANGLE_STEPS; ++i) + { + float const angle = (i * 2.0f * float(M_PI)) / ANGLE_STEPS; + float const destX = nearestSpirit->GetPositionX() + std::cos(angle) * radius; + float const destY = nearestSpirit->GetPositionY() + std::sin(angle) * radius; + + if (TryDest(destX, destY)) + return true; + } + } + + return false; +} + +bool IccLichKingAddsAction::HandleShamblingHorrors(Unit* /*boss*/, bool /*hasPlague*/) +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || !IsLkShambling(unit->GetEntry())) + continue; + + if (botAI->HasAura("Enrage", unit)) + { + botAI->CastSpell("Tranquilizing Shot", unit); + return true; + } + } + + return false; +} + +bool IccLichKingAddsAction::HandleAssistTankAddManagement(Unit* boss, Difficulty diff) +{ + if (!botAI->IsAssistTank(bot) || !boss) + return false; + + // Below 71%: stun all shamblings until winter starts so they don't + // shockwave the raid during the transition gap. + if (boss->HealthBelowPct(72) && boss->HealthAbovePct(70)) + { + if (!IccBossHasRemorselessWinter(boss)) + { + GuidVector const& stunTargets = AI_VALUE(GuidVector, "possible targets"); + for (ObjectGuid const& guid : stunTargets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + if (!IsLkShambling(unit->GetEntry())) + continue; + if (!unit->HasAura(SPELL_HAMMER_OF_JUSTICE)) + bot->AddAura(SPELL_HAMMER_OF_JUSTICE, unit); + } + } + + return false; + } + + Position const& holdPos = IsHeroicLk(diff) + ? ICC_LICH_KING_ASSISTHC_POSITION + : ICC_LICH_KING_ADDS_POSITION; + + // Categorise visible adds + GuidVector const& targets = AI_VALUE(GuidVector, "possible targets"); + + std::vector addsOnUs; + std::vector addsElsewhere; + + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!IsLkCollectibleAdd(unit)) + continue; + + if (unit->GetVictim() == bot) + addsOnUs.push_back(unit); + else + addsElsewhere.push_back(unit); + } + + bool const isHeroic = IsHeroicLk(diff); + + // Non-winter Raging Spirit pickup: AT grabs ALL spirits before joining MT. + if (!IccBossHasRemorselessWinter(boss)) + { + std::vector looseSpirits; + std::vector spiritsOnUs; + + for (Unit* add : addsOnUs) + { + if (IsLkRagingSpirit(add->GetEntry())) + spiritsOnUs.push_back(add); + } + + for (Unit* add : addsElsewhere) + { + if (!IsLkRagingSpirit(add->GetEntry())) + continue; + if (bot->GetExactDist2d(add) > 60.0f) + continue; + looseSpirits.push_back(add); + } + + if (!looseSpirits.empty() || !spiritsOnUs.empty()) + { + // Taunt every loose spirit that we can reach + Unit* nearestLoose = nullptr; + float nearestLooseDist = FLT_MAX; + for (Unit* spirit : looseSpirits) + { + IccCastClassTaunt(bot, botAI,spirit); + float const d = bot->GetExactDist2d(spirit); + if (d < nearestLooseDist) + { + nearestLooseDist = d; + nearestLoose = spirit; + } + } + + // If any spirit is loose, go fetch the nearest loose one (do not drag yet) + if (nearestLoose) + { + bot->SetTarget(nearestLoose->GetGUID()); + bot->SetFacingToObject(nearestLoose); + Attack(nearestLoose); + + if (nearestLooseDist > 4.0f) + { + float const dx = nearestLoose->GetPositionX() - bot->GetPositionX(); + float const dy = nearestLoose->GetPositionY() - bot->GetPositionY(); + float const len = std::hypot(dx, dy); + if (len > 0.1f) + { + float const step = std::min(7.0f, len - 3.0f); + if (step > 0.0f) + { + float const goalX = bot->GetPositionX() + (dx / len) * step; + float const goalY = bot->GetPositionY() + (dy / len) * step; + float goalZ = bot->GetPositionZ(); + bot->UpdateAllowedPositionZ(goalX, goalY, goalZ); + MoveTo(bot->GetMapId(), goalX, goalY, goalZ, + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + return false; + } + + // All spirits are on us: drag the pack toward boss + Unit* nearestOnUs = nullptr; + float nearestOnUsDist = FLT_MAX; + for (Unit* spirit : spiritsOnUs) + { + float const d = bot->GetExactDist2d(spirit); + if (d < nearestOnUsDist) + { + nearestOnUsDist = d; + nearestOnUs = spirit; + } + } + + if (nearestOnUs) + { + bot->SetTarget(nearestOnUs->GetGUID()); + bot->SetFacingToObject(nearestOnUs); + Attack(nearestOnUs); + + if (bot->GetExactDist2d(boss) > 5.0f) + { + float const dx = boss->GetPositionX() - bot->GetPositionX(); + float const dy = boss->GetPositionY() - bot->GetPositionY(); + float const len = std::hypot(dx, dy); + if (len > 0.1f) + { + float const step = std::min(5.0f, len - 4.0f); + if (step > 0.0f) + { + float const goalX = bot->GetPositionX() + (dx / len) * step; + float const goalY = bot->GetPositionY() + (dy / len) * step; + float goalZ = bot->GetPositionZ(); + bot->UpdateAllowedPositionZ(goalX, goalY, goalZ); + MoveTo(bot->GetMapId(), goalX, goalY, goalZ, + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + return false; + } + } + } + + // Hold position during phase 1 (boss above 70%) + if (boss->HealthAbovePct(70)) + { + if (!isHeroic) + { + if (bot->GetExactDist2d(holdPos) > 5.0f) + { + MoveTo(bot->GetMapId(), holdPos.GetPositionX(), holdPos.GetPositionY(), + holdPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else + { + Unit* mainTankStep1 = AI_VALUE(Unit*, "main tank"); + if (mainTankStep1 && mainTankStep1->IsAlive()) + { + float const distToMt = bot->GetExactDist2d(mainTankStep1); + + if (distToMt < 20.0f || distToMt > 30.0f) + { + float const axX = holdPos.GetPositionX() - mainTankStep1->GetPositionX(); + float const axY = holdPos.GetPositionY() - mainTankStep1->GetPositionY(); + float const axLen = std::hypot(axX, axY); + + if (axLen > 0.01f) + { + float const targetDist = std::max(20.0f, std::min(30.0f, distToMt)); + float const goalX = mainTankStep1->GetPositionX() + (axX / axLen) * targetDist; + float const goalY = mainTankStep1->GetPositionY() + (axY / axLen) * targetDist; + MoveTo(bot->GetMapId(), goalX, goalY, holdPos.GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + } + } + + // Taunt loose adds: shamblings first, then others; nearest wins within tier. + Unit* tauntTargetShambling = nullptr; + float tauntDistShambling = FLT_MAX; + Unit* tauntTargetOther = nullptr; + float tauntDistOther = FLT_MAX; + + for (Unit* add : addsElsewhere) + { + float const dist = bot->GetExactDist2d(add); + if (dist > 30.0f) + continue; + + if (IsLkShambling(add->GetEntry())) + { + if (dist < tauntDistShambling) + { + tauntDistShambling = dist; + tauntTargetShambling = add; + } + } + else if (dist < tauntDistOther) + { + tauntDistOther = dist; + tauntTargetOther = add; + } + } + + Unit* tauntTarget = tauntTargetShambling ? tauntTargetShambling : tauntTargetOther; + if (tauntTarget) + IccCastClassTaunt(bot, botAI,tauntTarget); + + // No adds at all — stay at hold position + if (addsOnUs.empty() && addsElsewhere.empty()) + return boss->HealthAbovePct(70); + + // Target priority by mob type (stable, not victim-dependent): + // Tier 1 — Shambling Horror + // Tier 0 — Ghoul / Raging Spirit / other collectible add + // Sticky: never switch away from a same-or-higher tier target. + auto TargetTier = [](Unit* t) -> int + { + if (!t || !t->IsAlive()) + return -1; + if (!IsLkCollectibleAdd(t)) + return -1; + return IsLkShambling(t->GetEntry()) ? 1 : 0; + }; + + Unit* currentTarget = bot->GetVictim(); + int const currentTier = TargetTier(currentTarget); + + // Current target is a valid add — keep it unless a higher-tier add exists + if (currentTier >= 0) + { + if (currentTier < 1) + { + for (Unit* add : addsOnUs) + { + if (IsLkShambling(add->GetEntry())) + { + bot->SetTarget(add->GetGUID()); + bot->SetFacingToObject(add); + Attack(add); + currentTarget = add; + break; + } + } + } + + if (currentTarget) + { + bot->SetFacingToObject(currentTarget); + Attack(currentTarget); + } + } + else + { + // Current target is dead/invalid — pick a new one. + // Prefer adds already on us, then nearby adds within melee reach. + Unit* bestTarget = nullptr; + int bestTier = -1; + float bestDist = FLT_MAX; + + for (Unit* add : addsOnUs) + { + int const tier = TargetTier(add); + if (tier < 0) + continue; + float const dist = bot->GetExactDist2d(add); + if (tier > bestTier || (tier == bestTier && dist < bestDist)) + { + bestTier = tier; + bestDist = dist; + bestTarget = add; + } + } + + if (!bestTarget) + { + for (Unit* add : addsElsewhere) + { + int const tier = TargetTier(add); + if (tier < 0) + continue; + float const dist = bot->GetExactDist2d(add); + if (dist > 8.0f) + continue; + if (tier > bestTier || (tier == bestTier && dist < bestDist)) + { + bestTier = tier; + bestDist = dist; + bestTarget = add; + } + } + } + + if (bestTarget) + { + bot->SetTarget(bestTarget->GetGUID()); + bot->SetFacingToObject(bestTarget); + Attack(bestTarget); + } + } + + // Face the nearest high-priority add (even when not actively attacking) + { + Unit* faceTarget = nullptr; + int faceTier = -1; + float faceDist = FLT_MAX; + + auto const& faceList = addsOnUs.empty() ? addsElsewhere : addsOnUs; + for (Unit* add : faceList) + { + int const tier = TargetTier(add); + if (tier < 0) + continue; + float const dist = bot->GetExactDist2d(add); + if (tier > faceTier || (tier == faceTier && dist < faceDist)) + { + faceTier = tier; + faceDist = dist; + faceTarget = add; + } + } + + if (faceTarget) + bot->SetFacingToObject(faceTarget); + } + + // Heroic: rotate any Shambling that is facing the MT + if (isHeroic && !addsOnUs.empty()) + { + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (mainTank && mainTank->IsAlive()) + { + Unit* worstShambling = nullptr; + float worstDot = -2.0f; + + for (Unit* add : addsOnUs) + { + if (!IsLkShambling(add->GetEntry())) + continue; + + float const toMtX = mainTank->GetPositionX() - add->GetPositionX(); + float const toMtY = mainTank->GetPositionY() - add->GetPositionY(); + float const toMtLen = std::hypot(toMtX, toMtY); + if (toMtLen < 0.01f) + continue; + + float const dot = (std::cos(add->GetOrientation()) * toMtX + + std::sin(add->GetOrientation()) * toMtY) / toMtLen; + if (dot > worstDot) + { + worstDot = dot; + worstShambling = add; + } + } + + // dot > 0.3: Shambling is facing toward MT — reposition to turn it away + if (worstShambling && worstDot > 0.3f) + { + float const axisX = worstShambling->GetPositionX() - mainTank->GetPositionX(); + float const axisY = worstShambling->GetPositionY() - mainTank->GetPositionY(); + float const axisLen = std::hypot(axisX, axisY); + + if (axisLen > 0.01f) + { + float const goalX = worstShambling->GetPositionX() + (axisX / axisLen) * 5.0f; + float const goalY = worstShambling->GetPositionY() + (axisY / axisLen) * 5.0f; + + if (std::hypot(goalX - bot->GetPositionX(), goalY - bot->GetPositionY()) > 1.0f) + { + MoveTo(bot->GetMapId(), goalX, goalY, bot->GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + } + } + } + + return false; +} + +bool IccLichKingAddsAction::HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff) +{ + if (!boss || !botAI->IsMelee(bot) || botAI->IsAssistTank(bot) || + boss->HealthBelowPct(71) || hasPlague || IsHeroicLk(diff)) + return false; + + float const distToPos = bot->GetDistance(ICC_LICH_KING_MELEE_POSITION); + if (distToPos <= 6.0f) + return false; + + if (!botAI->IsMainTank(bot)) + { + MoveTo(bot->GetMapId(), + ICC_LICH_KING_MELEE_POSITION.GetPositionX(), + ICC_LICH_KING_MELEE_POSITION.GetPositionY(), + ICC_LICH_KING_MELEE_POSITION.GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; + } + + // Main tank: step toward position in 3-yard increments to avoid overshooting + if (boss->GetVictim() != bot) + return false; + + float const dx = ICC_LICH_KING_MELEE_POSITION.GetPositionX() - bot->GetPositionX(); + float const dy = ICC_LICH_KING_MELEE_POSITION.GetPositionY() - bot->GetPositionY(); + float const len = std::hypot(dx, dy); + if (len < 0.1f) + return false; + + float const step = std::min(3.0f, len - 1.0f); + if (step <= 0.0f) + { + MoveTo(bot->GetMapId(), + ICC_LICH_KING_MELEE_POSITION.GetPositionX(), + ICC_LICH_KING_MELEE_POSITION.GetPositionY(), + bot->GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; + } + + MoveTo(bot->GetMapId(), + bot->GetPositionX() + (dx / len) * step, + bot->GetPositionY() + (dy / len) * step, + bot->GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + + return true; +} + +bool IccLichKingAddsAction::HandleMainTankTargeting(Unit* boss, Difficulty diff) +{ + if (!botAI->IsMainTank(bot) || !boss) + return false; + + if (boss->GetVictim() == bot) + return false; + + // Non-winter Raging Spirit case: stay on boss only when an assist tank is alive + // to take the spirit. If AT is dead, fall through so MT can swap. + if (!IccBossHasRemorselessWinter(boss)) + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + bool spiritAlive = false; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && IsLkRagingSpirit(unit->GetEntry())) + { + spiritAlive = true; + break; + } + } + + if (spiritAlive) + { + bool assistTankAlive = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member != bot && + botAI->IsAssistTank(member)) + { + assistTankAlive = true; + break; + } + } + } + + if (assistTankAlive) + { + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + Attack(boss); + return true; + } + } + } + + if (!IsHeroicLk(diff) || boss->HealthBelowPct(71)) + return false; + + bot->SetTarget(boss->GetGUID()); + bot->SetFacingToObject(boss); + Attack(boss); + + return true; +} + +bool IccLichKingAddsAction::HandleNonTankHeroicPositioning(Unit* boss, Difficulty diff, bool hasPlague) +{ + if (botAI->IsTank(bot) || !boss || !IsHeroicLk(diff)) + return false; + + if (boss->HealthBelowPct(71) || hasPlague) + return false; + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank) + return false; + + float const distToTank = bot->GetDistance2d(mainTank); + + if (bot->getClass() == CLASS_HUNTER) + { + // Hunters stay within 10 yd of MT but away from the assist tank position + if (distToTank > 10.0f) + { + float goalX = mainTank->GetPositionX(); + float goalY = mainTank->GetPositionY(); + + float const atDx = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX() - mainTank->GetPositionX(); + float const atDy = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY() - mainTank->GetPositionY(); + float const atLen = std::hypot(atDx, atDy); + if (atLen > 0.01f) + { + goalX -= (atDx / atLen) * 8.0f; + goalY -= (atDy / atLen) * 8.0f; + } + + MoveTo(bot->GetMapId(), goalX, goalY, bot->GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; + } + } + else + { + // Everyone else stacks on main tank + if (distToTank > 3.0f) + { + MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), + bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; + } + } + + return false; +} + +bool IccLichKingAddsAction::HandleRangedPositioning(Unit* boss, bool hasPlague, Difficulty diff) +{ + if (!boss || !botAI->IsRanged(bot) || boss->HealthBelowPct(71) || + hasPlague || IsHeroicLk(diff)) + return false; + + if (bot->GetDistance(ICC_LICH_KING_RANGED_POSITION) > 2.0f) + { + MoveTo(bot->GetMapId(), + ICC_LICH_KING_RANGED_POSITION.GetPositionX(), + ICC_LICH_KING_RANGED_POSITION.GetPositionY(), + ICC_LICH_KING_RANGED_POSITION.GetPositionZ(), + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; + } + + return false; +} + +bool IccLichKingAddsAction::HandleCenterStacking(Unit* boss, Difficulty diff) +{ + if (!boss || !boss->HealthBelowPct(67) || IccBossHasRemorselessWinter(boss)) + return false; + + // Defile target: let HandleDefileMechanics() handle movement + // (perpendicular run). Don't override with slot/center movement. + auto const& defileInfo = IcecrownHelpers::IccState(bot->GetInstanceId()).defileCast; + if (!defileInfo.targetGuid.IsEmpty() && + getMSTimeDiff(defileInfo.castTime, getMSTime()) <= 3000 && + defileInfo.targetGuid == bot->GetGUID()) + { + return false; + } + + // Marked Val'kyrs (Skull/Cross/Star) are being kited by assist - bots + // assigned to them must not be locked to the center stack. + if (Group* group = bot->GetGroup()) + { + static constexpr std::array ValkyrIcons = {RtiTargetValue::skullIndex, RtiTargetValue::crossIndex, + RtiTargetValue::starIndex}; + for (uint8 const iconIdx : ValkyrIcons) + { + Unit* marked = botAI->GetUnit(group->GetTargetIcon(iconIdx)); + if (marked && marked->IsAlive() && IsLkValkyr(marked)) + return false; + } + } + + static constexpr float HUNTER_DISTANCE = 8.0f; + static constexpr float OTHER_DISTANCE = 5.0f; + // Worst-case stack radius: hunters sit furthest out, plus safety margin. + // Defile reach = dist - radius; unsafe if reach < stack + safety. + static constexpr float STACK_SAFETY = 3.0f; + static constexpr float STACK_DEFILE_BUFFER = HUNTER_DISTANCE + STACK_SAFETY; + + bool const lastPhase = boss->HealthBelowPct(37); + bool const spiritPhase = boss->HealthBelowPct(43) && boss->HealthAbovePct(37); + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector defiles; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (IsLkRagingSpirit(unit->GetEntry()) || IsLkVileSpirit(unit) || + IsIceSphere(unit->GetEntry())) + return false; + + if (unit->GetEntry() == DEFILE_NPC_ID) + defiles.push_back(unit); + } + + Position dest; + if (spiritPhase || lastPhase) + { + // Phase 3: pick the safe vile-spirit slot closest to the bot. + Position const slots[3] = { + ICC_LK_VILE_SPIRIT1_POSITION, + ICC_LK_VILE_SPIRIT2_POSITION, + ICC_LK_VILE_SPIRIT3_POSITION, + }; + + auto IsSlotSafe = [&](Position const& slot) -> bool + { + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const d = std::hypot(slot.GetPositionX() - defile->GetPositionX(), + slot.GetPositionY() - defile->GetPositionY()); + if (d < radius + STACK_DEFILE_BUFFER) + return false; + } + return true; + }; + + // Among safe slots (0 and 2), pick the one closest to the group + // centroid. Centroid is identical for every bot in the raid so all + // bots converge on the same anchor. Cached per boss GUID for 2s to + // dampen flicker if defile state shifts between ticks. + auto& s_stackChoice = IcecrownHelpers::IccState(boss->GetInstanceId()).mtStackChoice; + static constexpr uint32 STACK_CHOICE_TTL_MS = 2000; + ObjectGuid const stackKey = boss->GetGUID(); + + uint32 const now = getMSTime(); + int chosen = -1; + auto cacheIt = s_stackChoice.find(stackKey); + if (cacheIt != s_stackChoice.end() && + getMSTimeDiff(cacheIt->second.evaluatedMs, now) < STACK_CHOICE_TTL_MS) + { + chosen = cacheIt->second.slotIdx; + } + else + { + Position const centroid = ComputeGroupCentroid(bot); + float bestDist = std::numeric_limits::max(); + for (int const i : {0, 2}) + { + if (!IsSlotSafe(slots[i])) + continue; + + float const d = std::hypot(centroid.GetPositionX() - slots[i].GetPositionX(), + centroid.GetPositionY() - slots[i].GetPositionY()); + if (d < bestDist) + { + bestDist = d; + chosen = i; + } + } + s_stackChoice[stackKey] = {now, chosen}; + } + + if (chosen < 0) + return false; + + dest = slots[chosen]; + } + else + { + // Phase 2: stack on platform center if it's safe from defiles. + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const d = std::hypot( + defile->GetPositionX() - ICC_LICH_KING_CENTER_POSITION.GetPositionX(), + defile->GetPositionY() - ICC_LICH_KING_CENTER_POSITION.GetPositionY()); + if (d < radius + STACK_DEFILE_BUFFER) + return false; + } + dest = ICC_LICH_KING_CENTER_POSITION; + } + + // Main tank only moves to the stack point if he's still the boss's victim. + if (botAI->IsMainTank(bot) && boss->GetVictim() != bot) + return false; + + float const distToDest = bot->GetDistance2d(dest.GetPositionX(), dest.GetPositionY()); + float const threshold = (bot->getClass() == CLASS_HUNTER) ? HUNTER_DISTANCE : OTHER_DISTANCE; + if (distToDest <= threshold) + return false; + + auto const [stepX, stepY] = DefileAwareStep(dest.GetPositionX(), dest.GetPositionY(), defiles, diff); + float stepZ = dest.GetPositionZ(); + bot->UpdateAllowedPositionZ(stepX, stepY, stepZ); + + MoveTo(bot->GetMapId(), stepX, stepY, stepZ, + false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + return true; +} + +bool IccLichKingAddsAction::HandleDefileMechanics(Unit* boss, Difficulty diff) +{ + if (!boss) + return false; + + static constexpr float SAFETY_MARGIN = 3.0f; + static constexpr float MOVE_DISTANCE = 5.0f; + static constexpr float FIXED_Z = 840.857f; + static constexpr float MAX_HEIGHT_DIFF = 5.0f; + static constexpr int ANGLE_TESTS = 16; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + std::vector defiles; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) + defiles.push_back(unit); + } + + if (!defiles.empty()) + { + float const botX = bot->GetPositionX(); + float const botY = bot->GetPositionY(); + + bool needToMove = false; + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const dist = std::hypot(botX - defile->GetPositionX(), + botY - defile->GetPositionY()); + if (dist < radius + SAFETY_MARGIN) + { + needToMove = true; + break; + } + } + + if (needToMove) + { + float bestScore = 0.0f; + float bestAngle = 0.0f; + bool found = false; + + for (int i = 0; i < ANGLE_TESTS; ++i) + { + float const angle = i * float(M_PI) / 8.0f; + float const testX = botX + MOVE_DISTANCE * std::cos(angle); + float const testY = botY + MOVE_DISTANCE * std::sin(angle); + float testZ = FIXED_Z; + + bot->UpdateAllowedPositionZ(testX, testY, testZ); + + if (!bot->IsWithinLOS(testX, testY, testZ) || + std::abs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) + continue; + + float minDefileDist = std::numeric_limits::max(); + for (Unit const* defile : defiles) + { + float const d = std::hypot(testX - defile->GetPositionX(), + testY - defile->GetPositionY()); + minDefileDist = std::min(minDefileDist, d); + } + + float const bossProximity = 100.0f - std::min(100.0f, boss->GetDistance2d(testX, testY)); + float const score = minDefileDist + bossProximity * 0.5f; + + if (score > bestScore) + { + bestScore = score; + bestAngle = angle; + found = true; + } + } + + if (found) + { + float moveX = botX + MOVE_DISTANCE * std::cos(bestAngle); + float moveY = botY + MOVE_DISTANCE * std::sin(bestAngle); + float moveZ = FIXED_Z; + + if (bot->HasUnitState(UNIT_STATE_CASTING)) + bot->InterruptNonMeleeSpells(false); + + bot->UpdateAllowedPositionZ(moveX, moveY, moveZ); + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + return true; + } + } + } + + if (!boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(DEFILE_CAST_ID)) + return false; + + // Boss casting Defile — only the targeted player runs out. Target is + // stamped by IccLichKingListenerScript at OnSpellPrepare time (cast start). + auto const& info = IcecrownHelpers::IccState(bot->GetInstanceId()).defileCast; + if (info.targetGuid.IsEmpty() || getMSTimeDiff(info.castTime, getMSTime()) > 3000) + return false; + + Player* target = ObjectAccessor::FindPlayer(info.targetGuid); + if (!target || !target->IsAlive()) + return false; + + // Main tank yells once per cast. + uint32& lastYellMs = IcecrownHelpers::IccState(bot->GetInstanceId()).lkLastYellMs; + if (botAI->IsMainTank(bot) && info.castTime != lastYellMs) + { + botAI->Yell("Defile on " + target->GetName() + " - move to the edge!"); + lastYellMs = info.castTime; + } + + if (!target->HasAura(SPELL_NITRO_BOOSTS)) + target->AddAura(SPELL_NITRO_BOOSTS, target); + + // Real players run themselves; only the targeted bot moves. + if (target != bot) + return false; + + // During Vile Spirit phase, run perpendicular (left/right) to the line from + // spirit centroid -> this bot, so defile drops sideways instead of into the + // raid stack or into the spirit cluster. + float spiritSumX = 0.0f; + float spiritSumY = 0.0f; + uint32 spiritCount = 0; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || !IsLkVileSpirit(unit)) + continue; + spiritSumX += unit->GetPositionX(); + spiritSumY += unit->GetPositionY(); + ++spiritCount; + } + + float baseAngle; + if (spiritCount > 0) + { + float const cx = spiritSumX / spiritCount; + float const cy = spiritSumY / spiritCount; + float const sx = bot->GetPositionX() - cx; + float const sy = bot->GetPositionY() - cy; + float const sLen = std::hypot(sx, sy); + if (sLen < 0.01f) + baseAngle = (bot->GetGUID().GetCounter() % 16) * (float(M_PI) / 8.0f); + else + { + float const radial = std::atan2(sy, sx); + // Pick left or right perpendicular based on bot GUID for stable choice + float const sign = (bot->GetGUID().GetCounter() & 1) ? 1.0f : -1.0f; + baseAngle = radial + sign * (float(M_PI) / 2.0f); + } + } + else + { + Position centroid = ComputeGroupCentroid(bot); + float const dx = bot->GetPositionX() - centroid.GetPositionX(); + float const dy = bot->GetPositionY() - centroid.GetPositionY(); + float const len = std::hypot(dx, dy); + if (len < 0.01f) + baseAngle = (bot->GetGUID().GetCounter() % 16) * (float(M_PI) / 8.0f); + else + baseAngle = std::atan2(dy, dx); + } + + // Reject candidates landing inside any defile (radius from grow stacks). + auto IsSafeFromDefiles = [&](float x, float y) -> bool + { + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const d = std::hypot(x - defile->GetPositionX(), + y - defile->GetPositionY()); + if (d < radius + SAFETY_MARGIN) + return false; + } + return true; + }; + + // Try base direction first, then fan out in +/- 22.5 deg increments. + float destX = 0.0f; + float destY = 0.0f; + bool found = false; + for (int offset = 0; offset <= 8 && !found; ++offset) + { + for (int dir : {-1, 1}) + { + if (offset == 0 && dir > 0) + continue; + + float const angle = baseAngle + dir * offset * float(M_PI) / 8.0f; + float const tx = bot->GetPositionX() + 10.0f * std::cos(angle); + float const ty = bot->GetPositionY() + 10.0f * std::sin(angle); + if (!IsSafeFromDefiles(tx, ty)) + continue; + + destX = tx; + destY = ty; + found = true; + break; + } + } + + if (!found) + return false; + + float destZ = bot->GetPositionZ(); + bot->UpdateAllowedPositionZ(destX, destY, destZ); + + if (bot->HasUnitState(UNIT_STATE_CASTING)) + bot->InterruptNonMeleeSpells(false); + + MoveTo(bot->GetMapId(), destX, destY, destZ, + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + return true; +} + +bool IccLichKingAddsAction::HandleValkyrMechanics(Difficulty diff) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + Group* group = bot->GetGroup(); + if (!group) + return false; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + std::vector grabbingValkyrs; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || !IsLkValkyr(unit)) + continue; + + // Heroic: only target Val'kyrs above 49% health + bool const isGrabbing = unit->HasAura(SPELL_HARVEST_SOUL_VALKYR) && + (!IsHeroicLk(diff) || unit->HealthAbovePct(49)); + + if (isGrabbing) + grabbingValkyrs.push_back(unit); + } + + // No active valkyrs — reset bots still targeting an excluded valkyr + if (grabbingValkyrs.empty() || (boss && boss->HealthBelowPct(40))) + { + Unit* currentTarget = bot->GetVictim(); + if (currentTarget && IsLkValkyr(currentTarget) && boss) + { + bot->SetTarget(boss->GetGUID()); + context->GetValue("rti")->Set("skull"); + } + return false; + } + + if (botAI->IsMainTank(bot)) + return false; + + // Defile target: let HandleDefileMechanics() handle movement + // (perpendicular run). Don't override with Val'kyr chase. + auto const& defileInfo = IcecrownHelpers::IccState(bot->GetInstanceId()).defileCast; + if (!defileInfo.targetGuid.IsEmpty() && getMSTimeDiff(defileInfo.castTime, getMSTime()) <= 3000 && + defileInfo.targetGuid == bot->GetGUID()) + return false; + + HandleValkyrMarking(grabbingValkyrs, diff); + HandleValkyrAssignment(grabbingValkyrs); + + return true; +} + +bool IccLichKingAddsAction::HandleValkyrMarking(std::vector const& grabbingValkyrs, + Difficulty diff) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector sorted = grabbingValkyrs; + std::sort(sorted.begin(), sorted.end(), + [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + static constexpr std::array Icons = {RtiTargetValue::skullIndex, RtiTargetValue::crossIndex, + RtiTargetValue::starIndex}; + + // Heroic: clear stale markers for Val'kyrs no longer grabbing or at wrong Z + if (IsHeroicLk(diff)) + { + for (uint8 const iconIdx : Icons) + { + Unit* marked = botAI->GetUnit(group->GetTargetIcon(iconIdx)); + if (!marked || !IsLkValkyr(marked)) + continue; + + bool const stale = !marked->HasAura(SPELL_HARVEST_SOUL_VALKYR) || + std::abs(marked->GetPositionZ() - bot->GetPositionZ()) > 5.0f; + if (stale) + group->SetTargetIcon(iconIdx, bot->GetGUID(), ObjectGuid::Empty); + } + } + + // Clear icon slots beyond the current Val'kyr count + for (size_t i = sorted.size(); i < Icons.size(); ++i) + { + if (!group->GetTargetIcon(Icons[i]).IsEmpty()) + group->SetTargetIcon(Icons[i], bot->GetGUID(), ObjectGuid::Empty); + } + + // Assign an icon to each active Val'kyr. + // Skip skull if a Raging Spirit currently owns it. + for (size_t i = 0; i < sorted.size() && i < Icons.size(); ++i) + { + uint8 const iconIdx = Icons[i]; + + if (iconIdx == 7) + { + Unit* currentSkull = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + if (currentSkull && currentSkull->IsAlive() && IsLkRagingSpirit(currentSkull->GetEntry())) + continue; + } + + Unit* marked = botAI->GetUnit(group->GetTargetIcon(iconIdx)); + if (!marked || marked != sorted[i]) + group->SetTargetIcon(iconIdx, bot->GetGUID(), sorted[i]->GetGUID()); + } + + return true; +} + +bool IccLichKingAddsAction::HandleValkyrAssignment(std::vector const& grabbingValkyrs) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (boss && boss->HealthBelowPct(40)) + return false; + + std::vector valid; + for (Unit* valkyr : grabbingValkyrs) + { + if (valkyr && valkyr->IsAlive() && valkyr->HasAura(SPELL_HARVEST_SOUL_VALKYR)) + valid.push_back(valkyr); + } + + if (valid.empty()) + return false; + + std::sort(valid.begin(), valid.end(), + [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); + + // Build sorted list of non-main-tank members for deterministic assignment + std::vector assistMembers; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && !botAI->IsMainTank(member)) + assistMembers.push_back(member); + } + + if (assistMembers.empty()) + return false; + + std::sort(assistMembers.begin(), assistMembers.end(), + [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); + + auto const it = std::find(assistMembers.begin(), assistMembers.end(), bot); + if (it == assistMembers.end()) + return false; + + size_t const myIndex = std::distance(assistMembers.begin(), it); + auto const groupSizes = IccBalancedGroupSizes(assistMembers.size(), valid.size()); + size_t const valkyrIndex = IccAssignedBucketIndex(myIndex, groupSizes); + + if (valkyrIndex >= valid.size()) + return false; + + Unit* myValkyr = valid[valkyrIndex]; + context->GetValue("rti")->Set(IccRtiNameForBucket(valkyrIndex)); + + Attack(myValkyr); + + Difficulty const diff = bot->GetRaidDifficulty(); + if (sPlayerbotAIConfig.EnableICCBuffs && IsHeroicLk(diff) && + !myValkyr->HasAura(SPELL_HAMMER_OF_JUSTICE)) + bot->AddAura(SPELL_HAMMER_OF_JUSTICE, myValkyr); + + ApplyCCToValkyr(myValkyr); + + return true; +} + +std::pair IccLichKingAddsAction::DefileAwareStep(float tx, float ty, + std::vector const& defiles, + Difficulty diff) +{ + float const px = bot->GetPositionX(); + float const py = bot->GetPositionY(); + float const ddx = tx - px; + float const ddy = ty - py; + float const fullLen = std::hypot(ddx, ddy); + if (fullLen < 0.01f) + return {tx, ty}; + + static constexpr float STEP_DISTANCE = 10.0f; + static constexpr float MARGIN = 2.0f; + static constexpr int SAMPLES = 5; + + auto PathSafe = [&](float ex, float ey) -> bool + { + for (int s = 1; s <= SAMPLES; ++s) + { + float const t = float(s) / float(SAMPLES); + float const sx = px + (ex - px) * t; + float const sy = py + (ey - py) * t; + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const d = std::hypot(sx - defile->GetPositionX(), sy - defile->GetPositionY()); + if (d < radius + MARGIN) + return false; + } + } + return true; + }; + + float const stepLen = std::min(STEP_DISTANCE, fullLen); + float const baseAngle = std::atan2(ddy, ddx); + + for (int offset = 0; offset <= 8; ++offset) + { + for (int dir : {-1, 1}) + { + if (offset == 0 && dir > 0) + continue; + float const angle = baseAngle + dir * offset * float(M_PI) / 8.0f; + float const ex = px + stepLen * std::cos(angle); + float const ey = py + stepLen * std::sin(angle); + if (PathSafe(ex, ey)) + return {ex, ey}; + } + } + + return {tx, ty}; +} + +bool IccLichKingAddsAction::HandleVileSpiritMechanics() +{ + static constexpr float ARRIVE_TOLERANCE = 4.0f; + + // Defile target: let HandleDefileMechanics() handle movement + // (perpendicular run). Don't override with spirit chase or slot + // movement. + auto const& defileInfo = IcecrownHelpers::IccState(bot->GetInstanceId()).defileCast; + if (!defileInfo.targetGuid.IsEmpty() && getMSTimeDiff(defileInfo.castTime, getMSTime()) <= 3000 && + defileInfo.targetGuid == bot->GetGUID()) + return false; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + uint32 spiritCount = 0; + std::vector spirits; + std::vector defiles; + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (IsLkVileSpirit(unit)) + { + ++spiritCount; + spirits.push_back(unit); + } + else if (unit->GetEntry() == DEFILE_NPC_ID) + { + defiles.push_back(unit); + } + } + + // Shared raid-wide slot choice. All bots converge on the same position so + // they stay stacked. Reset when no spirits are alive. Keyed per-instance to + // avoid cross-instance pollution when multiple ICCs run simultaneously. + int& sharedSlot = IcecrownHelpers::IccState(bot->GetInstanceId()).lkSharedSlot; + + if (spiritCount == 0) + { + sharedSlot = -1; + return false; + } + + Difficulty const diff = bot->GetRaidDifficulty(); + auto IsSlotSafeFromDefile = [&](Position const& slot) -> bool + { + static constexpr float SAFETY_MARGIN = 2.0f; + for (Unit const* defile : defiles) + { + float const radius = GetDefileEffectiveRadius(defile, diff); + float const d = + std::hypot(slot.GetPositionX() - defile->GetPositionX(), slot.GetPositionY() - defile->GetPositionY()); + if (d < radius + SAFETY_MARGIN) + return false; + } + return true; + }; + + static constexpr float SPIRIT_NEAR_SLOT = 15.0f; + auto IsSlotSafeFromSpirits = [&](Position const& slot) -> bool + { + for (Unit const* spirit : spirits) + { + float const d = + std::hypot(slot.GetPositionX() - spirit->GetPositionX(), slot.GetPositionY() - spirit->GetPositionY()); + if (d < SPIRIT_NEAR_SLOT) + return false; + } + return true; + }; + + Position const slots[3] = { + ICC_LK_VILE_SPIRIT1_POSITION, + ICC_LK_VILE_SPIRIT2_POSITION, + ICC_LK_VILE_SPIRIT3_POSITION, + }; + + // Spirits beyond this radius from bot = previous wave still alive, block move + static constexpr float OLD_SPIRIT_RADIUS = 20.0f; + // Priority order: pos1 (0), pos3 (2), pos2 (1) + static constexpr int SLOT_PRIORITY[3] = {0, 2, 1}; + + auto HasOldSpirits = [&]() -> bool + { + for (Unit const* spirit : spirits) + { + float const d = std::hypot(bot->GetPositionX() - spirit->GetPositionX(), + bot->GetPositionY() - spirit->GetPositionY()); + if (d > OLD_SPIRIT_RADIUS) + return true; + } + return false; + }; + + if (sharedSlot >= 0 && sharedSlot < 3) + { + bool const defileHit = !IsSlotSafeFromDefile(slots[sharedSlot]); + bool const spiritHit = !IsSlotSafeFromSpirits(slots[sharedSlot]); + + if (defileHit) + { + // Defile is fatal — move regardless of old spirits + sharedSlot = -1; + } + else if (spiritHit) + { + // New spirits at current slot; wait if previous-wave spirits still alive + if (HasOldSpirits()) + return false; + + sharedSlot = -1; + } + else if (sharedSlot == 1) + { + // On fallback slot (pos2); upgrade to pos1 or pos3 if now safe + for (int pri : SLOT_PRIORITY) + { + if (pri == 1) + break; + if (IsSlotSafeFromDefile(slots[pri]) && IsSlotSafeFromSpirits(slots[pri])) + { + sharedSlot = pri; + break; + } + } + } + } + + if (sharedSlot < 0) + { + for (int pri : SLOT_PRIORITY) + { + if (!IsSlotSafeFromDefile(slots[pri])) + continue; + if (!IsSlotSafeFromSpirits(slots[pri])) + continue; + sharedSlot = pri; + break; + } + + if (sharedSlot < 0) + return false; + } + + if (sharedSlot < 0 || sharedSlot >= 3) + return false; + + int const chosen = sharedSlot; + Position const& slotPos = slots[chosen]; + + // Assist tank: stacks with the raid at the chosen vile slot, but allowed + // a 40y leash to intercept the spirit nearest the slot. Keeps nitro boost + // for chase speed. + if (botAI->IsAssistTank(bot)) + { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) + bot->AddAura(SPELL_PAIN_SUPPRESION, bot); + if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) + bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); + + static constexpr float LEASH_RADIUS = 40.0f; + + float const anchorX = slotPos.GetPositionX(); + float const anchorY = slotPos.GetPositionY(); + + // Find spirit closest to the slot (not to bot) — that's the spirit + // most likely to reach the raid stack first. + Unit* chaseTarget = nullptr; + float chaseTargetDist = std::numeric_limits::max(); + for (Unit* spirit : spirits) + { + float const d = std::hypot(spirit->GetPositionX() - anchorX, spirit->GetPositionY() - anchorY); + if (d < chaseTargetDist) + { + chaseTargetDist = d; + chaseTarget = spirit; + } + } + + float ax = anchorX; + float ay = anchorY; + if (chaseTarget) + { + float tx = chaseTarget->GetPositionX(); + float ty = chaseTarget->GetPositionY(); + float const dxc = tx - anchorX; + float const dyc = ty - anchorY; + float const lenc = std::hypot(dxc, dyc); + if (lenc > LEASH_RADIUS) + { + // Spirit too far from slot — clamp chase point to leash radius + tx = anchorX + dxc * LEASH_RADIUS / lenc; + ty = anchorY + dyc * LEASH_RADIUS / lenc; + } + ax = tx; + ay = ty; + } + + float az = slotPos.GetPositionZ(); + + float const distToAnchor = std::hypot(bot->GetPositionX() - ax, bot->GetPositionY() - ay); + if (distToAnchor <= ARRIVE_TOLERANCE) + return false; + + auto const [sx, sy] = DefileAwareStep(ax, ay, defiles, diff); + float sz = az; + bot->UpdateAllowedPositionZ(sx, sy, sz); + + return MoveTo(bot->GetMapId(), sx, sy, sz, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, + false); + } + + // Hunter trap duty: only the lowest-GUID alive bot hunter handles it so + // multiple hunters don't pile up at center. Real-player hunters are skipped + // — they may not know the strategy, so a bot owns the role. + bool isDesignatedHunter = false; + if (bot->getClass() == CLASS_HUNTER) + { + Group* hunterGroup = bot->GetGroup(); + if (hunterGroup) + { + ObjectGuid bestGuid; + for (GroupReference* ref = hunterGroup->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + if (member->getClass() != CLASS_HUNTER) + continue; + if (!GET_PLAYERBOT_AI(member)) + continue; + + if (bestGuid.IsEmpty() || member->GetGUID() < bestGuid) + bestGuid = member->GetGUID(); + } + isDesignatedHunter = (!bestGuid.IsEmpty() && bot->GetGUID() == bestGuid); + } + } + + // Skip the spirit-flee logic entirely if boss is casting Harvest Soul(s). + // The harvested player must stay alive — flee movement breaks range for + // healers and gets the soul victim killed, buffing LK and wiping raid. + Unit* Boss = AI_VALUE2(Unit*, "find target", "the lich king"); + bool const bossCastingHarvest = Boss && Boss->HasUnitState(UNIT_STATE_CASTING) && + (Boss->FindCurrentSpellBySpellId(SPELL_HARVEST_SOUL_LK) || + Boss->FindCurrentSpellBySpellId(SPELL_HARVEST_SOULS_LK_25) || + Boss->FindCurrentSpellBySpellId(SPELL_HARVEST_SOULS_LK_H1) || + Boss->FindCurrentSpellBySpellId(SPELL_HARVEST_SOULS_LK_H2) || + Boss->FindCurrentSpellBySpellId(SPELL_HARVEST_SOULS_LK_H3)); + + if (!botAI->IsTank(bot) && !bossCastingHarvest) + { + // Flee to MT if a spirit is targeting this bot OR is within FLEE_RANGE. + // Either condition is enough — proximity catches spirits that haven't + // committed a target yet, targeting catches faraway chasers. + static constexpr float FLEE_RANGE = 15.0f; + + Unit* chaser = nullptr; + for (Unit* spirit : spirits) + { + bool const isTargetingBot = spirit->GetVictim() && + spirit->GetVictim()->GetGUID() == bot->GetGUID(); + bool const isClose = bot->GetDistance2d(spirit) < FLEE_RANGE; + if (isTargetingBot || isClose) + { + chaser = spirit; + break; + } + } + + if (chaser) + { + // Flee toward main tank in 10y increments, leash ignored + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (mainTank && mainTank->IsAlive()) + { + auto const [fx, fy] = DefileAwareStep(mainTank->GetPositionX(), mainTank->GetPositionY(), defiles, diff); + float fz = slotPos.GetPositionZ(); + bot->UpdateAllowedPositionZ(fx, fy, fz); + return MoveTo(bot->GetMapId(), fx, fy, fz, false, false, false, true, MovementPriority::MOVEMENT_FORCED, + true, false); + } + // No MT — fall through to leash logic + } + } + + // Designated hunter: anchored at platform center with 30y leash. Drops + // Frost Trap to slow spirits. If a spirit is targeting the hunter, flee + // toward main tank in 10y steps (leash ignored, trap not dropped). + if (isDesignatedHunter) + { + static constexpr float HUNTER_CENTER_X = 503.62f; + static constexpr float HUNTER_CENTER_Y = -2124.73f; + static constexpr float HUNTER_LEASH = 4.0f; + static constexpr float HUNTER_DROP_TOLERANCE = 5.0f; + + float const dxh = bot->GetPositionX() - HUNTER_CENTER_X; + float const dyh = bot->GetPositionY() - HUNTER_CENTER_Y; + float const distToCenter = std::hypot(dxh, dyh); + + if (distToCenter > HUNTER_LEASH) + { + auto const [hx, hy] = DefileAwareStep(HUNTER_CENTER_X, HUNTER_CENTER_Y, defiles, diff); + float hz = slotPos.GetPositionZ(); + bot->UpdateAllowedPositionZ(hx, hy, hz); + return MoveTo(bot->GetMapId(), hx, hy, hz, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + if (distToCenter <= HUNTER_DROP_TOLERANCE && !bot->HasSpellCooldown(SPELL_FROST_TRAP1)) + { + if (botAI->CastSpell("frost trap", bot)) + return true; + } + + return false; + } + + // Healers may stay up to 20y from the slot so they can heal the assist tank + float const arriveTol = botAI->IsHeal(bot) ? 20.0f : ARRIVE_TOLERANCE; + + float const tx = slotPos.GetPositionX(); + float const ty = slotPos.GetPositionY(); + float const distToSlot = std::hypot(bot->GetPositionX() - tx, bot->GetPositionY() - ty); + if (distToSlot <= arriveTol) + return false; + + auto const [sx, sy] = DefileAwareStep(tx, ty, defiles, diff); + float sz = slotPos.GetPositionZ(); + bot->UpdateAllowedPositionZ(sx, sy, sz); + + return MoveTo(bot->GetMapId(), sx, sy, sz, false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool IccLichKingAddsAction::HandleIceSphereMechanics() +{ + if (!botAI->IsRangedDps(bot)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + static constexpr uint8 SPHERE_ICON = RtiTargetValue::circleIndex; + + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Unit* currentMark = botAI->GetUnit(group->GetTargetIcon(SPHERE_ICON)); + bool const sphereMarked = currentMark && currentMark->IsAlive() && + IsIceSphere(currentMark->GetEntry()); + + if (!sphereMarked) + { + Unit* nearestSphere = nullptr; + float nearestDist = std::numeric_limits::max(); + + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || !IsIceSphere(unit->GetEntry())) + continue; + + float const dist = bot->GetDistance(unit); + if (dist < nearestDist) + { + nearestDist = dist; + nearestSphere = unit; + } + } + + if (nearestSphere) + group->SetTargetIcon(SPHERE_ICON, bot->GetGUID(), nearestSphere->GetGUID()); + else + { + if (!group->GetTargetIcon(SPHERE_ICON).IsEmpty()) + group->SetTargetIcon(SPHERE_ICON, bot->GetGUID(), ObjectGuid::Empty); + return false; + } + + currentMark = botAI->GetUnit(group->GetTargetIcon(SPHERE_ICON)); + } + + if (currentMark && currentMark->IsAlive()) + { + bot->SetTarget(currentMark->GetGUID()); + bot->SetFacingToObject(currentMark); + Attack(currentMark); + return true; + } + + return false; +} + +bool IccLichKingAddsAction::IsValkyr(Unit* unit) +{ + return IsLkValkyr(unit); +} + +bool IccLichKingAddsAction::ApplyCCToValkyr(Unit* valkyr) +{ + switch (bot->getClass()) + { + case CLASS_MAGE: + if (!botAI->HasAura("Deep Freeze", valkyr) && botAI->CanCastSpell("Deep Freeze", valkyr)) + return botAI->CastSpell("Deep Freeze", valkyr); + if (!botAI->HasAura("Frost Nova", valkyr) && botAI->CanCastSpell("Frost Nova", valkyr)) + return botAI->CastSpell("Frost Nova", valkyr); + if (!botAI->HasAura("Cone of Cold", valkyr) && botAI->CanCastSpell("Cone of Cold", valkyr)) + return botAI->CastSpell("Cone of Cold", valkyr); + if (!botAI->HasAura("Frostbolt", valkyr) && botAI->CanCastSpell("Frostbolt", valkyr)) + return botAI->CastSpell("Frostbolt", valkyr); + if (!botAI->HasAura("Slow", valkyr) && botAI->CanCastSpell("Slow", valkyr)) + return botAI->CastSpell("Slow", valkyr); + break; + case CLASS_DRUID: + if (!botAI->HasAura("Bash", valkyr) && botAI->CanCastSpell("Bash", valkyr)) + return botAI->CastSpell("Bash", valkyr); + if (!botAI->HasAura("Maim", valkyr) && botAI->CanCastSpell("Maim", valkyr)) + return botAI->CastSpell("Maim", valkyr); + break; + case CLASS_PALADIN: + if (!botAI->HasAura("Hammer of Justice", valkyr) && botAI->CanCastSpell("Hammer of Justice", valkyr)) + return botAI->CastSpell("Hammer of Justice", valkyr); + break; + case CLASS_WARRIOR: + if (!botAI->HasAura("Concussion Blow", valkyr) && botAI->CanCastSpell("Concussion Blow", valkyr)) + return botAI->CastSpell("Concussion Blow", valkyr); + if (!botAI->HasAura("Shockwave", valkyr) && botAI->CanCastSpell("Shockwave", valkyr)) + return botAI->CastSpell("Shockwave", valkyr); + if (!botAI->HasAura("Intercept", valkyr) && botAI->CanCastSpell("Intercept", valkyr)) + return botAI->CastSpell("Intercept", valkyr); + if (!botAI->HasAura("Charge", valkyr) && botAI->CanCastSpell("Charge", valkyr)) + return botAI->CastSpell("Charge", valkyr); + if (!botAI->HasAura("Hamstring", valkyr) && botAI->CanCastSpell("Hamstring", valkyr)) + return botAI->CastSpell("Hamstring", valkyr); + if (!botAI->HasAura("Piercing Howl", valkyr) && botAI->CanCastSpell("Piercing Howl", valkyr)) + return botAI->CastSpell("Piercing Howl", valkyr); + break; + case CLASS_HUNTER: + if (!botAI->HasAura("Intimidation", valkyr) && botAI->CanCastSpell("Intimidation", valkyr)) + return botAI->CastSpell("Intimidation", valkyr); + if (!botAI->HasAura("Concussive Shot", valkyr) && botAI->CanCastSpell("Concussive Shot", valkyr)) + return botAI->CastSpell("Concussive Shot", valkyr); + if (!botAI->HasAura("Wing Clip", valkyr) && botAI->CanCastSpell("Wing Clip", valkyr)) + return botAI->CastSpell("Wing Clip", valkyr); + if (!botAI->HasAura("Freezing Trap", valkyr) && botAI->CanCastSpell("Freezing Trap", valkyr)) + return botAI->CastSpell("Freezing Trap", valkyr); + break; + case CLASS_ROGUE: + if (!botAI->HasAura("Kidney Shot", valkyr) && botAI->CanCastSpell("Kidney Shot", valkyr)) + return botAI->CastSpell("Kidney Shot", valkyr); + if (!botAI->HasAura("Gouge", valkyr) && botAI->CanCastSpell("Gouge", valkyr)) + return botAI->CastSpell("Gouge", valkyr); + if (!botAI->HasAura("Blind", valkyr) && botAI->CanCastSpell("Blind", valkyr)) + return botAI->CastSpell("Blind", valkyr); + if (!botAI->HasAura("Deadly Throw", valkyr) && botAI->CanCastSpell("Deadly Throw", valkyr)) + return botAI->CastSpell("Deadly Throw", valkyr); + break; + case CLASS_SHAMAN: + if (!botAI->HasAura("Thunderstorm", valkyr) && botAI->CanCastSpell("Thunderstorm", valkyr)) + return botAI->CastSpell("Thunderstorm", valkyr); + if (!botAI->HasAura("Frost Shock", valkyr) && botAI->CanCastSpell("Frost Shock", valkyr)) + return botAI->CastSpell("Frost Shock", valkyr); + if (!botAI->HasAura("Earthbind Totem", valkyr) && botAI->CanCastSpell("Earthbind Totem", valkyr)) + return botAI->CastSpell("Earthbind Totem", valkyr); + break; + case CLASS_DEATH_KNIGHT: + if (!botAI->HasAura("Hungering Cold", valkyr) && botAI->CanCastSpell("Hungering Cold", valkyr)) + return botAI->CastSpell("Hungering Cold", valkyr); + if (!botAI->HasAura("Gnaw", valkyr) && botAI->CanCastSpell("Gnaw", valkyr)) + return botAI->CastSpell("Gnaw", valkyr); + if (!botAI->HasAura("Chains of Ice", valkyr) && botAI->CanCastSpell("Chains of Ice", valkyr)) + return botAI->CastSpell("Chains of Ice", valkyr); + if (!botAI->HasAura("Desecration", valkyr) && botAI->CanCastSpell("Desecration", valkyr)) + return botAI->CastSpell("Desecration", valkyr); + break; + case CLASS_PRIEST: + if (!botAI->HasAura("Psychic Horror", valkyr) && botAI->CanCastSpell("Psychic Horror", valkyr)) + return botAI->CastSpell("Psychic Horror", valkyr); + if (!botAI->HasAura("Mind Flay", valkyr) && botAI->CanCastSpell("Mind Flay", valkyr)) + return botAI->CastSpell("Mind Flay", valkyr); + break; + case CLASS_WARLOCK: + if (!botAI->HasAura("Shadowfury", valkyr) && botAI->CanCastSpell("Shadowfury", valkyr)) + return botAI->CastSpell("Shadowfury", valkyr); + if (!botAI->HasAura("Death Coil", valkyr) && botAI->CanCastSpell("Death Coil", valkyr)) + return botAI->CastSpell("Death Coil", valkyr); + if (!botAI->HasAura("Curse of Exhaustion", valkyr) && botAI->CanCastSpell("Curse of Exhaustion", valkyr)) + return botAI->CastSpell("Curse of Exhaustion", valkyr); + break; + default: + break; + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LM.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LM.cpp new file mode 100644 index 000000000..a50255425 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_LM.cpp @@ -0,0 +1,536 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "RtiValue.h" +#include "Vehicle.h" + +// Lord Marrowgar + +// Group iteration filter for same-instance, alive, in-world members. +static bool IsValidLmMember(Player* member, Player* bot) +{ + if (!member || !member->IsInWorld() || !member->IsAlive()) + return false; + if (member->GetMapId() != bot->GetMapId()) + return false; + if (member->GetInstanceId() != bot->GetInstanceId()) + return false; + if (member->HasAura(SPELL_LM_IMPALED)) + return false; + return true; +} + +// Up to two lowest-GUID ranged bots in same instance, hunter-priority. +static std::vector PickBoneStormRangedTargets(Player* bot, PlayerbotAI* botAI) +{ + std::vector result; + + Group* group = bot->GetGroup(); + if (!group) + return result; + + std::vector ranged; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!IsValidLmMember(member, bot)) + continue; + if (botAI->IsTank(member)) + continue; + if (!botAI->IsRanged(member)) + continue; + + ranged.push_back(member); + } + + if (ranged.empty()) + return result; + + std::sort(ranged.begin(), ranged.end(), + [](Player const* a, Player const* b) { return a->GetGUID() < b->GetGUID(); }); + + for (Player* p : ranged) + { + if (p->getClass() != CLASS_HUNTER) + continue; + result.push_back(p); + if (result.size() == 2) + return result; + } + + for (Player* p : ranged) + { + if (p->getClass() == CLASS_HUNTER) + continue; + result.push_back(p); + if (result.size() == 2) + return result; + } + + return result; +} + +// True if any coldflame line sits within 10f of the anchor position. +// Used to widen the tank's "stay-put" tolerance so AvoidAoe can move them +// off the line without IccLmTankPositionAction dragging them back. +static bool ColdflameNearAnchor(Player* bot, Position const& anchor, float leash) +{ + std::list coldflames; + bot->GetCreatureListWithEntryInGrid(coldflames, NPC_COLDFLAME, 200.0f); + for (Creature* c : coldflames) + if (c->GetExactDist2d(anchor.GetPositionX(), anchor.GetPositionY()) < leash) + return true; + return false; +} + +bool IccLmTankPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); + if (!boss) + return false; + + bool const isBossInBoneStorm = botAI->GetAura("Bone Storm", boss) != nullptr; + float const maxDistanceThreshold = 3.0f; + + if (isBossInBoneStorm) + { + std::vector const rangedTargets = PickBoneStormRangedTargets(bot, botAI); + if (std::find(rangedTargets.begin(), rangedTargets.end(), bot) != rangedTargets.end()) + { + float const anchorDist = bot->GetExactDist2d(ICC_LM_BONE_STORM_AT_POSITION.GetPositionX(), + ICC_LM_BONE_STORM_AT_POSITION.GetPositionY()); + + float const bossDist = bot->GetExactDist2d(boss); + float const proximityTrigger = 20.0f; + float const leash = 10.0f; + + // Boss too close or standing in coldflame: reposition within leash from anchor + bool const bossNear = bossDist < proximityTrigger; + bool const inColdflame = [&]() + { + std::list coldflames; + bot->GetCreatureListWithEntryInGrid(coldflames, NPC_COLDFLAME, 4.0f); + return !coldflames.empty(); + }(); + + if (bossNear || inColdflame) + { + // Try eight candidate offsets from the anchor at the leash radius; + // pick first one that is far from boss and clear of coldflames. + float bestX = bot->GetPositionX(); + float bestY = bot->GetPositionY(); + float bestScore = -1.0f; + bool found = false; + + for (int i = 0; i < 8; ++i) + { + float const angle = (float)i * (float)M_PI / 4.0f; + float const cx = ICC_LM_BONE_STORM_AT_POSITION.GetPositionX() + std::cos(angle) * leash; + float const cy = ICC_LM_BONE_STORM_AT_POSITION.GetPositionY() + std::sin(angle) * leash; + + float const dx = cx - boss->GetPositionX(); + float const dy = cy - boss->GetPositionY(); + float const distToBoss = std::sqrt(dx * dx + dy * dy); + + std::list coldflames; + bot->GetCreatureListWithEntryInGrid(coldflames, NPC_COLDFLAME, 200.0f); + bool hitColdflame = false; + for (Creature* c : coldflames) + { + if (c->GetExactDist2d(cx, cy) < 4.0f) + { + hitColdflame = true; + break; + } + } + if (hitColdflame) + continue; + + if (distToBoss > bestScore) + { + bestScore = distToBoss; + bestX = cx; + bestY = cy; + found = true; + } + } + + if (found) + return MoveTo(bot->GetMapId(), bestX, bestY, ICC_LM_BONE_STORM_AT_POSITION.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + if (anchorDist > maxDistanceThreshold) + return MoveTo(bot->GetMapId(), ICC_LM_BONE_STORM_AT_POSITION.GetPositionX(), + ICC_LM_BONE_STORM_AT_POSITION.GetPositionY(), + ICC_LM_BONE_STORM_AT_POSITION.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + return true; + } + + float const tankLeash = + ColdflameNearAnchor(bot, ICC_LM_TANK_POSITION, 10.0f) ? 10.0f : maxDistanceThreshold; + + if (botAI->IsMainTank(bot)) + { + float const distance = + bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); + if (distance > tankLeash) + return MoveTowardPosition(ICC_LM_TANK_POSITION, maxDistanceThreshold); + return false; + } + + if (botAI->IsAssistTank(bot)) + { + float const distance = + bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); + if (distance > tankLeash) + return MoveTo(bot->GetMapId(), ICC_LM_TANK_POSITION.GetPositionX(), + ICC_LM_TANK_POSITION.GetPositionY(), ICC_LM_TANK_POSITION.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT); + return false; + } + + // Non-tanks: if too far from mid position, move toward it + float const distance = + bot->GetExactDist2d(ICC_LM_MID_POSITION.GetPositionX(), ICC_LM_MID_POSITION.GetPositionY()); + if (distance > 35.0f) + return MoveTowardPosition(ICC_LM_MID_POSITION, 15.0f); + + return false; + } + + float const tankLeash = + ColdflameNearAnchor(bot, ICC_LM_TANK_POSITION, 10.0f) ? 10.0f : maxDistanceThreshold; + + if (botAI->HasAggro(boss) && botAI->IsMainTank(bot) && boss->GetVictim() == bot) + { + float const distance = + bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); + + if (distance > tankLeash) + return MoveTowardPosition(ICC_LM_TANK_POSITION, maxDistanceThreshold); + } + + if (botAI->IsAssistTank(bot)) + { + float const distance = + bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); + + if (distance > tankLeash) + return MoveTo(bot->GetMapId(), ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY(), + ICC_LM_TANK_POSITION.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + + if (distance < maxDistanceThreshold) + { + bot->SetFacingToObject(boss); + return true; + } + } + + return false; +} + +bool IccLmTankPositionAction::MoveTowardPosition(Position const& position, float incrementSize) +{ + float moveX; + float moveY; + if (!IccStepToward(bot, position, incrementSize, moveX, moveY)) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +bool IccSpikeAction::Execute(Event /*event*/) +{ + if (bot->HasAura(SPELL_LM_IMPALED)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); + if (!boss) + return false; + + bool const isBossInBoneStorm = botAI->GetAura("Bone Storm", boss) != nullptr; + std::vector const spikes = FindAliveSpikes(); + + if (!spikes.empty()) + { + HandleSpikeMarking(spikes, boss); + return HandleSpikeAssignment(spikes, boss); + } + + // No spikes alive -- skull on boss, clear cross, all bots on skull + HandleNoSpikesMarking(boss); + + // Melee non-tanks in front of boss should reposition + if (boss->isInFront(bot) && !botAI->IsTank(bot) && !isBossInBoneStorm) + { + Position const safePosition = {-390.6757f, 2230.5283f, 0.0f}; + float const distance = bot->GetExactDist2d(safePosition.GetPositionX(), safePosition.GetPositionY()); + if (distance > 3.0f) + return MoveTowardPosition(safePosition, 3.0f); + } + + return false; +} + +std::vector IccSpikeAction::FindAliveSpikes() +{ + // All difficulty variants — AzerothCore spawns a different entry per difficulty mode. + // Bonespike NPCs have UNIT_FLAG_NOT_SELECTABLE | UNIT_FLAG_IMMUNE_TO_PC so they + // never appear in "possible targets no los". Use a direct grid search instead. + std::vector const found = IccGetCreaturesByEntries(bot, + {NPC_SPIKE1, NPC_SPIKE1_10H, NPC_SPIKE1_25N, NPC_SPIKE1_25H, + NPC_SPIKE2, NPC_SPIKE2_10H, NPC_SPIKE2_25N, NPC_SPIKE2_25H, + NPC_SPIKE3, NPC_SPIKE3_10H, NPC_SPIKE3_25N, NPC_SPIKE3_25H}, + 200.0f); + return std::vector(found.begin(), found.end()); +} + +bool IccSpikeAction::HandleSpikeMarking(std::vector const& spikes, Unit* boss) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + static uint8 const Icons[] = {RtiTargetValue::skullIndex, RtiTargetValue::crossIndex, RtiTargetValue::starIndex}; + + std::vector aliveSpikeGuids; + aliveSpikeGuids.reserve(spikes.size()); + for (Unit* spike : spikes) + aliveSpikeGuids.push_back(spike->GetGUID()); + + for (uint8 const iconIdx : Icons) + { + ObjectGuid const iconGuid = group->GetTargetIcon(iconIdx); + if (iconGuid.IsEmpty()) + continue; + if (std::find(aliveSpikeGuids.begin(), aliveSpikeGuids.end(), iconGuid) != aliveSpikeGuids.end()) + continue; + + Unit* marked = botAI->GetUnit(iconGuid); + if (marked && !marked->IsAlive()) + group->SetTargetIcon(iconIdx, bot->GetGUID(), ObjectGuid::Empty); + } + + // Check if the only spike left is a tank spike + Player* firstSpikeVictim = spikes.size() == 1 ? GetSpikeVictim(spikes[0]) : nullptr; + bool const onlyTankSpike = firstSpikeVictim && botAI->IsTank(firstSpikeVictim); + + if (onlyTankSpike) + { + // Skull on spike, Cross on boss + if (group->GetTargetIcon(RtiTargetValue::skullIndex) != spikes[0]->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), spikes[0]->GetGUID()); + + if (group->GetTargetIcon(RtiTargetValue::crossIndex) != boss->GetGUID()) + group->SetTargetIcon(RtiTargetValue::crossIndex, bot->GetGUID(), boss->GetGUID()); + + if (!group->GetTargetIcon(RtiTargetValue::starIndex).IsEmpty()) + group->SetTargetIcon(RtiTargetValue::starIndex, bot->GetGUID(), ObjectGuid::Empty); + + return true; + } + + // Clear icon slots beyond the current spike count + for (size_t i = spikes.size(); i < sizeof(Icons); ++i) + { + if (!group->GetTargetIcon(Icons[i]).IsEmpty()) + group->SetTargetIcon(Icons[i], bot->GetGUID(), ObjectGuid::Empty); + } + + // Assign Skull/Cross/Star to each alive spike + for (size_t i = 0; i < spikes.size() && i < sizeof(Icons); ++i) + { + uint8 const iconIdx = Icons[i]; + if (group->GetTargetIcon(iconIdx) != spikes[i]->GetGUID()) + group->SetTargetIcon(iconIdx, bot->GetGUID(), spikes[i]->GetGUID()); + } + + return true; +} + +bool IccSpikeAction::HandleNoSpikesMarking(Unit* boss) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Clear cross and star + for (uint8 const iconIdx : {uint8(6), uint8(0)}) + { + if (!group->GetTargetIcon(iconIdx).IsEmpty()) + group->SetTargetIcon(iconIdx, bot->GetGUID(), ObjectGuid::Empty); + } + + // Skull on boss + if (group->GetTargetIcon(RtiTargetValue::skullIndex) != boss->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + + // Per-bot context value -- every bot needs this for its own ChooseTarget. + context->GetValue("rti")->Set("skull"); + return true; +} + +bool IccSpikeAction::HandleSpikeAssignment(std::vector const& spikes, Unit* boss) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + bool const isMelee = botAI->IsMelee(bot) && !botAI->IsTank(bot); + bool const isAssistTank = botAI->IsAssistTank(bot); + + auto isTankSpike = [&](Unit* spike) -> bool + { + Player* victim = GetSpikeVictim(spike); + return victim && botAI->IsTank(victim); + }; + + // Assist tank: only attack tank spike, ignore all others + if (isAssistTank) + { + for (Unit* spike : spikes) + { + if (isTankSpike(spike)) + { + Attack(spike); + return false; + } + } + return false; + } + + // Only tank spike left -- ranged go skull (spike), melee go cross (boss) + bool const onlyTankSpike = spikes.size() == 1 && isTankSpike(spikes[0]); + if (onlyTankSpike) + { + if (isMelee) + { + context->GetValue("rti")->Set("cross"); + Attack(boss); + } + else + { + context->GetValue("rti")->Set("skull"); + Attack(spikes[0]); + } + return false; + } + + // Melee DPS: pick closest safe spike within 20y, never tank spikes + if (isMelee) + { + Unit* bestSpike = nullptr; + float bestDist = 20.0f; + for (Unit* spike : spikes) + { + if (isTankSpike(spike)) + continue; + + if (boss->isInFront(spike, 7.0f)) + continue; + + if (IsSpikeInColdFlame(spike)) + continue; + + float const dist = bot->GetExactDist2d(spike); + if (dist < bestDist) + { + bestDist = dist; + bestSpike = spike; + } + } + + if (bestSpike) + Attack(bestSpike); + + return false; + } + + // Ranged / healers: balanced assignment across all spikes + std::vector rangedMembers; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member->HasAura(SPELL_LM_IMPALED)) + continue; + + if (botAI->IsMainTank(member) || botAI->IsAssistTank(member)) + continue; + + if (botAI->IsMelee(member) && !botAI->IsTank(member)) + continue; + + rangedMembers.push_back(member); + } + + if (rangedMembers.empty()) + return false; + + std::sort(rangedMembers.begin(), rangedMembers.end(), + [](Player const* a, Player const* b) { return a->GetGUID() < b->GetGUID(); }); + + auto const it = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + if (it == rangedMembers.end()) + return false; + + size_t const myIndex = std::distance(rangedMembers.begin(), it); + std::vector const groupSizes = IccBalancedGroupSizes(rangedMembers.size(), spikes.size()); + size_t const spikeIndex = IccAssignedBucketIndex(myIndex, groupSizes); + + if (spikeIndex >= spikes.size()) + return false; + + Unit* mySpike = spikes[spikeIndex]; + context->GetValue("rti")->Set(IccRtiNameForBucket(spikeIndex)); + + Attack(mySpike); + return false; +} + +bool IccSpikeAction::MoveTowardPosition(Position const& position, float incrementSize) +{ + float moveX; + float moveY; + if (!IccStepToward(bot, position, incrementSize, moveX, moveY)) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +Player* IccSpikeAction::GetSpikeVictim(Unit* spike) +{ + // Spike holds player via vehicle; GetVictim() is unreliable (NPC not in combat) + if (Vehicle* veh = spike->GetVehicleKit()) + { + for (auto const& [seatId, seatInfo] : veh->Seats) + { + if (Unit* passenger = ObjectAccessor::GetUnit(*spike, seatInfo.Passenger.Guid)) + return passenger->ToPlayer(); + } + } + return nullptr; +} + +bool IccSpikeAction::IsSpikeInColdFlame(Unit* spike) +{ + float const checkRadius = 6.0f; + std::list coldflames; + spike->GetCreatureListWithEntryInGrid(coldflames, NPC_COLDFLAME, checkRadius); + return !coldflames.empty(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_PP.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_PP.cpp new file mode 100644 index 000000000..6007e4420 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_PP.cpp @@ -0,0 +1,1670 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "ICCScripts.h" +#include "RtiValue.h" +#include "Vehicle.h" +#include +#include +#include + +// Professor Putricide +bool IccPutricideMutatedPlagueAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + if (!botAI->IsTank(bot)) + return false; + + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + if (bot->GetTarget()) + bot->SetTarget(ObjectGuid::Empty); + return false; + } + + auto GetPlagueStacks = [&](Unit* unit) -> uint32 + { + if (!unit) + return 0; + + Aura* a = botAI->GetAura("mutated plague", unit, false, true); + return a ? a->GetStackAmount() : 0; + }; + + uint32 const myStacks = GetPlagueStacks(bot); + + bool shouldTaunt = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot || !member->IsAlive() || !member->IsInWorld()) + continue; + + if (!PlayerbotAI::IsTank(member)) + continue; + + if (myStacks < GetPlagueStacks(member)) + { + shouldTaunt = true; + break; + } + } + } + + if (shouldTaunt && boss->GetVictim() != bot) + IccCastClassTaunt(bot, botAI, boss); + + return false; +} + +bool IccPutricideGrowingOozePuddleAction::Execute(Event /*event*/) +{ + if (botAI->IsMainTank(bot) && + bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + if (bot->GetTarget()) + bot->SetTarget(ObjectGuid::Empty); + if (Unit* master = botAI->GetMaster()) + Follow(master); + return true; + } + + // Phase 3: only MT avoids hazards. Non-MT bots stack on MT blindly so + // they don't scatter when a puddle drops on the stack. + if (!botAI->IsMainTank(bot)) + { + Unit* bossP3 = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (bossP3 && bossP3->HealthBelowPct(35)) + return false; + } + + // Main tank rotation kite: when one or more active Growing Ooze Puddles + // sit close to the boss, walk the boss to a position safe from ALL of + // them. Tank picks an angle around the boss whose forward arc clears + // every nearby puddle. Boss turns to face the tank, rotating its frontal + // cone away from the puddle field. Stack bots line up behind the boss. + // Phase 3: skip kite (MT idles near boss). Flee logic below still runs so + // MT steps out of puddles. + if (botAI->IsMainTank(bot)) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (boss && boss->IsAlive() && boss->GetVictim() == bot && !boss->HealthBelowPct(35)) + { + constexpr float puddleNearBossRange = 8.0f; + constexpr float puddleSafeRadius = 10.0f; + constexpr float bossReach = 4.0f; + constexpr int kiteAngleSteps = 24; + + std::vector nearPuddles; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& g : npcs) + { + Unit* u = botAI->GetUnit(g); + if (!u || !u->IsAlive() || u->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + if (u->GetExactDist2d(boss) <= puddleNearBossRange) + nearPuddles.push_back(u); + } + + if (!nearPuddles.empty()) + { + // Centroid of near puddles → seed direction is boss-from-centroid. + float cx = 0.0f, cy = 0.0f; + for (Unit* p : nearPuddles) + { + cx += p->GetPositionX(); + cy += p->GetPositionY(); + } + cx /= static_cast(nearPuddles.size()); + cy /= static_cast(nearPuddles.size()); + + float seedX = boss->GetPositionX() - cx; + float seedY = boss->GetPositionY() - cy; + float seedLen = std::sqrt(seedX * seedX + seedY * seedY); + float seedAngle = (seedLen > 0.01f) ? std::atan2(seedY, seedX) : 0.0f; + + float bossX = boss->GetPositionX(); + float bossY = boss->GetPositionY(); + float botZ = bot->GetPositionZ(); + + // Scan angles around the boss starting from the seed (away + // from centroid) and spiraling outward. Pick the first angle + // whose tank-stand point clears every near puddle. + float bestGoalX = 0.0f, bestGoalY = 0.0f; + float bestMinDist = -FLT_MAX; + bool found = false; + bool foundClean = false; + + for (int i = 0; i < kiteAngleSteps; ++i) + { + // Alternate +/- around seedAngle to prefer minimal rotation. + int sign = (i % 2 == 0) ? 1 : -1; + int step = (i + 1) / 2; + float angle = seedAngle + sign * step * + (2.0f * static_cast(M_PI) / kiteAngleSteps); + + float gx = bossX + std::cos(angle) * bossReach; + float gy = bossY + std::sin(angle) * bossReach; + + float minDist = FLT_MAX; + for (Unit* p : nearPuddles) + { + float dd = p->GetDistance2d(gx, gy); + if (dd < minDist) + minDist = dd; + } + + if (!bot->IsWithinLOS(gx, gy, botZ)) + continue; + + if (minDist >= puddleSafeRadius) + { + bestGoalX = gx; + bestGoalY = gy; + foundClean = true; + found = true; + break; + } + + if (minDist > bestMinDist) + { + bestMinDist = minDist; + bestGoalX = gx; + bestGoalY = gy; + found = true; + } + } + + if (found && bot->GetExactDist2d(bestGoalX, bestGoalY) > 1.0f) + { + return MoveTo(bot->GetMapId(), bestGoalX, bestGoalY, botZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + + // No clean angle and already at best position: fall through + // to puddle-flee logic below so tank still avoids damage. + (void)foundClean; + } + } + } + + Unit* closestPuddle = FindClosestThreateningPuddle(); + if (!closestPuddle) + return false; + + Position movePosition = CalculateSafeMovePosition(closestPuddle); + return MoveTo(bot->GetMapId(), movePosition.GetPositionX(), movePosition.GetPositionY(), + movePosition.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +Unit* IccPutricideGrowingOozePuddleAction::FindClosestThreateningPuddle() +{ + constexpr float baseRadius = 2.0f; + constexpr float stackMultiplier = 0.8f; + constexpr float mainTankSafeDistance = 10.0f; + constexpr float minDistance = 0.1f; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + if (npcs.empty()) + return nullptr; + + // Phase 3: MT no longer kites, treat as regular bot for puddle avoidance. + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + bool const isMainTank = botAI->IsMainTank(bot) && !(boss && boss->HealthBelowPct(35)); + + Unit* closestPuddle = nullptr; + float closestDistance = FLT_MAX; + + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + + float currentDistance = std::max(minDistance, bot->GetExactDist2d(unit)); + float safeDistance = isMainTank ? mainTankSafeDistance : baseRadius; + + if (!isMainTank) + { + if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) + safeDistance += (grow->GetStackAmount() * stackMultiplier); + } + + if (currentDistance < safeDistance && currentDistance < closestDistance) + { + closestDistance = currentDistance; + closestPuddle = unit; + } + } + + return closestPuddle; +} + +Position IccPutricideGrowingOozePuddleAction::CalculateSafeMovePosition(Unit* closestPuddle) +{ + constexpr float baseRadius = 2.0f; + constexpr float stackMultiplier = 0.8f; + constexpr float mainTankSafeDistance = 10.0f; + constexpr float bufferDistance = 2.0f; + constexpr float minDistance = 0.1f; + constexpr int numAnglesToTest = 8; + constexpr float tankShoveDistance = 6.0f; + + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + bool const isMainTank = botAI->IsMainTank(bot) && !(boss && boss->HealthBelowPct(35)); + bool const isP3Tank = botAI->IsMainTank(bot) && boss && boss->HealthBelowPct(35); + + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + float botZ = bot->GetPositionZ(); + + float gateDx = 0.0f, gateDy = 0.0f, gateLen = 0.0f; + if (isP3Tank) + { + gateDx = ICC_PUTRICIDE_GATE_POSITION.GetPositionX() - botX; + gateDy = ICC_PUTRICIDE_GATE_POSITION.GetPositionY() - botY; + gateLen = std::sqrt(gateDx * gateDx + gateDy * gateDy); + if (gateLen > 0.01f) + { + gateDx /= gateLen; + gateDy /= gateLen; + } + } + + float currentDistance = std::max(minDistance, bot->GetExactDist2d(closestPuddle)); + bool const useTankSafeDistance = isMainTank || isP3Tank; + float safeDistance = useTankSafeDistance ? mainTankSafeDistance : baseRadius; + if (!useTankSafeDistance) + { + if (Aura* grow = closestPuddle->GetAura(SPELL_GROW_AURA)) + safeDistance += (grow->GetStackAmount() * stackMultiplier); + } + + float dx = botX - closestPuddle->GetPositionX(); + float dy = botY - closestPuddle->GetPositionY(); + float dist = std::max(minDistance, std::sqrt(dx * dx + dy * dy)); + + if (dist < minDistance * 2.0f) + { + float randomAngle = static_cast(rand()) / static_cast(RAND_MAX) * 2.0f * static_cast(M_PI); + dx = std::cos(randomAngle); + dy = std::sin(randomAngle); + } + else + { + dx /= dist; + dy /= dist; + } + + float moveDistance = safeDistance - currentDistance + bufferDistance; + + // If the bot is already inside the puddle, anchor candidates on the + // puddle's safe-radius circle instead of rotating around the bot's + // current (unsafe) position. Otherwise a rotated move can land deeper + // into the puddle. Outward radial from puddle center is always safe. + bool const insidePuddle = currentDistance < safeDistance; + + for (int i = 0; i < numAnglesToTest; ++i) + { + float angle = (2.0f * static_cast(M_PI) * i) / numAnglesToTest; + float rotatedDx = dx * std::cos(angle) - dy * std::sin(angle); + float rotatedDy = dx * std::sin(angle) + dy * std::cos(angle); + + float testX, testY; + if (insidePuddle) + { + // Stand on safe ring around puddle, rotated by `angle` from the + // bot-relative radial. i=0 → straight outward from puddle center. + float radius = safeDistance + bufferDistance; + testX = closestPuddle->GetPositionX() + rotatedDx * radius; + testY = closestPuddle->GetPositionY() + rotatedDy * radius; + } + else + { + testX = botX + rotatedDx * moveDistance; + testY = botY + rotatedDy * moveDistance; + } + + // Reject any candidate that still sits inside the closest puddle. + float candDist = closestPuddle->GetDistance2d(testX, testY); + if (candDist < safeDistance) + continue; + + if (!IsPositionTooCloseToOtherPuddles(testX, testY, closestPuddle) && bot->IsWithinLOS(testX, testY, botZ)) + { + if (isP3Tank && gateLen > 0.01f) + { + float moveDx = testX - botX; + float moveDy = testY - botY; + if (moveDx * gateDx + moveDy * gateDy <= 0.0f) + continue; + } + + if (PathCrossesAnyPuddle(botX, botY, testX, testY, nullptr)) + continue; + + if (botAI->IsTank(bot)) + { + float awayDx = testX - closestPuddle->GetPositionX(); + float awayDy = testY - closestPuddle->GetPositionY(); + float awayDist = std::sqrt(awayDx * awayDx + awayDy * awayDy); + if (awayDist > 0.001f) + { + awayDx /= awayDist; + awayDy /= awayDist; + testX += awayDx * tankShoveDistance; + testY += awayDy * tankShoveDistance; + } + } + return Position(testX, testY, botZ); + } + } + + // Fallback: straight outward from puddle center on its safe ring. + float fallbackRadius = safeDistance + bufferDistance; + float fallbackX = insidePuddle ? (closestPuddle->GetPositionX() + dx * fallbackRadius) + : (botX + dx * moveDistance); + float fallbackY = insidePuddle ? (closestPuddle->GetPositionY() + dy * fallbackRadius) + : (botY + dy * moveDistance); + if (isP3Tank && gateLen > 0.01f) + { + float fbDx = fallbackX - botX; + float fbDy = fallbackY - botY; + if (fbDx * gateDx + fbDy * gateDy <= 0.0f) + { + fallbackX = closestPuddle->GetPositionX() + gateDx * fallbackRadius; + fallbackY = closestPuddle->GetPositionY() + gateDy * fallbackRadius; + } + } + if (botAI->IsTank(bot)) + { + float awayDx = fallbackX - closestPuddle->GetPositionX(); + float awayDy = fallbackY - closestPuddle->GetPositionY(); + float awayDist = std::sqrt(awayDx * awayDx + awayDy * awayDy); + if (awayDist > 0.001f) + { + awayDx /= awayDist; + awayDy /= awayDist; + fallbackX += awayDx * tankShoveDistance; + fallbackY += awayDy * tankShoveDistance; + } + } + return Position(fallbackX, fallbackY, botZ); +} + +bool IccPutricideGrowingOozePuddleAction::PathCrossesAnyPuddle(float fromX, float fromY, float toX, float toY, Unit* ignorePuddle) +{ + constexpr float baseRadius = 2.0f; + constexpr float stackMultiplier = 0.8f; + constexpr float mainTankSafeDistance = 10.0f; + + bool const useTankSafeDistance = botAI->IsMainTank(bot); + + float segDx = toX - fromX; + float segDy = toY - fromY; + float segLenSq = segDx * segDx + segDy * segDy; + if (segLenSq < 0.01f) + return false; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || unit == ignorePuddle || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + + float radius = useTankSafeDistance ? mainTankSafeDistance : baseRadius; + if (!useTankSafeDistance) + { + if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) + radius += (grow->GetStackAmount() * stackMultiplier); + } + + float px = unit->GetPositionX(); + float py = unit->GetPositionY(); + float t = ((px - fromX) * segDx + (py - fromY) * segDy) / segLenSq; + if (t < 0.0f) t = 0.0f; + else if (t > 1.0f) t = 1.0f; + + float closestX = fromX + segDx * t; + float closestY = fromY + segDy * t; + float ddx = closestX - px; + float ddy = closestY - py; + if (ddx * ddx + ddy * ddy < radius * radius) + return true; + } + + return false; +} + +bool IccPutricideGrowingOozePuddleAction::IsPositionTooCloseToOtherPuddles(float x, float y, Unit* ignorePuddle) +{ + constexpr float baseRadius = 2.0f; + constexpr float stackMultiplier = 0.8f; + constexpr float mainTankSafeDistance = 10.0f; + + bool const isMainTank = botAI->IsMainTank(bot); + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || unit == ignorePuddle || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + + float safeDistance = isMainTank ? mainTankSafeDistance : baseRadius; + if (!isMainTank) + { + if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) + safeDistance += (grow->GetStackAmount() * stackMultiplier); + } + + float dist = unit->GetDistance2d(x, y); + if (dist < safeDistance) + return true; + } + + return false; +} + +bool IccPutricideVolatileOozeAction::Execute(Event /*event*/) +{ + constexpr float stackDistance = 7.0f; + + Unit* ooze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + if (!ooze) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + if (botAI->IsMainTank(bot) && + bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FOLLOW_MOTION_TYPE && + bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && + !boss->HealthBelowPct(36) && boss->GetVictim() == bot) + return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), + ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + + if (botAI->HasAura("Gaseous Bloat", bot) || botAI->HasAura("Unbound Plague", bot)) + return false; + + std::vector aliveOozes; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == ooze->GetEntry()) + aliveOozes.push_back(unit); + } + + if (aliveOozes.size() > 1) + { + for (size_t i = 0; i < aliveOozes.size() - 1; ++i) + bot->Kill(bot, aliveOozes[i]); + } + + MarkOozeWithSkull(ooze); + + if (botAI->IsMelee(bot) && !botAI->IsMainTank(bot)) + { + if (bot->IsWithinMeleeRange(ooze)) + { + Attack(ooze); + return false; + } + + // If we're closer to the ooze than the targeted stack player is, + // running to the stack point just drags the ooze further. Go attack + // the ooze directly — the stack target will end up here anyway. + Unit* stackTarget = FindAuraTarget(); + if (stackTarget) + { + float botToOoze = bot->GetDistance2d(ooze); + float stackToOoze = stackTarget->GetDistance2d(ooze); + if (botToOoze < stackToOoze) + { + Attack(ooze); + return false; + } + } + } + + if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) + { + constexpr float nearbyStackRange = 20.0f; + + Unit* stackTarget = FindAuraTarget(); + float distToStack = stackTarget ? bot->GetDistance2d(stackTarget) : 0.0f; + + // Only run to the stack target if it is reasonably close. Otherwise + // we'd chase across the room and barely attack. If it is far, check + // whether any other group member is already stacking near it — if + // yes, join them; if no, just stay put and attack from range. + bool shouldMoveToStack = false; + if (stackTarget && distToStack > stackDistance) + { + if (distToStack <= nearbyStackRange) + { + shouldMoveToStack = true; + } + else if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot || member == stackTarget) + continue; + if (member->GetDistance2d(stackTarget) <= stackDistance && + bot->GetDistance2d(member) <= nearbyStackRange) + { + shouldMoveToStack = true; + break; + } + } + } + } + + if (shouldMoveToStack) + { + return MoveTo(bot->GetMapId(), stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + if (ooze && !botAI->IsHeal(bot)) + { + bot->SetTarget(ooze->GetGUID()); + bot->SetFacingToObject(ooze); + if (bot->IsWithinRange(ooze, 25.0f)) + { + Attack(ooze); + return false; + } + } + } + + return false; +} + +bool IccPutricideVolatileOozeAction::MarkOozeWithSkull(Unit* ooze) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr uint8 skullIconId = RtiTargetValue::skullIndex; + ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); + Unit* markedUnit = botAI->GetUnit(skullGuid); + + if (markedUnit && (!markedUnit->IsAlive() || (ooze && markedUnit != ooze))) + group->SetTargetIcon(skullIconId, bot->GetGUID(), ObjectGuid::Empty); + + if (ooze && ooze->IsAlive() && (!skullGuid || !markedUnit)) + group->SetTargetIcon(skullIconId, bot->GetGUID(), ooze->GetGUID()); + + return false; +} + +Unit* IccPutricideVolatileOozeAction::FindAuraTarget() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + if (botAI->HasAura("Volatile Ooze Adhesive", member)) + return member; + } + + return nullptr; +} + +bool IccPutricideGasCloudAction::Execute(Event /*event*/) +{ + Unit* gasCloud = AI_VALUE2(Unit*, "find target", "gas cloud"); + if (!gasCloud) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + if (botAI->IsTank(bot) && + bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FOLLOW_MOTION_TYPE && + bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && !boss->HealthBelowPct(36) && + boss->GetVictim() == bot) + return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), + ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); + + if (botAI->IsMainTank(bot)) + return false; + + bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); + Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + + std::vector aliveGasCloud; + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == gasCloud->GetEntry()) + aliveGasCloud.push_back(unit); + } + + if (aliveGasCloud.size() > 1) + { + for (size_t i = 0; i < aliveGasCloud.size() - 1; ++i) + bot->Kill(bot, aliveGasCloud[i]); + } + + if (!hasGaseousBloat && volatileOoze) + return false; + + if (hasGaseousBloat) + return HandleGaseousBloatMovement(gasCloud); + + return HandleGroupAuraSituation(gasCloud); +} + +bool IccPutricideGasCloudAction::HandleGaseousBloatMovement(Unit* gasCloud) +{ + auto& g_bloatLastDir = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()).ppBloatLastDir; + + if (!botAI->HasAura("Gaseous Bloat", bot)) + { + g_bloatLastDir.erase(bot->GetGUID()); + return false; + } + + // Lookup prior committed flee direction (if any) so we can reject + // backtracking candidates this tick. + ObjectGuid botKey = bot->GetGUID(); + auto lastDirIt = g_bloatLastDir.find(botKey); + bool hasLastDir = lastDirIt != g_bloatLastDir.end(); + float lastDirX = hasLastDir ? lastDirIt->second.x : 0.0f; + float lastDirY = hasLastDir ? lastDirIt->second.y : 0.0f; + + auto isBacktrack = [&](float candDx, float candDy) -> bool + { + if (!hasLastDir) + return false; + // Require forward progress: dot must be positive (>0). This still + // allows up to ~89deg turns but blocks any move with a backward + // component. + return (candDx * lastDirX + candDy * lastDirY) <= 0.0f; + }; + + auto commitDir = [&](float fromX, float fromY, float toX, float toY) + { + float ddx = toX - fromX; + float ddy = toY - fromY; + float l = std::sqrt(ddx * ddx + ddy * ddy); + if (l < 0.01f) + return; + g_bloatLastDir[botKey] = { ddx / l, ddy / l }; + }; + + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + constexpr int numAngles = 32; + constexpr float gasBombSafeDist = 6.0f; + constexpr float movementIncrement = 5.0f; + constexpr float maxTestDist = 30.0f; + constexpr int checkDirs = 16; + constexpr float checkDist = 8.0f; + constexpr float minFreedomScore = 0.75f; + + Position botPos = bot->GetPosition(); + Position cloudPos = gasCloud->GetPosition(); + float cloudDist = gasCloud->GetExactDist2d(botPos); + + // Detect if bot is trapped near walls by testing 8 directions at 40yd. + // Threshold for "corner-ish" is intentionally low so we trigger the + // corner-escape branch before the bot fully parks against a wall. + constexpr int cornerCheckDirs = 8; + constexpr float cornerCheckDist = 40.0f; + int blockedAtStart = 0; + for (int i = 0; i < cornerCheckDirs; ++i) + { + float cA = (2.0f * static_cast(M_PI) * i) / cornerCheckDirs; + if (!bot->IsWithinLOS(botPos.GetPositionX() + std::cos(cA) * cornerCheckDist, + botPos.GetPositionY() + std::sin(cA) * cornerCheckDist, botPos.GetPositionZ())) + blockedAtStart++; + } + + // No distance early-exit: bloated bots must kite continuously because + // the gas cloud actively chases them. Stopping even briefly lets it catch up. + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector gasBombs; + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CHOKING_GAS_BOMB) + gasBombs.push_back(unit); + } + + float dx = botPos.GetPositionX() - cloudPos.GetPositionX(); + float dy = botPos.GetPositionY() - cloudPos.GetPositionY(); + float dist = std::max(0.1f, std::sqrt(dx * dx + dy * dy)); + dx /= dist; + dy /= dist; + + Position bestPos; + bool foundPath = false; + float bestScore = 0.0f; + + // Corner escape: test all 32 angles at a fixed medium distance with strict requirements. + // Trigger at >=3 blocked to catch edge-of-wall situations before parking. + if (blockedAtStart >= 3) + { + constexpr float escRadius = 10.0f; + for (int i = 0; i < 32; ++i) + { + float escAngle = (2.0f * static_cast(M_PI) * i) / 32; + float escX = botPos.GetPositionX() + std::cos(escAngle) * escRadius; + float escY = botPos.GetPositionY() + std::sin(escAngle) * escRadius; + float escZ = botPos.GetPositionZ(); + + if (!bot->IsWithinLOS(escX, escY, escZ)) + continue; + + if (isBacktrack(escX - botPos.GetPositionX(), escY - botPos.GetPositionY())) + continue; + + int freeDirs = 0; + for (int j = 0; j < checkDirs; ++j) + { + float cA = (2.0f * static_cast(M_PI) * j) / checkDirs; + if (bot->IsWithinLOS(escX + std::cos(cA) * checkDist, escY + std::sin(cA) * checkDist, escZ)) + freeDirs++; + } + + // Count long-range openness (25f rays) — reject positions near walls. + int farFreeDirs = 0; + for (int k = 0; k < 8; ++k) + { + float cA = (2.0f * static_cast(M_PI) * k) / 8; + if (bot->IsWithinLOS(escX + std::cos(cA) * 25.0f, escY + std::sin(cA) * 25.0f, escZ)) + farFreeDirs++; + } + + float escCloudDist = cloudPos.GetExactDist2d(escX, escY); + + // Must be moving away, open locally, and open at range. + if (escCloudDist > cloudDist && freeDirs >= 12 && farFreeDirs >= 6) + { + commitDir(botPos.GetPositionX(), botPos.GetPositionY(), escX, escY); + botAI->Reset(); + return MoveTo(bot->GetMapId(), escX, escY, escZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // Hard-corner relief: no strict candidate found. Re-scan and pick the + // angle with the most local openness regardless of cloud direction — + // even briefly stepping toward the cloud is preferable to standing + // still in a wedge while it walks into us. + int bestRelOpen = -1; + float bestRelX = 0.0f, bestRelY = 0.0f, bestRelZ = botPos.GetPositionZ(); + for (int i = 0; i < 32; ++i) + { + float escAngle = (2.0f * static_cast(M_PI) * i) / 32; + float escX = botPos.GetPositionX() + std::cos(escAngle) * escRadius; + float escY = botPos.GetPositionY() + std::sin(escAngle) * escRadius; + float escZ = botPos.GetPositionZ(); + + if (!bot->IsWithinLOS(escX, escY, escZ)) + continue; + + if (isBacktrack(escX - botPos.GetPositionX(), escY - botPos.GetPositionY())) + continue; + + int openCount = 0; + for (int j = 0; j < checkDirs; ++j) + { + float cA = (2.0f * static_cast(M_PI) * j) / checkDirs; + if (bot->IsWithinLOS(escX + std::cos(cA) * checkDist, + escY + std::sin(cA) * checkDist, escZ)) + openCount++; + } + if (openCount > bestRelOpen) + { + bestRelOpen = openCount; + bestRelX = escX; + bestRelY = escY; + bestRelZ = escZ; + } + } + if (bestRelOpen >= 0) + { + commitDir(botPos.GetPositionX(), botPos.GetPositionY(), bestRelX, bestRelY); + botAI->Reset(); + return MoveTo(bot->GetMapId(), bestRelX, bestRelY, bestRelZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + + // Normal search: scan 32 angles × distance increments, score each valid position. + for (int i = 0; i < numAngles; ++i) + { + float angle = (2.0f * static_cast(M_PI) * i) / numAngles; + float rotatedDx = dx * std::cos(angle) - dy * std::sin(angle); + float rotatedDy = dx * std::sin(angle) + dy * std::cos(angle); + + for (float testDist = movementIncrement; testDist <= maxTestDist; testDist += movementIncrement) + { + float testX = botPos.GetPositionX() + rotatedDx * testDist; + float testY = botPos.GetPositionY() + rotatedDy * testDist; + float testZ = botPos.GetPositionZ(); + + float newCloudDist = cloudPos.GetExactDist2d(testX, testY); + + // Reject positions on the far side of the cloud (through it). + // The move direction must point generally away from the cloud. + float toTestX = testX - botPos.GetPositionX(); + float toTestY = testY - botPos.GetPositionY(); + if (toTestX * dx + toTestY * dy <= 0.0f) + continue; + + // Anti-backtrack: don't pick a candidate that walks against the + // direction we already committed to this Bloat session. + if (isBacktrack(toTestX, toTestY)) + continue; + + float minGasBombDist = FLT_MAX; + for (Unit* bomb : gasBombs) + { + float bombDist = bomb->GetDistance2d(testX, testY); + if (bombDist < minGasBombDist) + minGasBombDist = bombDist; + } + + if (newCloudDist > cloudDist && minGasBombDist >= gasBombSafeDist && + bot->IsWithinLOS(testX, testY, testZ)) + { + int freeDirections = 0; + for (int j = 0; j < checkDirs; ++j) + { + float checkAngle = (2.0f * static_cast(M_PI) * j) / checkDirs; + float checkX = testX + std::cos(checkAngle) * checkDist; + float checkY = testY + std::sin(checkAngle) * checkDist; + if (bot->IsWithinLOS(checkX, checkY, testZ)) + freeDirections++; + } + + float freedomScore = static_cast(freeDirections) / static_cast(checkDirs); + if (freedomScore < minFreedomScore) + continue; + + // Long-range corner check: 8 rays at 25f. If too many blocked, skip. + int farFreeDirs = 0; + for (int k = 0; k < 8; ++k) + { + float cA = (2.0f * static_cast(M_PI) * k) / 8; + if (bot->IsWithinLOS(testX + std::cos(cA) * 25.0f, testY + std::sin(cA) * 25.0f, testZ)) + farFreeDirs++; + } + if (farFreeDirs < 5) + continue; + + // Score: cloud distance, freedom, gas bomb distance, continuity. + bool canContinueMoving = false; + { + float continueX = testX + rotatedDx * movementIncrement; + float continueY = testY + rotatedDy * movementIncrement; + if (bot->IsWithinLOS(continueX, continueY, testZ) && + farFreeDirs >= 6) + canContinueMoving = true; + } + + float continuity = canContinueMoving ? 5.0f : 0.0f; + float combinedScore = newCloudDist + (freedomScore * 15.0f) + minGasBombDist + + static_cast(farFreeDirs) * 2.0f + continuity; + + if (!foundPath || combinedScore > bestScore) + { + bestPos = Position(testX, testY, testZ); + bestScore = combinedScore; + foundPath = true; + } + } + } + } + + if (foundPath) + { + commitDir(botPos.GetPositionX(), botPos.GetPositionY(), + bestPos.GetPositionX(), bestPos.GetPositionY()); + botAI->Reset(); + return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + // Fallback pass: no candidate met the strict freedom/openness thresholds. + // Relax requirements and pick the most-open position that still moves us + // away from the cloud. Better to make imperfect progress than to stand + // still while the cloud walks into us. + float bestFallbackScore = -FLT_MAX; + Position bestFallbackPos; + bool foundFallback = false; + for (int i = 0; i < numAngles; ++i) + { + float angle = (2.0f * static_cast(M_PI) * i) / numAngles; + float rotatedDx = dx * std::cos(angle) - dy * std::sin(angle); + float rotatedDy = dx * std::sin(angle) + dy * std::cos(angle); + + for (float testDist = movementIncrement; testDist <= maxTestDist; testDist += movementIncrement) + { + float testX = botPos.GetPositionX() + rotatedDx * testDist; + float testY = botPos.GetPositionY() + rotatedDy * testDist; + float testZ = botPos.GetPositionZ(); + + // Must be moving away from the cloud. + float toTestX = testX - botPos.GetPositionX(); + float toTestY = testY - botPos.GetPositionY(); + if (toTestX * dx + toTestY * dy <= 0.0f) + continue; + + if (isBacktrack(toTestX, toTestY)) + continue; + + float newCloudDist = cloudPos.GetExactDist2d(testX, testY); + if (newCloudDist <= cloudDist) + continue; + + if (!bot->IsWithinLOS(testX, testY, testZ)) + continue; + + // Gas bomb hard-reject only. + float minGasBombDist = FLT_MAX; + for (Unit* bomb : gasBombs) + { + float bombDist = bomb->GetDistance2d(testX, testY); + if (bombDist < minGasBombDist) + minGasBombDist = bombDist; + } + if (minGasBombDist < gasBombSafeDist) + continue; + + // Score by cloud distance + local openness, no hard threshold. + int freeDirections = 0; + for (int j = 0; j < checkDirs; ++j) + { + float checkAngle = (2.0f * static_cast(M_PI) * j) / checkDirs; + if (bot->IsWithinLOS(testX + std::cos(checkAngle) * checkDist, + testY + std::sin(checkAngle) * checkDist, testZ)) + freeDirections++; + } + + float score = newCloudDist + static_cast(freeDirections) * 2.0f; + if (score > bestFallbackScore) + { + bestFallbackScore = score; + bestFallbackPos = Position(testX, testY, testZ); + foundFallback = true; + } + } + } + + if (foundFallback) + { + commitDir(botPos.GetPositionX(), botPos.GetPositionY(), + bestFallbackPos.GetPositionX(), bestFallbackPos.GetPositionY()); + botAI->Reset(); + return MoveTo(bot->GetMapId(), bestFallbackPos.GetPositionX(), bestFallbackPos.GetPositionY(), + bestFallbackPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + + // Last resort: move directly away from the cloud ignoring everything. + // Better to slide along a wall than to stand and eat it. + float lastX = botPos.GetPositionX() + dx * movementIncrement; + float lastY = botPos.GetPositionY() + dy * movementIncrement; + commitDir(botPos.GetPositionX(), botPos.GetPositionY(), lastX, lastY); + botAI->Reset(); + return MoveTo(bot->GetMapId(), lastX, lastY, botPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +bool IccPutricideGasCloudAction::HandleGroupAuraSituation(Unit* gasCloud) +{ + Group* group = bot->GetGroup(); + if (!group || botAI->IsHeal(bot)) + return false; + + constexpr float rangeMinSafeDistance = 15.0f; + constexpr uint8 skullIconId = RtiTargetValue::skullIndex; + + Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + if ((!volatileOoze || !volatileOoze->IsAlive()) && gasCloud && gasCloud->IsAlive()) + { + ObjectGuid currentSkull = group->GetTargetIcon(skullIconId); + Unit* markedUnit = botAI->GetUnit(currentSkull); + if (!markedUnit || !markedUnit->IsAlive() || markedUnit != gasCloud) + group->SetTargetIcon(skullIconId, bot->GetGUID(), gasCloud->GetGUID()); + } + + float currentDist = gasCloud ? bot->GetDistance(gasCloud) : 0.0f; + + if (!GroupHasGaseousBloat(group) && gasCloud && currentDist < rangeMinSafeDistance) + { + float dx = bot->GetPositionX() - gasCloud->GetPositionX(); + float dy = bot->GetPositionY() - gasCloud->GetPositionY(); + float dist = std::max(0.1f, std::sqrt(dx * dx + dy * dy)); + dx /= dist; + dy /= dist; + + float step = std::min(5.0f, rangeMinSafeDistance - currentDist); + return MoveTo(bot->GetMapId(), bot->GetPositionX() + dx * step, bot->GetPositionY() + dy * step, + bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool IccPutricideGasCloudAction::GroupHasGaseousBloat(Group* group) +{ + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && botAI->HasAura("Gaseous Bloat", member)) + return true; + } + return false; +} + +bool IccPutricideAvoidMalleableGooAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + if (botAI->IsTank(bot) && + bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + if (bot->GetTarget()) + bot->SetTarget(ObjectGuid::Empty); + if (Unit* master = botAI->GetMaster()) + Follow(master); + return true; + } + + // Reactive avoidance: find a position safe from ALL active Malleable Goo + // impact points (boss casts up to 3 simultaneously and they may be near + // each other - fleeing one can land in another). We sample a ring of + // candidates around the bot and score by minimum distance to every active + // hazard (goo impacts, ooze puddles, choking gas bombs). The best-scoring + // candidate that clears the danger radius for all goos is chosen. + // Only active in phase 1/2 (>35% HP) - in phase 3 bots must stack on the + // boss for Mutated Plague healing. + if (!boss->HealthBelowPct(35)) + { + constexpr float gooDangerRadius = 8.0f; // 5yd AoE + 3yd safety + constexpr float puddleAvoidRadius = 6.0f; + + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + float botZ = bot->GetPositionZ(); + + std::vector goos = IcecrownHelpers::ActiveGooPositions(bot->GetMap()->GetInstanceId(), 6000); + bool botInDanger = false; + for (Position const& goo : goos) + { + float dx = botX - goo.GetPositionX(); + float dy = botY - goo.GetPositionY(); + if (dx * dx + dy * dy < gooDangerRadius * gooDangerRadius) + botInDanger = true; + } + + if (botInDanger) + { + // Gather puddle / bomb obstacle positions to avoid in scoring + std::vector obstacles; + std::list nearbyUnits; + bot->GetCreatureListWithEntryInGrid(nearbyUnits, NPC_GROWING_OOZE_PUDDLE, 40.0f); + for (Creature* c : nearbyUnits) + if (c && c->IsAlive()) + obstacles.push_back(c->GetPosition()); + nearbyUnits.clear(); + bot->GetCreatureListWithEntryInGrid(nearbyUnits, NPC_CHOKING_GAS_BOMB, 40.0f); + for (Creature* c : nearbyUnits) + if (c && c->IsAlive()) + obstacles.push_back(c->GetPosition()); + + Position fromPos = bot->GetPosition(); + float bestScore = -1.0f; + float bestX = botX, bestY = botY; + bool found = false; + + // Sample ring: multiple radii × multiple angles. Prefer minimum + // displacement (closer rings tried first via scoring bias). + constexpr int angleSteps = 24; + float const radii[] = {8.0f, 11.0f, 14.0f, 17.0f}; + for (float r : radii) + { + for (int i = 0; i < angleSteps; ++i) + { + float a = (2.0f * float(M_PI) * i) / angleSteps; + float cx = botX + std::cos(a) * r; + float cy = botY + std::sin(a) * r; + Position toPos(cx, cy, botZ); + + // Must clear every goo's danger radius + float minGooDistSq = std::numeric_limits::max(); + bool safe = true; + for (Position const& g : goos) + { + float gdx = cx - g.GetPositionX(); + float gdy = cy - g.GetPositionY(); + float d2 = gdx * gdx + gdy * gdy; + if (d2 < gooDangerRadius * gooDangerRadius) + { + safe = false; + break; + } + if (d2 < minGooDistSq) + minGooDistSq = d2; + } + if (!safe) + continue; + + // Reachability checks + if (!bot->IsWithinLOS(cx, cy, botZ)) + continue; + if (HasObstacleBetween(fromPos, toPos)) + continue; + + // Score = min distance to nearest hazard at the candidate + // (goo, puddle, bomb), penalize travel distance lightly. + float minHazard = std::sqrt(minGooDistSq); + for (Position const& o : obstacles) + { + float odx = cx - o.GetPositionX(); + float ody = cy - o.GetPositionY(); + float od = std::sqrt(odx * odx + ody * ody); + // Treat obstacles as having a minimum stand-off + float effective = od - (puddleAvoidRadius - 4.0f); + if (effective < minHazard) + minHazard = effective; + } + + float travel = std::sqrt((cx - botX) * (cx - botX) + + (cy - botY) * (cy - botY)); + float score = minHazard - travel * 0.1f; + + if (score > bestScore) + { + bestScore = score; + bestX = cx; + bestY = cy; + found = true; + } + } + // If we found a clean candidate at this radius, stop expanding + if (found) + break; + } + + if (found) + { + return MoveTo(bot->GetMapId(), bestX, bestY, botZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + } + + if (HandleTankPositioning(boss)) + return false; + + if (AI_VALUE2(Unit*, "find target", "volatile ooze") || AI_VALUE2(Unit*, "find target", "gas cloud")) + return false; + + if (HandleUnboundPlague(boss)) + return false; + + // Only stack at boss in phase 3 (<=35% HP). Above 35% bots may stand + // anywhere; the goo flee block above handles emergencies. + if (!boss->HealthBelowPct(35)) + return false; + + return HandleBossPositioning(boss); +} + +bool IccPutricideAvoidMalleableGooAction::HandleTankPositioning(Unit* boss) +{ + if (!botAI->IsTank(bot)) + return false; + + if (boss && boss->IsAlive() && + !AI_VALUE2(Unit*, "find target", "volatile ooze") && !AI_VALUE2(Unit*, "find target", "gas cloud")) + { + if (Group* group = bot->GetGroup()) + { + constexpr uint8 skullIconId = RtiTargetValue::skullIndex; + ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); + Unit* markedUnit = botAI->GetUnit(skullGuid); + if (!skullGuid || !markedUnit || !markedUnit->IsAlive()) + group->SetTargetIcon(skullIconId, bot->GetGUID(), boss->GetGUID()); + } + } + + constexpr float bombSearchRange = 100.0f; + constexpr float safeDistance = 15.0f; + + Unit* bomb = bot->FindNearestCreature(NPC_CHOKING_GAS_BOMB, bombSearchRange); + if (!bomb) + return false; + + float currentDistance = bot->GetDistance2d(bomb); + + if (currentDistance < safeDistance) + return MoveAway(bomb, safeDistance - currentDistance); + + return false; +} + +bool IccPutricideAvoidMalleableGooAction::HandleUnboundPlague(Unit* boss) +{ + if (boss && boss->HealthBelowPct(35)) + return false; + + if (!botAI->HasAura("Unbound Plague", bot)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr float unboundPlagueDistance = 20.0f; + constexpr float unboundPlagueBuffer = 2.0f; + float closestDistance = unboundPlagueDistance; + Unit* closestPlayer = nullptr; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + float dist = bot->GetDistance2d(member); + if (dist < closestDistance) + { + closestDistance = dist; + closestPlayer = member; + } + } + + if (!closestPlayer || closestDistance >= unboundPlagueDistance) + { + bot->Kill(bot, bot); + return true; + } + + float dx = bot->GetPositionX() - closestPlayer->GetPositionX(); + float dy = bot->GetPositionY() - closestPlayer->GetPositionY(); + float dist = std::sqrt(dx * dx + dy * dy); + + if (dist <= 0.0f) + return false; + + dx /= dist; + dy /= dist; + float moveDistance = unboundPlagueDistance - closestDistance + unboundPlagueBuffer; + + float moveX = bot->GetPositionX() + dx * moveDistance; + float moveY = bot->GetPositionY() + dy * moveDistance; + + if (bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + { + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool IccPutricideAvoidMalleableGooAction::HandleBossPositioning(Unit* boss) +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!mainTank || !mainTank->IsAlive()) + return false; + + constexpr float stackTolerance = 2.0f; + constexpr float hunterMaxDistance = 12.0f; + constexpr float frontalConeHalfAngle = float(M_PI) / 3.0f; // 60 degrees -> 120 deg cone + + float distToTank = bot->GetExactDist2d(mainTank); + + if (bot->getClass() == CLASS_HUNTER) + { + // Hunter slack: stay up to 12f from main tank, but only if standing in + // the boss's frontal cone. Outside the cone or beyond 12f -> move to MT. + float bossOrient = boss->GetOrientation(); + float toBotX = bot->GetPositionX() - boss->GetPositionX(); + float toBotY = bot->GetPositionY() - boss->GetPositionY(); + float toBotLen = std::sqrt(toBotX * toBotX + toBotY * toBotY); + bool inFrontCone = false; + if (toBotLen > 0.01f) + { + float bearing = std::atan2(toBotY, toBotX); + float delta = bearing - bossOrient; + while (delta > float(M_PI)) delta -= 2.0f * float(M_PI); + while (delta < -float(M_PI)) delta += 2.0f * float(M_PI); + inFrontCone = std::fabs(delta) <= frontalConeHalfAngle; + } + + if (distToTank <= hunterMaxDistance && inFrontCone) + return false; + } + else if (distToTank <= stackTolerance) + { + return false; + } + + bot->SetFacingToObject(boss); + return MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), + false, false, false, botAI->IsRanged(bot), MovementPriority::MOVEMENT_COMBAT); +} + +bool IccPutricideAvoidMalleableGooAction::HasObstacleBetween(Position const& from, Position const& to) +{ + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + if (unit->GetEntry() == NPC_GROWING_OOZE_PUDDLE || unit->GetEntry() == NPC_CHOKING_GAS_BOMB) + { + if (IsOnPath(from, to, unit->GetPosition(), 3.0f)) + return true; + } + } + return false; +} + +bool IccPutricideAvoidMalleableGooAction::IsOnPath(Position const& from, Position const& to, Position const& point, + float threshold) +{ + float pathX = to.GetPositionX() - from.GetPositionX(); + float pathY = to.GetPositionY() - from.GetPositionY(); + float pathLen = std::sqrt(pathX * pathX + pathY * pathY); + + if (pathLen < 0.1f) + return false; + + float normX = pathX / pathLen; + float normY = pathY / pathLen; + + float toPointX = point.GetPositionX() - from.GetPositionX(); + float toPointY = point.GetPositionY() - from.GetPositionY(); + float proj = toPointX * normX + toPointY * normY; + + if (proj < 0.0f || proj > pathLen) + return false; + + float closestX = from.GetPositionX() + normX * proj; + float closestY = from.GetPositionY() + normY * proj; + float distToPath = std::sqrt((point.GetPositionX() - closestX) * (point.GetPositionX() - closestX) + + (point.GetPositionY() - closestY) * (point.GetPositionY() - closestY)); + + return distToPath < threshold; +} + +bool IccPutricideAbominationAction::IsSomeoneAlreadyPiloting() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* m = itr->GetSource(); + if (!m || m == bot || !m->IsAlive()) + continue; + if (Unit* vb = m->GetVehicleBase()) + { + uint32 e = vb->GetEntry(); + if (e == NPC_MUTATED_ABOMINATION_10 || e == NPC_MUTATED_ABOMINATION_25) + return true; + } + } + return false; +} + +Unit* IccPutricideAbominationAction::FindClosestPuddle(float maxRange) +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + Unit* best = nullptr; + float bestDist = maxRange; + for (auto const& g : npcs) + { + Unit* u = botAI->GetUnit(g); + if (!u || !u->IsAlive() || u->GetEntry() != NPC_GROWING_OOZE_PUDDLE) + continue; + float d = bot->GetExactDist2d(u); + if (d < bestDist) + { + bestDist = d; + best = u; + } + } + return best; +} + +bool IccPutricideAbominationAction::BecomeAbomination() +{ + GameObject* go = bot->FindNearestGameObject(GO_PUTRICIDE_DRINK_ME, 100.0f); + if (!go || !go->isSpawned()) + return false; + + float dist = bot->GetDistance(go); + if (dist > INTERACTION_DISTANCE) + { + return MoveTo(bot->GetMapId(), go->GetPositionX(), go->GetPositionY(), go->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + bot->SetFacingToObject(go); + bool hasAura = botAI->HasAura("Mutated Transformation", bot); + if (!hasAura) + { + go->Use(bot); + return true; + } + return false; +} + +Unit* IccPutricideAbominationAction::PickSlashTarget(Unit* boss) +{ + Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + if (volatileOoze && volatileOoze->IsAlive()) + return volatileOoze; + + Unit* gasCloud = AI_VALUE2(Unit*, "find target", "gas cloud"); + if (gasCloud && gasCloud->IsAlive()) + return gasCloud; + + return boss; +} + +bool IccPutricideAbominationAction::TryRegurgitate(Unit* abo, Unit* target) +{ + if (!abo || !target || !target->IsAlive()) + return false; + + if (abo->GetExactDist2d(target) > 50.0f) + return false; + + if (botAI->HasAura("Regurgitated Ooze", target)) + return false; + + uint32 spellId = AI_VALUE2(uint32, "vehicle spell id", "Regurgitated Ooze"); + if (!spellId) + return false; + + if (abo->HasSpellCooldown(spellId)) + return false; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return false; + + if (!abo->HasInArc(CAST_ANGLE_IN_FRONT, target, 100.0f)) + { + abo->SetFacingToObject(target); + return false; + } + + Spell* spell = new Spell(abo, spellInfo, TRIGGERED_IGNORE_POWER_AND_REAGENT_COST); + SpellCastTargets targets; + targets.SetUnitTarget(target); + spell->prepare(&targets); + + abo->AddSpellCooldown(spellId, 0, 1000); + return true; +} + +bool IccPutricideAbominationAction::TryEatOoze(Unit* abo, Unit* puddle) +{ + if (!abo || !puddle || !puddle->IsAlive()) + return false; + + constexpr float eatRange = 4.0f; + if (abo->GetExactDist2d(puddle) > eatRange) + { + return MoveTo(bot->GetMapId(), puddle->GetPositionX(), puddle->GetPositionY(), puddle->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + // In range of puddle — hold position until it dies, even on cooldown + uint32 spellId = AI_VALUE2(uint32, "vehicle spell id", "Eat Ooze"); + if (!spellId) + return true; + + if (abo->HasSpellCooldown(spellId)) + return true; + + if (botAI->CanCastVehicleSpell(spellId, puddle) && botAI->CastVehicleSpell(spellId, puddle)) + abo->AddSpellCooldown(spellId, 0, 1000); + + return true; +} + +bool IccPutricideAbominationAction::Execute(Event /*event*/) +{ + if (!botAI->IsAssistTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + Unit* abo = bot->GetVehicleBase(); + bool piloting = + abo && (abo->GetEntry() == NPC_MUTATED_ABOMINATION_10 || abo->GetEntry() == NPC_MUTATED_ABOMINATION_25); + + if (!piloting) + { + if (boss->HealthBelowPct(35)) + return false; + if (IsSomeoneAlreadyPiloting()) + return false; + Unit* nearestPuddle = FindClosestPuddle(50.0f); + if (!nearestPuddle) + return false; + return BecomeAbomination(); + } + + Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); + Unit* gasCloud = AI_VALUE2(Unit*, "find target", "gas cloud"); + Unit* puddle = FindClosestPuddle(100.0f); + + // Priority 1: Regurgitated Ooze on volatile ooze (aura-gated, energy bypassed) + if (volatileOoze && volatileOoze->IsAlive() && + !botAI->HasAura("Regurgitated Ooze", volatileOoze)) + { + if (TryRegurgitate(abo, volatileOoze)) + return true; + } + + // Priority 2: Regurgitated Ooze on gas cloud (aura-gated, energy bypassed) + if (gasCloud && gasCloud->IsAlive() && + !botAI->HasAura("Regurgitated Ooze", gasCloud)) + { + if (TryRegurgitate(abo, gasCloud)) + return true; + } + + // Priority 3: actively seek and eat puddle + if (puddle) + { + if (TryEatOoze(abo, puddle)) + return true; + } + + // Priority 4: slash target volatile > gas > boss + Unit* slashTarget = PickSlashTarget(boss); + if (slashTarget) + { + if (abo->GetExactDist2d(slashTarget) > 5.0f) + { + return MoveTo(bot->GetMapId(), slashTarget->GetPositionX(), slashTarget->GetPositionY(), + slashTarget->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + if (abo) + { + uint32 msSpellId = AI_VALUE2(uint32, "vehicle spell id", "Mutated Slash"); + if (msSpellId && !abo->HasSpellCooldown(msSpellId) && botAI->CanCastVehicleSpell(msSpellId, slashTarget) && + botAI->CastVehicleSpell(msSpellId, slashTarget)) + { + abo->AddSpellCooldown(msSpellId, 0, 1000); + return true; + } + } + + if (bot->GetTarget() != slashTarget->GetGUID()) + bot->SetTarget(slashTarget->GetGUID()); + Attack(slashTarget); + } + + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_RF.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_RF.cpp new file mode 100644 index 000000000..8b9632866 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_RF.cpp @@ -0,0 +1,1001 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include +#include +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCScripts.h" +#include "ICCTriggers.h" +#include "RtiValue.h" +#include "Timer.h" +#include "Vehicle.h" + +// Rotface +bool IccRotfaceTankPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss) + return false; + + Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); + + MarkBossWithSkull(boss); + + if (botAI->IsMainTank(bot)) + { + bool assistTankAlive = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member != bot && botAI->IsAssistTank(member)) + { + assistTankAlive = true; + break; + } + } + } + + Unit* bigOoze = bot->FindNearestCreature(NPC_BIG_OOZE, 100.0f); + bool const bigOozeHandled = bigOoze && bigOoze->IsAlive() && bigOoze->GetVictim() && + bigOoze->GetVictim()->IsPlayer() && + botAI->IsAssistTank(bigOoze->GetVictim()->ToPlayer()); + + if (bigOoze && bigOoze->IsAlive() && !assistTankAlive && !bigOozeHandled) + return HandleAssistTankPositioning(boss); + + return PositionMainTankAndMelee(boss, smallOoze); + } + + if (botAI->IsAssistTank(bot)) + return HandleAssistTankPositioning(boss); + + return false; +} + +bool IccRotfaceTankPositionAction::MarkBossWithSkull(Unit* boss) +{ + IccEnsureIconOn(bot, botAI, RtiTargetValue::skullIndex, boss); + return false; +} + +bool IccRotfaceTankPositionAction::PositionMainTankAndMelee(Unit* boss, Unit*) +{ + bool isBossCasting = false; + if (boss && boss->HasUnitState(UNIT_STATE_CASTING)) + isBossCasting = true; + + if (botAI->IsMainTank(bot) && boss && boss->GetVictim() == bot) + { + bool const bossInPosition = boss->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION_BOSS) <= 4.0f; + + if (!bossInPosition) + { + // Step 2y in the direction from boss toward center — tank leads, boss follows + float dirX = ICC_ROTFACE_CENTER_POSITION_BOSS.GetPositionX() - boss->GetPositionX(); + float dirY = ICC_ROTFACE_CENTER_POSITION_BOSS.GetPositionY() - boss->GetPositionY(); + float const len = std::sqrt(dirX * dirX + dirY * dirY); + if (len > 0.5f) + { + dirX /= len; + dirY /= len; + } + float const destX = bot->GetPositionX() + dirX * 2.0f; + float const destY = bot->GetPositionY() + dirY * 2.0f; + MoveTo(bot->GetMapId(), destX, destY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + else if (bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) > 7.0f) + { + MoveTo(bot->GetMapId(), ICC_ROTFACE_CENTER_POSITION.GetPositionX(), + ICC_ROTFACE_CENTER_POSITION.GetPositionY(), ICC_ROTFACE_CENTER_POSITION.GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_COMBAT); + } + else + { + if (bot->GetVictim() != boss) + bot->Attack(boss, true); + } + } + + if (boss && isBossCasting && !botAI->IsTank(bot)) + { + float const x = boss->GetPositionX(); + float const y = boss->GetPositionY(); + float const z = boss->GetPositionZ(); + + if (bot->GetExactDist2d(x, y) > 0.5f) + MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, + false); + } + + return false; +} + +bool IccRotfaceTankPositionAction::HandleAssistTankPositioning(Unit*) +{ + GuidVector bigOozes = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector activeBigOozes; + + for (auto const& guid : bigOozes) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_BIG_OOZE && unit->IsVisible()) + activeBigOozes.push_back(unit); + } + + if (activeBigOozes.empty()) + return false; + + Unit* uncollectedOoze = nullptr; + float minUncollectedDist = FLT_MAX; + for (Unit* ooze : activeBigOozes) + { + if (ooze->GetVictim() == bot) + continue; + + if (!bot->HasAura(SPELL_SPITEFULL_FURY)) + bot->AddAura(SPELL_SPITEFULL_FURY, bot); + IccCastClassTaunt(bot, botAI, ooze); + + float const dist = bot->GetExactDist2d(ooze); + if (dist < minUncollectedDist) + { + minUncollectedDist = dist; + uncollectedOoze = ooze; + } + } + + if (uncollectedOoze) + { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + return MoveTo(bot->GetMapId(), uncollectedOoze->GetPositionX(), uncollectedOoze->GetPositionY(), + uncollectedOoze->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + + Unit* targetOoze = nullptr; + float minDist = FLT_MAX; + for (Unit* ooze : activeBigOozes) + { + float const dist = bot->GetExactDist2d(ooze); + if (dist < minDist) + { + minDist = dist; + targetOoze = ooze; + } + } + + if (!targetOoze) + return false; + + return HandleBigOozeKiting(targetOoze); +} + +Unit* IccRotfaceTankPositionAction::FindAssignedBigOoze(Unit* /*boss*/, std::vector& bigOozes) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Unit* bestOoze = nullptr; + float minDistance = FLT_MAX; + + for (Unit* ooze : bigOozes) + { + if (!ooze || !ooze->IsAlive() || !ooze->IsVisible()) + continue; + + ObjectGuid oozeGuid = ooze->GetGUID(); + + bool isAssignedToOther = false; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsInWorld() || member == bot) + continue; + + Unit* memberTarget = botAI->GetUnit(member->GetTarget()); + if (memberTarget && memberTarget->GetGUID() == oozeGuid) + { + isAssignedToOther = true; + break; + } + } + + if (!isAssignedToOther) + { + float const dist = bot->GetExactDist2d(ooze); + if (dist < minDistance) + { + minDistance = dist; + bestOoze = ooze; + } + } + } + + return bestOoze; +} + +bool IccRotfaceTankPositionAction::HandleBigOozeKiting(Unit* bigOoze) +{ + if (bigOoze->GetVictim() != bot && bigOoze->IsAlive()) + { + if (!bot->HasAura(SPELL_SPITEFULL_FURY)) + bot->AddAura(SPELL_SPITEFULL_FURY, bot); + IccCastClassTaunt(bot, botAI, bigOoze); + } + + float const oozeDistance = bot->GetExactDist2d(bigOoze); + + if (oozeDistance > 12.0f) + { + bot->SetTarget(bigOoze->GetGUID()); + bot->SetFacingToObject(bigOoze); + if (bigOoze->GetVictim() != bot) + { + if (!bot->HasAura(SPELL_SPITEFULL_FURY)) + bot->AddAura(SPELL_SPITEFULL_FURY, bot); + IccCastClassTaunt(bot, botAI, bigOoze); + } + return false; + } + + float const minRadius = 24.0f; + float const maxRadius = 34.0f; + float const safeDistanceFromOoze = 13.0f; + + float const currentDistance = bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION); + + if (currentDistance < minRadius || currentDistance > maxRadius) + { + float dirX = bot->GetPositionX() - ICC_ROTFACE_CENTER_POSITION.GetPositionX(); + float dirY = bot->GetPositionY() - ICC_ROTFACE_CENTER_POSITION.GetPositionY(); + float length = std::sqrt(dirX * dirX + dirY * dirY); + dirX /= length; + dirY /= length; + + float const targetX = ICC_ROTFACE_CENTER_POSITION.GetPositionX() + dirX * maxRadius; + float const targetY = ICC_ROTFACE_CENTER_POSITION.GetPositionY() + dirY * maxRadius; + + if (bigOoze->GetExactDist2d(targetX, targetY) >= safeDistanceFromOoze) + return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + + float currentAngle = std::atan2(bot->GetPositionY() - ICC_ROTFACE_CENTER_POSITION.GetPositionY(), + bot->GetPositionX() - ICC_ROTFACE_CENTER_POSITION.GetPositionX()); + + for (int32 i = 0; i < 16; ++i) + { + float const angleOffset = (i % 2 == 0 ? 1 : -1) * (M_PI / 16.0f) * (i / 2.0f); + float const newAngle = currentAngle + angleOffset; + + float const newX = ICC_ROTFACE_CENTER_POSITION.GetPositionX() + maxRadius * std::cos(newAngle); + float const newY = ICC_ROTFACE_CENTER_POSITION.GetPositionY() + maxRadius * std::sin(newAngle); + + if (bigOoze->GetExactDist2d(newX, newY) >= safeDistanceFromOoze) + { + GuidVector puddles = AI_VALUE(GuidVector, "nearest hostile npcs"); + bool isSafeFromPuddles = true; + + for (auto const& puddleGuid : puddles) + { + Unit* puddle = botAI->GetUnit(puddleGuid); + if (puddle && botAI->GetAura("Ooze Flood", puddle)) + { + float const puddleDistance = puddle->GetDistance2d(newX, newY); + if (puddleDistance < 30.0f) + { + isSafeFromPuddles = false; + break; + } + } + } + + if (isSafeFromPuddles) + { + MoveTo(bot->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + return true; + } + } + } + + return false; +} + +bool IccRotfaceGroupPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss) + return false; + + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + bool const hasOozeFlood = botAI->HasAura("Ooze Flood", bot); + Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); + + if (!botAI->IsTank(bot) && HandlePuddleAvoidance(boss)) + return true; + + if (HandleOozeTargeting()) + return true; + + if (!(smallOoze && smallOoze->GetVictim() == bot) && !hasOozeFlood && PositionRangedAndHealers(boss, smallOoze)) + return true; + + return false; +} + +bool IccRotfaceGroupPositionAction::HandlePuddleAvoidance(Unit* boss) +{ + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !botAI->HasAura("Ooze Flood", unit)) + continue; + + float const puddleDistance = bot->GetExactDist2d(unit); + float const bossDistance = bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION); + + if (bossDistance < 15.0f) + return false; + + if (puddleDistance < 30.0f) + return MoveAwayFromPuddle(boss, unit, puddleDistance); + } + + return false; +} + +bool IccRotfaceGroupPositionAction::MoveAwayFromPuddle(Unit* boss, Unit* puddle, float) +{ + if (!boss || !puddle) + return false; + + float const dx = puddle->GetPositionX() - bot->GetPositionX(); + float const dy = puddle->GetPositionY() - bot->GetPositionY(); + float const angle = std::atan2(dy, dx); + + float const stepSize = 7.0f; + float const minPuddleDistance = 30.0f; + float const minCenterDistance = 15.0f; + float const maxCenterDistance = 25.0f; + int32 const directions = 8; + + for (int32 i = 0; i < directions; ++i) + { + float const testAngle = angle + M_PI + (i * M_PI / 4); + float const moveX = bot->GetPositionX() + stepSize * std::cos(testAngle); + float const moveY = bot->GetPositionY() + stepSize * std::sin(testAngle); + float const moveZ = bot->GetPositionZ(); + + float const newPuddleDistance = puddle->GetDistance2d(moveX, moveY); + float const newCenterDistance = std::sqrt(std::pow(moveX - ICC_ROTFACE_CENTER_POSITION.GetPositionX(), 2) + + std::pow(moveY - ICC_ROTFACE_CENTER_POSITION.GetPositionY(), 2)); + + if (newPuddleDistance >= minPuddleDistance && newCenterDistance >= minCenterDistance && + newCenterDistance <= maxCenterDistance && bot->IsWithinLOS(moveX, moveY, moveZ)) + { + return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + } + + return false; +} + +bool IccRotfaceGroupPositionAction::HandleOozeTargeting() +{ + if (botAI->IsMainTank(bot)) + return false; + + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + Player* mainTankWithOoze = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsMainTank(member)) + continue; + + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SMALL_OOZE && unit->GetVictim() == member) + { + mainTankWithOoze = member; + break; + } + } + break; + } + } + + Aura* infectionAura = botAI->GetAura("Mutated Infection", bot, false, false); + + // Find assist tank currently kiting a Big Ooze — preferred merge point + Player* assistTankKiting = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsAssistTank(member)) + continue; + + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_BIG_OOZE && unit->GetVictim() == member) + { + assistTankKiting = member; + break; + } + } + if (assistTankKiting) + break; + } + } + + bool const hasSmallOoze = [&]() -> bool { + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SMALL_OOZE && unit->GetVictim() == bot) + return true; + } + return false; + }(); + + bool const needsMerge = hasSmallOoze || (infectionAura && infectionAura->GetDuration() <= 4000); + + if (needsMerge) + { + if (assistTankKiting && bot->GetExactDist2d(assistTankKiting) > 2.0f) + return MoveTo(bot->GetMapId(), assistTankKiting->GetPositionX(), assistTankKiting->GetPositionY(), + assistTankKiting->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + + if (!assistTankKiting) + { + if (mainTankWithOoze && bot->GetExactDist2d(mainTankWithOoze) > 2.0f) + return MoveTo(bot->GetMapId(), mainTankWithOoze->GetPositionX(), mainTankWithOoze->GetPositionY(), + mainTankWithOoze->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + + return HandleOozeMemberPositioning(nullptr); + } + } + + return false; +} + +bool IccRotfaceGroupPositionAction::HandleOozeMemberPositioning(Unit* /*mySmallOoze*/) +{ + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector bigOozes; + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->IsVisible() && unit->GetEntry() == NPC_BIG_OOZE) + bigOozes.push_back(unit); + } + + if (!bigOozes.empty()) + { + Unit* target = nullptr; + float minDist = FLT_MAX; + + for (Unit* ooze : bigOozes) + { + Unit* victim = ooze->GetVictim(); + if (victim && victim->IsPlayer() && botAI->IsAssistTank(victim->ToPlayer())) + { + float const dist = bot->GetExactDist2d(ooze); + if (dist < minDist) + { + minDist = dist; + target = ooze; + } + } + } + + if (!target) + { + for (Unit* ooze : bigOozes) + { + float const dist = bot->GetExactDist2d(ooze); + if (dist < minDist) + { + minDist = dist; + target = ooze; + } + } + } + + if (target && bot->GetExactDist2d(target) > 2.0f && !botAI->IsAssistTank(bot)) + return MoveTo(bot->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + + return false; + } + + if (bot->GetExactDist2d(ICC_ROTFACE_BIG_OOZE_POSITION) > 5.0f) + return MoveTo(bot->GetMapId(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionX(), + ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionY(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionZ(), false, + false, false, true, MovementPriority::MOVEMENT_FORCED); + + return false; +} + +bool IccRotfaceGroupPositionAction::PositionRangedAndHealers(Unit* boss, Unit* smallOoze) +{ + if (!(botAI->IsRanged(bot) || botAI->IsHeal(bot))) + return false; + + if (smallOoze && smallOoze->GetVictim() == bot) + return false; + + Difficulty const diff = bot->GetRaidDifficulty(); + bool isBossCasting = false; + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_SLIME_SPRAY)) + isBossCasting = true; + + bool const isHeroic = (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC); + + if (boss && isBossCasting && !isHeroic) + { + float const x = boss->GetPositionX(); + float const y = boss->GetPositionY(); + float const z = boss->GetPositionZ(); + + if (bot->GetExactDist2d(x, y) > 0.5f) + { + MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, + false); + } + return false; + } + + if (!isHeroic && !isBossCasting && boss && bot->getClass() != CLASS_HUNTER && + (bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) < 2.0f || + bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) > 5.0f)) + { + float const angle = + std::atan2(bot->GetPositionY() - boss->GetPositionY(), bot->GetPositionX() - boss->GetPositionX()); + float const destX = boss->GetPositionX() + 3.5f * std::cos(angle); + float const destY = boss->GetPositionY() + 3.5f * std::sin(angle); + return MoveTo(bot->GetMapId(), destX, destY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + } + + if (!isHeroic) + return false; + + return PositionHeroicGrid(boss); +} + +bool IccRotfaceGroupPositionAction::PositionHeroicGrid(Unit* boss) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + static Position const rangedSpots[] = { + ICC_ROTFACE_RANGED_POSITION_HC_1, ICC_ROTFACE_RANGED_POSITION_HC_2, ICC_ROTFACE_RANGED_POSITION_HC_3, + ICC_ROTFACE_RANGED_POSITION_HC_4, ICC_ROTFACE_RANGED_POSITION_HC_5, ICC_ROTFACE_RANGED_POSITION_HC_6, + ICC_ROTFACE_RANGED_POSITION_HC_7, ICC_ROTFACE_RANGED_POSITION_HC_8, ICC_ROTFACE_RANGED_POSITION_HC_9, + ICC_ROTFACE_RANGED_POSITION_HC_10, ICC_ROTFACE_RANGED_POSITION_HC_11, ICC_ROTFACE_RANGED_POSITION_HC_12, + ICC_ROTFACE_RANGED_POSITION_HC_13, ICC_ROTFACE_RANGED_POSITION_HC_14, ICC_ROTFACE_RANGED_POSITION_HC_15, + ICC_ROTFACE_RANGED_POSITION_HC_16, ICC_ROTFACE_RANGED_POSITION_HC_17, ICC_ROTFACE_RANGED_POSITION_HC_18, + ICC_ROTFACE_RANGED_POSITION_HC_19, ICC_ROTFACE_RANGED_POSITION_HC_20, ICC_ROTFACE_RANGED_POSITION_HC_21, + ICC_ROTFACE_RANGED_POSITION_HC_22, + }; + constexpr int32 totalSpots = 22; + + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector floodPuddles; + for (auto const& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (unit && botAI->HasAura("Ooze Flood", unit)) + floodPuddles.push_back(unit); + } + + bool spotFlooded[totalSpots] = {}; + for (int32 i = 0; i < totalSpots; ++i) + { + for (Unit* puddle : floodPuddles) + { + if (puddle->GetExactDist2d(rangedSpots[i].GetPositionX(), rangedSpots[i].GetPositionY()) < 30.0f) + { + spotFlooded[i] = true; + break; + } + } + } + + std::vector hunterGuids; + std::vector rangedGuids; + std::vector healerGuids; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || botAI->IsTank(member)) + continue; + + if (!GET_PLAYERBOT_AI(member)) + continue; + + ObjectGuid const guid = member->GetGUID(); + if (botAI->IsHeal(member)) + healerGuids.push_back(guid); + else if (botAI->IsRanged(member)) + { + if (member->getClass() == CLASS_HUNTER) + hunterGuids.push_back(guid); + else + rangedGuids.push_back(guid); + } + } + std::sort(hunterGuids.begin(), hunterGuids.end()); + std::sort(rangedGuids.begin(), rangedGuids.end()); + std::sort(healerGuids.begin(), healerGuids.end()); + + std::vector ordered; + for (auto const& g : hunterGuids) + ordered.push_back(g); + for (auto const& g : rangedGuids) + ordered.push_back(g); + for (auto const& g : healerGuids) + ordered.push_back(g); + + auto it = std::find(ordered.begin(), ordered.end(), bot->GetGUID()); + if (it == ordered.end()) + return false; + + int32 const botRank = static_cast(std::distance(ordered.begin(), it)); + int32 const totalBots = static_cast(ordered.size()); + + bool const hasHomeSpot = (botRank < totalSpots); + bool const homeSpotSafe = hasHomeSpot && !spotFlooded[botRank]; + + auto moveTowardSpot = [&](Position const& spot) -> bool + { + float const dx = spot.GetPositionX() - bot->GetPositionX(); + float const dy = spot.GetPositionY() - bot->GetPositionY(); + float const dist = std::sqrt(dx * dx + dy * dy); + if (dist <= 1.0f) + return false; + constexpr float moveStep = 7.0f; + float const step = std::min(moveStep, dist); + float const targetX = bot->GetPositionX() + (dx / dist) * step; + float const targetY = bot->GetPositionY() + (dy / dist) * step; + + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + return MoveTo(bot->GetMapId(), targetX, targetY, spot.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + }; + + if (homeSpotSafe) + return moveTowardSpot(rangedSpots[botRank]); + + std::vector tempSpots; + for (int32 i = 0; i < totalSpots; ++i) + { + if (i >= totalBots && !spotFlooded[i]) + tempSpots.push_back(i); + } + + if (hasHomeSpot) + { + int32 displacedRank = 0; + for (int32 rank = 0; rank < totalSpots && rank < totalBots; ++rank) + { + if (rank == botRank) + break; + if (spotFlooded[rank]) + displacedRank++; + } + + if (displacedRank < static_cast(tempSpots.size())) + return moveTowardSpot(rangedSpots[tempSpots[displacedRank]]); + } + + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && !botAI->IsTank(bot)) + { + float const bx = boss->GetPositionX(); + float const by = boss->GetPositionY(); + float const bz = boss->GetPositionZ(); + if (bot->GetExactDist2d(bx, by) > 0.5f) + return MoveTo(bot->GetMapId(), bx, by, bz, false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); + return false; + } + + if (Group* grp = bot->GetGroup()) + { + for (GroupReference* itr = grp->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + if (botAI->IsMainTank(member)) + { + if (bot->getClass() == CLASS_HUNTER) + { + float const distToTank = bot->GetExactDist2d(member->GetPositionX(), member->GetPositionY()); + if (distToTank <= 12.0f) + return false; + + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + return MoveTo(bot->GetMapId(), member->GetPositionX(), member->GetPositionY(), + member->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + + Position const mainTankPos(member->GetPositionX(), member->GetPositionY(), member->GetPositionZ()); + return moveTowardSpot(mainTankPos); + } + } + } + return false; +} + +bool IccRotfaceMoveAwayFromExplosionAction::Execute(Event /*event*/) +{ + if (botAI->IsTank(bot)) + return false; + + Creature* bigOoze = bot->FindNearestCreature(NPC_BIG_OOZE, 100.0f); + bool const castingNow = bigOoze && bigOoze->IsAlive() && bigOoze->HasUnitState(UNIT_STATE_CASTING) && + bigOoze->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION); + + uint32 const now = getMSTime(); + bool const stillHolding = _hasEscape && _holdUntil != 0 && now < _holdUntil; + + if (!castingNow && !stillHolding) + { + _hasEscape = false; + _holdUntil = 0; + return false; + } + + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + Position const anchor = ICC_ROTFACE_CENTER_POSITION_BOSS; + + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector puddles; + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && botAI->HasAura("Ooze Flood", unit)) + puddles.push_back(unit); + } + + auto isSlotSafe = [&](float x, float y) -> bool + { + for (Unit* puddle : puddles) + { + if (puddle->GetDistance2d(x, y) < 30.0f) + return false; + } + return true; + }; + + constexpr uint32 TOTAL_SLOTS = 25; + constexpr float ESCAPE_RADIUS = 40.0f; + + auto slotPos = [&](uint32 i) -> std::pair + { + float const angle = static_cast(i) * (2.0f * static_cast(M_PI) / static_cast(TOTAL_SLOTS)); + return {anchor.GetPositionX() + ESCAPE_RADIUS * std::cos(angle), + anchor.GetPositionY() + ESCAPE_RADIUS * std::sin(angle)}; + }; + + uint32 const instanceId = bot->GetMap()->GetInstanceId(); + auto& sExplosionSlotMemory = IcecrownHelpers::IccState(instanceId).rfExplosionSlotMemory; + auto const myKey = bot->GetGUID(); + + // count how many OTHER bots in this instance occupy each slot + std::array otherCount{}; + for (auto const& [key, slot] : sExplosionSlotMemory) + { + if (key != bot->GetGUID()) + ++otherCount[slot]; + } + + auto assignSlot = [&]() -> int32 + { + int32 bestSlot = -1; + float bestDist = FLT_MAX; + for (uint32 i = 0; i < TOTAL_SLOTS; ++i) + { + if (otherCount[i] >= 2) + continue; + auto [x, y] = slotPos(i); + if (!isSlotSafe(x, y)) + continue; + float const d = bot->GetExactDist2d(x, y); + if (d < bestDist) + { + bestDist = d; + bestSlot = static_cast(i); + } + } + return bestSlot; + }; + + auto currentSlotIt = sExplosionSlotMemory.find(myKey); + bool needsAssignment = true; + + if (currentSlotIt != sExplosionSlotMemory.end()) + { + int32 const mySlot = currentSlotIt->second; + auto [x, y] = slotPos(mySlot); + if (otherCount[mySlot] < 2 && isSlotSafe(x, y)) + needsAssignment = false; + } + + if (needsAssignment) + { + int32 const slot = assignSlot(); + if (slot >= 0) + sExplosionSlotMemory[myKey] = slot; + else + sExplosionSlotMemory.erase(myKey); + } + + auto finalSlotIt = sExplosionSlotMemory.find(myKey); + + Position escapePos; + if (finalSlotIt != sExplosionSlotMemory.end()) + { + auto [x, y] = slotPos(finalSlotIt->second); + escapePos = Position(x, y, anchor.GetPositionZ()); + } + else + { + escapePos = anchor; + } + + _escapePosition = escapePos; + _hasEscape = true; + + if (castingNow) + { + _holdUntil = now + 2000; + if (bot->GetExactDist2d(_escapePosition) > 2.0f) + return MoveTo(bot->GetMapId(), _escapePosition.GetPositionX(), _escapePosition.GetPositionY(), + _escapePosition.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED); + return false; + } + + // Cast finished but still inside 2s hold window: stay at escape spot. + if (bot->GetExactDist2d(_escapePosition) <= 2.0f) + return true; + + return false; +} + +bool IccRotfaceAvoidVileGasAction::Execute(Event /*event*/) +{ + uint32 const now = getMSTime(); + + IcecrownHelpers::IccInstanceState& st = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()); + bool const isVictim = + st.rotfaceVileGas.victimGuid == bot->GetGUID() && + getMSTimeDiff(st.rotfaceVileGas.castTime, now) < 8000; + bool const hasAura = botAI->HasAura("Vile Gas", bot); + + auto& waitMap = st.rotfaceVileGasWaitUntil; + auto waitIt = waitMap.find(bot->GetGUID()); + bool const inWait = waitIt != waitMap.end() && now < waitIt->second; + + if (!isVictim && !hasAura && !inWait) + { + if (waitIt != waitMap.end()) + waitMap.erase(waitIt); + _hasSafeSpot = false; + return false; + } + + Position const anchor = ICC_ROTFACE_CENTER_POSITION_BOSS; + + Creature* bigOoze = bot->FindNearestCreature(NPC_BIG_OOZE, 100.0f); + + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector puddles; + for (auto const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && botAI->HasAura("Ooze Flood", unit)) + puddles.push_back(unit); + } + + auto isSafe = [&](float x, float y, float z) -> bool + { + if (bigOoze && bigOoze->IsAlive() && bigOoze->GetDistance2d(x, y) < 20.0f) + return false; + + for (Unit* puddle : puddles) + { + if (puddle->GetDistance2d(x, y) < 30.0f) + return false; + } + + if (!bot->IsWithinLOS(x, y, z)) + return false; + + return true; + }; + + if (!_hasSafeSpot) + { + float const baseAngle = std::atan2(bot->GetPositionY() - anchor.GetPositionY(), + bot->GetPositionX() - anchor.GetPositionX()); + + constexpr float escapeDistance = 40.0f; + constexpr int32 sweepSteps = 24; + constexpr float stepAngle = 2.0f * static_cast(M_PI) / sweepSteps; + + float chosenAngle = baseAngle; + for (int32 i = 0; i < sweepSteps; ++i) + { + int32 const sign = (i % 2 == 0) ? 1 : -1; + int32 const magnitude = (i + 1) / 2; + float const angle = baseAngle + sign * magnitude * stepAngle; + float const x = anchor.GetPositionX() + escapeDistance * std::cos(angle); + float const y = anchor.GetPositionY() + escapeDistance * std::sin(angle); + float const z = anchor.GetPositionZ(); + if (isSafe(x, y, z)) + { + chosenAngle = angle; + break; + } + } + + _safeSpot = Position(anchor.GetPositionX() + escapeDistance * std::cos(chosenAngle), + anchor.GetPositionY() + escapeDistance * std::sin(chosenAngle), + anchor.GetPositionZ()); + _hasSafeSpot = true; + } + + if (bot->GetExactDist2d(_safeSpot) > 2.0f) + { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + return MoveTo(bot->GetMapId(), _safeSpot.GetPositionX(), _safeSpot.GetPositionY(), + _safeSpot.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED); + } + + if (!inWait) + waitMap[bot->GetGUID()] = now + 4000; + + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SG.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SG.cpp new file mode 100644 index 000000000..1c68fbb9e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SG.cpp @@ -0,0 +1,1585 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCActions.h" +#include +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "Vehicle.h" +#include "RtiValue.h" +#include "GenericSpellActions.h" +#include "GenericActions.h" +#include "ICCTriggers.h" +#include "Multiplier.h" + +namespace +{ +bool SgMajorityLostMysticBuffet(Player* bot, PlayerbotAI* botAI) +{ + Group* group = bot->GetGroup(); + if (!group) + return true; + + uint32 total = 0; + uint32 without = 0; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + ++total; + if (!botAI->GetAura("mystic buffet", member)) + ++without; + } + + if (total == 0) + return true; + + return static_cast(without) / static_cast(total) >= 0.6f; +} + +// Safe stand spot for melee and healers: 15y from the boss toward the +// melee/ranged zone, clear of the tail (too close) and of the beacon drop +// (to the north). Tracks the live boss so her drift never pulls the raid +// into tail range the way a fixed point does. +Position SgSafeMeleeSpot(Unit const* boss) +{ + float const midX = + (ICC_SINDRAGOSA_MELEE_POSITION.GetPositionX() + ICC_SINDRAGOSA_RANGED_POSITION.GetPositionX()) * 0.5f; + float const midY = + (ICC_SINDRAGOSA_MELEE_POSITION.GetPositionY() + ICC_SINDRAGOSA_RANGED_POSITION.GetPositionY()) * 0.5f; + + float const dx = midX - boss->GetPositionX(); + float const dy = midY - boss->GetPositionY(); + float const len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) + return Position(midX, midY, ICC_SINDRAGOSA_MELEE_POSITION.GetPositionZ()); + + float const scale = 15.0f / len; + return Position(boss->GetPositionX() + dx * scale, boss->GetPositionY() + dy * scale, + ICC_SINDRAGOSA_MELEE_POSITION.GetPositionZ()); +} +} // namespace + +bool IccSindragosaGroupPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + // Block positioning only during the pre-combat intro fly-in. + // Once Sindragosa is in combat the air phase is handled separately, + // so ground bots must still be allowed to pre-position. + if (boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY) && !boss->IsInCombat()) + return false; + + // Both tanks hold the tank position for the whole ground fight so aggro + // flips between them cannot turn or move the boss. Air phase never gets + // here: the trigger is inactive while the boss is at the flying position. + if (botAI->IsTank(bot)) + return HandleTankPositioning(boss); + + // Everyone else: boss is not targeting this bot + if (boss->GetVictim() != bot) + return HandleNonTankPositioning(); + + return false; +} + +bool IccSindragosaGroupPositionAction::HandleTankPositioning(Unit* boss) +{ + float const distBossToCenter = boss->GetExactDist2d(ICC_SINDRAGOSA_CENTER_POSITION); + float const distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); + + // Compute how far the boss orientation deviates from east (PI/2) + float const targetOrientation = fmod(float(M_PI) / 2.0f + 2.0f * float(M_PI), 2.0f * float(M_PI)); + float const currentOrientation = fmod(boss->GetOrientation() + 2.0f * float(M_PI), 2.0f * float(M_PI)); + float orientationDiff = currentOrientation - targetOrientation; + + // Clamp difference to [-PI, PI] + if (orientationDiff > float(M_PI)) + orientationDiff -= 2.0f * float(M_PI); + else if (orientationDiff < -float(M_PI)) + orientationDiff += 2.0f * float(M_PI); + + if (botAI->IsMainTank(bot)) + { + // Taunt the boss back whenever anyone else has her + Unit* const victim = boss->GetVictim(); + if (victim && victim != bot) + IccCastClassTaunt(bot, botAI, boss); + + // Drag boss toward the arena centre when it has drifted too far + if (distBossToCenter > 16.0f && distToTankPos <= 20.0f) + { + float const dirX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() - boss->GetPositionX(); + float const dirY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() - boss->GetPositionY(); + + // Step 4 yards past centre to keep the boss moving through it + float const moveX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() + (dirX / distBossToCenter) * 8.0f; + float const moveY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() + (dirY / distBossToCenter) * 8.0f; + + return MoveTo(bot->GetMapId(), moveX, moveY, boss->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + // Arc around the boss to correct its facing toward east, but only once + // in position; further out the walk-to-tank-position step below wins. + if (distToTankPos <= 10.0f && std::abs(orientationDiff) > 0.15f) + { + float const centerX = boss->GetPositionX(); + float const centerY = boss->GetPositionY(); + float const radius = std::max(2.0f, bot->GetExactDist2d(centerX, centerY)); + + float angle = atan2(bot->GetPositionY() - centerY, bot->GetPositionX() - centerX); + + // Negative diff: step counterclockwise (north); positive: clockwise (south) + static constexpr float ARC_STEP = 0.125f; + angle += (orientationDiff < 0) ? ARC_STEP : -ARC_STEP; + + float const moveX = centerX + radius * cos(angle); + float const moveY = centerY + radius * sin(angle); + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + // Walk toward the designated tank position + if (distToTankPos > 10.0f) + { + Position const& botPos = bot->GetPosition(); + Position const& tankPos = ICC_SINDRAGOSA_TANK_POSITION; + + float const dx = tankPos.GetPositionX() - botPos.GetPositionX(); + float const dy = tankPos.GetPositionY() - botPos.GetPositionY(); + float const distance = std::hypot(dx, dy); + + // Advance one yard at a time for smooth pathing + float const scale = 1.0f / distance; + float const targetX = botPos.GetPositionX() + dx * scale; + float const targetY = botPos.GetPositionY() + dy * scale; + + return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + // Fine-tune Y-axis alignment with the tank position + float const yDiff = std::abs(bot->GetPositionY() - ICC_SINDRAGOSA_TANK_POSITION.GetPositionY()); + if (yDiff > 2.0f) + { + Position const& botPos = bot->GetPosition(); + Position const& tankPos = ICC_SINDRAGOSA_TANK_POSITION; + + float const newY = botPos.GetPositionY() + (tankPos.GetPositionY() > botPos.GetPositionY() ? 1.0f : -1.0f); + + return MoveTo(bot->GetMapId(), botPos.GetPositionX(), newY, botPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + if (!bot->HasInArc(CAST_ANGLE_IN_FRONT, boss)) + bot->SetFacingToObject(boss); + + return false; +} + +bool IccSindragosaGroupPositionAction::HandleNonTankPositioning() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Last phase is owned by IccSindragosaMysticBuffetAction (LOS2 hide, + // single skull). Multiplier branches evaluated earlier (heroic Unchained + // Magic) can leak this action through in P3, so hard-stop it here. + Unit* const p3Boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (p3Boss && p3Boss->HealthBelowPct(35)) + return false; + + // Collect all alive raid members + std::vector raidMembers; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + raidMembers.push_back(member); + } + + uint32 const totalMembers = static_cast(raidMembers.size()); + if (totalMembers == 0) + return false; + + static constexpr uint8 SKULL_ICON_INDEX = RtiTargetValue::skullIndex; + + // Priority: if a tank is ice-tombed (ground phase), mark that tomb skull + // immediately so the raid DPSes it and frees the tank. Any bot can issue + // the mark — redundant SetTargetIcon calls are idempotent. + Unit* const bossForFlyCheck = AI_VALUE2(Unit*, "find target", "sindragosa"); + bool const bossGrounded = bossForFlyCheck && + bossForFlyCheck->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), + ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) >= 30.0f; + + Player* entombedTank = nullptr; + if (bossGrounded) + { + for (Player* member : raidMembers) + { + if (botAI->IsTank(member) && member->HasAura(SPELL_ICE_TOMB)) + { + entombedTank = member; + break; + } + } + } + + std::vector const gridTombs = + IccGetCreaturesByEntries(bot, {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}, 150.0f); + + Unit* tankTomb = nullptr; + if (entombedTank) + { + float minDist = 4.0f; + for (Creature* tomb : gridTombs) + { + float const d = tomb->GetDistance(entombedTank); + if (d < minDist) + { + minDist = d; + tankTomb = tomb; + } + } + } + + if (tankTomb) + { + IccEnsureIconOn(bot, botAI, SKULL_ICON_INDEX, tankTomb); + context->GetValue("rti")->Set("skull"); + } + else + { + std::vector tombs(gridTombs.begin(), gridTombs.end()); + + if (tombs.empty()) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (boss && boss->IsAlive()) + IccEnsureIconOn(bot, botAI, SKULL_ICON_INDEX, boss); + + for (size_t i = 1; i < 3; ++i) + { + int8 const icon = ICC_BUCKET_MARKERS[i].icon; + if (!group->GetTargetIcon(icon).IsEmpty()) + group->SetTargetIcon(icon, bot->GetGUID(), ObjectGuid::Empty); + } + + context->GetValue("rti")->Set("skull"); + } + else + { + // Keep icons already sitting on live tombs (air phase marks carry + // over), fill free icons with unmarked tombs, drop dead icons. + std::array iconTomb = {nullptr, nullptr, nullptr}; + for (size_t i = 0; i < 3; ++i) + { + Unit* current = botAI->GetUnit(group->GetTargetIcon(ICC_BUCKET_MARKERS[i].icon)); + if (current && current->IsAlive() && std::find(tombs.begin(), tombs.end(), current) != tombs.end()) + iconTomb[i] = current; + } + + for (Unit* tomb : tombs) + { + if (std::find(iconTomb.begin(), iconTomb.end(), tomb) != iconTomb.end()) + continue; + for (size_t i = 0; i < 3; ++i) + { + if (!iconTomb[i]) + { + iconTomb[i] = tomb; + IccEnsureIconOn(bot, botAI, ICC_BUCKET_MARKERS[i].icon, tomb); + break; + } + } + } + + for (size_t i = 0; i < 3; ++i) + { + int8 const icon = ICC_BUCKET_MARKERS[i].icon; + if (!iconTomb[i] && !group->GetTargetIcon(icon).IsEmpty()) + group->SetTargetIcon(icon, bot->GetGUID(), ObjectGuid::Empty); + } + + // Each DPS keeps its air phase group mark while that tomb lives, + // then falls to the first live mark (skull first). + std::string rtiValue = "skull"; + if (!botAI->IsHeal(bot)) + { + auto const& assignments = IcecrownHelpers::IccState(bot->GetInstanceId()).sgGroupAssignments; + auto const it = assignments.find(bot->GetGUID()); + size_t const preferred = + (it != assignments.end() && it->second >= 0 && it->second < 3) ? static_cast(it->second) : 0; + + if (iconTomb[preferred]) + rtiValue = ICC_BUCKET_MARKERS[preferred].rti; + else + { + for (size_t i = 0; i < 3; ++i) + { + if (iconTomb[i]) + { + rtiValue = ICC_BUCKET_MARKERS[i].rti; + break; + } + } + } + } + + context->GetValue("rti")->Set(rtiValue); + } + } + + if (botAI->IsRanged(bot)) + { + static constexpr float RANGED_TOLERANCE = 9.0f; + static constexpr float RANGED_MAX_STEP = 5.0f; + + if (bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION) > RANGED_TOLERANCE) + return MoveIncrementallyToPosition(ICC_SINDRAGOSA_RANGED_POSITION, RANGED_MAX_STEP); + + return false; + } + + static constexpr float MELEE_TOLERANCE = 10.0f; + static constexpr float MELEE_MAX_STEP = 5.0f; + + if (bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION) > MELEE_TOLERANCE) + return MoveIncrementallyToPosition(ICC_SINDRAGOSA_MELEE_POSITION, MELEE_MAX_STEP); + + return false; +} + +bool IccSindragosaGroupPositionAction::MoveIncrementallyToPosition(Position const& targetPos, float maxStep) +{ + float const stepSize = std::min(maxStep, bot->GetExactDist2d(targetPos)); + float moveX; + float moveY; + if (!IccStepToward(bot, targetPos, stepSize, moveX, moveY)) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, targetPos.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); +} + +bool IccSindragosaFrostBeaconAction::TryDropTombFlares(Unit const* boss) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (!IccAnyGroupMemberHasAura(bot, SPELL_FROST_BEACON)) + return false; + + // Phase tracking: clear flared sets on phase boundary so each phase gets fresh markers. + // Keyed per-instance so concurrent ICC raids don't share phase state. + uint32 const instanceId = bot->GetInstanceId(); + IcecrownHelpers::IccInstanceState& sgState = IcecrownHelpers::IccState(instanceId); + bool const phase3 = boss->HealthBelowPct(35); + bool& lastPhase3 = sgState.sgLastPhase3; + if (phase3 != lastPhase3) + { + sgState.sgFlaredRedThisPhase.clear(); + sgState.sgFlaredBluePhase3 = false; + lastPhase3 = phase3; + } + + // Build position list for current phase. + std::vector> targets; + if (phase3) + { + targets.emplace_back(3, &ICC_SINDRAGOSA_THOMBMB2_POSITION); + } + else + { + Difficulty const diff = bot->GetRaidDifficulty(); + bool const is25Man = (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC); + targets.emplace_back(0, &ICC_SINDRAGOSA_THOMB1_POSITION); + if (is25Man) + targets.emplace_back(1, &ICC_SINDRAGOSA_THOMB2_POSITION); + targets.emplace_back(2, &ICC_SINDRAGOSA_THOMB3_POSITION); + } + + // Build marker pool: alive non-tank bots (skip real players). One bot per position + // sidesteps the item cooldown — each marker only casts once per phase. + std::vector preferred; // non-beaconed + std::vector fallback; // beaconed allowed + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + PlayerbotAI* memberAI = sPlayerbotsMgr.GetPlayerbotAI(member); + if (!memberAI) + continue; // real player + if (memberAI->IsTank(member)) + continue; + fallback.push_back(member); + if (!member->HasAura(SPELL_FROST_BEACON)) + preferred.push_back(member); + } + + auto byGuid = [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }; + std::sort(preferred.begin(), preferred.end(), byGuid); + std::sort(fallback.begin(), fallback.end(), byGuid); + + // Assign N markers (one per position) from preferred first, then top up from fallback. + size_t const needed = targets.size(); + std::vector markers; + markers.reserve(needed); + for (Player* p : preferred) + { + if (markers.size() >= needed) + break; + markers.push_back(p); + } + if (markers.size() < needed) + { + for (Player* p : fallback) + { + if (markers.size() >= needed) + break; + if (std::find(markers.begin(), markers.end(), p) != markers.end()) + continue; + markers.push_back(p); + } + } + + // Find this bot's slot in the marker list. + auto myIt = std::find(markers.begin(), markers.end(), bot); + if (myIt == markers.end()) + return false; + size_t const mySlot = std::distance(markers.begin(), myIt); + if (mySlot >= targets.size()) + return false; + + auto const& [chosenIdx, chosenPos] = targets[mySlot]; + + // Already flared by some bot — don't duplicate. + if (phase3 && sgState.sgFlaredBluePhase3) + return false; + if (!phase3 && sgState.sgFlaredRedThisPhase.count(chosenIdx)) + return false; + + // Skip if a tomb is already standing on this position. + static constexpr std::array tombEntries = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; + GuidVector const tombGuids = AI_VALUE(GuidVector, "possible targets no los"); + for (uint32 const entry : tombEntries) + { + for (auto const& guid : tombGuids) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != entry) + continue; + if (unit->GetExactDist2d(chosenPos->GetPositionX(), chosenPos->GetPositionY()) <= 5.0f) + return false; + } + } + + uint32 const itemId = phase3 ? ITEM_BLUE_SMOKE_FLARE : ITEM_RED_SMOKE_FLARE; + if (bot->GetItemCount(itemId, false) == 0) + bot->StoreNewItemInBestSlots(itemId, 10); + + Item* flare = bot->GetItemByEntry(itemId); + if (!flare) + return false; + + if (bot->IsNonMeleeSpellCast(false)) + return false; + if (bot->CanUseItem(flare) != EQUIP_ERR_OK) + return false; + + uint8 const bagIndex = flare->GetBagSlot(); + uint8 const slot = flare->GetSlot(); + uint8 const castCount = 1; + uint32 const spellId = flare->GetTemplate()->Spells[0].SpellId; + ObjectGuid const itemGuid = flare->GetGUID(); + uint32 const glyphIndex = 0; + uint8 const castFlags = 0; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << castCount << spellId << itemGuid << glyphIndex << castFlags; + packet << uint32(TARGET_FLAG_DEST_LOCATION); + packet.appendPackGUID(0); + packet << chosenPos->GetPositionX() << chosenPos->GetPositionY() << chosenPos->GetPositionZ(); + + bot->GetSession()->HandleUseItemOpcode(packet); + if (phase3) + sgState.sgFlaredBluePhase3 = true; + else + sgState.sgFlaredRedThisPhase.insert(chosenIdx); + return false; +} + +bool IccSindragosaFrostBeaconAction::Execute(Event /*event*/) +{ + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); + if (!boss) + return false; + + TryDropTombFlares(boss); + + HandleSupportActions(); + + if (bot->HasAura(SPELL_FROST_BEACON)) + { + return HandleBeaconedPlayer(boss); + } + + return HandleNonBeaconedPlayer(boss); +} + +bool IccSindragosaFrostBeaconAction::HandleSupportActions() +{ + Group* group = bot->GetGroup(); + + // Tank support - Paladin Hand of Freedom + if (group && bot->getClass() == CLASS_PALADIN) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsTank(member)) + { + continue; + } + + if (botAI->GetAura("Frost Breath", member) && !member->HasAura(SPELL_HAND_OF_FREEDOM)) + { + botAI->CastSpell(SPELL_HAND_OF_FREEDOM, member); + break; + } + } + } + + return false; +} + +bool IccSindragosaHotAction::Execute(Event /*event*/) +{ + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_FROST_BEACON)) + return false; + + auto const members = AI_VALUE(GuidVector, "group members"); + for (auto const& memberGuid : members) + { + Unit* member = botAI->GetUnit(memberGuid); + if (!member || !member->IsAlive() || !member->HasAura(SPELL_FROST_BEACON)) + continue; + + if (member->HasAura(SPELL_ICE_TOMB)) + continue; + + uint32 spellId = 0; + switch (bot->getClass()) + { + case CLASS_PRIEST: + spellId = 48068; // Renew + break; + case CLASS_SHAMAN: + spellId = 61301; // Riptide + break; + case CLASS_DRUID: + spellId = 48441; // Rejuvenation + break; + default: + return false; + } + + if (!member->HasAura(spellId) && botAI->CanCastSpell(spellId, member)) + botAI->CastSpell(spellId, member); + } + + return false; +} + +bool IccSindragosaFrostBeaconAction::HandleBeaconedPlayer(const Unit* boss) +{ + // Phase 3 positioning (below 35% health, not flying) + if (boss->HealthBelowPct(35) && !IsBossFlying(boss)) + { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + botAI->Reset(); + return MoveToPositionIfNeeded(ICC_SINDRAGOSA_THOMBMB2_POSITION, POSITION_TOLERANCE); + } + + // Regular beacon positioning using tomb spots + Group* group = bot->GetGroup(); + if (!group) + { + return false; + } + + // Collect and sort beaconed players by GUID for deterministic assignment + std::vector beaconedPlayers; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && member->HasAura(SPELL_FROST_BEACON)) + beaconedPlayers.push_back(member); + } + + std::sort(beaconedPlayers.begin(), beaconedPlayers.end(), + [](const Player* a, const Player* b) { return a->GetGUID() < b->GetGUID(); }); + + // Find this bot's index + auto const it = std::find(beaconedPlayers.begin(), beaconedPlayers.end(), bot); + if (it == beaconedPlayers.end()) + return false; + + size_t const myIndex = std::distance(beaconedPlayers.begin(), it); + size_t const beaconCount = beaconedPlayers.size(); + + // Calculate tomb spot based on beacon count + size_t spot = 0; + switch (beaconCount) + { + case 2: + spot = (myIndex == 0) ? 0 : 2; + break; + case 5: + spot = (myIndex < 2) ? 0 : ((myIndex == 2) ? 1 : 2); + break; + case 6: + spot = myIndex / 2; + break; + default: + spot = myIndex % 3; + break; + } + + // Get tomb position and move if needed + static constexpr std::array tombPositions = { + &ICC_SINDRAGOSA_THOMB1_POSITION, &ICC_SINDRAGOSA_THOMB2_POSITION, &ICC_SINDRAGOSA_THOMB3_POSITION}; + + const Position& tombPosition = *tombPositions[std::min(spot, tombPositions.size() - 1)]; + return MoveToPositionIfNeeded(tombPosition, TOMB_POSITION_TOLERANCE); +} + +bool IccSindragosaFrostBeaconAction::HandleNonBeaconedPlayer(const Unit* boss) +{ + // Collect beaconed players + std::vector beaconedPlayers; + auto const members = AI_VALUE(GuidVector, "group members"); + for (auto const& memberGuid : members) + { + Unit* player = botAI->GetUnit(memberGuid); + if (player && player->GetGUID() != bot->GetGUID() && player->HasAura(SPELL_FROST_BEACON)) + { + beaconedPlayers.push_back(player); + } + } + + if (beaconedPlayers.empty()) + { + return false; + } + + // Air phase positioning + if (IsBossFlying(boss)) + { + if (!bot->HasAura(SPELL_FROST_BEACON)) + { + const Difficulty diff = bot->GetRaidDifficulty(); + bool is25Man = false; + if (diff && (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + is25Man = true; + + const Position& safePosition = is25Man ? ICC_SINDRAGOSA_FBOMB_POSITION : ICC_SINDRAGOSA_FBOMB10_POSITION; + + float const dist = bot->GetExactDist2d(safePosition.GetPositionX(), safePosition.GetPositionY()); + if (dist > MOVE_TOLERANCE) + { + return MoveToPosition(safePosition); + } + + } + return botAI->IsHeal(bot); // Continue for healers, wait for others + } + + // Ground phase: tanks hold the tank position, never the stack. + if (botAI->IsTank(bot)) + return false; + + // Ground phase - position based on role and avoid beaconed players + bool const isRanged = botAI->IsRanged(bot) && !botAI->IsHeal(bot) /*(bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION.GetPositionX(),ICC_SINDRAGOSA_RANGED_POSITION.GetPositionY()) < + bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION.GetPositionX(),ICC_SINDRAGOSA_MELEE_POSITION.GetPositionY()))*/; + + // Melee and healers stand 15y from the boss (out of tail range) instead of + // the fixed melee stack; ranged keep their far spot. + Position const targetPosition = isRanged ? ICC_SINDRAGOSA_RANGED_POSITION : SgSafeMeleeSpot(boss); + + float const deltaX = std::abs(targetPosition.GetPositionX() - bot->GetPositionX()); + float const deltaY = std::abs(targetPosition.GetPositionY() - bot->GetPositionY()); + if (boss && boss->GetVictim() != bot) + { + if ((deltaX > MOVE_TOLERANCE) || (deltaY > MOVE_TOLERANCE)) + { + if (bot->HasUnitState(UNIT_STATE_CASTING)) + { + botAI->Reset(); + } + return MoveToPosition(targetPosition); + } + } + return false; +} + +bool IccSindragosaFrostBeaconAction::MoveToPositionIfNeeded(const Position& position, float tolerance) +{ + float const distance = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + if (distance > tolerance) + { + return MoveToPosition(position); + } + return distance <= tolerance; +} + +bool IccSindragosaFrostBeaconAction::MoveToPosition(const Position& position) +{ + float posX = position.GetPositionX(); + float posY = position.GetPositionY(); + float posZ = position.GetPositionZ(); + + bot->UpdateAllowedPositionZ(posX, posY, posZ); + + return MoveTo(bot->GetMapId(), posX, posY, posZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED, + true, false); +} + +bool IccSindragosaFrostBeaconAction::IsBossFlying(const Unit* boss) +{ + return boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), + ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f; +} + +bool IccSindragosaBlisteringColdAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + // Only non-tanks should move out + if (botAI->IsMainTank(bot)) + return false; + + float dist = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); + + if (dist >= 33.0f) + return false; + + Position const& targetPos = ICC_SINDRAGOSA_BLISTERING_COLD_POSITION; + + // Only move if we're too close to the boss (< 30 yards) + if (dist < 33.0f) + { + + float const STEP_SIZE = 15.0f; + float distToTarget = bot->GetDistance2d(targetPos.GetPositionX(), targetPos.GetPositionY()); + + if (distToTarget > 0.1f) // Avoid division by zero + { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + // Calculate direction vector + float dirX = targetPos.GetPositionX() - bot->GetPositionX(); + float dirY = targetPos.GetPositionY() - bot->GetPositionY(); + + // Normalize direction vector + float length = sqrt(dirX * dirX + dirY * dirY); + dirX /= length; + dirY /= length; + + // Move STEP_SIZE yards in that direction + float moveX = bot->GetPositionX() + dirX * STEP_SIZE; + float moveY = bot->GetPositionY() + dirY * STEP_SIZE; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + return false; +} + +bool IccSindragosaUnchainedMagicAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true); + if (!aura) + return false; + + Aura* aura1 = botAI->GetAura("Instability", bot, false, true); + + Difficulty diff = bot->GetRaidDifficulty(); + if (aura && (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_NORMAL)) + { + if (aura1 && aura1->GetStackAmount() >= 6) + return true; // Stop casting spells + } + + return false; +} + +bool IccSindragosaChilledToTheBoneAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("Chilled to the Bone", bot, false, true); + if (!aura) + return false; + + if (aura) // Chilled to the Bone + { + if (aura->GetStackAmount() >= 6) + { + botAI->Reset(); + bot->AttackStop(); + return true; + } + } + + return false; +} + +bool IccSindragosaMysticBuffetAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss || !bot || !bot->IsAlive()) + return false; + + if (!boss->HealthBelowPct(35)) + return false; + + if (boss->GetVictim() == bot) + return false; + + // Skip if we have Frost Beacon + if (bot->HasAura(SPELL_FROST_BEACON)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Grid-based tomb lookup so every bot agrees on tomb presence; the old + // per-bot "possible targets" value diverged between bots and made skull + // setters fight skull clearers every tick. + std::vector const tombs = + IccGetCreaturesByEntries(bot, {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}, 150.0f); + + // Last phase uses the skull only; drop cross/star left over from the air + // phase zone split. + for (size_t i = 1; i < 3; ++i) + { + int8 const icon = ICC_BUCKET_MARKERS[i].icon; + if (!group->GetTargetIcon(icon).IsEmpty()) + group->SetTargetIcon(icon, bot->GetGUID(), ObjectGuid::Empty); + } + + constexpr uint8 SKULL_ICON = RtiTargetValue::skullIndex; + + // No tomb: single skull on the boss. + if (tombs.empty()) + { + if (boss->IsAlive()) + IccEnsureIconOn(bot, botAI, SKULL_ICON, boss); + + // Between tombs, keep non-tanks out at their DPS spot instead of + // clustering at LOS2 (~6y from the tomb drop) where the next beacon + // would catch them. Only move to LOS2 once the tomb has actually + // formed (the branches below). During a beacon FrostBeaconAction + // owns positioning, so leave that to it. + if (!botAI->IsHeal(bot) && !IccAnyGroupMemberHasAura(bot, SPELL_FROST_BEACON)) + { + Position const spread = + botAI->IsRanged(bot) ? ICC_SINDRAGOSA_RANGED_POSITION : SgSafeMeleeSpot(boss); + if (bot->GetExactDist2d(spread) > 3.0f) + return MoveTo(bot->GetMapId(), spread.GetPositionX(), spread.GetPositionY(), spread.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + } + + return false; + } + + // Beacon out: FrostBeaconAction owns positioning (bots spread to their + // spots). Keep the tomb marked when the raid is clear so ranged DPS burn + // it from range; the multiplier stops melee and healers from chasing it. + // No LOS2 move here — it would fight the beacon repositioning. + if (IccAnyGroupMemberHasAura(bot, SPELL_FROST_BEACON)) + { + if (!botAI->IsHeal(bot)) + { + if (SgMajorityLostMysticBuffet(bot, botAI)) + { + context->GetValue("rti")->Set("skull"); + IccEnsureIconOn(bot, botAI, SKULL_ICON, tombs.front()); + } + else + { + ObjectGuid const currentIcon = group->GetTargetIcon(SKULL_ICON); + if (!currentIcon.IsEmpty()) + group->SetTargetIcon(SKULL_ICON, bot->GetGUID(), ObjectGuid::Empty); + } + } + return false; + } + + bool const atLOS2 = bot->GetExactDist2d(ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), + ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY()) <= 2.0f; + + if (!atLOS2) + { + botAI->Reset(); + return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), + ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY(), ICC_SINDRAGOSA_LOS2_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + + // In place: healers are free to heal, only their movement is blocked + // by the multiplier. + if (botAI->IsHeal(bot)) + return false; + + // Waiting for the raid to shed Mystic Buffet. Bots are already parked at + // LOS2, so pre-burn a single tomb down to 50% to make the eventual kill + // fast; below 50% park it there (unmark + strip its auras so lingering + // DoTs don't push it under) and wait for the debuff to clear, exactly as + // the air-phase frost bomb holds a tomb at its stop HP. No extra LOS move. + if (!SgMajorityLostMysticBuffet(bot, botAI)) + { + Creature* const tomb = tombs.front(); + + if (tombs.size() == 1 && tomb->GetHealthPct() > 50.0f) + { + context->GetValue("rti")->Set("skull"); + IccEnsureIconOn(bot, botAI, SKULL_ICON, tomb); + return false; + } + + ObjectGuid const currentIcon = group->GetTargetIcon(SKULL_ICON); + if (!currentIcon.IsEmpty()) + group->SetTargetIcon(SKULL_ICON, bot->GetGUID(), ObjectGuid::Empty); + if (tombs.size() == 1) + tomb->RemoveAllAuras(); + bot->AttackStop(); + return true; + } + + // Majority clear: single skull on the lowest-GUID tomb, burn it down. + context->GetValue("rti")->Set("skull"); + IccEnsureIconOn(bot, botAI, SKULL_ICON, tombs.front()); + return false; +} + +bool IccSindragosaFrostBombAction::Execute(Event /*event*/) +{ + if (!bot || !bot->IsAlive()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + IcecrownHelpers::IccInstanceState& sgState = IcecrownHelpers::IccState(bot->GetInstanceId()); + + if (bot->HasAura(SPELL_ICE_TOMB)) + { + PinGroupToCurrentZone(); + sgState.sgFreedFallback.insert(bot->GetGUID()); + return false; + } + + FrostBombContext ctx; + if (!CollectContext(ctx)) + { + bot->AttackStop(); + return true; + } + + int const groupIndex = ResolveGroupIndex(group); + if (groupIndex < 0) + return false; + + Difficulty const diff = bot->GetRaidDifficulty(); + int const groupCount = (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC) ? 3 : 2; + + // Fixed tomb zone positions per group: + // 25-man (3 groups): group 0→THOMB1, group 1→THOMB2, group 2→THOMB3 + // 10-man (2 groups): group 0→THOMB1, group 1→THOMB3 (beacons skip THOMB2 in 2-beacon case) + static constexpr std::array tombZones = { + &ICC_SINDRAGOSA_THOMB1_POSITION, + &ICC_SINDRAGOSA_THOMB2_POSITION, + &ICC_SINDRAGOSA_THOMB3_POSITION + }; + int const zoneIdx = (groupCount == 2) ? (groupIndex == 0 ? 0 : 2) : groupIndex; + Position const& myZone = *tombZones[zoneIdx]; + + std::vector myTombs = SelectTombs(ctx.tombs, groupIndex, groupCount); + + bool myZoneAllProtected = false; + { + static constexpr std::array raidIcons = {RtiTargetValue::skullIndex, RtiTargetValue::crossIndex, + RtiTargetValue::starIndex}; + static constexpr float STRIP_HP_PCT = 30.0f; + bool const is10Man = + (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_10MAN_HEROIC); + float const tombStopHpPct = is10Man ? 60.0f : 40.0f; + + auto isMarked = [&](Unit* tomb) -> bool + { + for (uint8 const icon : raidIcons) + if (group->GetTargetIcon(icon) == tomb->GetGUID()) + return true; + return false; + }; + + bool anyAlive = false; + bool anyKillable = false; + for (Unit* tomb : myTombs) + { + if (!tomb || !tomb->IsAlive()) + continue; + anyAlive = true; + bool const marked = isMarked(tomb); + + if (!marked && tomb->GetHealthPct() < STRIP_HP_PCT) + { + Unit::AuraMap& auras = tomb->GetOwnedAuras(); + for (Unit::AuraMap::iterator it = auras.begin(); it != auras.end();) + { + Aura* aura = it->second; + if (aura && aura->GetDuration() != -1) + { + tomb->RemoveOwnedAura(it); + continue; + } + ++it; + } + } + (void)marked; + if (tomb->GetHealthPct() > tombStopHpPct) + anyKillable = true; + } + + myZoneAllProtected = anyAlive && !anyKillable; + + std::vector pets; + if (Pet* mainPet = bot->GetPet()) + pets.push_back(mainPet); + for (Unit* controlled : bot->m_Controlled) + { + if (Creature* c = dynamic_cast(controlled)) + { + if (std::find(pets.begin(), pets.end(), c) == pets.end()) + pets.push_back(c); + } + } + + if (myZoneAllProtected) + { + for (Creature* pet : pets) + { + if (!pet || !pet->IsAlive()) + continue; + pet->SetReactState(REACT_PASSIVE); + pet->AttackStop(); + pet->InterruptNonMeleeSpells(true); + pet->CombatStop(); + pet->SetTarget(ObjectGuid::Empty); + if (CharmInfo* ci = pet->GetCharmInfo()) + { + ci->SetPlayerReactState(REACT_PASSIVE); + pet->GetMotionMaster()->MoveFollow(bot, PET_FOLLOW_DIST, + pet->GetFollowAngle()); + ci->SetCommandState(COMMAND_FOLLOW); + ci->SetIsCommandAttack(false); + ci->SetIsAtStay(false); + ci->SetIsReturning(true); + ci->SetIsFollowing(true); + } + } + } + else + { + for (Creature* pet : pets) + { + if (pet && pet->IsAlive() && pet->GetReactState() == REACT_PASSIVE) + { + pet->SetReactState(REACT_DEFENSIVE); + if (CharmInfo* ci = pet->GetCharmInfo()) + ci->SetPlayerReactState(REACT_DEFENSIVE); + } + } + } + } + + // No tomb in zone + if (myTombs.empty()) + { + // Freed-from-tomb fallback: hide behind the nearest alive tomb anywhere + // in the arena rather than running across to our pinned zone anchor. + if (sgState.sgFreedFallback.count(bot->GetGUID())) + { + Unit* nearest = nullptr; + float minDist = std::numeric_limits::max(); + for (Unit* tomb : ctx.tombs) + { + if (!tomb || !tomb->IsAlive()) + continue; + float const d = bot->GetExactDist2d(tomb); + if (d < minDist) + { + minDist = d; + nearest = tomb; + } + } + + if (nearest) + { + float const fbAngle = ctx.marker->GetAngle(nearest); + float const fbX = nearest->GetPositionX() + std::cos(fbAngle) * 6.5f; + float const fbY = nearest->GetPositionY() + std::sin(fbAngle) * 6.5f; + float const fbZ = nearest->GetPositionZ(); + + if (bot->GetDistance2d(fbX, fbY) > 0.1f) + { + botAI->Reset(); + bot->AttackStop(); + return MoveTo(bot->GetMapId(), fbX, fbY, fbZ, false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + return false; + } + } + + // Default: pre-position behind the zone's anchor point + float const preAngle = ctx.marker->GetAngle(myZone.GetPositionX(), myZone.GetPositionY()); + float const preX = myZone.GetPositionX() + std::cos(preAngle) * 6.5f; + float const preY = myZone.GetPositionY() + std::sin(preAngle) * 6.5f; + + if (bot->GetDistance2d(preX, preY) > 3.0f) + { + botAI->Reset(); + bot->AttackStop(); + return MoveTo(bot->GetMapId(), preX, preY, myZone.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + return false; + } + + // Pinned zone has tombs again — exit fallback mode + ObjectGuid const losKey = bot->GetGUID(); + sgState.sgFreedFallback.erase(losKey); + + Unit* losTomb = ResolveStickyTomb(myTombs); + if (!losTomb) + { + // LOS tomb died / lost mark mid-walk. If we recently issued an LOS + // move, replay it for up to 2 seconds so the bot finishes its path + // instead of freezing in the open until the next valid sticky tomb. + auto it = sgState.sgLastLosMove.find(losKey); + if (it != sgState.sgLastLosMove.end()) + { + uint32 const now = getMSTime(); + if (getMSTimeDiff(it->second.timestampMs, now) <= 2000 && + bot->GetDistance2d(it->second.x, it->second.y) > 0.1f) + { + botAI->Reset(); + bot->AttackStop(); + return MoveTo(bot->GetMapId(), it->second.x, it->second.y, it->second.z, + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + sgState.sgLastLosMove.erase(it); + } + return false; + } + + float const angle = ctx.marker->GetAngle(losTomb); + float const posX = losTomb->GetPositionX() + std::cos(angle) * 6.5f; + float const posY = losTomb->GetPositionY() + std::sin(angle) * 6.5f; + float const posZ = losTomb->GetPositionZ(); + + float const losDist = bot->GetDistance2d(posX, posY); + if (losDist > 0.1f) + { + botAI->Reset(); + bot->AttackStop(); + // Mark the tomb early (within 5yd) so the raid converges on the kill + // target while the bot is still walking the last few yards into LOS. + if (losDist <= 10.0f) + HandleRtiMarking(group, groupIndex, myTombs, losTomb); + + // Stamp this LOS move so we can replay it for up to 2 seconds if the + // tomb dies/loses mark before we arrive. + IcecrownHelpers::LastLosMove& stamp = sgState.sgLastLosMove[losKey]; + stamp.timestampMs = getMSTime(); + stamp.x = posX; + stamp.y = posY; + stamp.z = posZ; + + return MoveTo(bot->GetMapId(), posX, posY, posZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + + // Reached LOS spot — clear the replay stamp. + sgState.sgLastLosMove.erase(losKey); + + // Bot is parked at LOS spot. Face away from the LOS tomb only when our + // zone has no kill-candidates left (every remaining tomb is protected). + // While extras are still up, the bot must keep facing them so it can DPS. + if (myZoneAllProtected) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + bot->SetTarget(ObjectGuid::Empty); + bot->SetFacingTo(losTomb->GetAngle(bot)); + } + + return HandleRtiMarking(group, groupIndex, myTombs, losTomb); +} + +bool IccSindragosaFrostBombAction::CollectContext(FrostBombContext& ctx) const +{ + constexpr uint32 tombEntries[] = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; + + std::list units; + float const range = 200.0f; + Acore::AnyUnitInObjectRangeCheck check(bot, range); + Acore::UnitListSearcher searcher(bot, units, check); + Cell::VisitObjects(bot, searcher, range); + + for (Unit* unit : units) + { + if (!unit || !unit->IsAlive()) + continue; + + if (unit->HasAura(SPELL_FROST_BOMB_VISUAL)) + ctx.marker = unit; + + for (uint32 entry : tombEntries) + { + if (unit->GetEntry() == entry) + { + ctx.tombs.push_back(unit); + break; + } + } + } + + return ctx.marker && !ctx.tombs.empty(); +} + +int IccSindragosaFrostBombAction::ResolveGroupIndex(Group* group) const +{ + // Collect bot-only GUIDs from the current group snapshot. + // Real players are excluded so they are never assigned a group index and + // never become the designated marker; only bots manage icons. + uint32 const instanceId = bot->GetInstanceId(); + auto& s_groupAssignments = IcecrownHelpers::IccState(instanceId).sgGroupAssignments; + std::vector currentGuids; + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !sPlayerbotsMgr.GetPlayerbotAI(member)) + continue; + currentGuids.push_back(member->GetGUID()); + } + std::sort(currentGuids.begin(), currentGuids.end()); + + Difficulty const diff = bot->GetRaidDifficulty(); + int const groupCount = (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC) ? 3 : 2; + + // Assign any GUIDs not yet seen, preserving all existing assignments. + // Never clear s_groupAssignments — this ensures bots that temporarily + // drop from the group (die, get entombed) keep their original group index + // and don't trigger a full reshuffle that migrates other bots. + for (ObjectGuid const& guid : currentGuids) + { + if (s_groupAssignments.find(guid) == s_groupAssignments.end()) + { + std::array counts = {}; + for (auto const& assign : s_groupAssignments) + if (assign.second < groupCount) + ++counts[assign.second]; + + int minGroup = 0; + for (int g = 1; g < groupCount; ++g) + if (counts[g] < counts[minGroup]) + minGroup = g; + + s_groupAssignments[guid] = minGroup; + } + } + + auto it = s_groupAssignments.find(bot->GetGUID()); + return it != s_groupAssignments.end() ? it->second : -1; +} + +void IccSindragosaFrostBombAction::PinGroupToCurrentZone() +{ + static constexpr std::array tombZones = { + &ICC_SINDRAGOSA_THOMB1_POSITION, + &ICC_SINDRAGOSA_THOMB2_POSITION, + &ICC_SINDRAGOSA_THOMB3_POSITION + }; + + Difficulty const diff = bot->GetRaidDifficulty(); + int const groupCount = + (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC) ? 3 : 2; + + // Find the tomb the bot is inside (ice tomb spawns on top of the frozen bot). + // Mapping the bot's group via the actual tomb position guarantees consistency + // with SelectTombs, which classifies tombs by nearest anchor. Falling back to + // raw bot position vs anchor would mis-pin when the boss/beacon dragged the + // bot into a different zone than where the tomb was assigned. + constexpr uint32 tombEntries[] = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; + + std::list units; + float const range = 15.0f; + Acore::AnyUnitInObjectRangeCheck check(bot, range); + Acore::UnitListSearcher searcher(bot, units, check); + Cell::VisitObjects(bot, searcher, range); + + Unit* myTomb = nullptr; + float minDist = std::numeric_limits::max(); + for (Unit* unit : units) + { + if (!unit || !unit->IsAlive()) + continue; + bool isTomb = false; + for (uint32 entry : tombEntries) + { + if (unit->GetEntry() == entry) + { + isTomb = true; + break; + } + } + if (!isTomb) + continue; + float const d = bot->GetExactDist2d(unit); + if (d < minDist) + { + minDist = d; + myTomb = unit; + } + } + + int bestGroup = 0; + float bestDist = std::numeric_limits::max(); + + // Anchor-from-tomb path (preferred): map nearest tomb -> nearest anchor -> group. + if (myTomb) + { + for (int g = 0; g < groupCount; ++g) + { + int const zoneIdx = (groupCount == 2) ? (g == 0 ? 0 : 2) : g; + float const d = myTomb->GetExactDist2d(*tombZones[zoneIdx]); + if (d < bestDist) + { + bestDist = d; + bestGroup = g; + } + } + } + else + { + // Fallback: use bot position if no tomb visible (shouldn't happen while tombed, + // but guard against odd states). + for (int g = 0; g < groupCount; ++g) + { + int const zoneIdx = (groupCount == 2) ? (g == 0 ? 0 : 2) : g; + float const d = bot->GetExactDist2d(*tombZones[zoneIdx]); + if (d < bestDist) + { + bestDist = d; + bestGroup = g; + } + } + } + + IcecrownHelpers::IccState(bot->GetInstanceId()).sgGroupAssignments[bot->GetGUID()] = bestGroup; +} + +std::vector IccSindragosaFrostBombAction::SelectTombs(std::vector const& tombs, int groupIndex, int groupCount) const +{ + if (tombs.empty()) + return {}; + + // Map group index to its intended zone anchor + static constexpr std::array tombZones = { + &ICC_SINDRAGOSA_THOMB1_POSITION, + &ICC_SINDRAGOSA_THOMB2_POSITION, + &ICC_SINDRAGOSA_THOMB3_POSITION + }; + int const zoneIdx = (groupCount == 2) ? (groupIndex == 0 ? 0 : 2) : groupIndex; + + static constexpr float MAX_ZONE_RADIUS = 3.0f; + std::vector zoneTombs; + for (Unit* tomb : tombs) + { + int closestZone = 0; + float closestDist = tomb->GetExactDist2d(*tombZones[0]); + for (int z = 1; z < 3; ++z) + { + // Skip zone 1 (THOMB2) in 2-group mode — it is never assigned + if (groupCount == 2 && z == 1) + continue; + float d = tomb->GetExactDist2d(*tombZones[z]); + if (d < closestDist) + { + closestDist = d; + closestZone = z; + } + } + if (closestZone != zoneIdx) + continue; + if (closestDist > MAX_ZONE_RADIUS) + continue; + zoneTombs.push_back(tomb); + } + return zoneTombs; +} + +Unit* IccSindragosaFrostBombAction::ResolveStickyTomb(std::vector const& myTombs) +{ + // Keep the previously assigned tomb while it is still a valid member of + // this group's zone. This avoids the cascading reassignment where a tomb's + // HP fluctuation causes every bot to flip to a different LOS spot each tick. + auto& s_tombAssignments = IcecrownHelpers::IccState(bot->GetInstanceId()).sgTombAssignments; + ObjectGuid const key = bot->GetGUID(); + auto it = s_tombAssignments.find(key); + if (it != s_tombAssignments.end()) + { + for (Unit* tomb : myTombs) + { + if (tomb->GetGUID() == it->second && tomb->IsAlive()) + return tomb; + } + } + + // No valid sticky — pick the tomb nearest to the bot so the first + // assignment snaps to where the bot already stands. + Unit* nearest = nullptr; + float minDist = std::numeric_limits::max(); + for (Unit* tomb : myTombs) + { + if (!tomb->IsAlive()) + continue; + float const d = bot->GetExactDist2d(tomb); + if (d < minDist) + { + minDist = d; + nearest = tomb; + } + } + + if (nearest) + s_tombAssignments[key] = nearest->GetGUID(); + else + s_tombAssignments.erase(key); + + return nearest; +} + +bool IccSindragosaFrostBombAction::HandleRtiMarking(Group* group, int groupIndex, std::vector const& myTombs, Unit* losTomb) +{ + constexpr uint8 SKULL_ICON = RtiTargetValue::skullIndex; + constexpr uint8 CROSS_ICON = RtiTargetValue::crossIndex; + constexpr uint8 STAR_ICON = RtiTargetValue::starIndex; + constexpr float TOMB_STOP_HP_PCT = 40.0f; + constexpr float TOMB_STOP_HP_PCT_10_MAN = 60.0f; + + Difficulty const diff = bot->GetRaidDifficulty(); + bool const is10Man = (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_10MAN_HEROIC); + + uint8 iconIndex = 0; + std::string rtiValue; + + switch (groupIndex) + { + case 0: iconIndex = SKULL_ICON; rtiValue = "skull"; break; + case 1: iconIndex = CROSS_ICON; rtiValue = "cross"; break; + case 2: iconIndex = STAR_ICON; rtiValue = "star"; break; + default: return false; + } + + context->GetValue("rti")->Set(rtiValue); + + Unit* currentIconUnit = botAI->GetUnit(group->GetTargetIcon(iconIndex)); + + Unit* tombToMark = nullptr; + + // Prefer to keep the current icon if it is still a valid extra in our zone. + if (currentIconUnit && currentIconUnit->IsAlive() && currentIconUnit != losTomb && + !(is10Man && currentIconUnit->GetHealthPct() <= TOMB_STOP_HP_PCT_10_MAN)) + { + for (Unit* tomb : myTombs) + { + if (tomb == currentIconUnit) + { + tombToMark = currentIconUnit; + break; + } + } + } + + // No valid current icon — pick a new extra deterministically (lowest GUID) + // so every bot in the group agrees on the same choice. + if (!tombToMark) + { + ObjectGuid bestGuid; + for (Unit* tomb : myTombs) + { + if (!tomb || !tomb->IsAlive() || tomb == losTomb) + continue; + if (is10Man && tomb->GetHealthPct() <= TOMB_STOP_HP_PCT_10_MAN) + continue; + if (!tombToMark || tomb->GetGUID() < bestGuid) + { + tombToMark = tomb; + bestGuid = tomb->GetGUID(); + } + } + } + + // No extras left — DPS the sticky down to the stop threshold + float const stickyStopPct = is10Man ? TOMB_STOP_HP_PCT_10_MAN : TOMB_STOP_HP_PCT; + if (!tombToMark && losTomb && losTomb->IsAlive() && losTomb->GetHealthPct() > stickyStopPct) + tombToMark = losTomb; + + if (!tombToMark) + { + // All tombs at/below threshold — clear icon and stand idle + ObjectGuid const currentIcon = group->GetTargetIcon(iconIndex); + if (!currentIcon.IsEmpty()) + group->SetTargetIcon(iconIndex, bot->GetGUID(), ObjectGuid::Empty); + + bot->AttackStop(); + return true; + } + + if (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != tombToMark) + group->SetTargetIcon(iconIndex, bot->GetGUID(), tombToMark->GetGUID()); + + // Let combat actions fire so this bot DPSes the marked tomb + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SS.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SS.cpp new file mode 100644 index 000000000..fdcfd4d21 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_SS.cpp @@ -0,0 +1,67 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCActions.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "Vehicle.h" +#include "RtiValue.h" +#include "GenericSpellActions.h" +#include "GenericActions.h" +#include "ICCTriggers.h" +#include "Multiplier.h" + +bool IccValkyreSpearAction::Execute(Event /*event*/) +{ + // Find the nearest spear + Creature* spear = bot->FindNearestCreature(NPC_SPEAR, 100.0f); + if (!spear) + return false; + + // Move to the spear if not in range + if (!spear->IsWithinDistInMap(bot, INTERACTION_DISTANCE)) + return MoveTo(spear, INTERACTION_DISTANCE); + + // Remove shapeshift forms + botAI->RemoveShapeshift(); + + // Stop movement and click the spear + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + spear->HandleSpellClick(bot); + + // Dismount if mounted + WorldPacket emptyPacket; + bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); + + return false; +} + +bool IccSisterSvalnaAction::Execute(Event /*event*/) +{ + Unit* svalna = AI_VALUE2(Unit*, "find target", "sister svalna"); + if (!svalna || !svalna->HasAura(SPELL_AETHER_SHIELD)) + return false; + + // Check if bot has the spear item + if (!botAI->HasItemInInventory(ITEM_SPEAR)) + return false; + + // Get all items from inventory + std::vector items = botAI->GetInventoryItems(); + for (Item* item : items) + { + if (item->GetEntry() == ITEM_SPEAR) + { + // Use spear on Svalna + botAI->ImbueItem(item, svalna); + return false; + } + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_VT.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_VT.cpp new file mode 100644 index 000000000..56de92aa9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/Action/ICCActions_VT.cpp @@ -0,0 +1,1229 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include + +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "Multiplier.h" +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "ICCActions.h" +#include "ICCTriggers.h" +#include "RtiValue.h" +#include "Vehicle.h" + +namespace +{ +// How long (ms) all bots stay at a cloud before advancing to the next one. +// This window lets every portal bot teleport in and collect the cloud together. +constexpr uint32 CLOUD_SYNC_WAIT_MS = 1000; +} + +bool IccValithriaGroupAction::Execute(Event /*event*/) +{ + std::vector const portalList = IccGetCreaturesByEntries( + bot, {NPC_DREAM_PORTAL, NPC_DREAM_PORTAL_PRE_EFFECT, NPC_NIGHTMARE_PORTAL, NPC_NIGHTMARE_PORTAL_PRE_EFFECT}, + 100.0f); + Creature* portal = portalList.empty() ? nullptr : portalList.front(); + + Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); + Creature* manaVoid = bot->FindNearestCreature(NPC_MANA_VOID, 100.0f); + + // Column of Frost units - still hostile so the hostile list is fine here + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector frostColumns; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_COLUMN_OF_FROST) + frostColumns.push_back(unit); + } + + // Tanks collect stray Gluttonous Abominations / Rot Worms anywhere. + // Each tank picks NEAREST stray so two tanks naturally split work. + // In taunt range -> taunt. Out of range -> step toward add to close gap. + if (botAI->IsTank(bot)) + { + constexpr float ADD_TAUNT_RANGE = 30.0f; + constexpr float CHASE_STEP = 10.0f; + + Unit* strayAdd = nullptr; + float bestDist = FLT_MAX; + for (ObjectGuid const& guid : AI_VALUE(GuidVector, "possible targets")) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + if (unit->GetEntry() != NPC_GLUTTONOUS_ABOMINATION && unit->GetEntry() != NPC_ROT_WORM) + continue; + + Unit* victim = unit->GetVictim(); + Player* victimPlayer = victim ? victim->ToPlayer() : nullptr; + if (victimPlayer && botAI->IsTank(victimPlayer)) + continue; + + float const d = bot->GetExactDist2d(unit); + if (d < bestDist) + { + bestDist = d; + strayAdd = unit; + } + } + + if (strayAdd) + { + if (bestDist <= ADD_TAUNT_RANGE) + { + IccCastClassTaunt(bot, botAI, strayAdd); + } + else + { + float dx = strayAdd->GetPositionX() - bot->GetPositionX(); + float dy = strayAdd->GetPositionY() - bot->GetPositionY(); + float const len = std::sqrt(dx * dx + dy * dy); + if (len > 0.001f) + { + dx /= len; + dy /= len; + float const step = std::min(CHASE_STEP, bestDist - ADD_TAUNT_RANGE + 5.0f); + float const moveX = bot->GetPositionX() + dx * step; + float const moveY = bot->GetPositionY() + dy * step; + MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + return true; + } + } + } + } + + // Healers move toward the heal position when no portal is active + if (botAI->IsHeal(bot) && !portal && bot->GetExactDist2d(ICC_VDW_HEAL_POSITION) > 30.0f) + { + return MoveTo(bot->GetMapId(), ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY(), + ICC_VDW_HEAL_POSITION.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_NORMAL); + } + + // Avoidance + if (manaVoid && bot->GetExactDist2d(manaVoid) < 10.0f && !botAI->GetAura("Twisted Nightmares", bot) && + !botAI->GetAura("Emerald Vigor", bot)) + { + botAI->Reset(); + FleePosition(manaVoid->GetPosition(), 11.0f, 250U); + } + + for (Unit* column : frostColumns) + { + if (column && bot->GetExactDist2d(column) < 7.0f) + { + botAI->Reset(); + FleePosition(column->GetPosition(), 8.0f, 250U); + } + } + + if (worm && worm->IsAlive() && worm->GetVictim() == bot && !botAI->IsTank(bot)) + { + botAI->Reset(); + FleePosition(worm->GetPosition(), 10.0f, 250U); + } + + // Zombie handling: only exploding-zombie flee here (any non-tank within 20f). + // Per-bot kiting when zombie victim handled by IccValithriaZombieKiteAction + // (higher priority trigger ACTION_EMERGENCY+9, blocks lower actions). + std::list allZombies; + bot->GetCreatureListWithEntryInGrid(allZombies, NPC_BLISTERING_ZOMBIE, 100.0f); + + Creature* explodingZombie = nullptr; + Creature* nearbyZombie = nullptr; + float nearbyDist = FLT_MAX; + + for (Creature* z : allZombies) + { + if (!z || !z->IsAlive()) + continue; + + bool const exploding = + z->HealthBelowPct(5) || z->FindCurrentSpellBySpellId(SPELL_ACID_BURST) || z->HasAura(SPELL_ACID_BURST); + float const d = bot->GetExactDist2d(z); + + if (exploding && d < 20.0f && (!explodingZombie || d < bot->GetExactDist2d(explodingZombie))) + explodingZombie = z; + + if (d < nearbyDist) + { + nearbyDist = d; + nearbyZombie = z; + } + } + + if (explodingZombie) + { + botAI->Reset(); + return FleePosition(explodingZombie->GetPosition(), 20.0f, 250U); + } + + // Non-tank melee must never sit inside 15f of any zombie: the zombie can + // flip victim at any time and at melee range the bot has no time to kite. + if (nearbyZombie && botAI->IsMelee(bot) && !botAI->IsTank(bot) && nearbyDist < 15.0f) + { + botAI->Reset(); + return FleePosition(nearbyZombie->GetPosition(), 15.0f, 250U); + } + + if (nearbyZombie && !botAI->IsMainTank(bot) && !botAI->IsHeal(bot) && nearbyZombie->GetVictim() != bot) + ApplyCrowdControl(nearbyZombie); + + // Leash: every role stays within 35f of the boss anchor unless doing an + // exclusive task (zombie kite, portal work, dream-state cloud collection). + // Zombie kite action runs at ACTION_EMERGENCY+9 so it preempts this action; + // extra skip-conditions below guard the remaining exclusive tasks. + constexpr float LEASH_RADIUS = 35.0f; + bool const inDreamState = bot->HasAura(SPELL_DREAM_STATE); + bool hasPortalClaim = false; + if (botAI->IsHeal(bot)) + { + auto& claims = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()).vtPortalClaim; + if (claims.find(bot->GetGUID()) != claims.end()) + hasPortalClaim = true; + } + bool const hasZombieThreat = nearbyZombie && nearbyZombie->GetVictim() == bot; + + Unit* const victim = bot->GetVictim(); + bool const engagedDps = victim && !botAI->IsTank(bot) && !botAI->IsHeal(bot) && + victim->GetExactDist2d(ICC_VDW_HEAL_POSITION) <= 50.0f; + + if (!inDreamState && !hasPortalClaim && !hasZombieThreat && !engagedDps) + { + float const distToAnchor = bot->GetExactDist2d(ICC_VDW_HEAL_POSITION); + if (distToAnchor > LEASH_RADIUS) + { + constexpr float STEP = 8.0f; + float const dx = ICC_VDW_HEAL_POSITION.GetPositionX() - bot->GetPositionX(); + float const dy = ICC_VDW_HEAL_POSITION.GetPositionY() - bot->GetPositionY(); + float const dz = ICC_VDW_HEAL_POSITION.GetPositionZ() - bot->GetPositionZ(); + float const dist = std::hypot(dx, dy); + float moveX; + float moveY; + float moveZ; + if (dist > STEP) + { + moveX = bot->GetPositionX() + (dx / dist) * STEP; + moveY = bot->GetPositionY() + (dy / dist) * STEP; + moveZ = bot->GetPositionZ() + (dz / dist) * STEP; + } + else + { + moveX = ICC_VDW_HEAL_POSITION.GetPositionX(); + moveY = ICC_VDW_HEAL_POSITION.GetPositionY(); + moveZ = ICC_VDW_HEAL_POSITION.GetPositionZ(); + } + MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); + return true; + } + } + + Difficulty const diff = bot->GetRaidDifficulty(); + Group* group = bot->GetGroup(); + + if (group && (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + return Handle25ManGroupLogic(); + + return Handle10ManGroupLogic(); +} + +bool IccValithriaGroupAction::ApplyCrowdControl(Unit* zombie) +{ + return IccTryClassCC(bot, botAI, zombie).value_or(false); +} + +bool IccValithriaGroupAction::Handle25ManGroupLogic() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + // Priority order follows Wowhead/Icy-Veins strategy: + // Blazing Skeleton (AoE Lay Waste) -> Suppresser (stacks -10% heal) -> + // Risen Archmage (Mana Void/Column of Frost) -> Blistering Zombie (ranged + // kites it, melee flees at 15f) -> Gluttonous Abomination (tank keeps it + // faced away) -> Rot Worm (cleanup from dead Abominations). + static constexpr std::array ADD_PRIORITY_CHECK = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, + NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, + NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; + constexpr float MARK_RADIUS = 45.0f; + + int priorityAddsAlive = 0; + GuidVector const targets = AI_VALUE(GuidVector, "possible targets"); + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + uint32 const e = unit->GetEntry(); + bool isPriority = false; + for (uint32 pe : ADD_PRIORITY_CHECK) + { + if (pe == e) + { + isPriority = true; + break; + } + } + if (!isPriority) + continue; + float const d = + unit->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY()); + if (d > MARK_RADIUS) + continue; + ++priorityAddsAlive; + if (priorityAddsAlive >= 2) + break; + } + + bool const singleMarkMode = priorityAddsAlive <= 1; + + std::vector eligible; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); + if (!memberAI || memberAI->IsRealPlayer()) + continue; + + if (memberAI->IsHeal(member) && member->HasAura(SPELL_DREAM_STATE)) + continue; + + if (!memberAI->IsTank(member) && !memberAI->IsDps(member) && !memberAI->IsHeal(member)) + continue; + + eligible.push_back(member); + } + + std::sort(eligible.begin(), eligible.end(), + [](Player const* a, Player const* b) { return a->GetGUID() < b->GetGUID(); }); + + bool inGroup1 = false; + bool inGroup2 = false; + + if (singleMarkMode) + { + inGroup1 = std::any_of(eligible.begin(), eligible.end(), [this](Player* p) { return p == bot; }); + } + else + { + // Bucket by role (tank / melee dps / ranged dps / healer) so each group + // gets roughly the same number of each role. Within a bucket alternate + // assignment; flip the starting side on odd-sized buckets so leftover + // slots don't all pile onto Group1. + std::vector tanks; + std::vector meleeDps; + std::vector rangedDps; + std::vector healers; + for (Player* p : eligible) + { + PlayerbotAI* pai = GET_PLAYERBOT_AI(p); + if (!pai) + continue; + if (pai->IsTank(p)) + tanks.push_back(p); + else if (pai->IsHeal(p)) + healers.push_back(p); + else if (pai->IsMelee(p)) + meleeDps.push_back(p); + else + rangedDps.push_back(p); + } + + std::vector group1; + std::vector group2; + int parity = 0; + auto splitBucket = [&](std::vector const& bucket) + { + for (size_t i = 0; i < bucket.size(); ++i) + { + if (((i + parity) & 1) == 0) + group1.push_back(bucket[i]); + else + group2.push_back(bucket[i]); + } + if (bucket.size() & 1) + parity ^= 1; + }; + splitBucket(tanks); + splitBucket(meleeDps); + splitBucket(rangedDps); + splitBucket(healers); + + inGroup1 = std::any_of(group1.begin(), group1.end(), [this](Player* p) { return p == bot; }); + inGroup2 = std::any_of(group2.begin(), group2.end(), [this](Player* p) { return p == bot; }); + } + + if (botAI->IsTank(bot) || botAI->IsDps(bot) || botAI->IsHeal(bot)) + HandleMarkingLogic(inGroup1, inGroup2, singleMarkMode); + + return false; +} + +bool IccValithriaGroupAction::HandleMarkingLogic(bool inGroup1, bool inGroup2, bool singleMarkMode) +{ + static constexpr uint8 SKULL_ICON = RtiTargetValue::skullIndex; + static constexpr uint8 CROSS_ICON = RtiTargetValue::crossIndex; + // Kill priority per Wowhead/Icy-Veins: Lay Waste skeletons first, then + // heal-debuff suppressers, then archmages, then zombies (ranged only - + // melee flee at 15f), then abominations (tank-faced), then rot worm cleanup. + static constexpr std::array ADD_PRIORITY = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, + NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, + NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; + + uint8 iconIndex; + std::string rtiValue; + + if (singleMarkMode) + { + if (!inGroup1) + return false; + iconIndex = SKULL_ICON; + rtiValue = "skull"; + } + else if (inGroup1) + { + iconIndex = SKULL_ICON; + rtiValue = "skull"; + } + else if (inGroup2) + { + iconIndex = CROSS_ICON; + rtiValue = "cross"; + } + else + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr float MARK_RADIUS = 45.0f; + + auto inMarkRange = [&](Unit* unit) -> bool + { + return unit->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY()) <= + MARK_RADIUS; + }; + + context->GetValue("rti")->Set(rtiValue); + + bool const dispatchRti = !botAI->IsTank(bot) && !botAI->IsHeal(bot); + bool const isMeleeDps = botAI->IsMelee(bot) && !botAI->IsTank(bot); + + auto tryDispatchRti = [&](ObjectGuid rtiGuid) + { + if (!dispatchRti || rtiGuid.IsEmpty()) + return; + // Melee DPS must not attack zombies - they explode at melee range. + // Ranged DPS still handle the marked zombie via RTI. + Unit* rtiUnit = botAI->GetUnit(rtiGuid); + if (rtiUnit && rtiUnit->GetEntry() == NPC_BLISTERING_ZOMBIE && isMeleeDps) + return; + Unit* const victim = bot->GetVictim(); + if (victim && victim->GetGUID() == rtiGuid) + return; + botAI->DoSpecificAction("attack rti target"); + }; + + GuidVector const adds = AI_VALUE(GuidVector, "possible targets"); + + auto getPriorityRank = [&](uint32 entry) -> int + { + for (size_t i = 0; i < ADD_PRIORITY.size(); ++i) + if (ADD_PRIORITY[i] == entry) + return static_cast(i); + return -1; + }; + + auto isValidAdd = [&](Unit* unit) -> bool + { + if (!unit || !unit->IsAlive()) + return false; + if (getPriorityRank(unit->GetEntry()) < 0) + return false; + return inMarkRange(unit); + }; + + ObjectGuid otherIconGuid; + if (!singleMarkMode) + { + uint8 const otherIcon = (iconIndex == SKULL_ICON) ? CROSS_ICON : SKULL_ICON; + otherIconGuid = group->GetTargetIcon(otherIcon); + } + + Unit* currentIconUnit = botAI->GetUnit(group->GetTargetIcon(iconIndex)); + + // Split the room by a world-Y line through the heal anchor. Group1 (skull) + // owns one half, Group2 (cross) owns the other. A group only falls back to + // the other half when its own side has no marking candidate. + auto isOwnSide = [&](Unit* unit) -> bool + { + if (singleMarkMode) + return true; + bool const sideA = unit->GetPositionY() < ICC_VDW_HEAL_POSITION.GetPositionY(); + return inGroup1 ? sideA : !sideA; + }; + + // Pick the reference point for "closest add" selection so DPS travels the + // shortest distance when the mark flips: prefer the current icon's + // position (alive or corpse), then the other group's still-live mark, + // then the heal anchor. + Position refPos = ICC_VDW_HEAL_POSITION; + if (currentIconUnit) + refPos = currentIconUnit->GetPosition(); + else if (!singleMarkMode) + { + if (Unit* otherUnit = botAI->GetUnit(otherIconGuid)) + if (otherUnit->IsAlive()) + refPos = otherUnit->GetPosition(); + } + + Unit* bestTarget = nullptr; + int bestRank = static_cast(ADD_PRIORITY.size()); + bool bestTargetOwnSide = false; + + // Two-pass scan: own side first, then any side. Within each pass walk + // priorities top-down and within the first tier that has any candidate + // pick the one closest to refPos. + for (int pass = 0; pass < 2 && !bestTarget; ++pass) + { + bool const ownSideOnly = (pass == 0); + for (size_t rank = 0; rank < ADD_PRIORITY.size(); ++rank) + { + uint32 const entry = ADD_PRIORITY[rank]; + float bestDist = FLT_MAX; + for (ObjectGuid const& guid : adds) + { + if (!singleMarkMode && guid == otherIconGuid) + continue; + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != entry) + continue; + if (!inMarkRange(unit)) + continue; + if (ownSideOnly && !isOwnSide(unit)) + continue; + float const d = unit->GetExactDist2d(refPos.GetPositionX(), refPos.GetPositionY()); + if (d < bestDist) + { + bestDist = d; + bestTarget = unit; + bestRank = static_cast(rank); + bestTargetOwnSide = ownSideOnly; + } + } + if (bestTarget) + break; + } + } + + if (isValidAdd(currentIconUnit)) + { + int currentRank = -1; + if (currentIconUnit) + currentRank = getPriorityRank(currentIconUnit->GetEntry()); + + bool const currentOnOwnSide = isOwnSide(currentIconUnit); + + // Side takes precedence: if an own-side candidate exists and the current + // icon is on the other side, flip regardless of priority rank. + bool const sideFlip = bestTarget && bestTargetOwnSide && !currentOnOwnSide; + + if (!sideFlip && (!bestTarget || bestRank >= currentRank)) + { + if (currentIconUnit) + tryDispatchRti(currentIconUnit->GetGUID()); + return false; + } + } + + if (!bestTarget) + { + if (currentIconUnit && !group->GetTargetIcon(iconIndex).IsEmpty()) + group->SetTargetIcon(iconIndex, bot->GetGUID(), ObjectGuid::Empty); + return false; + } + + group->SetTargetIcon(iconIndex, bot->GetGUID(), bestTarget->GetGUID()); + + if (singleMarkMode) + { + uint8 const crossIcon = CROSS_ICON; + Unit* currentCrossUnit = botAI->GetUnit(group->GetTargetIcon(crossIcon)); + if (currentCrossUnit && !group->GetTargetIcon(crossIcon).IsEmpty()) + group->SetTargetIcon(crossIcon, bot->GetGUID(), ObjectGuid::Empty); + } + + tryDispatchRti(bestTarget->GetGUID()); + + return false; +} + +bool IccValithriaGroupAction::Handle10ManGroupLogic() +{ + static constexpr uint8 DEFAULT_ICON = RtiTargetValue::skullIndex; + // Kill priority per Wowhead/Icy-Veins: skeleton > suppresser > archmage > + // zombie (ranged) > abomination (tank) > rot worm (cleanup). + static constexpr std::array ADD_PRIORITY = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, + NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, + NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + PlayerbotAI* selfAI = GET_PLAYERBOT_AI(bot); + if (!selfAI || selfAI->IsRealPlayer()) + return false; + if (botAI->IsHeal(bot) && bot->HasAura(SPELL_DREAM_STATE)) + return false; + + constexpr float MARK_RADIUS = 45.0f; + + auto inMarkRange = [&](Unit* unit) -> bool + { + return unit->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY()) <= + MARK_RADIUS; + }; + + context->GetValue("rti")->Set("skull"); + + bool const dispatchRti = !botAI->IsTank(bot) && !botAI->IsHeal(bot); + bool const isMeleeDps = botAI->IsMelee(bot) && !botAI->IsTank(bot); + + auto tryDispatchRti = [&](ObjectGuid rtiGuid) + { + if (!dispatchRti || rtiGuid.IsEmpty()) + return; + // Melee DPS must not attack zombies - ranged only. + Unit* rtiUnit = botAI->GetUnit(rtiGuid); + if (rtiUnit && rtiUnit->GetEntry() == NPC_BLISTERING_ZOMBIE && isMeleeDps) + return; + Unit* const victim = bot->GetVictim(); + if (victim && victim->GetGUID() == rtiGuid) + return; + botAI->DoSpecificAction("attack rti target"); + }; + + GuidVector const adds = AI_VALUE(GuidVector, "possible targets"); + + auto getPriorityRank = [&](uint32 entry) -> int + { + for (size_t i = 0; i < ADD_PRIORITY.size(); ++i) + if (ADD_PRIORITY[i] == entry) + return static_cast(i); + return -1; + }; + + auto isValidAdd = [&](Unit* unit) -> bool + { + if (!unit || !unit->IsAlive()) + return false; + if (getPriorityRank(unit->GetEntry()) < 0) + return false; + return inMarkRange(unit); + }; + + Unit* currentIconUnit = botAI->GetUnit(group->GetTargetIcon(DEFAULT_ICON)); + + // Use the current mark's position (alive or corpse) as the reference so + // DPS moves the least when the mark flips; fall back to the heal anchor. + Position refPos = ICC_VDW_HEAL_POSITION; + if (currentIconUnit) + refPos = currentIconUnit->GetPosition(); + + Unit* bestTarget = nullptr; + int bestRank = static_cast(ADD_PRIORITY.size()); + + for (size_t rank = 0; rank < ADD_PRIORITY.size(); ++rank) + { + uint32 const entry = ADD_PRIORITY[rank]; + float bestDist = FLT_MAX; + for (ObjectGuid const& guid : adds) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != entry) + continue; + if (!inMarkRange(unit)) + continue; + float const d = unit->GetExactDist2d(refPos.GetPositionX(), refPos.GetPositionY()); + if (d < bestDist) + { + bestDist = d; + bestTarget = unit; + bestRank = static_cast(rank); + } + } + if (bestTarget) + break; + } + if (isValidAdd(currentIconUnit)) + { + int currentRank; + if (currentIconUnit) + currentRank = getPriorityRank(currentIconUnit->GetEntry()); + else + { + // Handle the null case appropriately, e.g.: + currentRank = -1; // or another default/error value + } + + if (!bestTarget || bestRank >= currentRank) + { + if (currentIconUnit) + tryDispatchRti(currentIconUnit->GetGUID()); + return false; + } + } + + if (!bestTarget) + { + if (currentIconUnit && !group->GetTargetIcon(DEFAULT_ICON).IsEmpty()) + group->SetTargetIcon(DEFAULT_ICON, bot->GetGUID(), ObjectGuid::Empty); + return false; + } + + group->SetTargetIcon(DEFAULT_ICON, bot->GetGUID(), bestTarget->GetGUID()); + + tryDispatchRti(bestTarget->GetGUID()); + + return false; +} + +bool IccValithriaPortalAction::Execute(Event /*event*/) +{ + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE)) + return false; + + // Healer just dropped Dream State - if still airborne (portal exit leaves + // bots above the floor) snap to floor Z immediately so they don't fall and die. + constexpr float MAX_Z = 367.961f; + constexpr float TARGET_Z = 365.0f; + if (bot->GetPositionZ() > MAX_Z) + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TARGET_Z, bot->GetOrientation()); + + constexpr float SEARCH_RANGE = 200.0f; + + std::vector preEffectPortals = + IccGetCreaturesByEntries(bot, {NPC_DREAM_PORTAL_PRE_EFFECT, NPC_NIGHTMARE_PORTAL_PRE_EFFECT}, SEARCH_RANGE); + + std::vector realPortals = + IccGetCreaturesByEntries(bot, {NPC_DREAM_PORTAL, NPC_NIGHTMARE_PORTAL}, SEARCH_RANGE); + + // Evict stale claims BEFORE early-return so claims don't leak after fight. + // Scope eviction to this instance only - other ICC instances have their + // own portal GUID universes and we must not erase their claims. + uint32 const instanceId = bot->GetMap()->GetInstanceId(); + auto& claims = IcecrownHelpers::IccState(instanceId).vtPortalClaim; + + std::unordered_set livePortalGuids; + for (Creature* p : preEffectPortals) + if (p) + livePortalGuids.insert(p->GetGUID()); + for (Creature* p : realPortals) + if (p) + livePortalGuids.insert(p->GetGUID()); + + for (auto it = claims.begin(); it != claims.end();) + { + if (!livePortalGuids.count(it->second)) + it = claims.erase(it); + else + ++it; + } + + if (preEffectPortals.empty() && realPortals.empty()) + return false; + + // Collect OTHER healers' claims so we never pick a portal already taken. + std::unordered_set reservedPortals; + for (auto const& kv : claims) + if (kv.first != bot->GetGUID()) + reservedPortals.insert(kv.second); + + auto pickClosestUnreserved = [&](std::vector const& portals) -> Creature* + { + Creature* best = nullptr; + float bestDist = FLT_MAX; + for (Creature* p : portals) + { + if (!p) + continue; + if (reservedPortals.count(p->GetGUID())) + continue; + float const d = bot->GetExactDist2d(p); + if (d < bestDist) + { + bestDist = d; + best = p; + } + } + return best; + }; + + // Prefer pre-effect (claim early so others see it). Fall through to real + // portal if no pre-effect available. + Creature* assigned = nullptr; + if (!preEffectPortals.empty()) + assigned = pickClosestUnreserved(preEffectPortals); + if (!assigned && !realPortals.empty()) + assigned = pickClosestUnreserved(realPortals); + + // If everything reserved (more healers than portals), fall back to nearest + // overall so this healer still does something useful. + if (!assigned) + { + std::vector const& pool = !preEffectPortals.empty() ? preEffectPortals : realPortals; + float bestDist = FLT_MAX; + for (Creature* p : pool) + { + if (!p) + continue; + float const d = bot->GetExactDist2d(p); + if (d < bestDist) + { + bestDist = d; + assigned = p; + } + } + } + + if (!assigned) + return false; + + claims[bot->GetGUID()] = assigned->GetGUID(); + + if (bot->GetDistance2d(assigned->GetPositionX(), assigned->GetPositionY()) > 0.5f) + { + MoveTo(assigned->GetMapId(), assigned->GetPositionX(), assigned->GetPositionY(), assigned->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_NORMAL); + } + + botAI->RemoveShapeshift(); + + // Click the real portal once it spawns within reach. Only click our claimed + // portal (if it became real) or any real portal at our exact location. + Creature* clickTarget = nullptr; + float minDist = FLT_MAX; + for (Creature* portal : realPortals) + { + if (!portal) + continue; + float const d = bot->GetDistance2d(portal); + if (d < 3.0f && d < minDist) + { + clickTarget = portal; + minDist = d; + } + } + + if (clickTarget) + { + botAI->RemoveShapeshift(); + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + bot->SetFacingToObject(clickTarget); + clickTarget->HandleSpellClick(bot); + return true; + } + + return false; +} + +bool IccValithriaHealAction::Execute(Event /*event*/) +{ + if (!botAI->IsHeal(bot)) + return false; + + // Snap to floor Z first - healer may have just dropped Dream State and be + // airborne above MAX_Z. Run BEFORE the HP gate so it fires every tick. + constexpr float MAX_Z = 367.961f; + constexpr float TARGET_Z = 365.0f; + if (bot->GetPositionZ() > MAX_Z) + bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TARGET_Z, bot->GetOrientation()); + + if (bot->GetHealthPct() < 50.0f) + return false; + + if (!bot->HasAura(SPELL_DREAM_STATE)) + { + constexpr float NORMAL_SPEED = 1.0f; + bot->SetSpeed(MOVE_RUN, NORMAL_SPEED, true); + bot->SetSpeed(MOVE_WALK, NORMAL_SPEED, true); + bot->SetSpeed(MOVE_FLIGHT, NORMAL_SPEED, true); + } + + Creature* valithria = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!valithria) + return false; + + switch (bot->getClass()) + { + case CLASS_DRUID: + { + constexpr uint32 SPELL_REJUVENATION = 48441; + constexpr uint32 SPELL_REGROWTH = 48443; + constexpr uint32 SPELL_LIFEBLOOM = 48451; + constexpr uint32 SPELL_WILD_GROWTH = 53251; + constexpr uint8 LIFEBLOOM_MAX = 3; + + if (!valithria->HasAura(SPELL_REJUVENATION, bot->GetGUID())) + return botAI->CastSpell(SPELL_REJUVENATION, valithria); + + if (!valithria->HasAura(SPELL_REGROWTH, bot->GetGUID())) + return botAI->CastSpell(SPELL_REGROWTH, valithria); + + Aura* lb = valithria->GetAura(SPELL_LIFEBLOOM, bot->GetGUID()); + if (!lb || lb->GetStackAmount() < LIFEBLOOM_MAX) + return botAI->CastSpell(SPELL_LIFEBLOOM, valithria); + + return botAI->CastSpell(SPELL_WILD_GROWTH, valithria); + } + case CLASS_SHAMAN: + { + constexpr uint32 SPELL_RIPTIDE = 61301; + constexpr uint32 SPELL_HEALING_WAVE = 49273; + return valithria->HasAura(SPELL_RIPTIDE, bot->GetGUID()) ? botAI->CastSpell(SPELL_HEALING_WAVE, valithria) + : botAI->CastSpell(SPELL_RIPTIDE, valithria); + } + case CLASS_PRIEST: + { + constexpr uint32 SPELL_RENEW = 48068; + constexpr uint32 SPELL_GREATER_HEAL = 48063; + return valithria->HasAura(SPELL_RENEW, bot->GetGUID()) ? botAI->CastSpell(SPELL_GREATER_HEAL, valithria) + : botAI->CastSpell(SPELL_RENEW, valithria); + } + case CLASS_PALADIN: + { + constexpr uint32 SPELL_BEACON = 53563; + constexpr uint32 SPELL_HOLY_LIGHT = 48782; + return valithria->HasAura(SPELL_BEACON, bot->GetGUID()) ? botAI->CastSpell(SPELL_HOLY_LIGHT, valithria) + : botAI->CastSpell(SPELL_BEACON, valithria); + } + default: + break; + } + return false; +} + +bool IccValithriaDreamCloudAction::Execute(Event /*event*/) +{ + if (!bot->HasAura(SPELL_DREAM_STATE)) + return false; + + bot->SetSpeed(MOVE_RUN, 2.0f, true); + bot->SetSpeed(MOVE_WALK, 2.0f, true); + bot->SetSpeed(MOVE_FLIGHT, 2.0f, true); + + std::vector allDream; + std::vector realDream; + + Map::PlayerList const& playerList = bot->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator itr = playerList.begin(); itr != playerList.end(); ++itr) + { + Player* player = itr->GetSource(); + if (!player || !player->IsAlive() || !player->HasAura(SPELL_DREAM_STATE)) + continue; + + allDream.push_back(player); + + PlayerbotAI* playerBotAI = GET_PLAYERBOT_AI(player); + if (!playerBotAI || playerBotAI->IsRealPlayer()) + realDream.push_back(player); + } + + if (allDream.empty()) + return false; + + std::vector& leaderPool = realDream.empty() ? allDream : realDream; + + std::sort(leaderPool.begin(), leaderPool.end(), + [](Player const* a, Player const* b) { return a->GetGUID() < b->GetGUID(); }); + + Player* leader = leaderPool.front(); + + if (!realDream.empty()) + { + constexpr float STACK_DIST = 1.0f; + if (bot->GetDistance(leader) > STACK_DIST) + { + bot->TeleportTo(bot->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), leader->GetPositionZ(), + bot->GetOrientation()); + } + return false; + } + + // No real players - synchronized bot cloud collection + uint32 const instanceId = bot->GetInstanceId(); + uint32 const nowMs = getMSTime(); + + IcecrownHelpers::ValithriaCloudSync& sync = IcecrownHelpers::IccState(instanceId).vtCloudSync; + + std::vector dreamClouds = CollectClouds(NPC_DREAM_CLOUD, leader); + std::vector nightmareClouds = CollectClouds(NPC_NIGHTMARE_CLOUD, leader); + + std::vector allClouds; + allClouds.insert(allClouds.end(), dreamClouds.begin(), dreamClouds.end()); + allClouds.insert(allClouds.end(), nightmareClouds.begin(), nightmareClouds.end()); + + if (allClouds.empty()) + { + sync = IcecrownHelpers::ValithriaCloudSync{}; + return false; + } + + // Find whether the shared target cloud is still alive + Creature* target = nullptr; + if (!sync.targetCloudGuid.IsEmpty()) + { + for (Creature* c : allClouds) + { + if (c->GetGUID() == sync.targetCloudGuid) + { + target = c; + break; + } + } + } + + // Advance to a new cloud only when the current one is gone or the wait window has passed. + // The wait window gives every portal bot time to teleport in and collect the same cloud. + if (!target || nowMs >= sync.moveOnAfterMs) + { + Creature* closest = nullptr; + float minDist = FLT_MAX; + for (Creature* c : allClouds) + { + float const d = leader->GetExactDist(c); + if (d < minDist) + { + minDist = d; + closest = c; + } + } + + target = closest; + if (target) + sync.targetCloudGuid = target->GetGUID(); + else + sync.targetCloudGuid = ObjectGuid::Empty; + sync.moveOnAfterMs = nowMs + CLOUD_SYNC_WAIT_MS; + } + + // All bots teleport to the shared target so none miss the cloud + if (target) + bot->TeleportTo(target->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), + bot->GetOrientation()); + + return false; +} + +// Collect all live clouds sorted by distance from the given reference unit +std::vector IccValithriaDreamCloudAction::CollectClouds(uint32 entry, Unit* reference) +{ + constexpr float SEARCH_RANGE = 200.0f; + + std::list raw; + bot->GetCreatureListWithEntryInGrid(raw, entry, SEARCH_RANGE); + + std::vector result; + result.reserve(raw.size()); + for (Creature* c : raw) + if (c && c->IsAlive()) + result.push_back(c); + + // Sort by reference unit's distance so all bots get the same ordering + std::sort(result.begin(), result.end(), [reference](Creature const* a, Creature const* b) + { return reference->GetExactDist(a) < reference->GetExactDist(b); }); + + return result; +} + +bool IccValithriaZombieKiteAction::Execute(Event /*event*/) +{ + if (botAI->IsTank(bot)) + return false; + + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + std::list allZombies; + bot->GetCreatureListWithEntryInGrid(allZombies, NPC_BLISTERING_ZOMBIE, 100.0f); + + std::vector threatZombies; + Creature* nearestThreat = nullptr; + float nearestDist = FLT_MAX; + for (Creature* z : allZombies) + { + if (!z || !z->IsAlive()) + continue; + if (z->GetVictim() != bot) + continue; + threatZombies.push_back(z); + float const d = bot->GetExactDist2d(z); + if (d < nearestDist) + { + nearestDist = d; + nearestThreat = z; + } + } + + if (threatZombies.empty()) + return false; + + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + + // Stop spell-casting that would root the bot. DON'T call botAI->Reset() - + // it nukes the motion master mid-tick which restarts pathing every tick + // and lets zombies catch up. + if (bot->IsNonMeleeSpellCast(true)) + bot->InterruptNonMeleeSpells(true); + + constexpr float ANCHOR_RADIUS_LIMIT = 25.0f; // kite stays within this radius of heal anchor + constexpr float STEP = 18.0f; // how far we move per tick + constexpr float SAFE_FROM_ZOMBIE = 14.0f; // candidate must be this far from every zombie + constexpr float PATH_CLEARANCE = 8.0f; // path segment must miss every zombie by this much + constexpr int NUM_CANDIDATES = 36; + + float const anchorX = ICC_VDW_HEAL_POSITION.GetPositionX(); + float const anchorY = ICC_VDW_HEAL_POSITION.GetPositionY(); + + auto segmentDistToPoint = [](float x1, float y1, float x2, float y2, float px, float py) -> float + { + float const vx = x2 - x1; + float const vy = y2 - y1; + float const wx = px - x1; + float const wy = py - y1; + float const len2 = vx * vx + vy * vy; + float t = len2 > 0.0001f ? (vx * wx + vy * wy) / len2 : 0.0f; + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + float const cx = x1 + t * vx; + float const cy = y1 + t * vy; + float const dx = px - cx; + float const dy = py - cy; + return std::sqrt(dx * dx + dy * dy); + }; + + // Compute weighted-away-from-all-zombies vector. Closer zombies pull harder. + float awayX = 0.0f; + float awayY = 0.0f; + for (Creature* z : threatZombies) + { + float const dx = bot->GetPositionX() - z->GetPositionX(); + float const dy = bot->GetPositionY() - z->GetPositionY(); + float const d = std::sqrt(dx * dx + dy * dy); + if (d < 0.1f) + continue; + float const w = 1.0f / std::max(d, 1.0f); + awayX += (dx / d) * w; + awayY += (dy / d) * w; + } + float const awayLen = std::sqrt(awayX * awayX + awayY * awayY); + float baseAngle = 0.0f; + if (awayLen > 0.001f) + baseAngle = std::atan2(awayY / awayLen, awayX / awayLen); + else if (nearestThreat) + baseAngle = std::atan2(bot->GetPositionY() - nearestThreat->GetPositionY(), + bot->GetPositionX() - nearestThreat->GetPositionX()); + + float const bx = bot->GetPositionX(); + float const by = bot->GetPositionY(); + + float bestX = bx; + float bestY = by; + float bestScore = -FLT_MAX; + + // Sample arc centered on away-direction, +/-150 degrees + for (int i = 0; i < NUM_CANDIDATES; ++i) + { + float const offset = (-150.0f + (300.0f * i) / (NUM_CANDIDATES - 1)) * static_cast(M_PI) / 180.0f; + float const angle = baseAngle + offset; + float const cx = bx + STEP * std::cos(angle); + float const cy = by + STEP * std::sin(angle); + + // Stay within the anchor radius so kite doesn't drag bot out of raid + float const dxAnchor = cx - anchorX; + float const dyAnchor = cy - anchorY; + if (std::sqrt(dxAnchor * dxAnchor + dyAnchor * dyAnchor) > ANCHOR_RADIUS_LIMIT) + continue; + + // Reject if endpoint too close to any zombie OR straight path passes near one + float minEndDist = FLT_MAX; + bool unsafe = false; + for (Creature* z : threatZombies) + { + float const zx = z->GetPositionX(); + float const zy = z->GetPositionY(); + float const dxe = cx - zx; + float const dye = cy - zy; + float const endDist = std::sqrt(dxe * dxe + dye * dye); + if (endDist < SAFE_FROM_ZOMBIE) + { + unsafe = true; + break; + } + float const pathDist = segmentDistToPoint(bx, by, cx, cy, zx, zy); + if (pathDist < PATH_CLEARANCE) + { + unsafe = true; + break; + } + if (endDist < minEndDist) + minEndDist = endDist; + } + if (unsafe) + continue; + + // Prefer points that maximize distance from the closest zombie. + // Mild bonus for being aligned with away-direction (smaller offset). + float const alignBonus = 1.5f * (1.0f - std::fabs(offset) / static_cast(M_PI)); + float const score = minEndDist + alignBonus; + if (score > bestScore) + { + bestScore = score; + bestX = cx; + bestY = cy; + } + } + + if (bestScore == -FLT_MAX) + { + // No safe arc point - sprint straight away from the weighted center, + // but clamp endpoint to the anchor radius so we don't flee out of raid. + float fx = bx + STEP * std::cos(baseAngle); + float fy = by + STEP * std::sin(baseAngle); + float const dxA = fx - anchorX; + float const dyA = fy - anchorY; + float const distA = std::sqrt(dxA * dxA + dyA * dyA); + if (distA > ANCHOR_RADIUS_LIMIT) + { + fx = anchorX + (dxA / distA) * ANCHOR_RADIUS_LIMIT; + fy = anchorY + (dyA / distA) * ANCHOR_RADIUS_LIMIT; + } + return MoveTo(bot->GetMapId(), fx, fy, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED); + } + + return MoveTo(bot->GetMapId(), bestX, bestY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/RaidIccActionContext.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCActionContext.h similarity index 79% rename from modules/mod-playerbots/src/Ai/Raid/Icecrown/RaidIccActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/ICC/ICCActionContext.h index e648b09b7..dccc29c21 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/RaidIccActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_RAIDICCACTIONCONTEXT_H -#define _PLAYERBOT_RAIDICCACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCACTIONCONTEXT_H +#define PLAYERBOTS_ICCACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "RaidIccActions.h" +#include "ICCActions.h" class RaidIccActionContext : public NamedObjectContext { @@ -21,23 +27,29 @@ public: creators["icc rotting frost giant tank position"] = &RaidIccActionContext::icc_rotting_frost_giant_tank_position; creators["icc cannon fire"] = &RaidIccActionContext::icc_cannon_fire; creators["icc gunship enter cannon"] = &RaidIccActionContext::icc_gunship_enter_cannon; - creators["icc gunship teleport ally"] = &RaidIccActionContext::icc_gunship_teleport_ally; - creators["icc gunship teleport horde"] = &RaidIccActionContext::icc_gunship_teleport_horde; + creators["icc gunship rocket jump"] = &RaidIccActionContext::icc_gunship_rocket_jump; + creators["icc gunship rocket pack setup"] = &RaidIccActionContext::icc_gunship_rocket_pack_setup; creators["icc dbs tank position"] = &RaidIccActionContext::icc_dbs_tank_position; creators["icc adds dbs"] = &RaidIccActionContext::icc_adds_dbs; + creators["icc dogs tank position"] = &RaidIccActionContext::icc_dogs_tank_position; + creators["icc festergut group position"] = &RaidIccActionContext::icc_festergut_group_position; creators["icc festergut spore"] = &RaidIccActionContext::icc_festergut_spore; + creators["icc festergut avoid malleable goo"] = &RaidIccActionContext::icc_festergut_avoid_malleable_goo; creators["icc rotface tank position"] = &RaidIccActionContext::icc_rotface_tank_position; creators["icc rotface group position"] = &RaidIccActionContext::icc_rotface_group_position; creators["icc rotface move away from explosion"] = &RaidIccActionContext::icc_rotface_move_away_from_explosion; + creators["icc rotface avoid vile gas"] = &RaidIccActionContext::icc_rotface_avoid_vile_gas; + creators["icc putricide mutated plague"] = &RaidIccActionContext::icc_putricide_mutated_plague; creators["icc putricide volatile ooze"] = &RaidIccActionContext::icc_putricide_volatile_ooze; creators["icc putricide gas cloud"] = &RaidIccActionContext::icc_putricide_gas_cloud; creators["icc putricide growing ooze puddle"] = &RaidIccActionContext::icc_putricide_growing_ooze_puddle; creators["icc putricide avoid malleable goo"] = &RaidIccActionContext::icc_putricide_avoid_malleable_goo; + creators["icc putricide abomination"] = &RaidIccActionContext::icc_putricide_abomination; creators["icc bpc keleseth tank"] = &RaidIccActionContext::icc_bpc_keleseth_tank; creators["icc bpc main tank"] = &RaidIccActionContext::icc_bpc_main_tank; @@ -56,20 +68,22 @@ public: creators["icc valithria portal"] = &RaidIccActionContext::icc_valithria_portal; creators["icc valithria heal"] = &RaidIccActionContext::icc_valithria_heal; creators["icc valithria dream cloud"] = &RaidIccActionContext::icc_valithria_dream_cloud; + creators["icc valithria zombie kite"] = &RaidIccActionContext::icc_valithria_zombie_kite; creators["icc sindragosa group position"] = &RaidIccActionContext::icc_sindragosa_group_position; creators["icc sindragosa frost beacon"] = &RaidIccActionContext::icc_sindragosa_frost_beacon; + creators["icc sindragosa hot"] = &RaidIccActionContext::icc_sindragosa_hot; creators["icc sindragosa blistering cold"] = &RaidIccActionContext::icc_sindragosa_blistering_cold; creators["icc sindragosa unchained magic"] = &RaidIccActionContext::icc_sindragosa_unchained_magic; creators["icc sindragosa chilled to the bone"] = &RaidIccActionContext::icc_sindragosa_chilled_to_the_bone; creators["icc sindragosa mystic buffet"] = &RaidIccActionContext::icc_sindragosa_mystic_buffet; creators["icc sindragosa frost bomb"] = &RaidIccActionContext::icc_sindragosa_frost_bomb; - creators["icc sindragosa tank swap position"] = &RaidIccActionContext::icc_sindragosa_tank_swap_position; creators["icc lich king shadow trap"] = &RaidIccActionContext::icc_lich_king_shadow_trap; creators["icc lich king necrotic plague"] = &RaidIccActionContext::icc_lich_king_necrotic_plague; creators["icc lich king winter"] = &RaidIccActionContext::icc_lich_king_winter; creators["icc lich king adds"] = &RaidIccActionContext::icc_lich_king_adds; + creators["icc lich king spirit bomb"] = &RaidIccActionContext::icc_lich_king_spirit_bomb; } private: @@ -84,23 +98,29 @@ private: static Action* icc_rotting_frost_giant_tank_position(PlayerbotAI* ai) { return new IccRottingFrostGiantTankPositionAction(ai); } static Action* icc_cannon_fire(PlayerbotAI* ai) { return new IccCannonFireAction(ai); } static Action* icc_gunship_enter_cannon(PlayerbotAI* ai) { return new IccGunshipEnterCannonAction(ai); } - static Action* icc_gunship_teleport_ally(PlayerbotAI* ai) { return new IccGunshipTeleportAllyAction(ai); } - static Action* icc_gunship_teleport_horde(PlayerbotAI* ai) { return new IccGunshipTeleportHordeAction(ai); } + static Action* icc_gunship_rocket_jump(PlayerbotAI* ai) { return new IccGunshipRocketJumpAction(ai); } + static Action* icc_gunship_rocket_pack_setup(PlayerbotAI* ai) { return new IccGunshipRocketPackSetupAction(ai); } static Action* icc_dbs_tank_position(PlayerbotAI* ai) { return new IccDbsTankPositionAction(ai); } static Action* icc_adds_dbs(PlayerbotAI* ai) { return new IccAddsDbsAction(ai); } + static Action* icc_dogs_tank_position(PlayerbotAI* ai) { return new IccDogsTankPositionAction(ai); } + static Action* icc_festergut_group_position(PlayerbotAI* ai) { return new IccFestergutGroupPositionAction(ai); } static Action* icc_festergut_spore(PlayerbotAI* ai) { return new IccFestergutSporeAction(ai); } + static Action* icc_festergut_avoid_malleable_goo(PlayerbotAI* ai) { return new IccFestergutAvoidMalleableGooAction(ai); } static Action* icc_rotface_tank_position(PlayerbotAI* ai) { return new IccRotfaceTankPositionAction(ai); } static Action* icc_rotface_group_position(PlayerbotAI* ai) { return new IccRotfaceGroupPositionAction(ai); } static Action* icc_rotface_move_away_from_explosion(PlayerbotAI* ai) { return new IccRotfaceMoveAwayFromExplosionAction(ai); } + static Action* icc_rotface_avoid_vile_gas(PlayerbotAI* ai) { return new IccRotfaceAvoidVileGasAction(ai); } + static Action* icc_putricide_mutated_plague(PlayerbotAI* ai) { return new IccPutricideMutatedPlagueAction(ai); } static Action* icc_putricide_volatile_ooze(PlayerbotAI* ai) { return new IccPutricideVolatileOozeAction(ai); } static Action* icc_putricide_gas_cloud(PlayerbotAI* ai) { return new IccPutricideGasCloudAction(ai); } static Action* icc_putricide_growing_ooze_puddle(PlayerbotAI* ai) { return new IccPutricideGrowingOozePuddleAction(ai); } static Action* icc_putricide_avoid_malleable_goo(PlayerbotAI* ai) { return new IccPutricideAvoidMalleableGooAction(ai); } + static Action* icc_putricide_abomination(PlayerbotAI* ai) { return new IccPutricideAbominationAction(ai); } static Action* icc_bpc_keleseth_tank(PlayerbotAI* ai) { return new IccBpcKelesethTankAction(ai); } static Action* icc_bpc_main_tank(PlayerbotAI* ai) { return new IccBpcMainTankAction(ai); } @@ -119,20 +139,22 @@ private: static Action* icc_valithria_portal(PlayerbotAI* ai) { return new IccValithriaPortalAction(ai); } static Action* icc_valithria_heal(PlayerbotAI* ai) { return new IccValithriaHealAction(ai); } static Action* icc_valithria_dream_cloud(PlayerbotAI* ai) { return new IccValithriaDreamCloudAction(ai); } + static Action* icc_valithria_zombie_kite(PlayerbotAI* ai) { return new IccValithriaZombieKiteAction(ai); } static Action* icc_sindragosa_group_position(PlayerbotAI* ai) { return new IccSindragosaGroupPositionAction(ai); } static Action* icc_sindragosa_frost_beacon(PlayerbotAI* ai) { return new IccSindragosaFrostBeaconAction(ai); } + static Action* icc_sindragosa_hot(PlayerbotAI* ai) { return new IccSindragosaHotAction(ai); } static Action* icc_sindragosa_blistering_cold(PlayerbotAI* ai) { return new IccSindragosaBlisteringColdAction(ai); } static Action* icc_sindragosa_unchained_magic(PlayerbotAI* ai) { return new IccSindragosaUnchainedMagicAction(ai); } static Action* icc_sindragosa_chilled_to_the_bone(PlayerbotAI* ai) { return new IccSindragosaChilledToTheBoneAction(ai); } static Action* icc_sindragosa_mystic_buffet(PlayerbotAI* ai) { return new IccSindragosaMysticBuffetAction(ai); } static Action* icc_sindragosa_frost_bomb(PlayerbotAI* ai) { return new IccSindragosaFrostBombAction(ai); } - static Action* icc_sindragosa_tank_swap_position(PlayerbotAI* ai) { return new IccSindragosaTankSwapPositionAction(ai); } static Action* icc_lich_king_shadow_trap(PlayerbotAI* ai) { return new IccLichKingShadowTrapAction(ai); } static Action* icc_lich_king_necrotic_plague(PlayerbotAI* ai) { return new IccLichKingNecroticPlagueAction(ai); } static Action* icc_lich_king_winter(PlayerbotAI* ai) { return new IccLichKingWinterAction(ai); } static Action* icc_lich_king_adds(PlayerbotAI* ai) { return new IccLichKingAddsAction(ai); } + static Action* icc_lich_king_spirit_bomb(PlayerbotAI* ai) { return new IccLichKingSpiritBombAction(ai); } }; diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.cpp new file mode 100644 index 000000000..d0e9a1568 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.cpp @@ -0,0 +1,1222 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCMultipliers.h" + +#include "AttackAction.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "FollowActions.h" +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "MovementActions.h" +#include "PaladinActions.h" +#include "PriestActions.h" +#include "ICCActions.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "UseMeetingStoneAction.h" +#include "WarriorActions.h" +#include "PlayerbotAI.h" +#include "ICCTriggers.h" +#include "ICCScripts.h" + +// Lady Deathwhisper +float IccLadyDeathwhisperMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); + if (!boss) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE; + + // Get the nearest hostile NPCs + const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + + // Allow the IccShadeLadyDeathwhisperAction to run + if (dynamic_cast(action)) + return 1.0f; + + for (auto const& npcGuid : npcs) + { + Unit* shade = botAI->GetUnit(npcGuid); + + if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID) + continue; + + if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID()) + continue; + + return 0.0f; // Cancel all other actions when we need to handle Vengeful Shade + } + + return 1.0f; +} + +// dbs +float IccAddsDbsMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); + if (!boss) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (botAI->IsRanged(bot)) + if (dynamic_cast(action)) + return 0.0f; + + if (botAI->IsTank(bot)) + { + Aura* aura = botAI->GetAura("rune of blood", bot); + if (aura) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 1.0f; + + return 0.0f; + } + } + + return 1.0f; +} + +// Gunship +float IccGunshipMultiplier::GetValue(Action* action) +{ + // Detect gunship encounter via hostile enemy captain nearby + Unit* saurfang = bot->FindNearestCreature(NPC_HIGH_OVERLORD_SAURFANG, 200.0f); + Unit* muradin = bot->FindNearestCreature(NPC_MURADIN_BRONZEBEARD, 200.0f); + bool const inGunship = (saurfang && saurfang->IsAlive() && saurfang->IsHostileTo(bot)) || + (muradin && muradin->IsAlive() && muradin->IsHostileTo(bot)); + if (!inGunship) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + // Main tank is locked to captain via IccGunshipRocketJumpAction — block RTI targeting + if (botAI->IsMainTank(bot) && dynamic_cast(action)) + return 0.0f; + + // Bot in transit between ships: lock to rocket-jump action only so combat/movement + // actions don't interfere with jump packet timing. + bool const isHordeSide = muradin && muradin->IsAlive() && muradin->IsHostileTo(bot); + //@TODO use isAllySide or remove. + //bool const isAllySide = saurfang && saurfang->IsAlive() && saurfang->IsHostileTo(bot); + Position const friendlyPoint = isHordeSide ? ICC_GUNSHIP_ROCKET_JUMP_HORDE_FRIENDLY_POINT + : ICC_GUNSHIP_ROCKET_JUMP_ALLY_FRIENDLY_POINT; + Position const middlePoint = isHordeSide ? ICC_GUNSHIP_ROCKET_JUMP_HORDE_MIDDLE_POINT + : ICC_GUNSHIP_ROCKET_JUMP_ALLY_MIDDLE_POINT; + Position const attackPos = isHordeSide ? ICC_GUNSHIP_ROCKET_JUMP_HORDE + : ICC_GUNSHIP_ROCKET_JUMP_ALLY; + static constexpr float JUMP_GATE = 30.0f; + bool const nearFriendly = bot->GetExactDist2d(friendlyPoint) <= JUMP_GATE; + bool const nearMiddle = bot->GetExactDist2d(middlePoint) <= JUMP_GATE; + bool const nearAttack = bot->GetExactDist2d(attackPos) <= JUMP_GATE; + if (!nearFriendly && !nearMiddle && !nearAttack) + { + if (!dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// Dogs +float IccDogsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "stinky") && !AI_VALUE2(Unit*, "find target", "precious")) + return 1.0f; + + if (botAI->IsTank(bot)) + { + Aura* aura = botAI->GetAura("mortal wound", bot, false, true); + if (aura && aura->GetStackAmount() >= 8) + { + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 0.0f; + } + } + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Festergut +float IccFestergutMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (botAI->IsTank(bot)) + { + Aura* aura = botAI->GetAura("gastric bloat", bot, false, true); + if (aura && aura->GetStackAmount() >= 6) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 1.0f; + + return 0.0f; + } + } + + if (dynamic_cast(action)) + return 1.0f; + + if (bot->HasAura(SPELL_GAS_SPORE)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + // Hold position during the 8s malleable goo wait window so the bot can + // keep DPS/heal rotations running without drifting back into the impact. + // Avoid action itself is whitelisted (may still need to dodge new goos). + { + auto const& waitMap = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()).festergutGooWaitUntil; + auto it = waitMap.find(bot->GetGUID()); + if (it != waitMap.end() && getMSTime() < it->second) + { + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + } + } + + return 1.0f; +} + +// Rotface +float IccRotfaceMultiplier::GetValue(Action* action) +{ + Unit* boss1 = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss1) + return 1.0f; + + { + uint32 const now = getMSTime(); + IcecrownHelpers::IccInstanceState& st = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()); + auto const& waitMap = st.rotfaceVileGasWaitUntil; + auto it = waitMap.find(bot->GetGUID()); + bool const inWait = it != waitMap.end() && now < it->second; + bool const isVictim = + st.rotfaceVileGas.victimGuid == bot->GetGUID() && + getMSTimeDiff(st.rotfaceVileGas.castTime, now) < 8000; + + if (isVictim || inWait || botAI->HasAura("Vile Gas", bot)) + { + if (dynamic_cast(action)) + return 1.0f; + if (dynamic_cast(action)) + return 0.0f; + } + } + + if (botAI->HasAura("Vile Gas", bot)) + return 0.0f; + + if (botAI->IsTank(bot) && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && !(bot->getClass() == CLASS_HUNTER)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (botAI->IsAssistTank(bot) && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + return 0.0f; + + if (botAI->IsAssistTank(bot) && boss1 && bot->GetVictim() == boss1) + { + bot->AttackStop(); + bot->SetTarget(ObjectGuid::Empty); + return 0.0f; + } + + // Never cure/dispel Mutated Infection — it must expire naturally to spawn a small ooze + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + { + Creature* bigOoze = bot->FindNearestCreature(NPC_BIG_OOZE, 100.0f); + bool castingNow = bigOoze && bigOoze->IsAlive() && + bigOoze->HasUnitState(UNIT_STATE_CASTING) && bigOoze->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION); + + if (castingNow && (dynamic_cast(action) || dynamic_cast(action)) && + !dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +// pp +float IccAddsPutricideMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return 1.0f; + Unit* boss1 = AI_VALUE2(Unit*, "find target", "rotface"); + if (boss1) + return 1.0f; + Unit* boss2 = AI_VALUE2(Unit*, "find target", "festergut"); + if (boss2) + return 1.0f; + + if (Unit* vehBase = bot->GetVehicleBase()) + { + uint32 e = vehBase->GetEntry(); + if (e == NPC_MUTATED_ABOMINATION_10 || e == NPC_MUTATED_ABOMINATION_25) + { + if (dynamic_cast(action)) + return 1.0f; + return 0.0f; + } + } + + bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); + bool hasUnboundPlague = botAI->HasAura("Unbound Plague", bot); + + if (botAI->IsTank(bot) && + bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == FOLLOW_MOTION_TYPE) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + return 0.0f; + } + + if (!(bot->getClass() == CLASS_HUNTER) && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (botAI->IsTank(bot)) + { + auto GetPlagueStacks = [&](Unit* unit) -> uint32 + { + if (!unit) + return 0; + Aura* a = botAI->GetAura("mutated plague", unit, false, true); + return a ? a->GetStackAmount() : 0; + }; + + uint32 const myStacks = GetPlagueStacks(bot); + + // Another tank has fewer stacks — they should be tanking instead of us. + // Block generic taunts (so we don't fight for aggro), keep movement, and + // cancel the rest of the rotation (IccPutricideMutatedPlagueAction owns + // the AttackStop/taunt handoff). + bool anotherTankHasFewer = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot || !member->IsAlive() || !member->IsInWorld()) + continue; + if (!PlayerbotAI::IsTank(member)) + continue; + + if (GetPlagueStacks(member) < myStacks) + { + anotherTankHasFewer = true; + break; + } + } + } + + if (anotherTankHasFewer) + { + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + return 0.0f; + } + } + + if (hasGaseousBloat) + { + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + if (botAI->IsHeal(bot)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Gaseous Bloat + } + + if (hasUnboundPlague && boss && !boss->HealthBelowPct(35)) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Unbound Plague + } + + if (dynamic_cast(action)) + { + if (dynamic_cast(action)) + return 1.0f; + if (dynamic_cast(action) && !botAI->IsMainTank(bot)) + return 0.0f; + } + + return 1.0f; +} + +// bpc +float IccBpcAssistMultiplier::GetValue(Action* action) +{ + Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); + if (!keleseth) + return 1.0f; + + if (keleseth && (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (botAI->IsTank(bot) && + (dynamic_cast(action) || dynamic_cast(action))) + { + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && botAI->IsAssistTank(member)) + return 0.0f; + } + } + } + + Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); + + // Bomb assignment check (done early so it can override shadow prison stack limits) + static const std::array bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, + NPC_KINETIC_BOMB4}; + GuidVector const bombs = AI_VALUE(GuidVector, "possible targets no los"); + + std::vector kineticBombs; + for (auto const& guid : bombs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + if (std::find(bombEntries.begin(), bombEntries.end(), unit->GetEntry()) == bombEntries.end()) + continue; + + if (unit->GetPositionZ() - bot->GetPositionZ() > 35.0f) + continue; + + kineticBombs.push_back(unit); + } + + bool botAssignedToBomb = false; + if (!kineticBombs.empty() && botAI->IsRangedDps(bot) && + !(aura && aura->GetStackAmount() > 18)) + { + std::sort(kineticBombs.begin(), kineticBombs.end(), + [](Unit* a, Unit* b) { return a->GetPositionZ() < b->GetPositionZ(); }); + + std::vector rangedDps; + Group* group = bot->GetGroup(); + if (group) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && botAI->IsRangedDps(member)) + rangedDps.push_back(member); + } + } + + static float const MAX_ASSIGN_RANGE = 80.0f; + std::set assigned; + for (Unit* bomb : kineticBombs) + { + Player* nearest = nullptr; + float nearestDist = std::numeric_limits::max(); + + // Priority classes: hunter > druid > any + static constexpr std::array classPriority = {CLASS_HUNTER, CLASS_DRUID, 0}; + for (uint8 priorityClass : classPriority) + { + nearest = nullptr; + nearestDist = std::numeric_limits::max(); + + for (Player* dps : rangedDps) + { + if (assigned.count(dps)) + continue; + + if (priorityClass != 0 && dps->getClass() != priorityClass) + continue; + + float dist = dps->GetDistance(bomb); + if (dist < nearestDist && dist < MAX_ASSIGN_RANGE) + { + nearestDist = dist; + nearest = dps; + } + } + + if (nearest) + break; + } + + if (nearest) + { + assigned.insert(nearest); + if (nearest == bot) + botAssignedToBomb = true; + } + } + } + + // Bomb-assigned bot: block target switching and non-bomb BPC actions, allow combat rotation + if (botAssignedToBomb) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + // Shadow prison movement block (non-bomb bots use normal 12 stack limit). + // AttackAction derives from MovementAction, so it must be excluded here or + // bots stop attacking entirely (not just repositioning) once stacked high. + // ReachTargetAction (and ReachMeleeAction, which derives from it) is what + // actually walks a ranged/caster bot into range of a new target - Attack() + // alone does not move them - so it must be excluded too or they can never + // close distance on a newly marked target while stacked. + if (aura && aura->GetStackAmount() > (botAI->IsTank(bot) ? 18 : 12) && + dynamic_cast(action) && !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + if (!valanar) + return 1.0f; + + if (valanar && valanar->HasUnitState(UNIT_STATE_CASTING) && + (valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) || + valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4))) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + Unit* flame1 = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f); + Unit* flame2 = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f); + bool ballOfFlame = flame1 && flame1->GetVictim() == bot; + bool infernoFlame = flame2 && flame2->GetVictim() == bot; + + if (flame2) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 1.0f; + } + + if (ballOfFlame || infernoFlame) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + // For assist tank during BPC fight + if (botAI->IsAssistTank(bot) && !(aura && aura->GetStackAmount() > 18)) + { + // Allow BPC-specific actions + if (dynamic_cast(action)) + return 1.0f; + + // Disable normal assist behavior + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +//BQL +float IccBqlMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); + if (!boss) + return 1.0f; + + Aura* aura2 = botAI->GetAura("Swarming Shadows", bot); + Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); + + if (botAI->IsRanged(bot)) + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + // If bot has Pact of Darkfallen aura, return 0 for all other actions + if (bot->HasAura(SPELL_PACT_OF_THE_DARKFALLEN)) + { + if (dynamic_cast(action)) + return 1.0f; // Allow Pact of Darkfallen action + else + return 0.0f; // Cancel all other actions when we need to handle Pact of Darkfallen + } + + // Air phase: block movement/chase actions, allow combat rotation (attacks/heals) + if (((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f) && !aura) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + // If bot has frenzied bloodthirst, allow highest priority for bite action + if (aura) // If bot has frenzied bloodthirst + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + if (aura2 && !aura) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; // Cancel all other actions when we need to handle Swarming Shadows + } + + if ((boss->GetExactDist2d(ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY()) > 10.0f) && + botAI->IsRanged(bot) && !((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f)) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +//VDW +float IccValithriaDreamCloudMultiplier::GetValue(Action* action) +{ + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + + Aura* twistedNightmares = botAI->GetAura("Twisted Nightmares", bot); + Aura* emeraldVigor = botAI->GetAura("Emerald Vigor", bot); + + if (!boss && !bot->HasAura(SPELL_DREAM_STATE)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + // Zombie victim: only the kite action runs. Blocks combat/movement so bot + // doesn't try to attack/cast/move toward marks while being chased. + if (boss && !botAI->IsTank(bot)) + { + Creature* attackingZombie = nullptr; + std::list zombies; + bot->GetCreatureListWithEntryInGrid(zombies, NPC_BLISTERING_ZOMBIE, 100.0f); + for (Creature* z : zombies) + { + if (z && z->IsAlive() && z->GetVictim() == bot) + { + attackingZombie = z; + break; + } + } + if (attackingZombie) + { + if (dynamic_cast(action)) + return 1.0f; + return 0.0f; + } + } + + if (botAI->IsTank(bot)) + { + if (dynamic_cast(action)) + return 0.0f; + } + else + { + // Non-tanks must strictly follow RTI marks. Block generic assist actions + // so bots never attack unmarked adds; AttackRtiTargetAction drives them to + // skull/cross targets set by HandleMarkingLogic. + if (dynamic_cast(action)) + return 0.0f; + + // Melee bots must not engage Blistering Zombies (one-shot melee swing). + // Only ranged DPS handle them. If RTI/current target is a zombie, block + // attack actions so melee falls through to other priorities. + if (!PlayerbotAI::IsRangedDps(bot) && !botAI->IsHeal(bot)) + { + Unit* victim = bot->GetVictim(); + bool victimIsZombie = victim && victim->GetEntry() == NPC_BLISTERING_ZOMBIE; + + bool rtiIsZombie = false; + if (Group* group = bot->GetGroup()) + { + ObjectGuid rtiGuid = group->GetTargetIcon(RtiTargetValue::skullIndex); + if (!rtiGuid.IsEmpty()) + { + Unit* rtiUnit = ObjectAccessor::GetUnit(*bot, rtiGuid); + if (rtiUnit && rtiUnit->GetEntry() == NPC_BLISTERING_ZOMBIE) + rtiIsZombie = true; + } + } + + if (victimIsZombie || rtiIsZombie) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + } + } + + if (botAI->IsHeal(bot) && (twistedNightmares || emeraldVigor)) + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (bot->HasAura(SPELL_DREAM_STATE) && !bot->HealthBelowPct(50)) + { + if (dynamic_cast(action)) + return 1.0f; // Allow Dream Cloud action + else + return 0.0f; // Cancel all other actions when we need to handle Dream Cloud + } + + return 1.0f; + +} + +//SINDRAGOSA + +float IccSindragosaMultiplier::GetValue(Action* action) +{ + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); + if (!boss) + return 1.0f; + + // HoT support is an instant cast that never moves the bot. Always allow so + // beaconed targets stay topped up across air phase, blistering cold cast, + // phase 3 tank lockdown, and other "everything else 0.0f" branches below. + if (dynamic_cast(action)) + return 1.0f; + + // Tanks never chase raid icons; they stay on the boss at the tank spot. + if (botAI->IsTank(bot) && dynamic_cast(action)) + return 0.0f; + + Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true); + + Difficulty diff = bot->GetRaidDifficulty(); + + if (boss->HealthBelowPct(95)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + if (aura && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) && + !dynamic_cast(action)) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + // Check if boss is casting blistering cold (using both normal and heroic spell IDs) + if (boss->HasUnitState(UNIT_STATE_CASTING) && IccBossCastingBlisteringCold(boss)) + { + // If this is the blistering cold action, give it highest priority + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + + // Ranged / healer already beyond the blast radius: keep DPSing or + // healing, just block any movement so they don't wander back in. + bool const safe = bot->GetExactDist2d(boss) >= 33.0f; + if (safe && (botAI->IsRanged(bot) || botAI->IsHeal(bot))) + { + if (dynamic_cast(action)) + return 0.0f; + return 1.0f; + } + + // Disable all other actions while blistering cold is casting + return 0.0f; + } + + // Highest priority if we have beacon + if (bot->HasAura(SPELL_FROST_BEACON)) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + bool anyoneHasFrostBeacon = IccAnyGroupMemberHasAura(bot, SPELL_FROST_BEACON); + + if (anyoneHasFrostBeacon && boss && + boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), + ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && + !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) + { + if (dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + + if (anyoneHasFrostBeacon && !botAI->IsTank(bot)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + // Pin healers at the LOS2 hide spot while the hide is in effect (last + // phase, tomb up, no beacon). A beacon releases the pin so healers can + // reposition with the raid. + if (botAI->IsHeal(bot) && dynamic_cast(action) && !anyoneHasFrostBeacon && + boss->HealthBelowPct(35) && + bot->GetExactDist2d(ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), + ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY()) <= 2.0f && + !IccGetCreaturesByEntries(bot, {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}, 150.0f).empty()) + return 0.0f; + + // Last phase with a beacon out: only ranged DPS burn the tomb. Melee and + // healers reposition (via FrostBeaconAction) instead of chasing the skull. + if (anyoneHasFrostBeacon && boss->HealthBelowPct(35) && !botAI->IsTank(bot) && + !(botAI->IsRanged(bot) && !botAI->IsHeal(bot)) && dynamic_cast(action)) + return 0.0f; + + if (!botAI->IsTank(bot) && boss && boss->HealthBelowPct(35)) + { + if (dynamic_cast(action)) + return 0.0f; + } + + if (boss && botAI->IsTank(bot)) + { + if (boss->HealthBelowPct(35)) + { + // Assist tank: hold the tank position and face the boss only, never + // chase the marked tomb. A beaconed assist tank returns at the Frost + // Beacon branch above and moves to its beacon spot instead. + if (!botAI->IsMainTank(bot)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + return 0.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + else + return 0.0f; + } + } + + if (boss && boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) + { + if (dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float IccLichKingAddsMultiplier::GetValue(Action* action) +{ + if (bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f)) + return 1.0f; + + Unit* terenas = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); + if (terenas) + { + // Warlocks and melee stay functional (movement + adds action only) + if (botAI->IsMelee(bot) || bot->getClass() == CLASS_WARLOCK) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + return 0.0f; + } + + // Main tank near another tank: suppress movement jitter + Unit* mainTank = AI_VALUE(Unit*, "main tank"); + if (!botAI->IsMainTank(bot) && mainTank && bot->GetExactDist2d(mainTank) < 2.0f && + dynamic_cast(action)) + return 0.0f; + + // Suppress all these regardless of role + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; + } + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return 1.0f; + + // Allow cure actions only after a brief delay so the plague can spread once + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + static constexpr float DELIVER_RANGE = 3.0f; + static constexpr std::array HorrorEntries = {NPC_SHAMBLING_HORROR1, NPC_SHAMBLING_HORROR2, + NPC_SHAMBLING_HORROR3, NPC_SHAMBLING_HORROR4}; + + // Check whether any Shambling Horror is alive anywhere in the encounter + auto const anyHorrorAlive = [&]() -> bool + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive()) + continue; + + uint32 const entry = unit->GetEntry(); + if (entry == NPC_SHAMBLING_HORROR1 || entry == NPC_SHAMBLING_HORROR2 || + entry == NPC_SHAMBLING_HORROR3 || entry == NPC_SHAMBLING_HORROR4) + return true; + } + return false; + }; + + bool anyPlagued = false; + bool allDelivered = true; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (!botAI->HasAura("Necrotic Plague", member)) + continue; + + anyPlagued = true; + + bool nearHorror = false; + for (uint32 const entry : HorrorEntries) + { + Creature* horror = member->FindNearestCreature(entry, DELIVER_RANGE); + if (horror && horror->IsAlive()) + { + nearHorror = true; + break; + } + } + + if (!nearHorror) + { + allDelivered = false; + break; + } + } + + if (!anyPlagued) + return 1.0f; + + // No Horror alive at all — allow immediate dispel to prevent + // uncontrolled spread wiping the raid + if (!anyHorrorAlive()) + return 1.0f; + + // Horrors exist but not everyone has delivered yet — suppress cures + return allDelivered ? 1.0f : 0.0f; + } + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + // Hunters may flee (kite mechanics); everyone else stays put + if (dynamic_cast(action) && bot->getClass() != CLASS_HUNTER) + return 0.0f; + + if (boss->HealthAbovePct(71)) + { + // Assist tank targeting is fully managed by HandleAssistTankAddManagement — + // suppress generic target-switching actions so they don't override it. + if (botAI->IsAssistTank(bot) && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (!botAI->IsTank(bot) && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + + if (IccBossHasRemorselessWinter(boss)) + { + // Winter action and facing take priority + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + + // Staging window: while boss is casting Winter, non-tanks must commit + // to the staging move. Only heals are allowed; everything else blocked. + if (IccBossCastingRemorselessWinter(boss) && !botAI->IsTank(bot)) + { + if (dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + return 0.0f; + } + + // Sphere-targeted bot holds at the winter midpoint spot: block all other movement + if (!botAI->IsTank(bot) && dynamic_cast(action)) + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (ObjectGuid const& guid : npcs) + { + uint32 const entry = guid.GetEntry(); + if (entry != NPC_ICE_SPHERE1 && entry != NPC_ICE_SPHERE2 && + entry != NPC_ICE_SPHERE3 && entry != NPC_ICE_SPHERE4) + continue; + + Unit* sphere = botAI->GetUnit(guid); + if (!sphere || !sphere->IsAlive()) + continue; + + if (sphere->GetVictim() == bot) + return 0.0f; + } + } + + // Adds action is suppressed during winter + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + // Assist tank should not pick up adds independently during winter + if (botAI->IsAssistTank(bot) && dynamic_cast(action)) + return 0.0f; + + // MT movement is owned by the winter hold logic; reach actions chase + // far taunt targets and tug him off the hold spot (adds come to him). + if (botAI->IsMainTank(bot) && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + // Suppress movement/attack toward the boss if we are far away + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + if (currentTarget && currentTarget == boss && bot->GetDistance2d(boss) > 50.0f) + { + if (dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + // Suppress movement toward boss/sphere — but allow target-switching actions + // (DpsAssistAction, AttackRtiTargetAction) so bots can pick up skull-marked adds. + if (currentTarget && + (currentTarget == boss || currentTarget->GetEntry() == NPC_ICE_SPHERE1 || + currentTarget->GetEntry() == NPC_ICE_SPHERE2 || + currentTarget->GetEntry() == NPC_ICE_SPHERE3 || currentTarget->GetEntry() == NPC_ICE_SPHERE4)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + } + + if (botAI->IsRanged(bot) && !botAI->GetAura("Harvest Soul", bot, false, false)) + { + GuidVector const& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + bool defilePresent = false; + for (ObjectGuid const& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) + { + defilePresent = true; + break; + } + } + + if (defilePresent && (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + } + + if (botAI->IsAssistTank(bot) && boss->HealthAbovePct(71)) + { + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + if (currentTarget && currentTarget == boss && dynamic_cast(action)) + return 0.0f; + } + + return 1.0f; +} + +float IccLichKingSpiritBombMultiplier::GetValue(Action* action) +{ + if (!IccLichKingSpiritBombAction::IsBombThreatActive(botAI, bot)) + return 1.0f; + + // Allowlist: only the avoidance move and facing run during a bomb threat. + // Everything else is suppressed so the avoidance move sticks. + if (dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + + return 0.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.h rename to modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.h index 69d1ac51b..89d486b9c 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDICCMULTIPLIERS_H -#define _PLAYERBOT_RAIDICCMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCMULTIPLIERS_H +#define PLAYERBOTS_ICCMULTIPLIERS_H #include "Multiplier.h" @@ -99,4 +105,19 @@ public: virtual float GetValue(Action* action); }; +class IccLichKingSpiritBombMultiplier : public Multiplier +{ +public: + IccLichKingSpiritBombMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc lich king spirit bomb") {} + virtual float GetValue(Action* action); +}; + +//GUNSHIP +class IccGunshipMultiplier : public Multiplier +{ +public: + IccGunshipMultiplier(PlayerbotAI* ai) : Multiplier(ai, "icc gunship") {} + virtual float GetValue(Action* action); +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.cpp new file mode 100644 index 000000000..0664ca78a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.cpp @@ -0,0 +1,196 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCScripts.h" +#include "Creature.h" +#include "Player.h" +#include "ICCTriggers.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "SpellInfo.h" +#include "Timer.h" +#include +#include + +namespace +{ + std::unordered_map g_state; + std::mutex g_stateMutex; +} + +namespace IcecrownHelpers +{ + IccInstanceState& IccState(uint32 instanceId) + { + std::lock_guard lock(g_stateMutex); + return g_state[instanceId]; + } + + void IccResetInstance(uint32 instanceId) + { + std::lock_guard lock(g_stateMutex); + g_state.erase(instanceId); + } + + std::vector ActiveGooPositions(uint32 instanceId, uint32 lifetimeMs) + { + std::vector out; + IccInstanceState& st = IccState(instanceId); + + uint32 const now = getMSTime(); + for (auto const& impact : st.malleableGoo) + if (getMSTimeDiff(impact.castTime, now) <= lifetimeMs) + out.push_back(impact.position); + + return out; + } +} + +class IccPutricideListenerScript : public AllSpellScript +{ +public: + IccPutricideListenerScript() : AllSpellScript("IccPutricideListenerScript") { } + + void OnSpellCast(Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (!caster || !spellInfo) + return; + + if (spellInfo->Id != SPELL_MALLEABLE_GOO_10N && + spellInfo->Id != SPELL_MALLEABLE_GOO_25N && + spellInfo->Id != SPELL_MALLEABLE_GOO_10H && + spellInfo->Id != SPELL_MALLEABLE_GOO_25H && + spellInfo->Id != SPELL_MALLEABLE_GOO_BALCONY) + return; + + // Malleable Goo is cast triggered, so m_UniqueTargetInfo is not yet + // populated at this point; read the explicit unit target directly. + Unit* target = spell->m_targets.GetUnitTarget(); + if (!target || !target->IsPlayer()) + return; + + uint32 now = getMSTime(); + + IcecrownHelpers::MalleableGooImpact impact; + impact.position = target->GetPosition(); + impact.castTime = now; + + auto& impacts = IcecrownHelpers::IccState(caster->GetMap()->GetInstanceId()).malleableGoo; + impacts.push_back(impact); + + // Evict stale entries to keep the list bounded. Retention covers the + // longest consumer window (Festergut avoid: 8s) + slack. + impacts.erase( + std::remove_if(impacts.begin(), impacts.end(), + [now](IcecrownHelpers::MalleableGooImpact const& i) + { return getMSTimeDiff(i.castTime, now) > 9000; }), + impacts.end()); + } +}; + +class IccRotfaceListenerScript : public AllSpellScript +{ +public: + IccRotfaceListenerScript() : AllSpellScript("IccRotfaceListenerScript") { } + + void OnSpellCast(Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (!caster || !spellInfo) + return; + + if (spellInfo->Id != SPELL_VILE_GAS_H) + return; + + // Professor Putricide casts vile gas from the balcony during Rotface + // heroic. Filtering on caster entry keeps this hook scoped to the + // Rotface encounter only (Festergut also uses 'vile gas' as the gas + // spore aura name but a different spell ID). + if (caster->GetEntry() != NPC_PROFESSOR_PUTRICIDE) + return; + + Unit* target = spell->m_targets.GetUnitTarget(); + if (!target || !target->IsPlayer()) + return; + + IcecrownHelpers::VileGasVictim& entry = + IcecrownHelpers::IccState(caster->GetMap()->GetInstanceId()).rotfaceVileGas; + entry.victimGuid = target->GetGUID(); + entry.castTime = getMSTime(); + } +}; + +class IccLichKingListenerScript : public AllSpellScript +{ +public: + IccLichKingListenerScript() : AllSpellScript("IccLichKingListenerScript") { } + + // OnSpellPrepare fires at cast START (Spell::prepare). OnSpellCast fires + // at cast END, which for Defile (2s cast time) is too late - the puddle + // is already spawning and bots have no time to move out. + void OnSpellPrepare(Spell* spell, Unit* caster, SpellInfo const* spellInfo) override + { + if (!caster || !spellInfo) + return; + + if (spellInfo->Id != DEFILE_CAST_ID) + return; + + Unit* target = spell->m_targets.GetUnitTarget(); + if (!target || !target->IsPlayer()) + return; + + IcecrownHelpers::DefileCastInfo& entry = + IcecrownHelpers::IccState(caster->GetMap()->GetInstanceId()).defileCast; + entry.targetGuid = target->GetGUID(); + entry.castTime = getMSTime(); + } +}; + +class IccBossStateResetScript : public AllCreatureScript +{ +public: + IccBossStateResetScript() : AllCreatureScript("IccBossStateResetScript") { } + + void OnAllCreatureUpdate(Creature* creature, uint32 /*diff*/) override + { + if (!creature || creature->GetMapId() != ICC_MAP_ID || !creature->IsDungeonBoss()) + return; + + uint32 const instanceId = creature->GetInstanceId(); + uint32 const now = getMSTime(); + IcecrownHelpers::IccInstanceState& st = IcecrownHelpers::IccState(instanceId); + + if (creature->IsInCombat()) + { + st.lastBossCombatMs = now; + return; + } + + if (st.lastBossCombatMs != 0 && getMSTimeDiff(st.lastBossCombatMs, now) > ICC_RESET_GRACE_MS) + IcecrownHelpers::IccResetInstance(instanceId); + } +}; + +class IccMapCleanupScript : public AllMapScript +{ +public: + IccMapCleanupScript() : AllMapScript("IccMapCleanupScript") { } + + void OnDestroyMap(Map* map) override + { + if (map->GetId() == ICC_MAP_ID) + IcecrownHelpers::IccResetInstance(map->GetInstanceId()); + } +}; + +void AddSC_IcecrownBotScripts() +{ + new IccPutricideListenerScript(); + new IccRotfaceListenerScript(); + new IccLichKingListenerScript(); + new IccBossStateResetScript(); + new IccMapCleanupScript(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.h new file mode 100644 index 000000000..4e9fc30c5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCScripts.h @@ -0,0 +1,149 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCSCRIPTS_H +#define PLAYERBOTS_ICCSCRIPTS_H + +#include +#include +#include +#include +#include "ObjectGuid.h" +#include "Position.h" + +namespace IcecrownHelpers +{ + struct MalleableGooImpact + { + Position position; + uint32 castTime; + }; + + struct DefileCastInfo + { + ObjectGuid targetGuid; + uint32 castTime = 0; + }; + + struct VileGasVictim + { + ObjectGuid victimGuid; + uint32 castTime = 0; + }; + + struct WinterStageState + { + uint32 startMs; + Position const* pos; + }; + + struct StackChoice + { + uint32 evaluatedMs; + int slotIdx; + }; + + struct LastLosMove + { + uint32 timestampMs = 0; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + }; + + struct ValithriaCloudSync + { + ObjectGuid targetCloudGuid; + uint32 moveOnAfterMs = 0; + }; + + struct SpreadSlotState + { + uint32 lastCallMs = 0; + int currentSlot = 1; + }; + + struct BloatDir + { + float x; + float y; + }; + + struct IccInstanceState + { + std::vector malleableGoo; + std::map festergutGooWaitUntil; + DefileCastInfo defileCast; + VileGasVictim rotfaceVileGas; + std::map rotfaceVileGasWaitUntil; + std::map lastTauntMs; + std::map lastCcMs; + std::map lastBuffMs; + uint32 lastBossCombatMs = 0; + + std::map winterStage; + std::map winterStageInbound; + std::map mtAddInbound; + std::map rangedInbound; + std::map mtStackChoice; + float defileLastUnsafeX = 0.0f; + float defileLastUnsafeY = 0.0f; + uint32 defileLastUnsafeTime = 0; + uint32 lkLastYellMs = 0; + int lkSharedSlot = -1; + + std::set sgFlaredRedThisPhase; + bool sgFlaredBluePhase3 = false; + bool sgLastPhase3 = false; + std::map sgGroupAssignments; + std::map sgTombAssignments; + std::set sgFreedFallback; + std::map sgLastLosMove; + + std::map bqlBotCurrentCurve; + bool bqlGroundPhaseEstablished = false; + bool bqlBossWasAirborne = false; + bool bqlPostAirLanding = false; + std::map bqlAirSlotMemory; + std::map bqlBotSlotMemory; + + ValithriaCloudSync vtCloudSync; + std::map vtPortalClaim; + + Position gsbLastStarPos; + std::map gsbLastRocketPackUse; + + std::map dbsBotSlotMemory; + + std::map bpcSpreadLockTimers; + + SpreadSlotState fgSlotState; + + std::map rfExplosionSlotMemory; + + std::map ppBloatLastDir; + }; + + IccInstanceState& IccState(uint32 instanceId); + void IccResetInstance(uint32 instanceId); + + std::vector ActiveGooPositions(uint32 instanceId, uint32 lifetimeMs); +} + +constexpr uint32 ICC_MAP_ID = 631; +constexpr uint32 ICC_RESET_GRACE_MS = 5000; + +// Putricide - Mutated Abomination vehicle +constexpr uint32 GO_PUTRICIDE_DRINK_ME = 201584; +constexpr uint32 NPC_MUTATED_ABOMINATION_10 = 37672; +constexpr uint32 NPC_MUTATED_ABOMINATION_25 = 38285; +constexpr uint32 SPELL_MUTATED_TRANSFORMATION = 70311; +constexpr uint32 SPELL_ABO_EAT_OOZE = 70346; +constexpr uint32 SPELL_ABO_REGURGITATED_OOZE = 70539; + +void AddSC_IcecrownBotScripts(); + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.cpp new file mode 100644 index 000000000..c5d9df240 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.cpp @@ -0,0 +1,301 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCShared.h" + +#include +#include + +#include "ICCActions.h" +#include "ICCScripts.h" +#include "ICCTriggers.h" +#include "Playerbots.h" +#include "ThreatManager.h" +#include "Timer.h" + +namespace +{ + std::vector IccGetCreaturesByEntry(WorldObject* searcher, uint32 entry, float range) + { + std::list raw; + searcher->GetCreatureListWithEntryInGrid(raw, entry, range); + + std::vector out; + out.reserve(raw.size()); + for (Creature* c : raw) + if (c && c->IsAlive()) + out.push_back(c); + + return out; + } +} + +std::vector IccGetCreaturesByEntries(WorldObject* searcher, std::initializer_list entries, + float range) +{ + std::vector out; + for (uint32 entry : entries) + { + auto part = IccGetCreaturesByEntry(searcher, entry, range); + out.insert(out.end(), part.begin(), part.end()); + } + std::sort(out.begin(), out.end(), [](Creature const* a, Creature const* b) { return a->GetGUID() < b->GetGUID(); }); + out.erase(std::unique(out.begin(), out.end()), out.end()); + + return out; +} + +bool IccCastClassTaunt(Player* bot, PlayerbotAI* botAI, Unit* target) +{ + if (!target || !target->IsAlive()) + return false; + + uint32 const now = getMSTime(); + uint32& last = IcecrownHelpers::IccState(bot->GetInstanceId()).lastTauntMs[bot->GetGUID()]; + if (last != 0 && getMSTimeDiff(last, now) < 500) + return false; + last = now; + + bool const threatReset = botAI->HasCheat(BotCheatMask::raid); + + if (threatReset && target->GetVictim() != bot) + { + ThreatManager& mgr = target->GetThreatMgr(); + mgr.AddThreat(bot, 1000000.0f, nullptr, true, true); + mgr.FixateTarget(bot); + } + + switch (bot->getClass()) + { + case CLASS_PALADIN: + if (threatReset) + bot->RemoveSpellCooldown(SPELL_TAUNT_PALADIN, true); + if (botAI->CastSpell("hand of reckoning", target)) + return true; + break; + case CLASS_DEATH_KNIGHT: + if (threatReset) + bot->RemoveSpellCooldown(SPELL_TAUNT_DK, true); + if (botAI->CastSpell("dark command", target)) + return true; + break; + case CLASS_DRUID: + if (threatReset) + bot->RemoveSpellCooldown(SPELL_TAUNT_DRUID, true); + if (botAI->CastSpell("growl", target)) + return true; + break; + case CLASS_WARRIOR: + if (threatReset) + bot->RemoveSpellCooldown(SPELL_TAUNT_WARRIOR, true); + if (botAI->CastSpell("taunt", target)) + return true; + break; + default: + break; + } + + if (botAI->CastSpell("shoot", target) || botAI->CastSpell("throw", target)) + return true; + + return false; +} + +std::optional IccTryClassCC(Player* bot, PlayerbotAI* botAI, Unit* target) +{ + if (!target || !target->IsAlive()) + return std::nullopt; + + char const* spell = nullptr; + switch (bot->getClass()) + { + case CLASS_MAGE: spell = "Frost Nova"; break; + case CLASS_DRUID: spell = "Entangling Roots"; break; + case CLASS_PALADIN: spell = "Hammer of Justice"; break; + case CLASS_WARRIOR: spell = "Hamstring"; break; + case CLASS_HUNTER: spell = "Concussive Shot"; break; + case CLASS_ROGUE: spell = "Kidney Shot"; break; + case CLASS_SHAMAN: spell = "Frost Shock"; break; + case CLASS_DEATH_KNIGHT: spell = "Chains of Ice"; break; + case CLASS_PRIEST: spell = "Psychic Scream"; break; + case CLASS_WARLOCK: spell = "Fear"; break; + default: return std::nullopt; + } + + if (botAI->HasAura(spell, target)) + return std::nullopt; + + uint32 const now = getMSTime(); + uint32& last = IcecrownHelpers::IccState(bot->GetInstanceId()).lastCcMs[bot->GetGUID()]; + if (last != 0 && getMSTimeDiff(last, now) < 500) + return std::nullopt; + last = now; + + return botAI->CastSpell(spell, target); +} + +void IccEnsureIconOn(Player* bot, PlayerbotAI* botAI, int8 icon, Unit* target) +{ + if (!target) + return; + + Group* group = bot->GetGroup(); + if (!group) + return; + + Unit* current = botAI->GetUnit(group->GetTargetIcon(icon)); + if (!current || !current->IsAlive() || current != target) + group->SetTargetIcon(icon, bot->GetGUID(), target->GetGUID()); +} + +void IccApplyHeroicBuffToMember(PlayerbotAI* botAI, Player* member, bool applyPainSupp, bool applyNoThreat) +{ + uint32 const now = getMSTime(); + uint32& last = IcecrownHelpers::IccState(member->GetInstanceId()).lastBuffMs[member->GetGUID()]; + if (last != 0 && getMSTimeDiff(last, now) < 500) + return; + last = now; + + if (!member->HasAura(SPELL_EXPERIENCED)) + member->AddAura(SPELL_EXPERIENCED, member); + + if (!member->HasAura(SPELL_AGEIS_OF_DALARAN)) + member->AddAura(SPELL_AGEIS_OF_DALARAN, member); + + if (applyPainSupp && !member->HasAura(SPELL_PAIN_SUPPRESION)) + member->AddAura(SPELL_PAIN_SUPPRESION, member); + + if (applyNoThreat && !botAI->IsTank(member) && !member->HasAura(SPELL_NO_THREAT)) + member->AddAura(SPELL_NO_THREAT, member); +} + +void IccStripExperienced(Player* bot) +{ + if (bot->HasAura(SPELL_EXPERIENCED)) + bot->RemoveAura(SPELL_EXPERIENCED); +} + +bool IccBossCastingBlisteringCold(Unit* boss) +{ + return boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || + boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4)); +} + +bool IccBossCastingRemorselessWinter(Unit* boss) +{ + if (!boss || !boss->HasUnitState(UNIT_STATE_CASTING)) + return false; + + return boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER1) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER2) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER3) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER4) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER5) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER6) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER7) || + boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER8); +} + +bool IccBossHasRemorselessWinter(Unit* boss) +{ + if (!boss) + return false; + + if (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || + boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4) || + boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || + boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8)) + return true; + + return IccBossCastingRemorselessWinter(boss); +} + +bool IccAnyGroupMemberHasAura(Player* bot, uint32 spellId) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->HasAura(spellId)) + return true; + } + + return false; +} + +bool IccAnyBloodPrincePresent(PlayerbotAI* botAI) +{ + auto* context = botAI->GetAiObjectContext(); + return AI_VALUE2(Unit*, "find target", "prince valanar") || + AI_VALUE2(Unit*, "find target", "prince taldaram") || + AI_VALUE2(Unit*, "find target", "prince keleseth"); +} + +std::optional IccValithriaShouldHealRaid(Player* bot, PlayerbotAI* botAI) +{ + Group* group = bot->GetGroup(); + if (!group) + return std::nullopt; + + std::vector healerGuids; + std::vector druidGuids; + int healerCount = 0; + + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || botAI->IsRealPlayer()) + continue; + + if (botAI->IsHeal(member) && !botAI->IsRealPlayer()) + { + healerCount++; + healerGuids.push_back(member->GetGUID()); + if (member->getClass() == CLASS_DRUID) + druidGuids.push_back(member->GetGUID()); + } + } + + std::sort(healerGuids.begin(), healerGuids.end()); + std::sort(druidGuids.begin(), druidGuids.end()); + + auto botGuidPos = std::find(healerGuids.begin(), healerGuids.end(), bot->GetGUID()); + if (botGuidPos == healerGuids.end()) + return std::nullopt; + + int healerIndex = std::distance(healerGuids.begin(), botGuidPos); + bool isDruid = (bot->getClass() == CLASS_DRUID); + + bool shouldHealRaid = false; + int druidCount = druidGuids.size(); + + if (druidCount > 0) + { + if (isDruid) + { + int raidHealersNeeded = healerCount > 3 ? 2 : 1; + int druidIndex = + std::distance(druidGuids.begin(), std::find(druidGuids.begin(), druidGuids.end(), bot->GetGUID())); + shouldHealRaid = (druidIndex < raidHealersNeeded); + } + else if (healerCount > 3 && druidCount == 1) + { + if (healerIndex == (healerCount - 1)) + shouldHealRaid = true; + } + } + else if (healerCount > 3) + shouldHealRaid = (healerIndex >= (healerCount - 2)); + else + shouldHealRaid = (healerIndex == (healerCount - 1)); + + return shouldHealRaid; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.h new file mode 100644 index 000000000..db7f0efa2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCShared.h @@ -0,0 +1,101 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCSHARED_H +#define PLAYERBOTS_ICCSHARED_H + +#include +#include +#include +#include +#include + +#include "Playerbots.h" +#include "RtiTargetValue.h" + +class Action; + +struct IccBucketMarker +{ + int8 icon; + char const* rti; +}; + +inline constexpr IccBucketMarker ICC_BUCKET_MARKERS[3] = { + {RtiTargetValue::skullIndex, "skull"}, + {RtiTargetValue::crossIndex, "cross"}, + {RtiTargetValue::starIndex, "star"}, +}; + +inline char const* IccRtiNameForBucket(size_t bucket) +{ + return ICC_BUCKET_MARKERS[bucket < 3 ? bucket : 0].rti; +} + +inline bool IccStepToward(Player* bot, Position const& dest, float stepSize, float& outX, float& outY) +{ + float const dx = dest.GetPositionX() - bot->GetPositionX(); + float const dy = dest.GetPositionY() - bot->GetPositionY(); + float const len = std::sqrt(dx * dx + dy * dy); + if (len < 0.001f) + return false; + + outX = bot->GetPositionX() + (dx / len) * stepSize; + outY = bot->GetPositionY() + (dy / len) * stepSize; + return true; +} + +inline std::vector IccBalancedGroupSizes(size_t total, size_t buckets) +{ + std::vector sizes(buckets, 0); + if (buckets == 0) + return sizes; + + size_t const base = total / buckets; + size_t const remainder = total % buckets; + for (size_t i = 0; i < buckets; ++i) + { + sizes[i] = base; + if (i < remainder) + ++sizes[i]; + } + + return sizes; +} + +inline size_t IccAssignedBucketIndex(size_t memberIndex, std::vector const& sizes) +{ + size_t cursor = 0; + for (size_t i = 0; i < sizes.size(); ++i) + { + if (memberIndex < cursor + sizes[i]) + return i; + cursor += sizes[i]; + } + + return 0; +} + +struct IccGuidLess +{ + bool operator()(WorldObject const* a, WorldObject const* b) const { return a->GetGUID() < b->GetGUID(); } +}; + +bool IccCastClassTaunt(Player* bot, PlayerbotAI* botAI, Unit* target); +std::optional IccTryClassCC(Player* bot, PlayerbotAI* botAI, Unit* target); +void IccEnsureIconOn(Player* bot, PlayerbotAI* botAI, int8 icon, Unit* target); +void IccApplyHeroicBuffToMember(PlayerbotAI* botAI, Player* member, bool applyPainSupp, bool applyNoThreat); +void IccStripExperienced(Player* bot); +bool IccBossCastingBlisteringCold(Unit* boss); +bool IccBossHasRemorselessWinter(Unit* boss); +bool IccBossCastingRemorselessWinter(Unit* boss); +bool IccAnyGroupMemberHasAura(Player* bot, uint32 spellId); +bool IccAnyBloodPrincePresent(PlayerbotAI* botAI); +std::optional IccValithriaShouldHealRaid(Player* bot, PlayerbotAI* botAI); +std::vector IccGetCreaturesByEntries(WorldObject* searcher, std::initializer_list entries, + float range); + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCStrategy.cpp similarity index 78% rename from modules/mod-playerbots/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/ICC/ICCStrategy.cpp index e0dbe8230..f7e7fe73e 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCStrategy.cpp @@ -1,6 +1,12 @@ -#include "RaidIccStrategy.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidIccMultipliers.h" +#include "ICCStrategy.h" + +#include "ICCMultipliers.h" void RaidIccStrategy::InitTriggers(std::vector& triggers) { @@ -28,34 +34,32 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("icc in cannon", { NextAction("icc cannon fire", ACTION_RAID+5) })); - triggers.push_back(new TriggerNode("icc gunship teleport ally", - { NextAction("icc gunship teleport ally", ACTION_RAID + 4) })); + triggers.push_back(new TriggerNode("icc gunship rocket jump", + { NextAction("icc gunship rocket jump", ACTION_RAID + 4)})); - triggers.push_back(new TriggerNode("icc gunship teleport horde", - { NextAction("icc gunship teleport horde", ACTION_RAID + 4) })); + triggers.push_back(new TriggerNode("icc gunship rocket pack setup", + { NextAction("icc gunship rocket pack setup", ACTION_RAID + 2)})); //DBS triggers.push_back(new TriggerNode("icc dbs", { NextAction("icc dbs tank position", ACTION_RAID + 3), NextAction("icc adds dbs", ACTION_RAID + 5) })); - triggers.push_back(new TriggerNode("icc dbs main tank rune of blood", - { NextAction("taunt spell", ACTION_EMERGENCY + 4) })); + // Boss taunt on Rune of Blood is handled inside icc dbs tank position action - //DOGS - triggers.push_back(new TriggerNode("icc stinky precious main tank mortal wound", - { NextAction("taunt spell", ACTION_EMERGENCY + 4) })); + triggers.push_back(new TriggerNode("icc dogs", + { NextAction("icc dogs tank position", ACTION_RAID + 3) })); //FESTERGUT triggers.push_back(new TriggerNode("icc festergut group position", { NextAction("icc festergut group position", ACTION_MOVE + 4) })); - triggers.push_back(new TriggerNode("icc festergut main tank gastric bloat", - { NextAction("taunt spell", ACTION_EMERGENCY + 6) })); - triggers.push_back(new TriggerNode("icc festergut spore", { NextAction("icc festergut spore", ACTION_MOVE + 5) })); + triggers.push_back(new TriggerNode("icc festergut avoid malleable goo", + { NextAction("icc festergut avoid malleable goo", ACTION_RAID + 7) })); + //ROTFACE triggers.push_back(new TriggerNode("icc rotface tank position", { NextAction("icc rotface tank position", ACTION_RAID + 5) })); @@ -66,6 +70,9 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc rotface move away from explosion", { NextAction("icc rotface move away from explosion", ACTION_RAID +7) })); + triggers.push_back(new TriggerNode("icc rotface avoid vile gas", + { NextAction("icc rotface avoid vile gas", ACTION_RAID + 8) })); + //PP triggers.push_back(new TriggerNode("icc putricide volatile ooze", { NextAction("icc putricide volatile ooze", ACTION_RAID + 4) })); @@ -76,11 +83,14 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc putricide growing ooze puddle", { NextAction("icc putricide growing ooze puddle", ACTION_RAID + 3) })); - triggers.push_back(new TriggerNode("icc putricide main tank mutated plague", - { NextAction("taunt spell", ACTION_RAID + 10) })); + triggers.push_back(new TriggerNode("icc putricide mutated plague", + { NextAction("icc putricide mutated plague", ACTION_RAID + 3) })); triggers.push_back(new TriggerNode("icc putricide malleable goo", - { NextAction("icc putricide avoid malleable goo", ACTION_RAID + 2) })); + { NextAction("icc putricide avoid malleable goo", ACTION_RAID + 6) })); + + triggers.push_back(new TriggerNode("icc putricide abomination", + { NextAction("icc putricide abomination", ACTION_RAID + 7) })); //BPC triggers.push_back(new TriggerNode("icc bpc keleseth tank", @@ -119,6 +129,9 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc valithria group", { NextAction("icc valithria group", ACTION_RAID + 1) })); + triggers.push_back(new TriggerNode("icc valithria zombie kite", + { NextAction("icc valithria zombie kite", ACTION_EMERGENCY + 9) })); + triggers.push_back(new TriggerNode("icc valithria portal", { NextAction("icc valithria portal", ACTION_RAID + 5) })); @@ -135,6 +148,9 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc sindragosa frost beacon", { NextAction("icc sindragosa frost beacon", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("icc sindragosa hot", + { NextAction("icc sindragosa hot", ACTION_RAID + 6) })); + triggers.push_back(new TriggerNode("icc sindragosa blistering cold", { NextAction("icc sindragosa blistering cold", ACTION_EMERGENCY + 4) })); @@ -147,15 +163,9 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc sindragosa mystic buffet", { NextAction("icc sindragosa mystic buffet", ACTION_RAID + 3) })); - triggers.push_back(new TriggerNode("icc sindragosa main tank mystic buffet", - { NextAction("taunt spell", ACTION_EMERGENCY + 3) })); - triggers.push_back(new TriggerNode("icc sindragosa frost bomb", { NextAction("icc sindragosa frost bomb", ACTION_RAID + 7) })); - triggers.push_back(new TriggerNode("icc sindragosa tank swap position", - { NextAction("icc sindragosa tank swap position", ACTION_EMERGENCY + 2) })); - //LICH KING triggers.push_back(new TriggerNode("icc lich king shadow trap", { NextAction("icc lich king shadow trap", ACTION_RAID + 6) })); @@ -168,6 +178,9 @@ void RaidIccStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("icc lich king adds", { NextAction("icc lich king adds", ACTION_RAID +2) })); + + triggers.push_back(new TriggerNode("icc lich king spirit bomb", + { NextAction("icc lich king spirit bomb", ACTION_RAID +7) })); } void RaidIccStrategy::InitMultipliers(std::vector& multipliers) @@ -183,4 +196,6 @@ void RaidIccStrategy::InitMultipliers(std::vector& multipliers) multipliers.push_back(new IccValithriaDreamCloudMultiplier(botAI)); multipliers.push_back(new IccSindragosaMultiplier(botAI)); multipliers.push_back(new IccLichKingAddsMultiplier(botAI)); + multipliers.push_back(new IccLichKingSpiritBombMultiplier(botAI)); + multipliers.push_back(new IccGunshipMultiplier(botAI)); } diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCStrategy.h similarity index 53% rename from modules/mod-playerbots/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/ICC/ICCStrategy.h index 53967c334..5e36d061c 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Strategy/RaidIccStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCStrategy.h @@ -1,10 +1,13 @@ -#ifndef _PLAYERBOT_RAIDICCSTRATEGY_H -#define _PLAYERBOT_RAIDICCSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCSTRATEGY_H +#define PLAYERBOTS_ICCSTRATEGY_H -#include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" -#include "RaidIccMultipliers.h" class RaidIccStrategy : public Strategy { diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggerContext.h similarity index 78% rename from modules/mod-playerbots/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggerContext.h index 64c320c72..ead7fdff6 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/RaidIccTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggerContext.h @@ -1,9 +1,14 @@ -#ifndef _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCTRIGGERCONTEXT_H +#define PLAYERBOTS_ICCTRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "NamedObjectContext.h" -#include "RaidIccTriggers.h" +#include "ICCTriggers.h" class RaidIccTriggerContext : public NamedObjectContext { @@ -18,27 +23,29 @@ public: creators["icc rotting frost giant tank position"] = &RaidIccTriggerContext::icc_rotting_frost_giant_tank_position; creators["icc in cannon"] = &RaidIccTriggerContext::icc_in_cannon; creators["icc gunship cannon near"] = &RaidIccTriggerContext::icc_gunship_cannon_near; - creators["icc gunship teleport ally"] = &RaidIccTriggerContext::icc_gunship_teleport_ally; - creators["icc gunship teleport horde"] = &RaidIccTriggerContext::icc_gunship_teleport_horde; + creators["icc gunship rocket jump"] = &RaidIccTriggerContext::icc_gunship_rocket_jump; + creators["icc gunship rocket pack setup"] = &RaidIccTriggerContext::icc_gunship_rocket_pack_setup; creators["icc dbs"] = &RaidIccTriggerContext::icc_dbs; creators["icc dbs main tank rune of blood"] = &RaidIccTriggerContext::icc_dbs_main_tank_rune_of_blood; - creators["icc stinky precious main tank mortal wound"] = &RaidIccTriggerContext::icc_stinky_precious_main_tank_mortal_wound; + creators["icc dogs"] = &RaidIccTriggerContext::icc_dogs; creators["icc festergut group position"] = &RaidIccTriggerContext::icc_festergut_group_position; - creators["icc festergut main tank gastric bloat"] = &RaidIccTriggerContext::icc_festergut_main_tank_gastric_bloat; creators["icc festergut spore"] = &RaidIccTriggerContext::icc_festergut_spore; + creators["icc festergut avoid malleable goo"] = &RaidIccTriggerContext::icc_festergut_avoid_malleable_goo; creators["icc rotface tank position"] = &RaidIccTriggerContext::icc_rotface_tank_position; creators["icc rotface group position"] = &RaidIccTriggerContext::icc_rotface_group_position; creators["icc rotface move away from explosion"] = &RaidIccTriggerContext::icc_rotface_move_away_from_explosion; + creators["icc rotface avoid vile gas"] = &RaidIccTriggerContext::icc_rotface_avoid_vile_gas; creators["icc putricide volatile ooze"] = &RaidIccTriggerContext::icc_putricide_volatile_ooze; creators["icc putricide gas cloud"] = &RaidIccTriggerContext::icc_putricide_gas_cloud; creators["icc putricide growing ooze puddle"] = &RaidIccTriggerContext::icc_putricide_growing_ooze_puddle; - creators["icc putricide main tank mutated plague"] = &RaidIccTriggerContext::icc_putricide_main_tank_mutated_plague; + creators["icc putricide mutated plague"] = &RaidIccTriggerContext::icc_putricide_mutated_plague; creators["icc putricide malleable goo"] = &RaidIccTriggerContext::icc_putricide_malleable_goo; + creators["icc putricide abomination"] = &RaidIccTriggerContext::icc_putricide_abomination; creators["icc bpc keleseth tank"] = &RaidIccTriggerContext::icc_bpc_keleseth_tank; creators["icc bpc main tank"] = &RaidIccTriggerContext::icc_bpc_main_tank; @@ -57,21 +64,22 @@ public: creators["icc valithria portal"] = &RaidIccTriggerContext::icc_valithria_portal; creators["icc valithria heal"] = &RaidIccTriggerContext::icc_valithria_heal; creators["icc valithria dream cloud"] = &RaidIccTriggerContext::icc_valithria_dream_cloud; + creators["icc valithria zombie kite"] = &RaidIccTriggerContext::icc_valithria_zombie_kite; creators["icc sindragosa group position"] = &RaidIccTriggerContext::icc_sindragosa_group_position; creators["icc sindragosa frost beacon"] = &RaidIccTriggerContext::icc_sindragosa_frost_beacon; + creators["icc sindragosa hot"] = &RaidIccTriggerContext::icc_sindragosa_hot; creators["icc sindragosa blistering cold"] = &RaidIccTriggerContext::icc_sindragosa_blistering_cold; creators["icc sindragosa unchained magic"] = &RaidIccTriggerContext::icc_sindragosa_unchained_magic; creators["icc sindragosa chilled to the bone"] = &RaidIccTriggerContext::icc_sindragosa_chilled_to_the_bone; creators["icc sindragosa mystic buffet"] = &RaidIccTriggerContext::icc_sindragosa_mystic_buffet; - creators["icc sindragosa main tank mystic buffet"] = &RaidIccTriggerContext::icc_sindragosa_main_tank_mystic_buffet; creators["icc sindragosa frost bomb"] = &RaidIccTriggerContext::icc_sindragosa_frost_bomb; - creators["icc sindragosa tank swap position"] = &RaidIccTriggerContext::icc_sindragosa_tank_swap_position; creators["icc lich king shadow trap"] = &RaidIccTriggerContext::icc_lich_king_shadow_trap; creators["icc lich king necrotic plague"] = &RaidIccTriggerContext::icc_lich_king_necrotic_plague; creators["icc lich king winter"] = &RaidIccTriggerContext::icc_lich_king_winter; creators["icc lich king adds"] = &RaidIccTriggerContext::icc_lich_king_adds; + creators["icc lich king spirit bomb"] = &RaidIccTriggerContext::icc_lich_king_spirit_bomb; } private: @@ -83,27 +91,29 @@ private: static Trigger* icc_rotting_frost_giant_tank_position(PlayerbotAI* ai) { return new IccRottingFrostGiantTankPositionTrigger(ai); } static Trigger* icc_in_cannon(PlayerbotAI* ai) { return new IccInCannonTrigger(ai); } static Trigger* icc_gunship_cannon_near(PlayerbotAI* ai) { return new IccGunshipCannonNearTrigger(ai); } - static Trigger* icc_gunship_teleport_ally(PlayerbotAI* ai) { return new IccGunshipTeleportAllyTrigger(ai); } - static Trigger* icc_gunship_teleport_horde(PlayerbotAI* ai) { return new IccGunshipTeleportHordeTrigger(ai); } + static Trigger* icc_gunship_rocket_jump(PlayerbotAI* ai) { return new IccGunshipRocketJumpTrigger(ai); } + static Trigger* icc_gunship_rocket_pack_setup(PlayerbotAI* ai) { return new IccGunshipRocketPackSetupTrigger(ai); } static Trigger* icc_dbs(PlayerbotAI* ai) { return new IccDbsTrigger(ai); } static Trigger* icc_dbs_main_tank_rune_of_blood(PlayerbotAI* ai) { return new IccDbsMainTankRuneOfBloodTrigger(ai); } - static Trigger* icc_stinky_precious_main_tank_mortal_wound(PlayerbotAI* ai) { return new IccStinkyPreciousMainTankMortalWoundTrigger(ai); } + static Trigger* icc_dogs(PlayerbotAI* ai) { return new IccDogsTrigger(ai); } static Trigger* icc_festergut_group_position(PlayerbotAI* ai) { return new IccFestergutGroupPositionTrigger(ai); } - static Trigger* icc_festergut_main_tank_gastric_bloat(PlayerbotAI* ai) { return new IccFestergutMainTankGastricBloatTrigger(ai); } static Trigger* icc_festergut_spore(PlayerbotAI* ai) { return new IccFestergutSporeTrigger(ai); } + static Trigger* icc_festergut_avoid_malleable_goo(PlayerbotAI* ai) { return new IccFestergutAvoidMalleableGooTrigger(ai); } static Trigger* icc_rotface_tank_position(PlayerbotAI* ai) { return new IccRotfaceTankPositionTrigger(ai); } static Trigger* icc_rotface_group_position(PlayerbotAI* ai) { return new IccRotfaceGroupPositionTrigger(ai); } static Trigger* icc_rotface_move_away_from_explosion(PlayerbotAI* ai) { return new IccRotfaceMoveAwayFromExplosionTrigger(ai); } + static Trigger* icc_rotface_avoid_vile_gas(PlayerbotAI* ai) { return new IccRotfaceAvoidVileGasTrigger(ai); } static Trigger* icc_putricide_volatile_ooze(PlayerbotAI* ai) { return new IccPutricideVolatileOozeTrigger(ai); } static Trigger* icc_putricide_gas_cloud(PlayerbotAI* ai) { return new IccPutricideGasCloudTrigger(ai); } static Trigger* icc_putricide_growing_ooze_puddle(PlayerbotAI* ai) { return new IccPutricideGrowingOozePuddleTrigger(ai); } - static Trigger* icc_putricide_main_tank_mutated_plague(PlayerbotAI* ai) { return new IccPutricideMainTankMutatedPlagueTrigger(ai); } + static Trigger* icc_putricide_mutated_plague(PlayerbotAI* ai) { return new IccPutricideMutatedPlagueTrigger(ai); } static Trigger* icc_putricide_malleable_goo(PlayerbotAI* ai) { return new IccPutricideMalleableGooTrigger(ai); } + static Trigger* icc_putricide_abomination(PlayerbotAI* ai) { return new IccPutricideAbominationTrigger(ai); } static Trigger* icc_bpc_keleseth_tank(PlayerbotAI* ai) { return new IccBpcKelesethTankTrigger(ai); } static Trigger* icc_bpc_main_tank(PlayerbotAI* ai) { return new IccBpcMainTankTrigger(ai); } @@ -121,22 +131,23 @@ private: static Trigger* icc_valithria_group(PlayerbotAI* ai) { return new IccValithriaGroupTrigger(ai); } static Trigger* icc_valithria_portal(PlayerbotAI* ai) { return new IccValithriaPortalTrigger(ai); } static Trigger* icc_valithria_heal(PlayerbotAI* ai) { return new IccValithriaHealTrigger(ai); } + static Trigger* icc_valithria_zombie_kite(PlayerbotAI* ai) { return new IccValithriaZombieKiteTrigger(ai); } static Trigger* icc_valithria_dream_cloud(PlayerbotAI* ai) { return new IccValithriaDreamCloudTrigger(ai); } static Trigger* icc_sindragosa_group_position(PlayerbotAI* ai) { return new IccSindragosaGroupPositionTrigger(ai); } static Trigger* icc_sindragosa_frost_beacon(PlayerbotAI* ai) { return new IccSindragosaFrostBeaconTrigger(ai); } + static Trigger* icc_sindragosa_hot(PlayerbotAI* ai) { return new IccSindragosaHotTrigger(ai); } static Trigger* icc_sindragosa_blistering_cold(PlayerbotAI* ai) { return new IccSindragosaBlisteringColdTrigger(ai); } static Trigger* icc_sindragosa_unchained_magic(PlayerbotAI* ai) { return new IccSindragosaUnchainedMagicTrigger(ai); } static Trigger* icc_sindragosa_chilled_to_the_bone(PlayerbotAI* ai) { return new IccSindragosaChilledToTheBoneTrigger(ai); } static Trigger* icc_sindragosa_mystic_buffet(PlayerbotAI* ai) { return new IccSindragosaMysticBuffetTrigger(ai); } - static Trigger* icc_sindragosa_main_tank_mystic_buffet(PlayerbotAI* ai) { return new IccSindragosaMainTankMysticBuffetTrigger(ai); } static Trigger* icc_sindragosa_frost_bomb(PlayerbotAI* ai) { return new IccSindragosaFrostBombTrigger(ai); } - static Trigger* icc_sindragosa_tank_swap_position(PlayerbotAI* ai) { return new IccSindragosaTankSwapPositionTrigger(ai); } static Trigger* icc_lich_king_shadow_trap(PlayerbotAI* ai) { return new IccLichKingShadowTrapTrigger(ai); } static Trigger* icc_lich_king_necrotic_plague(PlayerbotAI* ai) { return new IccLichKingNecroticPlagueTrigger(ai); } static Trigger* icc_lich_king_winter(PlayerbotAI* ai) { return new IccLichKingWinterTrigger(ai); } static Trigger* icc_lich_king_adds(PlayerbotAI* ai) { return new IccLichKingAddsTrigger(ai); } + static Trigger* icc_lich_king_spirit_bomb(PlayerbotAI* ai) { return new IccLichKingSpiritBombTrigger(ai); } }; diff --git a/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.cpp new file mode 100644 index 000000000..1526f4286 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.cpp @@ -0,0 +1,1101 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ICCTriggers.h" +#include "ICCActions.h" +#include "NearestNpcsValue.h" +#include "PlayerbotAIConfig.h" +#include "ObjectAccessor.h" +#include "GenericTriggers.h" +#include "Playerbots.h" +#include "Trigger.h" +#include "GridNotifiers.h" +#include "Vehicle.h" +#include "ICCScripts.h" + +//Lord Marrogwar +bool IccLmTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); + if (!boss) + return false; + + IccStripExperienced(bot); + + return true; +} + +//Lady Deathwhisper +bool IccDarkReckoningTrigger::IsActive() +{ + if (bot->HasAura(SPELL_DARK_RECKONING)) + return true; + + return false; +} + +bool IccLadyDeathwhisperTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); + if (!boss) + return false; + + return true; +} + +//Gunship Battle +bool IccRottingFrostGiantTankPositionTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotting frost giant"); + if (!boss) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccInCannonTrigger::IsActive() +{ + Unit* vehicleBase = bot->GetVehicleBase(); + Vehicle* vehicle = bot->GetVehicle(); + if (!vehicleBase || !vehicle) + return false; + + uint32 entry = vehicleBase->GetEntry(); + return entry == NPC_CANNONA || entry == NPC_CANNONH; +} + +bool IccGunshipCannonNearTrigger::IsActive() +{ + if (bot->GetVehicle()) + return false; + + Unit* mount1 = bot->FindNearestCreature(NPC_CANNONA, 100.0f); + Unit* mount2 = bot->FindNearestCreature(NPC_CANNONH, 100.0f); + + if (!mount1 && !mount2) + return false; + + // If cannons have Below Zero aura, don't try to enter them + Unit* friendlyCannon = nullptr; + if (mount1 && mount1->IsFriendlyTo(bot)) + friendlyCannon = mount1; + else if (mount2 && mount2->IsFriendlyTo(bot)) + friendlyCannon = mount2; + + if (friendlyCannon && friendlyCannon->HasAura(SPELL_BELOW_ZERO)) + return false; + + if (!botAI->IsDps(bot)) + return false; + + return true; +} + +bool IccGunshipRocketJumpTrigger::IsActive() +{ + // The rocket jump mechanic is only needed when the gunship battle is active. + // We detect which ship we are on by checking which enemy boss is present: + // - Saurfang hostile => we are on the Alliance ship + // - Muradin hostile => we are on the Horde ship + // Using the hostile boss (not cannon friendliness) avoids conflicting with + // the cannon-near trigger that fires on the same condition. + Unit* saurfang = bot->FindNearestCreature(NPC_HIGH_OVERLORD_SAURFANG, 100.0f); + if (saurfang && saurfang->IsAlive() && saurfang->IsHostileTo(bot)) + return true; + + Unit* muradin = bot->FindNearestCreature(NPC_MURADIN_BRONZEBEARD, 100.0f); + if (muradin && muradin->IsAlive() && muradin->IsHostileTo(bot)) + return true; + + return false; +} + +bool IccGunshipRocketPackSetupTrigger::IsActive() +{ + // Fires any time a bot is standing on a friendly gunship deck, regardless of + // combat state. Lets bots walk to Zafod and equip the rocket pack before the + // encounter starts (and keep it ready if they acquire it mid-fight). + Unit* cannonA = bot->FindNearestCreature(NPC_CANNONA, 100.0f); + if (cannonA && cannonA->IsFriendlyTo(bot)) + return true; + + Unit* cannonH = bot->FindNearestCreature(NPC_CANNONH, 100.0f); + if (cannonH && cannonH->IsFriendlyTo(bot)) + return true; + + return false; +} + +//DBS +bool IccDbsTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); + if (!boss) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccDbsMainTankRuneOfBloodTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); + + if (!boss) + return false; + + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* mt = AI_VALUE(Unit*, "main tank"); + if (!mt) + return false; + + Aura* aura = botAI->GetAura("rune of blood", mt); + if (!aura) + return false; + + return true; +} + +bool IccDogsTrigger::IsActive() +{ + if (AI_VALUE2(Unit*, "find target", "stinky") || AI_VALUE2(Unit*, "find target", "precious")) + return true; + + return false; +} + +//FESTERGUT +bool IccFestergutGroupPositionTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccFestergutSporeTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss || botAI->IsTank(bot)) + return false; + + return IccAnyGroupMemberHasAura(bot, SPELL_GAS_SPORE); +} + +bool IccFestergutAvoidMalleableGooTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); + if (!boss) + return false; + + // Tanks hold the boss at the fixed tank spot; goo can land on tanks but + // moving would lose threat and let goo land on melee stack anyway. + if (botAI->IsTank(bot)) + return false; + + // During spore phase, position switching handles goo avoidance — free-dodge + // would pull bots out of their assigned spore spots. + if (IccAnyGroupMemberHasAura(bot, SPELL_GAS_SPORE)) + return false; + + constexpr uint32 impactLifetimeMs = 8000; + constexpr float gooDangerRadius = 12.0f; + + uint32 now = getMSTime(); + float botX = bot->GetPositionX(); + float botY = bot->GetPositionY(); + ObjectGuid botGuid = bot->GetGUID(); + + IcecrownHelpers::IccInstanceState& st = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()); + + for (auto const& impact : st.malleableGoo) + { + if (getMSTimeDiff(impact.castTime, now) > impactLifetimeMs) + continue; + float dx = botX - impact.position.GetPositionX(); + float dy = botY - impact.position.GetPositionY(); + if (dx * dx + dy * dy < gooDangerRadius * gooDangerRadius) + { + uint32 waitUntil = impact.castTime + impactLifetimeMs; + auto& slot = st.festergutGooWaitUntil[botGuid]; + if (waitUntil > slot) + slot = waitUntil; + return true; + } + } + + auto it = st.festergutGooWaitUntil.find(botGuid); + if (it != st.festergutGooWaitUntil.end()) + { + if (now < it->second) + return true; + st.festergutGooWaitUntil.erase(it); + } + + return false; +} + +//ROTFACE +bool IccRotfaceTankPositionTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss || !(botAI->IsTank(bot) || botAI->IsMelee(bot))) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccRotfaceGroupPositionTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss) + return false; + + return true; +} + +bool IccRotfaceMoveAwayFromExplosionTrigger::IsActive() +{ + Creature* boss = bot->FindNearestCreature(NPC_BIG_OOZE, 100.0f); + bool castingNow = boss && boss->IsAlive() && + boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION); + + if (castingNow) + { + _wasCasting = true; + _castEndTime = 0; + return true; + } + + // Cast just ended — record the time + if (_wasCasting) + { + _wasCasting = false; + if (_castEndTime == 0) + _castEndTime = time(nullptr); + } + + // Stay active for 6 seconds after cast ended (2s wait + return movement) + if (_castEndTime > 0 && time(nullptr) - _castEndTime < 6) + return true; + + _castEndTime = 0; + return false; +} + +bool IccRotfaceAvoidVileGasTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); + if (!boss) + return false; + + uint32 const now = getMSTime(); + + IcecrownHelpers::IccInstanceState& st = IcecrownHelpers::IccState(bot->GetMap()->GetInstanceId()); + bool const isVictim = + st.rotfaceVileGas.victimGuid == bot->GetGUID() && + getMSTimeDiff(st.rotfaceVileGas.castTime, now) < 8000; + if (isVictim) + return true; + + if (botAI->HasAura("Vile Gas", bot)) + return true; + + auto const& waitMap = st.rotfaceVileGasWaitUntil; + auto it = waitMap.find(bot->GetGUID()); + if (it != waitMap.end() && now < it->second) + return true; + + return false; +} + +//PP + +bool IccPutricideGrowingOozePuddleTrigger::IsActive() +{ + // Early return if boss doesn't exist + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& npc : npcs) + { + if (Unit* unit = botAI->GetUnit(npc)) + { + const uint32 entry = unit->GetEntry(); + if (entry == NPC_GROWING_OOZE_PUDDLE) + return true; + } + } + + return false; +} + +bool IccPutricideVolatileOozeTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "volatile ooze"); + if (!boss) + return false; + + if (botAI->HasAura("Gaseous Bloat", bot)) + return false; + + return true; +} + +bool IccPutricideGasCloudTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "gas cloud"); + if (!boss) + return false; + + Unit* boss1 = AI_VALUE2(Unit*, "find target", "volatile ooze"); + + bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); + + if (hasGaseousBloat && boss1) + return true; + + if (boss1) + return false; + + return true; +} + +bool IccPutricideMutatedPlagueTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "professor putricide") != nullptr; +} + +bool IccPutricideMalleableGooTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + Difficulty const diff = bot->GetRaidDifficulty(); + + // Heroic cheat buffs — apply to all group members (bots + real players) + if (boss && boss->IsInCombat() && sPlayerbotAIConfig.EnableICCBuffs && + (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + if (Group* buffGroup = bot->GetGroup()) + { + for (GroupReference* itr = buffGroup->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !member->IsInWorld()) + continue; + + IccApplyHeroicBuffToMember(botAI, member, false, true); + + if (PlayerbotAI::IsTank(member) && !member->HasAura(SPELL_SPITEFULL_FURY) && + boss->GetVictim() != member) + member->AddAura(SPELL_SPITEFULL_FURY, member); + } + } + } + + return true; +} + +bool IccPutricideAbominationTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); + if (!boss) + return false; + + if (!botAI->IsAssistTank(bot)) + return false; + + // Already piloting - keep action firing until vehicle drops. + if (Unit* veh = bot->GetVehicleBase()) + { + uint32 e = veh->GetEntry(); + if (e == NPC_MUTATED_ABOMINATION_10 || e == NPC_MUTATED_ABOMINATION_25) + return true; + } + + // Phase 3: boss takes toy back. No transformation. + if (boss->HealthBelowPct(35)) + return false; + + // Someone else already piloting - do not drink. + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* m = itr->GetSource(); + if (!m || m == bot || !m->IsAlive()) + continue; + if (Unit* vb = m->GetVehicleBase()) + { + uint32 e = vb->GetEntry(); + if (e == NPC_MUTATED_ABOMINATION_10 || e == NPC_MUTATED_ABOMINATION_25) + return false; + } + } + } + + // Require at least one Growing Ooze Puddle nearby. + GuidVector const npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + for (auto const& g : npcs) + { + if (Unit* u = botAI->GetUnit(g)) + if (u->GetEntry() == NPC_GROWING_OOZE_PUDDLE) + return true; + } + + return false; +} + +//BPC +bool IccBpcKelesethTankTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); + if (!boss) + return false; + + IccStripExperienced(bot); + + if (!botAI->IsAssistTank(bot)) + return false; + + Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); + if (aura) + if (aura->GetStackAmount() > 18) + return false; + + return true; +} + +bool IccBpcMainTankTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + if (!IccAnyBloodPrincePresent(botAI)) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccBpcEmpoweredVortexTrigger::IsActive() +{ + // Tanks should ignore this mechanic + if (botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) + return false; + + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + if (!valanar) + return false; + + Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); + if (aura) + if (aura->GetStackAmount() > 12) + return false; + + Aura* auraValanar = botAI->GetAura("Invocation of Blood", valanar); + if (!auraValanar) + return false; + + return true; +} + +bool IccBpcKineticBombTrigger::IsActive() +{ + if (!IccAnyBloodPrincePresent(botAI)) + return false; + + if (!botAI->IsRanged(bot) || botAI->IsHeal(bot)) + return false; + + // Allow up to 18 stacks for bomb-assigned bots (multiplier handles assignment) + if (Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true)) + { + if (aura->GetStackAmount() > 18) + return false; + } + + static const std::array bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, + NPC_KINETIC_BOMB4}; + const GuidVector bombs = AI_VALUE(GuidVector, "possible targets no los"); + + bool bombFound = false; + + for (const auto entry : bombEntries) + { + for (auto const& guid : bombs) + { + if (Unit* unit = botAI->GetUnit(guid)) + { + if (unit->GetEntry() == entry) + { + // Check if bomb is within valid Z-axis range + if (unit->GetPositionZ() - bot->GetPositionZ() < 35.0f) + { + bombFound = true; + break; + } + } + } + } + if (bombFound) + break; + } + + return botAI->IsRangedDps(bot) && bombFound; +} + +bool IccBpcBallOfFlameTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); + Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); + Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); + + if (!(valanar || taldaram || keleseth)) + return false; + + Aura* auraTaldaram = botAI->GetAura("Invocation of Blood", taldaram); + if (!auraTaldaram) + return false; + + return true; +} + +// BQL +bool IccBqlGroupPositionTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); + if (!boss) + return false; + + if (IccAnyBloodPrincePresent(botAI)) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccBqlPactOfDarkfallenTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); + if (!boss) + return false; + + if (IccAnyBloodPrincePresent(botAI)) + return false; + + Aura* aura = botAI->GetAura("Pact of the Darkfallen", bot); + if (!aura) + return false; + + return true; +} + +bool IccBqlVampiricBiteTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); + if (!boss) + return false; + + if (IccAnyBloodPrincePresent(botAI)) + return false; + + Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); + if (!aura) + return false; + + return true; +} + +// Sister Svalna +bool IccValkyreSpearTrigger::IsActive() +{ + // Check if there's a spear nearby + if (bot->FindNearestCreature(NPC_SPEAR, 100.0f)) + return true; + + return false; +} + +bool IccSisterSvalnaTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sister svalna"); + if (!boss) + return false; + + if (bot->GetExactDist2d(boss) > 30.0f) + return false; + + return true; +} + +// VDW +bool IccValithriaGroupTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + IccStripExperienced(bot); + + return true; +} + +bool IccValithriaZombieKiteTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + if (botAI->IsTank(bot)) + return false; + + std::list zombies; + bot->GetCreatureListWithEntryInGrid(zombies, NPC_BLISTERING_ZOMBIE, 100.0f); + for (Creature* z : zombies) + { + if (z && z->IsAlive() && z->GetVictim() == bot) + return true; + } + + return false; +} + +bool IccValithriaPortalTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("Twisted Nightmares", bot, false, true); + if (aura && aura->GetStackAmount() >= 25) + return false; + + // Only healers should use portals + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE)) + return false; + + Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); + Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); + + if ((worm && worm->GetVictim() == bot) || (zombie && zombie->GetVictim() == bot)) + return false; + + auto const shouldHealRaid = IccValithriaShouldHealRaid(bot, botAI); + if (!shouldHealRaid.has_value() || *shouldHealRaid) + return false; + + // Find the nearest portal creature + Creature* portal1 = bot->FindNearestCreature(NPC_DREAM_PORTAL, 100.0f); + if (!portal1) + portal1 = bot->FindNearestCreature(NPC_DREAM_PORTAL_PRE_EFFECT, 100.0f); + + Creature* portal2 = bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 100.0f); + if (!portal2) + portal2 = bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL_PRE_EFFECT, 100.0f); + + return portal1 || portal2; +} + +bool IccValithriaHealTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (!boss) + return false; + + // Only healers should use healing + if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE) || bot->HealthBelowPct(50)) + return false; + + Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); + Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); + + if ((worm && worm->GetVictim() == bot) || (zombie && zombie->GetVictim() == bot)) + return false; + + auto const shouldHealRaid = IccValithriaShouldHealRaid(bot, botAI); + if (!shouldHealRaid.has_value() || *shouldHealRaid) + return false; + + if (bot->GetHealthPct() < 50.0f) + return false; + + // For Valithria healers, check portal logic + // If no portal is found within 100 yards, we should heal + if (!bot->FindNearestCreature(NPC_DREAM_PORTAL, 100.0f) && !bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 100.0f)) + return true; + + if (bot->FindNearestCreature(NPC_DREAM_PORTAL, 10.0f) || bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 10.0f)) + return false; + + // If portal is far but within 100 yards, heal while moving to it + return true; +} + +bool IccValithriaDreamCloudTrigger::IsActive() +{ + if (!bot->HasAura(SPELL_DREAM_STATE) || bot->HealthBelowPct(50)) + return false; + + return true; +} + +//SINDRAGOSA +bool IccSindragosaGroupPositionTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); // sindra + if (!boss) + return false; + + Difficulty diff = bot->GetRaidDifficulty(); + + if (sPlayerbotAIConfig.EnableICCBuffs && boss->IsInCombat() && diff && + (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) + { + //-------CHEAT------- + // Apply to every alive group member so real players benefit too, + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !member->IsInWorld()) + continue; + + IccApplyHeroicBuffToMember(botAI, member, false, true); + + if (botAI->IsMainTank(member) && boss->GetVictim() != member && + !member->HasAura(SPELL_SPITEFULL_FURY)) + member->AddAura(SPELL_SPITEFULL_FURY, member); + } + } + //-------CHEAT------- + } + + // Air phase: give all tanks nitro boosts so they can quickly reposition to tombs + if (boss->IsInCombat() && botAI->IsTank(bot) && + boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f) + { + if (!bot->HasAura(SPELL_NITRO_BOOSTS)) + bot->AddAura(SPELL_NITRO_BOOSTS, bot); + } + + // Last phase: the main tank must keep tanking, never run to a tomb spot. + // Strip its Frost Beacon so the tomb-positioning logic doesn't apply. The + // assist tank keeps its beacon and is allowed to move to the beacon spot. + if (botAI->IsMainTank(bot) && bot->HasAura(SPELL_FROST_BEACON) && boss->HealthBelowPct(35) && + boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) >= 30.0f) + bot->RemoveAura(SPELL_FROST_BEACON); + + if (botAI->IsTank(bot)) + { + // Strip Mystic Buffet: the cheat that makes single-tanking P3 survivable. + if (Aura* aura = botAI->GetAura("mystic buffet", bot, false, false)) + bot->RemoveAura(aura->GetId()); + + // Shield tanks while a tomb is up, Blistering Cold is casting, or a last + // phase beacon is out (tomb about to form, healers already scattering). + bool const shield = (boss->HasUnitState(UNIT_STATE_CASTING) && IccBossCastingBlisteringCold(boss)) || + (boss->HealthBelowPct(35) && IccAnyGroupMemberHasAura(bot, SPELL_FROST_BEACON)) || + !IccGetCreaturesByEntries(bot, {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}, 150.0f).empty(); + if (shield && !bot->HasAura(SPELL_MAGIC_BARRIER)) + bot->AddAura(SPELL_MAGIC_BARRIER, bot); + else if (!shield && bot->HasAura(SPELL_MAGIC_BARRIER)) + bot->RemoveAura(SPELL_MAGIC_BARRIER); + + // Assist tanks generate no threat while another main tank is alive, so + // they can never win the threat race at the pull; dropped on promotion. + bool mtAlive = false; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member != bot && member->IsAlive() && botAI->IsMainTank(member)) + { + mtAlive = true; + break; + } + } + } + + if (mtAlive && !botAI->IsMainTank(bot)) + { + if (!bot->HasAura(SPELL_NO_THREAT)) + bot->AddAura(SPELL_NO_THREAT, bot); + } + else if (bot->HasAura(SPELL_NO_THREAT)) + bot->RemoveAura(SPELL_NO_THREAT); + } + + if (!boss || bot->HasAura(SPELL_FROST_BEACON) || boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f) + return false; + + return true; +} + +bool IccSindragosaFrostBeaconTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); + if (!boss) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + if (bot->HasAura(SPELL_FROST_BEACON)) + return true; + + Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); + for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); ++itr) + { + Player* member = ObjectAccessor::FindPlayer(itr->guid); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_FROST_BEACON)) // If any player has Frost Beacon, keep trigger active + return true; + } + + return false; +} + +bool IccSindragosaHotTrigger::IsActive() +{ + if (!botAI->IsHeal(bot)) + return false; + + if (bot->HasAura(SPELL_FROST_BEACON)) + return false; + + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); + if (!boss) + return false; + + return IccAnyGroupMemberHasAura(bot, SPELL_FROST_BEACON); +} + +bool IccSindragosaBlisteringColdTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + if (botAI->IsTank(bot)) + return false; + + // Don't move if any bot in group has ice tomb + Group* group = bot->GetGroup(); + if (!group) + return false; + + float dist = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); + + if (dist >= 33.0f) + return false; + + bool isCasting = false; + if (boss && boss->HasUnitState(UNIT_STATE_CASTING)) + isCasting = true; + + bool isBlisteringCold = IccBossCastingBlisteringCold(boss); + + return isCasting && isBlisteringCold; +} + +bool IccSindragosaUnchainedMagicTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("unchained magic", bot, false, false); + if (!aura) + return false; + + bool isBlisteringCold = IccBossCastingBlisteringCold(boss); + + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && isBlisteringCold) + return false; + + return true; +} + +bool IccSindragosaChilledToTheBoneTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + Aura* aura = botAI->GetAura("Chilled to the Bone", bot, false, false); + if (!aura) + return false; + + bool isBlisteringCold = IccBossCastingBlisteringCold(boss); + + if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && isBlisteringCold) + return false; + + return true; +} + +bool IccSindragosaMysticBuffetTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); + if (!boss) + return false; + + if (botAI->IsTank(bot)) + return false; + + if (boss->GetVictim() == bot) + return false; + + // Last phase only. Do not gate on the bot's own Mystic Buffet aura: + // hiding sheds it, so aura-less bots must still run the action or nobody + // is left to mark and kill the tomb once the raid has shed its stacks. + if (!boss->HealthBelowPct(35)) + return false; + + if (bot->HasAura(SPELL_FROST_BEACON)) + return false; + + // Blistering Cold takes priority over tomb-hiding in the last phase: + // skip hiding so the bot can run to the safe spot instead. + if (boss->HasUnitState(UNIT_STATE_CASTING) && IccBossCastingBlisteringCold(boss)) + return false; + + return true; +} + +bool IccSindragosaFrostBombTrigger::IsActive() +{ + Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); + if (!boss) + return false; + + if (!bot->IsAlive()) // Skip if dead + return false; + + // Tombed bots intentionally pass through: the action pins their group to + // the current tomb's zone so when freed they don't migrate to the wrong + // zone. The action returns false for tombed bots without moving them. + if (boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) + return true; + + return false; +} + +//LK + +bool IccLichKingShadowTrapTrigger::IsActive() +{ + Unit* vdw = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (vdw) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return false; + + bool hasPlague = botAI->HasAura("Necrotic Plague", bot); + if (hasPlague) + return false; + + if (!botAI->IsMainTank(bot)) + return false; + + if (boss->HealthBelowPct(65)) + return false; + + // search for all nearby traps + GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); + std::vector nearbyTraps; + + for (auto& npc : npcs) + { + Unit* unit = botAI->GetUnit(npc); + if (!unit || !unit->IsAlive()) + continue; + + if (unit->GetEntry() == NPC_SHADOW_TRAP && bot->GetExactDist2d(unit) < 11.0f) // shadow trap + return true; + } + + return false; +} + +bool IccLichKingNecroticPlagueTrigger::IsActive() +{ + Unit* vdw = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (vdw) + return false; + + if (!AI_VALUE2(Unit*, "find target", "the lich king")) + return false; + + return botAI->HasAura("Necrotic Plague", bot); +} + +bool IccLichKingWinterTrigger::IsActive() +{ + Unit* vdw = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (vdw) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); + if (!boss) + return false; + + return IccBossHasRemorselessWinter(boss); +} + +bool IccLichKingAddsTrigger::IsActive() +{ + Unit* vdw = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); + if (vdw) + return false; + + if (bot->HasAura(SPELL_HARVEST_SOUL_VALKYR)) + return false; + + if (botAI->HasAura("Necrotic Plague", bot)) + return false; + + if (bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f) || + bot->FindNearestCreature(NPC_TERENAS_MENETHIL, 55.0f)) + return true; + + if (!bot->FindNearestCreature(NPC_THE_LICH_KING, 100.0f)) + return false; + + return true; +} + +bool IccLichKingSpiritBombTrigger::IsActive() +{ + return IccLichKingSpiritBombAction::IsBombThreatActive(botAI, bot); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.h b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.h similarity index 73% rename from modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.h index cd332e004..c9f5bb16e 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/ICC/ICCTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDICCTRIGGERS_H -#define _PLAYERBOT_RAIDICCTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ICCTRIGGERS_H +#define PLAYERBOTS_ICCTRIGGERS_H #include "PlayerbotAI.h" #include "Playerbots.h" @@ -9,11 +15,23 @@ enum CreatureIdsICC { // Lord Marrowgar - NPC_SPIKE1 = 36619, - NPC_SPIKE2 = 38711, - NPC_SPIKE3 = 38712, + NPC_SPIKE1 = 36619, // 10N base + NPC_SPIKE1_10H = 38233, // 10H + NPC_SPIKE1_25N = 38459, // 25N + NPC_SPIKE1_25H = 38460, // 25H + NPC_SPIKE2 = 38711, // 25N base + NPC_SPIKE2_10H = 38970, + NPC_SPIKE2_25N = 38971, + NPC_SPIKE2_25H = 38972, + NPC_SPIKE3 = 38712, // 25N base + NPC_SPIKE3_10H = 38973, + NPC_SPIKE3_25N = 38974, + NPC_SPIKE3_25H = 38975, + NPC_COLDFLAME = 36672, // Lady Deathwhisper + NPC_DARNAVAN_10 = 38472, + NPC_DARNAVAN_25 = 38485, NPC_SHADE = 38222, // Gunship Battle @@ -29,6 +47,8 @@ enum CreatureIdsICC NPC_CANNONH = 36839, NPC_MURADIN_BRONZEBEARD = 36948, NPC_HIGH_OVERLORD_SAURFANG = 36939, + NPC_ZAFOD_BOOMBOX = 37184, + ITEM_GOBLIN_ROCKET_PACK = 49278, // Deathbringer Saurfang NPC_BLOOD_BEAST1 = 38508, @@ -38,9 +58,11 @@ enum CreatureIdsICC // Rotface NPC_PUDDLE = 37013, + NPC_SMALL_OOZE = 36897, NPC_BIG_OOZE = 36899, // Putricide + NPC_PROFESSOR_PUTRICIDE = 36678, NPC_MALLEABLE_OOZE_STALKER = 38556, NPC_GROWING_OOZE_PUDDLE = 37690, NPC_CHOKING_GAS_BOMB = 38159, @@ -56,6 +78,7 @@ enum CreatureIdsICC NPC_KINETIC_BOMB4 = 38777, NPC_BALL_OF_FLAME = 38332, NPC_BALL_OF_INFERNO_FLAME = 38451, + NPC_SHOCK_VORTEX = 38422, // Blood Queen Lana'thel NPC_SWARMING_SHADOWS = 38163, @@ -133,26 +156,48 @@ enum SpellIdsICC SPELL_NO_THREAT = 70115, //reduce threat SPELL_SPITEFULL_FURY = 36886, //500% more threat SPELL_NITRO_BOOSTS = 54861, //Speed + SPELL_MAGIC_BARRIER = 38112, //damage reduction shield + SPELL_FROST_TRAP1 = 13809, //Hunter slow trap SPELL_PAIN_SUPPRESION = 69910, //40% dmg reduction SPELL_AGEIS_OF_DALARAN = 71638, //268 all ress SPELL_CYCLONE = 33786, SPELL_HAMMER_OF_JUSTICE = 10308, //stun + // Taunt spells (used to reset cooldowns for assist tank) + SPELL_TAUNT_WARRIOR = 355, + SPELL_TAUNT_PALADIN = 62124, // Hand of Reckoning + SPELL_TAUNT_DK = 56222, // Dark Command + SPELL_TAUNT_DRUID = 6795, // Growl + SPELL_VIPER_STING = 3034, + + // Lord Marrowgar + SPELL_LM_IMPALED = 69065, // Lady Deathwhisper SPELL_DARK_RECKONING = 69483, + SPELL_TOUCH_OF_INSIGNIFICANCE = 71204, // Gunship Battle SPELL_DEATH_PLAGUE = 72865, + SPELL_FROZEN_CANNON = 69704, SPELL_BELOW_ZERO = 69705, + SPELL_ROCKET_PACK_USE = 68645, + SPELL_ROCKET_PACK_USEABLE = 70348, + SPELL_BATTLE_FURY1 = 69637, + SPELL_BATTLE_FURY2 = 69638, + SPELL_BATTLE_FURY3 = 72306, + SPELL_BATTLE_FURY4 = 72307, + SPELL_BATTLE_FURY5 = 72308, // Festergut SPELL_GAS_SPORE = 69279, - // Rotface SPELL_SLIME_SPRAY = 69508, SPELL_OOZE_FLOOD = 71215, SPELL_UNSTABLE_OOZE_EXPLOSION = 69839, SPELL_OOZE_FLOOD_VISUAL = 69785, + // Cast by Professor Putricide from balcony during Rotface heroic. + // Single ID (no difficulty variants in spelldifficulty_dbc). + SPELL_VILE_GAS_H = 69240, // Putricide SPELL_MALLEABLE_GOO = 70852, @@ -166,6 +211,7 @@ enum SpellIdsICC // Blood Queen Lana'thel SPELL_PACT_OF_THE_DARKFALLEN = 71340, + SPELL_BLOODBOLT_WHIRL = 71772, // Sister Svalna SPELL_AETHER_SHIELD = 71463, @@ -173,6 +219,7 @@ enum SpellIdsICC // Valithria Dreamwalker SPELL_DREAM_STATE = 70766, SPELL_EMERALD_VIGOR = 70873, + SPELL_ACID_BURST = 70744, // Sindragosa SPELL_FROST_BEACON = 70126, @@ -182,6 +229,9 @@ enum SpellIdsICC SPELL_BLISTERING_COLD2 = 71047, SPELL_BLISTERING_COLD3 = 71048, SPELL_BLISTERING_COLD4 = 71049, + SPELL_HAND_OF_FREEDOM = 1044, + ITEM_RED_SMOKE_FLARE = 23769, + ITEM_BLUE_SMOKE_FLARE = 23770, // The Lich King SPELL_HARVEST_SOUL_VALKYR = 68985, @@ -194,12 +244,26 @@ enum SpellIdsICC SPELL_REMORSELESS_WINTER6 = 74270, SPELL_REMORSELESS_WINTER7 = 74271, SPELL_REMORSELESS_WINTER8 = 74272, + SPELL_VALKYR_CARRY = 30440, + SPELL_HARVEST_SOUL_LK = 68980, + SPELL_HARVEST_SOULS_LK_25 = 73654, + SPELL_HARVEST_SOULS_LK_H1 = 74295, + SPELL_HARVEST_SOULS_LK_H2 = 74296, + SPELL_HARVEST_SOULS_LK_H3 = 74297, }; -const uint32 DEFILE_AURAS[] = {72756, 74162, 74163, 74164}; -const uint32 DEFILE_CAST_ID = 72762; -const uint32 DEFILE_NPC_ID = 38757; -const size_t DEFILE_AURA_COUNT = 4; +inline constexpr uint32 DEFILE_AURAS[] = {72756, 74162, 74163, 74164}; +inline constexpr uint32 DEFILE_CAST_ID = 72762; +inline constexpr uint32 DEFILE_NPC_ID = 38757; +inline constexpr size_t DEFILE_AURA_COUNT = 4; + +// Malleable Goo (Putricide / Festergut-heroic). Multiple variants because of +// SpellDifficulty remapping and the balcony stalker variant. +inline constexpr uint32 SPELL_MALLEABLE_GOO_10N = 70852; +inline constexpr uint32 SPELL_MALLEABLE_GOO_25N = 72297; +inline constexpr uint32 SPELL_MALLEABLE_GOO_10H = 74280; +inline constexpr uint32 SPELL_MALLEABLE_GOO_25H = 74281; +inline constexpr uint32 SPELL_MALLEABLE_GOO_BALCONY = 72296; // All fanatics and adherents entry ids Lady Deathwhisper static const std::array addEntriesLady = { @@ -212,10 +276,8 @@ const std::vector spellEntriesFlood = { 69799, 69801, 69802, 69795}; const std::vector availableTargetsGS = { - NPC_KOR_KRON_AXETHROWER, NPC_KOR_KRON_ROCKETEER, NPC_KOR_KRON_BATTLE_MAGE, NPC_IGB_HIGH_OVERLORD_SAURFANG, - NPC_SKYBREAKER_RIFLEMAN, NPC_SKYBREAKER_MORTAR_SOLDIER, NPC_SKYBREAKER_SORCERER, NPC_IGB_MURADIN_BRONZEBEARD}; - -static std::vector sporeOrder; + NPC_KOR_KRON_ROCKETEER, NPC_KOR_KRON_AXETHROWER, NPC_KOR_KRON_BATTLE_MAGE, NPC_IGB_HIGH_OVERLORD_SAURFANG, + NPC_SKYBREAKER_MORTAR_SOLDIER, NPC_SKYBREAKER_RIFLEMAN, NPC_SKYBREAKER_SORCERER, NPC_IGB_MURADIN_BRONZEBEARD}; //Lord Marrowgar class IccLmTrigger : public Trigger @@ -262,17 +324,17 @@ public: bool IsActive() override; }; -class IccGunshipTeleportAllyTrigger : public Trigger +class IccGunshipRocketJumpTrigger : public Trigger { public: - IccGunshipTeleportAllyTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc gunship teleport ally") {} + IccGunshipRocketJumpTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc gunship rocket jump") {} bool IsActive() override; }; -class IccGunshipTeleportHordeTrigger : public Trigger +class IccGunshipRocketPackSetupTrigger : public Trigger { public: - IccGunshipTeleportHordeTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc gunship teleport horde") {} + IccGunshipRocketPackSetupTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc gunship rocket pack setup") {} bool IsActive() override; }; @@ -291,11 +353,10 @@ public: bool IsActive() override; }; -//DOGS -class IccStinkyPreciousMainTankMortalWoundTrigger : public Trigger +class IccDogsTrigger : public Trigger { public: - IccStinkyPreciousMainTankMortalWoundTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc stinky precious main tank mortal wound") {} + IccDogsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc dogs") {} bool IsActive() override; }; @@ -321,6 +382,14 @@ public: bool IsActive() override; }; +class IccFestergutAvoidMalleableGooTrigger : public Trigger +{ +public: + IccFestergutAvoidMalleableGooTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "icc festergut avoid malleable goo") {} + bool IsActive() override; +}; + //ROTFACE class IccRotfaceTankPositionTrigger : public Trigger { @@ -339,7 +408,20 @@ public: class IccRotfaceMoveAwayFromExplosionTrigger : public Trigger { public: - IccRotfaceMoveAwayFromExplosionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc rotface move away from explosion") {} + IccRotfaceMoveAwayFromExplosionTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "icc rotface move away from explosion"), _castEndTime(0), _wasCasting(false) {} + bool IsActive() override; + +private: + time_t _castEndTime; + bool _wasCasting; +}; + +class IccRotfaceAvoidVileGasTrigger : public Trigger +{ +public: + IccRotfaceAvoidVileGasTrigger(PlayerbotAI* botAI) + : Trigger(botAI, "icc rotface avoid vile gas") {} bool IsActive() override; }; @@ -365,10 +447,10 @@ public: bool IsActive() override; }; -class IccPutricideMainTankMutatedPlagueTrigger : public Trigger +class IccPutricideMutatedPlagueTrigger : public Trigger { public: - IccPutricideMainTankMutatedPlagueTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc putricide main tank mutated plague") {} + IccPutricideMutatedPlagueTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc putricide mutated plague") {} bool IsActive() override; }; @@ -379,6 +461,13 @@ public: bool IsActive() override; }; +class IccPutricideAbominationTrigger : public Trigger +{ +public: + IccPutricideAbominationTrigger(PlayerbotAI* ai) : Trigger(ai, "icc putricide abomination") {} + bool IsActive() override; +}; + //BPC class IccBpcKelesethTankTrigger : public Trigger { @@ -461,6 +550,13 @@ public: bool IsActive() override; }; +class IccValithriaZombieKiteTrigger : public Trigger +{ +public: + IccValithriaZombieKiteTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc valithria zombie kite") {} + bool IsActive() override; +}; + class IccValithriaPortalTrigger : public Trigger { public: @@ -497,6 +593,13 @@ public: bool IsActive() override; }; +class IccSindragosaHotTrigger : public Trigger +{ +public: + IccSindragosaHotTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc sindragosa hot") {} + bool IsActive() override; +}; + class IccSindragosaBlisteringColdTrigger : public Trigger { public: @@ -525,20 +628,6 @@ public: bool IsActive() override; }; -class IccSindragosaMainTankMysticBuffetTrigger : public Trigger -{ -public: - IccSindragosaMainTankMysticBuffetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc sindragosa main tank mystic buffet") {} - bool IsActive() override; -}; - -class IccSindragosaTankSwapPositionTrigger : public Trigger -{ -public: - IccSindragosaTankSwapPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc sindragosa tank swap position") {} - bool IsActive() override; -}; - class IccSindragosaFrostBombTrigger : public Trigger { public: @@ -575,4 +664,11 @@ public: bool IsActive() override; }; +class IccLichKingSpiritBombTrigger : public Trigger +{ +public: + IccLichKingSpiritBombTrigger(PlayerbotAI* botAI) : Trigger(botAI, "icc lich king spirit bomb") {} + bool IsActive() override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp deleted file mode 100644 index 95f5bc3bf..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.cpp +++ /dev/null @@ -1,9334 +0,0 @@ -#include "RaidIccActions.h" -#include "NearestNpcsValue.h" -#include "ObjectAccessor.h" -#include "Playerbots.h" -#include "Vehicle.h" -#include "RtiValue.h" -#include "GenericSpellActions.h" -#include "GenericActions.h" -#include "RaidIccTriggers.h" -#include "Multiplier.h" - -// Lord Marrowgwar -bool IccLmTankPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); - if (!boss) - return false; - - if (!botAI->IsTank(bot)) - return false; - - const bool isBossInBoneStorm = botAI->GetAura("Bone Storm", boss) != nullptr; - - if (isBossInBoneStorm) - return false; - - if (botAI->HasAggro(boss) && botAI->IsMainTank(bot) && boss->GetVictim() == bot) - { - const float maxDistanceThreshold = 3.0f; - const float distance = bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); - - if (distance > maxDistanceThreshold) - return MoveTowardPosition(ICC_LM_TANK_POSITION, maxDistanceThreshold); - } - - if (botAI->IsAssistTank(bot)) - { - const float maxDistanceThreshold = 3.0f; - const float distance = bot->GetExactDist2d(ICC_LM_TANK_POSITION.GetPositionX(), ICC_LM_TANK_POSITION.GetPositionY()); - - if (distance > maxDistanceThreshold) - return MoveTowardPosition(ICC_LM_TANK_POSITION, maxDistanceThreshold); - - if (distance < maxDistanceThreshold) - { - bot->SetFacingToObject(boss); - return true; - } - } - - return false; -} - -bool IccLmTankPositionAction::MoveTowardPosition(const Position& position, float incrementSize) -{ - // Calculate direction vector - const float dirX = position.GetPositionX() - bot->GetPositionX(); - const float dirY = position.GetPositionY() - bot->GetPositionY(); - const float length = std::sqrt(dirX * dirX + dirY * dirY); - - // Normalize direction vector - const float normalizedDirX = dirX / length; - const float normalizedDirY = dirY / length; - - // Calculate new position with increment - const float moveX = bot->GetPositionX() + normalizedDirX * incrementSize; - const float moveY = bot->GetPositionY() + normalizedDirY * incrementSize; - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); -} - -bool IccSpikeAction::Execute(Event /*event*/) -{ - // If we're impaled, we can't do anything - if (botAI->GetAura("Impaled", bot)) - return false; - - // Find the boss - Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); - if (!boss) - return false; - - const bool isBossInBoneStorm = botAI->GetAura("Bone Storm", boss) != nullptr; - const bool shouldMoveToSafePosition = boss->isInFront(bot) && !botAI->IsTank(bot) && !isBossInBoneStorm; - - if (shouldMoveToSafePosition) - { - const Position safePosition{-390.6757f, 2230.5283f, 0.0f}; // Z value to be overridden by actual bot Z - const float distance = bot->GetExactDist2d(safePosition.GetPositionX(), safePosition.GetPositionY()); - const float maxDistanceThreshold = 3.0f; - - if (distance > maxDistanceThreshold) - return MoveTowardPosition(safePosition, maxDistanceThreshold); - - return false; - } - - if (!botAI->IsTank(bot)) - return false; - - return HandleSpikeTargeting(boss); -} - -bool IccSpikeAction::HandleSpikeTargeting(Unit* boss) -{ - static const std::array spikeEntries = {NPC_SPIKE1, NPC_SPIKE2, NPC_SPIKE3}; - const GuidVector spikes = AI_VALUE(GuidVector, "possible targets no los"); - - Unit* priorityTarget = nullptr; - bool anySpikesExist = false; - - // First check for alive spikes - for (const auto entry : spikeEntries) - { - for (auto const& guid : spikes) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->GetEntry() == entry) - { - anySpikesExist = true; // At least one spike exists - - if (unit->IsAlive()) - { // Only consider alive ones for targeting - priorityTarget = unit; - break; - } - } - } - } - if (priorityTarget) - break; - } - - // Only fallback to boss if NO spikes exist at all (alive or dead) - if (!anySpikesExist && boss->IsAlive()) - priorityTarget = boss; - - // Update raid target icon if needed - if (priorityTarget) - UpdateRaidTargetIcon(priorityTarget); - - return false; -} - -bool IccSpikeAction::MoveTowardPosition(const Position& position, float incrementSize) -{ - // Calculate direction vector - const float dirX = position.GetPositionX() - bot->GetPositionX(); - const float dirY = position.GetPositionY() - bot->GetPositionY(); - const float length = std::sqrt(dirX * dirX + dirY * dirY); - - // Normalize direction vector - const float normalizedDirX = dirX / length; - const float normalizedDirY = dirY / length; - - // Calculate new position with increment - const float moveX = bot->GetPositionX() + normalizedDirX * incrementSize; - const float moveY = bot->GetPositionY() + normalizedDirY * incrementSize; - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); -} - -void IccSpikeAction::UpdateRaidTargetIcon(Unit* target) -{ - static constexpr uint8_t SKULL_ICON_INDEX = 7; - - if (Group* group = bot->GetGroup()) - { - const ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - const bool needsUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != target; - - if (needsUpdate) - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), target->GetGUID()); - } -} - -// Lady Deathwhisper -bool IccDarkReckoningAction::Execute(Event /*event*/) -{ - constexpr float SAFE_DISTANCE_THRESHOLD = 2.0f; - - // Check if the bot needs to move to the safe position - if (bot->HasAura(SPELL_DARK_RECKONING) && - bot->GetExactDist2d(ICC_DARK_RECKONING_SAFE_POSITION) > SAFE_DISTANCE_THRESHOLD) - { - // Move to the safe position with the same parameters as before - return MoveTo(bot->GetMapId(), ICC_DARK_RECKONING_SAFE_POSITION.GetPositionX(), - ICC_DARK_RECKONING_SAFE_POSITION.GetPositionY(), - ICC_DARK_RECKONING_SAFE_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - - return false; -} - -bool IccRangedPositionLadyDeathwhisperAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); - if (!boss) - return false; - - const float currentDistance = bot->GetDistance2d(boss); - const float minDistance = 7.0f; - const float maxDistance = 30.0f; - - if (currentDistance < minDistance || currentDistance > maxDistance) - return false; - - if (!botAI->IsRanged(bot) && !botAI->IsHeal(bot)) - return false; - - return MaintainRangedSpacing(); -} - -bool IccRangedPositionLadyDeathwhisperAction::MaintainRangedSpacing() -{ - const float safeSpacingRadius = 3.0f; - const float moveIncrement = 2.0f; - const float maxMoveDistance = 5.0f; // Limit maximum movement distance - const bool isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); - - if (!isRanged) - return false; - - // Ranged: spread from other members - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - // Calculate a combined vector representing all nearby members' positions - float totalX = 0.0f; - float totalY = 0.0f; - int nearbyCount = 0; - - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - { - continue; - } - - const float distance = bot->GetExactDist2d(member); - if (distance < safeSpacingRadius) - { - // Calculate vector from member to bot - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - - // Weight by inverse distance (closer members have more influence) - float weight = (safeSpacingRadius - distance) / safeSpacingRadius; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - // If we have nearby members, move away in the combined direction - if (nearbyCount > 0) - { - // Normalize the combined vector - float magnitude = std::sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) // Avoid division by zero - { - totalX /= magnitude; - totalY /= magnitude; - - // Calculate move distance based on nearest member - float moveDistance = std::min(moveIncrement, maxMoveDistance); - - // Create target position in the combined direction - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); // Maintain current Z - - // Check if the target position is valid and move there - if (bot->IsWithinLOS(targetX, targetY, targetZ)) - { - Position targetPos(targetX, targetY, targetZ); - MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - else - { - // If los check fails, try shorter distance - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - Position targetPos(targetX, targetY, targetZ); - MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - } - } - - return false; // Everyone is properly spaced -} - -bool IccAddsLadyDeathwhisperAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); - if (!boss) - return false; - - if (botAI->HasAura("Dominate Mind", bot, false, false) && !bot->HasAura(SPELL_CYCLONE)) - bot->AddAura(SPELL_CYCLONE, bot); - else if (bot->HasAura(SPELL_CYCLONE) && !botAI->HasAura("Dominate Mind", bot, false, false)) - bot->RemoveAura(SPELL_CYCLONE); - - const uint32 shadeEntryId = NPC_SHADE; - - if (botAI->IsTank(bot) && boss && boss->HealthBelowPct(95) && boss->GetVictim() == bot) - { - // Check if the bot is not the victim of a shade - if (IsTargetedByShade(shadeEntryId)) - return false; - - const float maxDistanceToTankPosition = 20.0f; - const float moveIncrement = 3.0f; - - const float distance = bot->GetExactDist2d(ICC_LDW_TANK_POSTION.GetPositionX(), ICC_LDW_TANK_POSTION.GetPositionY()); - - if (distance > maxDistanceToTankPosition) - { - return MoveTowardPosition(ICC_LDW_TANK_POSTION, moveIncrement); - } - } - - if (!botAI->IsTank(bot)) - return false; - - return HandleAddTargeting(boss); -} - -bool IccAddsLadyDeathwhisperAction::IsTargetedByShade(uint32 shadeEntry) -{ - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& npcGuid : npcs) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == shadeEntry && unit->GetVictim() == bot) - return true; - } - return false; -} - -bool IccAddsLadyDeathwhisperAction::MoveTowardPosition(const Position& position, float incrementSize) -{ - // Calculate direction vector - const float dirX = position.GetPositionX() - bot->GetPositionX(); - const float dirY = position.GetPositionY() - bot->GetPositionY(); - const float length = std::sqrt(dirX * dirX + dirY * dirY); - - // Normalize direction vector - const float normalizedDirX = dirX / length; - const float normalizedDirY = dirY / length; - - // Calculate new position with increment - const float moveX = bot->GetPositionX() + normalizedDirX * incrementSize; - const float moveY = bot->GetPositionY() + normalizedDirY * incrementSize; - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); -} - -bool IccAddsLadyDeathwhisperAction::HandleAddTargeting(Unit* boss) -{ - const GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); - - Unit* priorityTarget = nullptr; - bool hasValidAdds = false; - - // First check for alive adds - for (auto const& entry : addEntriesLady) - { - for (auto const& guid : targets) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == entry) - { - priorityTarget = unit; - hasValidAdds = true; - break; - } - } - if (priorityTarget) - break; - } - - // Only fallback to boss if NO adds exist - if (!hasValidAdds && boss->IsAlive()) - priorityTarget = boss; - - // Update skull icon if needed - if (priorityTarget) - UpdateRaidTargetIcon(priorityTarget); - - return false; -} - -void IccAddsLadyDeathwhisperAction::UpdateRaidTargetIcon(Unit* target) -{ - static constexpr uint8_t SKULL_ICON_INDEX = 7; - - if (Group* group = bot->GetGroup()) - { - const ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - const bool needsUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != target; - - if (needsUpdate) - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), target->GetGUID()); - } -} - -bool IccShadeLadyDeathwhisperAction::Execute(Event /*event*/) -{ - static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE; - static constexpr float SAFE_DISTANCE = 12.0f; - - // Get the nearest hostile NPCs - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - for (auto const& npcGuid : npcs) - { - Unit* shade = botAI->GetUnit(npcGuid); - - // Skip if not a vengeful shade - if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID) - continue; - - // Only run away if the shade is targeting us - // Check by GUID comparison to ensure we're accurately identifying the specific shade - // This is especially important in 25HC where multiple shades can spawn - if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID()) - continue; - - const float currentDistance = bot->GetDistance2d(shade); - - // Move away from the Vengeful Shade if the bot is too close - if (currentDistance < SAFE_DISTANCE) - { - // Calculate direction away from shade - float dx = bot->GetPositionX() - shade->GetPositionX(); - float dy = bot->GetPositionY() - shade->GetPositionY(); - float dist = std::sqrt(dx * dx + dy * dy); - - if (dist < 0.001f) - continue; - - dx /= dist; - dy /= dist; - - float moveDistance = SAFE_DISTANCE - currentDistance; - float targetX = bot->GetPositionX() + dx * moveDistance; - float targetY = bot->GetPositionY() + dy * moveDistance; - float targetZ = bot->GetPositionZ(); - - if (bot->IsWithinLOS(targetX, targetY, targetZ)) - { - botAI->Reset(); - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - } - } - - return false; -} - -bool IccRottingFrostGiantTankPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "rotting frost giant"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("death plague", bot, false, false); - if (aura) - bot->RemoveAura(aura->GetId()); - -/* TODO: code works for handling plague, but atm script is bugged and one bot can have 2 plagues at the same time or when cured, which should not happen, and it is immpossible to handle plague atm the legit way. - const bool hasCure = botAI->GetAura("recently infected", bot) != nullptr; - - // Tank behavior - unchanged - if (botAI->IsTank(bot) && botAI->HasAggro(boss) && !isInfected) - if (bot->GetExactDist2d(ICC_ROTTING_FROST_GIANT_TANK_POSITION) > 5.0f) - return MoveTo(bot->GetMapId(), ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionX(), - ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionY(), - ICC_ROTTING_FROST_GIANT_TANK_POSITION.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - - if (botAI->IsTank(bot)) - return false; - - // Handle infected bot behavior - move near a non-infected, non-cured bot - if (isInfected) - { - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - // Count how many bots are targeting each potential target - std::map targetCounts; - - // First, identify all infected bots and their current targets (approximate) - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - continue; - - const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; - - if (memberIsInfected) - { - // Find the nearest non-infected bot to this infected bot (as a guess of its target) - float minDist = 5.0f; // Only count if they're close enough to likely be targeting - Unit* likelyTarget = nullptr; - - for (auto const& targetGuid : members) - { - Unit* potentialTarget = botAI->GetUnit(targetGuid); - if (!potentialTarget || !potentialTarget->IsAlive() || potentialTarget == member) - continue; - - const bool targetIsInfected = botAI->GetAura("death plague", potentialTarget) != nullptr; - const bool targetHasCure = botAI->GetAura("recently infected", potentialTarget) != nullptr; - - if (!targetIsInfected && !targetHasCure) - { - float dist = member->GetExactDist2d(potentialTarget); - if (dist < minDist) - { - minDist = dist; - likelyTarget = potentialTarget; - } - } - } - - if (likelyTarget) - { - targetCounts[likelyTarget->GetGUID()]++; - } - } - } - - // Find viable targets and score them based on various factors - std::vector> viableTargets; - - // First try to find ranged, non-infected, non-cured bots - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - continue; - - const bool memberHasCure = botAI->GetAura("recently infected", member) != nullptr; - const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; - - if (!memberIsInfected && !memberHasCure) - { - // Base score is distance (lower is better) - float score = bot->GetExactDist2d(member); - - // Prefer ranged targets - if (botAI->IsRanged(bot)) - { - score *= 0.7f; // Bonus for ranged targets - } - - // Apply penalty based on how many other infected bots are targeting this one - int targetingCount = targetCounts[member->GetGUID()]; - score *= (1.0f + targetingCount * 0.5f); // Increase score (worse) for heavily targeted bots - - viableTargets.push_back(std::make_pair(member, score)); - } - } - - // Sort targets by score (lowest/best first) - std::sort(viableTargets.begin(), viableTargets.end(), - [](const std::pair& a, const std::pair& b) - { return a.second < b.second; }); - - // Choose the best target - Unit* targetBot = nullptr; - if (!viableTargets.empty()) - { - targetBot = viableTargets[0].first; - } - - // Move to target bot if found - if (targetBot) - { - // If we're already close enough (1 yard), no need to move - if (bot->GetExactDist2d(targetBot) > 1.0f) - { - // Calculate a unique angle based on bot's GUID to ensure different approach angles - // This helps spread infected bots around the target - uint32 guidLow = bot->GetGUID().GetCounter(); - float angleOffset = float(guidLow % 628) / 100.0f; // Random angle between 0 and 2π - - // Calculate position 1 yard away from target at our unique angle - float angle = targetBot->GetOrientation() + angleOffset; - float targetX = targetBot->GetPositionX() + cos(angle) * 1.0f; - float targetY = targetBot->GetPositionY() + sin(angle) * 1.0f; - float targetZ = targetBot->GetPositionZ(); - - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } - return true; // Already in position - } - // No suitable target found, continue with normal behavior - } - - // For ranged bots, only spread from non-infected bots - if (botAI->IsRanged(bot)) - { - const float safeSpacingRadius = 11.0f; - const float moveIncrement = 2.0f; - const float maxMoveDistance = 15.0f; - - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - // Calculate a combined vector representing all nearby NON-INFECTED members' positions - float totalX = 0.0f; - float totalY = 0.0f; - int nearbyCount = 0; - - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - continue; - - // Only spread from non-infected bots (can stay near infected or cured bots) - const bool memberIsInfected = botAI->GetAura("death plague", member) != nullptr; - if (memberIsInfected) - continue; - - const float distance = bot->GetExactDist2d(member); - if (distance < safeSpacingRadius) - { - // Calculate vector from member to bot - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - - // Weight by inverse distance (closer members have more influence) - float weight = (safeSpacingRadius - distance) / safeSpacingRadius; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - // If we have nearby non-infected members, move away in the combined direction - if (nearbyCount > 0) - { - // Normalize the combined vector - float magnitude = std::sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) // Avoid division by zero - { - totalX /= magnitude; - totalY /= magnitude; - - // Calculate move distance based on nearest member - float moveDistance = std::min(moveIncrement, maxMoveDistance); - - // Create target position in the combined direction - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); // Maintain current Z - - // Check if the target position is valid and move there - if (bot->IsWithinLOS(targetX, targetY, targetZ)) - { - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - else - { - // If los check fails, try shorter distance - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - } - } - } -*/ - return false; // No movement needed -} - -//Gunship -bool IccCannonFireAction::Execute(Event /*event*/) -{ - Unit* vehicleBase = bot->GetVehicleBase(); - Vehicle* vehicle = bot->GetVehicle(); - - if (!vehicleBase || !vehicle) - return false; - - Unit* target = FindValidCannonTarget(); - if (!target) - return false; - - // Try to cast Incinerating Blast if we have enough energy - const float energyThreshold = 90.0f; - if (vehicleBase->GetPower(POWER_ENERGY) >= energyThreshold) - { - const uint32 blastSpellId = AI_VALUE2(uint32, "vehicle spell id", "incinerating blast"); - if (TryCastCannonSpell(blastSpellId, target, vehicleBase)) - return true; - } - - // Otherwise just use regular Cannon Blast - const uint32 cannonSpellId = AI_VALUE2(uint32, "vehicle spell id", "cannon blast"); - return TryCastCannonSpell(cannonSpellId, target, vehicleBase); -} - -Unit* IccCannonFireAction::FindValidCannonTarget() -{ - const GuidVector attackers = AI_VALUE(GuidVector, "possible targets no los"); - - for (auto const& attackerGuid : attackers) - { - Unit* unit = botAI->GetUnit(attackerGuid); - if (!unit) - continue; - - for (const uint32 entry : availableTargetsGS) - { - if (unit->GetEntry() == entry) - return unit; - } - } - - return nullptr; -} - -bool IccCannonFireAction::TryCastCannonSpell(uint32 spellId, Unit* target, Unit* vehicleBase) -{ - static constexpr uint32 cooldownMs = 1000; - - if (botAI->CanCastVehicleSpell(spellId, target) && botAI->CastVehicleSpell(spellId, target)) - { - vehicleBase->AddSpellCooldown(spellId, 0, cooldownMs); - return true; - } - - return false; -} - -bool IccGunshipEnterCannonAction::Execute(Event /*event*/) -{ - // Do not switch vehicles if already in one - if (bot->GetVehicle()) - return false; - - Unit* bestVehicle = FindBestAvailableCannon(); - if (!bestVehicle) - return false; - - return EnterVehicle(bestVehicle, true); -} - -Unit* IccGunshipEnterCannonAction::FindBestAvailableCannon() -{ - const uint32 validCannonEntries[] = {NPC_CANNONA, NPC_CANNONH}; - Unit* bestVehicle = nullptr; - - const GuidVector npcs = AI_VALUE(GuidVector, "nearest vehicles"); - for (auto const& npcGuid : npcs) - { - Unit* vehicleBase = botAI->GetUnit(npcGuid); - if (!IsValidCannon(vehicleBase, validCannonEntries)) - continue; - - // Choose the closest valid cannon - if (!bestVehicle || bot->GetExactDist(vehicleBase) < bot->GetExactDist(bestVehicle)) - bestVehicle = vehicleBase; - } - - return bestVehicle; -} - -bool IccGunshipEnterCannonAction::IsValidCannon(Unit* vehicle, const uint32 validEntries[]) -{ - if (!vehicle) - return false; - - // Must be selectable - if (vehicle->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) - return false; - - // Must be friendly - if (!vehicle->IsFriendlyTo(bot)) - return false; - - // Must have available seats - if (!vehicle->GetVehicleKit() || !vehicle->GetVehicleKit()->GetAvailableSeatCount()) - return false; - - // Must be one of the cannon entries - const uint32 entry = vehicle->GetEntry(); - bool isValidEntry = false; - for (size_t i = 0; i < 2; ++i) - { // 2 is the size of validEntries - if (entry == validEntries[i]) - { - isValidEntry = true; - break; - } - } - - if (!isValidEntry) - return false; - - // Must not have these auras (frozen or disabled) - if (vehicle->HasAura(69704) || vehicle->HasAura(69705)) - return false; - - return true; -} - -bool IccGunshipEnterCannonAction::EnterVehicle(Unit* vehicleBase, bool moveIfFar) -{ - const float dist = bot->GetDistance(vehicleBase); - - if (dist > INTERACTION_DISTANCE && !moveIfFar) - return false; - - if (dist > INTERACTION_DISTANCE) - return MoveTo(vehicleBase); - - // Prepare for entering vehicle - botAI->RemoveShapeshift(); - bot->GetMotionMaster()->Clear(); - bot->StopMoving(); - - // Enter the vehicle - vehicleBase->HandleSpellClick(bot); - - if (!bot->IsOnVehicle(vehicleBase)) - return false; - - // Dismount because bots can enter vehicle while mounted - WorldPacket emptyPacket; - bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); - - return true; -} - -bool IccGunshipTeleportAllyAction::Execute(Event /*event*/) -{ - static constexpr float MAX_WAITING_DISTANCE = 45.0f; - static constexpr float MAX_ATTACK_DISTANCE = 15.0f; - static constexpr uint8_t SKULL_ICON_INDEX = 7; - - // Find the Battle-Mage boss - Unit* boss = AI_VALUE2(Unit*, "find target", "kor'kron battle-mage"); - - // Check if we need to remove skull icon when boss is dead - CleanupSkullIcon(SKULL_ICON_INDEX); - - // If no boss found or boss is dead or not casting, check waiting position - if (!boss || !boss->IsAlive() || !boss->HasUnitState(UNIT_STATE_CASTING)) - { - // If we're too far from waiting position, go there - if (bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_ALLY2) > MAX_WAITING_DISTANCE) - return TeleportTo(ICC_GUNSHIP_TELEPORT_ALLY2); - } - else if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_BELOW_ZERO) && - boss->IsAlive()) - { - // Mark the boss with skull icon - UpdateBossSkullIcon(boss, SKULL_ICON_INDEX); - - // Teleport non-tank bots to attack position if not already there - if (!botAI->IsAssistTank(bot) && bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_ALLY) > MAX_ATTACK_DISTANCE) - return TeleportTo(ICC_GUNSHIP_TELEPORT_ALLY); - } - - return false; -} - -bool IccGunshipTeleportAllyAction::TeleportTo(const Position& position) -{ - return bot->TeleportTo(bot->GetMapId(), position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), - bot->GetOrientation()); -} - -void IccGunshipTeleportAllyAction::CleanupSkullIcon(uint8_t SKULL_ICON_INDEX) -{ - if (Group* group = bot->GetGroup()) - { - const ObjectGuid currentSkullTarget = group->GetTargetIcon(SKULL_ICON_INDEX); - - if (!currentSkullTarget.IsEmpty()) - { - Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); - - if (!skullTarget || !skullTarget->IsAlive()) - { - // Target is dead or doesn't exist, remove icon - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), ObjectGuid::Empty); - } - } - } -} - -void IccGunshipTeleportAllyAction::UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX) -{ - if (Group* group = bot->GetGroup()) - { - if (group->GetTargetIcon(SKULL_ICON_INDEX) != boss->GetGUID()) - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), boss->GetGUID()); - } -} - -bool IccGunshipTeleportHordeAction::Execute(Event /*event*/) -{ - static constexpr float MAX_WAITING_DISTANCE = 45.0f; - static constexpr float MAX_ATTACK_DISTANCE = 15.0f; - static constexpr uint8_t SKULL_ICON_INDEX = 7; - - // Find the Sorcerer boss - Unit* boss = AI_VALUE2(Unit*, "find target", "skybreaker sorcerer"); - - // Check if we need to remove skull icon when boss is dead - CleanupSkullIcon(SKULL_ICON_INDEX); - - // If no boss found or boss is dead or not casting, check waiting position - if (!boss || !boss->IsAlive() || !boss->HasUnitState(UNIT_STATE_CASTING)) - { - // If we're too far from waiting position, go there - if (bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_HORDE2) > MAX_WAITING_DISTANCE) - return TeleportTo(ICC_GUNSHIP_TELEPORT_HORDE2); - } - else if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_BELOW_ZERO) && - boss->IsAlive()) - { - // Mark the boss with skull icon - UpdateBossSkullIcon(boss, SKULL_ICON_INDEX); - - // Teleport non-tank bots to attack position if not already there - if (!botAI->IsAssistTank(bot) && bot->GetExactDist2d(ICC_GUNSHIP_TELEPORT_HORDE) > MAX_ATTACK_DISTANCE) - return TeleportTo(ICC_GUNSHIP_TELEPORT_HORDE); - } - - return false; -} - -bool IccGunshipTeleportHordeAction::TeleportTo(const Position& position) -{ - return bot->TeleportTo(bot->GetMapId(), position.GetPositionX(), position.GetPositionY(), position.GetPositionZ(), - bot->GetOrientation()); -} - -void IccGunshipTeleportHordeAction::CleanupSkullIcon(uint8_t SKULL_ICON_INDEX) -{ - if (Group* group = bot->GetGroup()) - { - const ObjectGuid currentSkullTarget = group->GetTargetIcon(SKULL_ICON_INDEX); - - if (!currentSkullTarget.IsEmpty()) - { - Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); - - if (!skullTarget || !skullTarget->IsAlive()) - { - // Target is dead or doesn't exist, remove icon - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), ObjectGuid::Empty); - } - } - } -} - -void IccGunshipTeleportHordeAction::UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX) -{ - if (Group* group = bot->GetGroup()) - { - if (group->GetTargetIcon(SKULL_ICON_INDEX) != boss->GetGUID()) - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), boss->GetGUID()); - } -} - -//DBS -bool IccDbsTankPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); - if (!boss) - return false; - - Unit* beast = AI_VALUE2(Unit*, "find target", "blood beast"); - - // Handle tank positioning - if (botAI->IsTank(bot) && !beast) - { - if (bot->GetExactDist2d(ICC_DBS_TANK_POSITION) > 5.0f) - return MoveTo(bot->GetMapId(), ICC_DBS_TANK_POSITION.GetPositionX(), ICC_DBS_TANK_POSITION.GetPositionY(), - ICC_DBS_TANK_POSITION.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - - // Early return if this tank has Rune of Blood - if (botAI->GetAura("Rune of Blood", bot)) - return true; - } - - if (!botAI->IsTank(bot)) - { - if (CrowdControlBloodBeasts()) - return true; - } - - // Handle ranged and healer positioning - if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) - { - // Handle evasion from blood beasts - if (EvadeBloodBeasts()) - return true; - - // Position in formation - return PositionInRangedFormation(); - } - - return false; -} - -bool IccDbsTankPositionAction::CrowdControlBloodBeasts() -{ - const std::array bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, - NPC_BLOOD_BEAST4}; - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - bool appliedCC = false; - - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - - // Check if this is a blood beast - const bool isBloodBeast = - std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != bloodBeastEntries.end(); - - if (!isBloodBeast) - continue; - - // Apply class-specific CC - switch (bot->getClass()) - { - case CLASS_MAGE: - if (!botAI->HasAura("Frost Nova", unit)) - { - botAI->CastSpell("Frost Nova", unit); - appliedCC = true; - } - break; - case CLASS_DRUID: - if (!botAI->HasAura("Entangling Roots", unit)) - { - botAI->CastSpell("Entangling Roots", unit); - appliedCC = true; - } - break; - case CLASS_PALADIN: - if (!botAI->HasAura("Hammer of Justice", unit)) - { - botAI->CastSpell("Hammer of Justice", unit); - appliedCC = true; - } - break; - case CLASS_WARRIOR: - if (!botAI->HasAura("Hamstring", unit)) - { - botAI->CastSpell("Hamstring", unit); - appliedCC = true; - } - break; - case CLASS_HUNTER: - if (!botAI->HasAura("Concussive Shot", unit)) - { - botAI->CastSpell("Concussive Shot", unit); - appliedCC = true; - } - break; - case CLASS_ROGUE: - if (!botAI->HasAura("Kidney Shot", unit)) - { - botAI->CastSpell("Kidney Shot", unit); - appliedCC = true; - } - break; - case CLASS_SHAMAN: - if (!botAI->HasAura("Frost Shock", unit)) - { - botAI->CastSpell("Frost Shock", unit); - appliedCC = true; - } - break; - case CLASS_DEATH_KNIGHT: - if (!botAI->HasAura("Chains of Ice", unit)) - { - botAI->CastSpell("Chains of Ice", unit); - appliedCC = true; - } - break; - case CLASS_PRIEST: - if (!botAI->HasAura("Psychic Scream", unit)) - { - botAI->CastSpell("Psychic Scream", unit); - appliedCC = true; - } - break; - case CLASS_WARLOCK: - if (!botAI->HasAura("Fear", unit)) - { - botAI->CastSpell("Fear", unit); - appliedCC = true; - } - break; - default: - break; - } - } - - return false; -} - -bool IccDbsTankPositionAction::EvadeBloodBeasts() -{ - const float evasionDistance = 12.0f; - const std::array bloodBeastEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, NPC_BLOOD_BEAST4}; - - // Get the nearest hostile NPCs - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit) - continue; - - // Check if this is a blood beast - const bool isBloodBeast = std::find(bloodBeastEntries.begin(), bloodBeastEntries.end(), unit->GetEntry()) != bloodBeastEntries.end(); - - // Only evade if it's a blood beast targeting us - if (isBloodBeast && unit->GetVictim() == bot) - { - float currentDistance = bot->GetDistance2d(unit); - - // Move away if too close - if (currentDistance < evasionDistance) - return MoveAway(unit, evasionDistance - currentDistance); - } - } - - return false; -} - -bool IccDbsTankPositionAction::PositionInRangedFormation() -{ - // Get group - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Find this bot's position among ranged/healers in the group - int rangedIndex = -1; - int currentIndex = 0; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if ((botAI->IsRanged(member) || botAI->IsHeal(member)) && !botAI->IsTank(member)) - { - if (member == bot) - { - rangedIndex = currentIndex; - break; - } - currentIndex++; - } - } - - if (rangedIndex == -1) - return false; - - // Fixed positions calculation - constexpr float tankToBossAngle = 3.14f; - constexpr float minBossDistance = 11.0f; - constexpr float spreadDistance = 10.0f; - constexpr int columnsPerRow = 5; - - // Calculate position in a fixed grid (3 rows x 5 columns) - int row = rangedIndex / columnsPerRow; - int col = rangedIndex % columnsPerRow; - - // Calculate base position - float xOffset = (col - 2) * spreadDistance; // Center around tank position - float yOffset = minBossDistance + (row * spreadDistance); // Each row further back - - // Add zigzag offset for odd rows - if (row % 2 == 1) - xOffset += spreadDistance / 2; - - // Rotate position based on tank-to-boss angle - float finalX = - ICC_DBS_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); - float finalY = - ICC_DBS_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); - float finalZ = ICC_DBS_TANK_POSITION.GetPositionZ(); - - // Update Z coordinate - bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); - - // Move if not in position - if (bot->GetExactDist2d(finalX, finalY) > 3.0f) - return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - - return false; -} - -bool IccAddsDbsAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); - if (!boss) - return false; - - // This action is only for melee - if (!botAI->IsMelee(bot)) - return false; - - Unit* priorityTarget = FindPriorityTarget(boss); - - // Update raid target icons if needed - UpdateSkullMarker(priorityTarget); - - return false; -} - -Unit* IccAddsDbsAction::FindPriorityTarget(Unit* boss) -{ - const GuidVector targets = AI_VALUE(GuidVector, "possible targets no los"); - - // Blood beast entry IDs - constexpr std::array addEntries = {NPC_BLOOD_BEAST1, NPC_BLOOD_BEAST2, NPC_BLOOD_BEAST3, NPC_BLOOD_BEAST4}; - - // First check for alive adds - for (uint32_t entry : addEntries) - { - for (const ObjectGuid& guid : targets) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == entry) - return unit; - } - } - - // Only fallback to boss if it's alive - return boss->IsAlive() ? const_cast(boss) : nullptr; -} - -void IccAddsDbsAction::UpdateSkullMarker(Unit* priorityTarget) -{ - if (!priorityTarget) - return; - - Group* group = bot->GetGroup(); - if (!group) - return; - - constexpr uint8_t skullIconId = 7; - - // Get current skull target - ObjectGuid currentSkull = group->GetTargetIcon(skullIconId); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - // Determine if skull marker needs updating - bool needsUpdate = !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != priorityTarget; - - // Update if needed - if (needsUpdate) - group->SetTargetIcon(skullIconId, bot->GetGUID(), priorityTarget->GetGUID()); -} - -// Festergut -bool IccFestergutGroupPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss) - return false; - - bot->SetTarget(boss->GetGUID()); - - // Handle tank positioning - if ((botAI->HasAggro(boss) && botAI->IsMainTank(bot)) || botAI->IsAssistTank(bot)) - { - if (bot->GetExactDist2d(ICC_FESTERGUT_TANK_POSITION) > 5.0f) - return MoveTo(bot->GetMapId(), ICC_FESTERGUT_TANK_POSITION.GetPositionX(), - ICC_FESTERGUT_TANK_POSITION.GetPositionY(), ICC_FESTERGUT_TANK_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - - // Check for spores in the group - if (HasSporesInGroup()) - return false; - - // Position non-tank ranged and healers - return PositionNonTankMembers(); -} - -bool IccFestergutGroupPositionAction::HasSporesInGroup() -{ - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - for (auto const& memberGuid : members) - { - Unit* unit = botAI->GetUnit(memberGuid); - if (unit && unit->HasAura(SPELL_GAS_SPORE)) - return true; - } - - return false; -} - -bool IccFestergutGroupPositionAction::PositionNonTankMembers() -{ - // Only position ranged and healers without spores - if (!(botAI->IsRanged(bot) || botAI->IsHeal(bot))) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - int positionIndex = CalculatePositionIndex(group); - if (positionIndex == -1) - return false; - - // Position calculation parameters - constexpr float tankToBossAngle = 4.58f; - constexpr float minBossDistance = 15.0f; - constexpr float spreadDistance = 10.0f; - constexpr int columnsPerRow = 6; - - // Calculate grid position - int row = positionIndex / columnsPerRow; - int col = positionIndex % columnsPerRow; - - // Calculate base position - float xOffset = (col - 2) * spreadDistance; // Center around tank position - float yOffset = minBossDistance + (row * spreadDistance); // Each row further back - - // Add zigzag offset for odd rows - if (row % 2 == 1) - xOffset += spreadDistance / 2; - - // Rotate position based on tank-to-boss angle - float finalX = - ICC_FESTERGUT_TANK_POSITION.GetPositionX() + (cos(tankToBossAngle) * yOffset - sin(tankToBossAngle) * xOffset); - float finalY = - ICC_FESTERGUT_TANK_POSITION.GetPositionY() + (sin(tankToBossAngle) * yOffset + cos(tankToBossAngle) * xOffset); - float finalZ = ICC_FESTERGUT_TANK_POSITION.GetPositionZ(); - - // Update Z coordinate - bot->UpdateAllowedPositionZ(finalX, finalY, finalZ); - - // Move if not in position - if (bot->GetExactDist2d(finalX, finalY) > 3.0f) - return MoveTo(bot->GetMapId(), finalX, finalY, finalZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - - return false; -} - -int IccFestergutGroupPositionAction::CalculatePositionIndex(Group* group) -{ - std::vector healerGuids; - std::vector rangedDpsGuids; - std::vector hunterGuids; - - // Collect all eligible members with their GUIDs - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || botAI->IsTank(member)) - continue; - - ObjectGuid memberGuid = member->GetGUID(); - - if (botAI->IsHeal(member)) - { - healerGuids.push_back(memberGuid); - } - else if (botAI->IsRanged(member)) - { - if (member->getClass() == CLASS_HUNTER) - { - hunterGuids.push_back(memberGuid); - } - else - { - rangedDpsGuids.push_back(memberGuid); - } - } - } - - // Sort GUIDs for consistent ordering - std::sort(healerGuids.begin(), healerGuids.end()); - std::sort(rangedDpsGuids.begin(), rangedDpsGuids.end()); - std::sort(hunterGuids.begin(), hunterGuids.end()); - - ObjectGuid botGuid = bot->GetGUID(); - - // Find bot's position among healers - auto healerIt = std::find(healerGuids.begin(), healerGuids.end(), botGuid); - if (healerIt != healerGuids.end()) - { - int healerIndex = std::distance(healerGuids.begin(), healerIt); - int totalHealers = healerGuids.size(); - - // Healers in first two rows, distributed evenly - int healersPerRow = (totalHealers + 1) / 2; // Round up for first row - - if (healerIndex < healersPerRow) - { - // First row of healers (positions 0-5) - return healerIndex; - } - else - { - // Second row of healers (positions 6-11) - return healerIndex - healersPerRow + 6; - } - } - - // Find bot's position among non-hunter ranged DPS - auto rangedIt = std::find(rangedDpsGuids.begin(), rangedDpsGuids.end(), botGuid); - if (rangedIt != rangedDpsGuids.end()) - { - int rangedIndex = std::distance(rangedDpsGuids.begin(), rangedIt); - int totalHealers = healerGuids.size(); - - // Non-hunter ranged DPS fill remaining spots in healer rows first - int healerSpotsUsed = totalHealers; - int remainingSpotsInHealerRows = 12 - healerSpotsUsed; // 2 rows of 6 spots each - - if (rangedIndex < remainingSpotsInHealerRows) - { - // Fill remaining spots in healer rows - if (healerSpotsUsed < 6) - { - // Fill remaining spots in first row - return healerSpotsUsed + rangedIndex; - } - else - { - // Fill remaining spots in second row - int spotsInFirstRow = 6; - int spotsInSecondRow = healerSpotsUsed - 6; - int remainingInSecondRow = 6 - spotsInSecondRow; - - if (rangedIndex < remainingInSecondRow) - { - return 6 + spotsInSecondRow + rangedIndex; - } - else - { - // Move to third row - return 12 + (rangedIndex - remainingInSecondRow); - } - } - } - else - { - // Start new rows for remaining ranged DPS - return 12 + (rangedIndex - remainingSpotsInHealerRows); - } - } - - // Find bot's position among hunters - auto hunterIt = std::find(hunterGuids.begin(), hunterGuids.end(), botGuid); - if (hunterIt != hunterGuids.end()) - { - int hunterIndex = std::distance(hunterGuids.begin(), hunterIt); - int totalHealers = healerGuids.size(); - int totalRangedDps = rangedDpsGuids.size(); - - // Hunters start after healers and non-hunter ranged DPS - // But ensure they're at least in the second row (position 6+) - int basePosition = totalHealers + totalRangedDps + hunterIndex; - - // If position would be in first row (0-5), move to second row minimum - if (basePosition < 6) - basePosition = 6 + hunterIndex; - - return basePosition; - } - - return -1; -} - -bool IccFestergutSporeAction::Execute(Event /*event*/) -{ - constexpr float POSITION_TOLERANCE = 4.0f; - constexpr float SPREAD_RADIUS = 2.0f; - - // Check if bot has spore - bool hasSpore = bot->HasAura(SPELL_GAS_SPORE); // gas spore - - // Stop attacking if spored - if (hasSpore) - botAI->Reset(); - - // Calculate unique spread position for ranged - Position spreadRangedPos = CalculateSpreadPosition(); - - // Find spored players - SporeInfo sporeInfo = FindSporedPlayers(); - - // Determine target position - Position targetPos = DetermineTargetPosition(hasSpore, sporeInfo, spreadRangedPos); - - // Move to position if not already there - if (bot->GetExactDist2d(targetPos) > POSITION_TOLERANCE) - { - botAI->Reset(); - return MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - true, false, false, true, MovementPriority::MOVEMENT_FORCED); - } - - return hasSpore; -} - -Position IccFestergutSporeAction::CalculateSpreadPosition() -{ - constexpr float SPREAD_RADIUS = 2.0f; - - // Unique angle based on bot's GUID - float angle = (bot->GetGUID().GetCounter() % 16) * (M_PI / 8); - - Position spreadRangedPos = ICC_FESTERGUT_RANGED_SPORE; - spreadRangedPos.Relocate(spreadRangedPos.GetPositionX() + cos(angle) * SPREAD_RADIUS, - spreadRangedPos.GetPositionY() + sin(angle) * SPREAD_RADIUS, - spreadRangedPos.GetPositionZ(), - spreadRangedPos.GetOrientation()); - - return spreadRangedPos; -} - -IccFestergutSporeAction::SporeInfo IccFestergutSporeAction::FindSporedPlayers() -{ - SporeInfo info; - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - for (auto const& memberGuid : members) - { - Unit* unit = botAI->GetUnit(memberGuid); - if (!unit) - continue; - - if (unit->HasAura(SPELL_GAS_SPORE)) - { - info.sporedPlayers.push_back(unit); - - if (!info.hasLowestGuid || unit->GetGUID() < info.lowestGuid) - { - info.lowestGuid = unit->GetGUID(); - info.hasLowestGuid = true; - } - } - } - - return info; -} - -Position IccFestergutSporeAction::DetermineTargetPosition(bool hasSpore, const SporeInfo& sporeInfo, const Position& spreadRangedPos) -{ - // No spores at all - if (sporeInfo.sporedPlayers.empty()) - return botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; - - // Bot has no spore, go to standard position - if (!hasSpore) - return botAI->IsMelee(bot) ? ICC_FESTERGUT_MELEE_SPORE : spreadRangedPos; - - // Check if main tank has spore - bool mainTankHasSpore = CheckMainTankSpore(); - - // Determine position based on spore logic - if (botAI->IsMainTank(bot)) - return ICC_FESTERGUT_MELEE_SPORE; - - if (bot->GetGUID() == sporeInfo.lowestGuid && !botAI->IsTank(bot) && !mainTankHasSpore) - return ICC_FESTERGUT_MELEE_SPORE; - - return spreadRangedPos; -} - -bool IccFestergutSporeAction::CheckMainTankSpore() -{ - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - for (auto const& memberGuid : members) - { - Unit* unit = botAI->GetUnit(memberGuid); - if (!unit) - continue; - - if (botAI->IsMainTank(unit->ToPlayer()) && unit->HasAura(SPELL_GAS_SPORE)) - return true; - } - - return false; -} - -// Rotface -bool IccRotfaceTankPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss) - return false; - - Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); - bool victimOfSmallOoze = smallOoze && smallOoze->GetVictim() == bot; - // Mark Rotface with skull - MarkBossWithSkull(boss); - - // Main tank positioning and melee positioning - if ((botAI->IsMainTank(bot) || botAI->IsMelee(bot)) && !botAI->IsAssistTank(bot) && !victimOfSmallOoze) - return PositionMainTankAndMelee(boss); - - // Assist tank positioning for big ooze - if (botAI->IsAssistTank(bot)) - return HandleAssistTankPositioning(boss); - - return false; -} - -void IccRotfaceTankPositionAction::MarkBossWithSkull(Unit* boss) -{ - Group* group = bot->GetGroup(); - if (!group) - return; - - constexpr uint8_t skullIconId = 7; - ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); - if (skullGuid != boss->GetGUID()) - group->SetTargetIcon(skullIconId, bot->GetGUID(), boss->GetGUID()); -} - -bool IccRotfaceTankPositionAction::PositionMainTankAndMelee(Unit* boss) -{ - bool isBossCasting = false; - if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->GetCurrentSpell(SPELL_SLIME_SPRAY)) - bool isBossCasting = true; - - if (bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) > 7.0f && botAI->HasAggro(boss) && botAI->IsMainTank(bot)) - MoveTo(bot->GetMapId(), ICC_ROTFACE_CENTER_POSITION.GetPositionX(), - ICC_ROTFACE_CENTER_POSITION.GetPositionY(), ICC_ROTFACE_CENTER_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - - if (boss && isBossCasting && !botAI->IsTank(bot)) - { - float x = boss->GetPositionX(); - float y = boss->GetPositionY(); - float z = boss->GetPositionZ(); - - // If not already close to the boss's position, move there - if (bot->GetExactDist2d(x, y) > 0.5f) - { - MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, - false); - } - // Otherwise, already at the correct position - return false; - } - - if (!isBossCasting && (bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) < 2.0f || bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION) > 7.0f) && !botAI->IsTank(bot)) - { - MoveTo(bot->GetMapId(), ICC_ROTFACE_CENTER_POSITION.GetPositionX(), ICC_ROTFACE_CENTER_POSITION.GetPositionY(), - bot->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - } - - return false; -} - -bool IccRotfaceTankPositionAction::HandleAssistTankPositioning(Unit* boss) -{ - // Handle big ooze positioning - return HandleBigOozePositioning(boss); -} - -bool IccRotfaceTankPositionAction::HandleBigOozePositioning(Unit*) -{ - // Find all big oozes - GuidVector bigOozes = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector activeBigOozes; - - for (auto const& guid : bigOozes) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_BIG_OOZE && unit->IsVisible()) - activeBigOozes.push_back(unit); - } - - if (activeBigOozes.empty()) - return false; - - // Iterate through all big oozes and handle them - for (Unit* bigOoze : activeBigOozes) - { - // Taunt if not targeting us - if (bigOoze->GetVictim() != bot && bigOoze->IsAlive() && bigOoze->IsVisible()) - { - if (botAI->CastSpell("taunt", bigOoze)) - return true; - bot->SetTarget(bigOoze->GetGUID()); - bot->SetFacingToObject(bigOoze); - return Attack(bigOoze); - } - - // Calculate distances - float oozeDistance = bot->GetExactDist2d(bigOoze); - - // Stop moving if ooze is far enough - if (oozeDistance > 12.0f) - { - bot->SetTarget(bigOoze->GetGUID()); - bot->SetFacingToObject(bigOoze); - return true; - } - - // If we have the ooze's aggro, kite it in a larger circular pattern between 20f and 30f from the center - if (bigOoze->GetVictim() == bot) - { - const float minRadius = 24.0f; - const float maxRadius = 34.0f; - const float safeDistanceFromOoze = 13.0f; - const float puddleSafeDistance = 30.0f; - const Position centerPosition = ICC_ROTFACE_CENTER_POSITION; - - float currentDistance = bot->GetExactDist2d(centerPosition); - - // If too close or too far, adjust position - if (currentDistance < minRadius || currentDistance > maxRadius) - { - // Calculate direction vector from bot to center - float dirX = bot->GetPositionX() - centerPosition.GetPositionX(); - float dirY = bot->GetPositionY() - centerPosition.GetPositionY(); - float length = std::sqrt(dirX * dirX + dirY * dirY); - - // Normalize direction vector - dirX /= length; - dirY /= length; - - // Adjust position to stay within the desired radius - float targetX = centerPosition.GetPositionX() + dirX * maxRadius; - float targetY = centerPosition.GetPositionY() + dirY * maxRadius; - - // Ensure the position is at least 10f away from the ooze - if (bigOoze->GetExactDist2d(targetX, targetY) >= safeDistanceFromOoze) - { - return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } - } - - // If within the desired radius, continue kiting in a circular pattern - float currentAngle = atan2(bot->GetPositionY() - centerPosition.GetPositionY(), - bot->GetPositionX() - centerPosition.GetPositionX()); - - // Adjust rotation direction to find a safe position - for (int i = 0; i < 16; ++i) // Try 16 directions around the circle - { - float angleOffset = (i % 2 == 0 ? 1 : -1) * (M_PI / 16.0f) * (i / static_cast(2)); - float newAngle = currentAngle + angleOffset; - - // Calculate new position along the circle - float newX = centerPosition.GetPositionX() + maxRadius * cos(newAngle); - float newY = centerPosition.GetPositionY() + maxRadius * sin(newAngle); - - // Ensure the position is at least 10f away from the ooze - if (bigOoze->GetExactDist2d(newX, newY) >= safeDistanceFromOoze) - { - // Check if the position is at least 30f away from any puddle - GuidVector puddles = AI_VALUE(GuidVector, "nearest hostile npcs"); - bool isSafeFromPuddles = true; - - for (auto const& puddleGuid : puddles) - { - Unit* puddle = botAI->GetUnit(puddleGuid); - if (puddle && botAI->GetAura("Ooze Flood", puddle)) - { - float puddleDistance = std::sqrt(std::pow(newX - puddle->GetPositionX(), 2) + - std::pow(newY - puddle->GetPositionY(), 2)); - if (puddleDistance < puddleSafeDistance) - { - isSafeFromPuddles = false; - break; - } - } - } - - if (isSafeFromPuddles) - { - return MoveTo(bot->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } - } - } - } - } - - return false; -} - -bool IccRotfaceGroupPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss) - return false; - - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - bool floodPresent = false; - - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !botAI->HasAura("Ooze Flood", unit)) - continue; - - float puddleDistance = bot->GetExactDist2d(unit); - - if (puddleDistance < 30.0f) - floodPresent = true; - } - - Unit* bigOoze = AI_VALUE2(Unit*, "find target", "big ooze"); - bool hasOozeFlood = botAI->HasAura("Ooze Flood", bot); - Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); - bool hasMutatedInfection = botAI->HasAura("Mutated Infection", bot); - - // Handle puddle avoidance - if (!botAI->IsTank(bot) && HandlePuddleAvoidance(boss)) - return true; - - // Handle little ooze or mutated infection - if (HandleOozeTargeting()) - return true; - - // Position ranged and healers - if (/*!floodPresent && */ !((smallOoze && smallOoze->GetVictim() == bot) || hasMutatedInfection) && !hasOozeFlood && PositionRangedAndHealers(boss, smallOoze)) - return true; - - //if (!hasOozeFlood && bigOoze && bigOoze->IsAlive() && MoveAwayFromBigOoze(bigOoze)) - //return true; - - return false; -} - -bool IccRotfaceGroupPositionAction::HandlePuddleAvoidance(Unit* boss) -{ - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !botAI->HasAura("Ooze Flood", unit)) - continue; - - float puddleDistance = bot->GetExactDist2d(unit); - float bossDistance = bot->GetExactDist2d(ICC_ROTFACE_CENTER_POSITION); - - if (bossDistance < 15.0f) // Reduced center distance threshold - return false; - - if (puddleDistance < 30.0f) - return MoveAwayFromPuddle(boss, unit, puddleDistance); - } - - return false; -} - -bool IccRotfaceGroupPositionAction::MoveAwayFromPuddle(Unit* boss, Unit* puddle, float) -{ - if (!boss || !puddle) - return false; - - // Calculate angle and move direction - float dx = puddle->GetPositionX() - bot->GetPositionX(); - float dy = puddle->GetPositionY() - bot->GetPositionY(); - float angle = atan2(dy, dx); - - // Try to find a valid position in 8 directions - const float increment = 5.0f; - const float minPuddleDistance = 30.0f; - const float minCenterDistance = 15.0f; // Reduced center distance threshold - const float maxCenterDistance = 25.0f; // New maximum center distance threshold - const int directions = 8; // Number of directions to check - float bestX = bot->GetPositionX(); - float bestY = bot->GetPositionY(); - float bestZ = bot->GetPositionZ(); - float maxSafetyScore = 0.0f; - - for (int i = 0; i < directions; ++i) - { - float testAngle = angle + (i * M_PI / 4); // 8 directions (45-degree increments) - for (float distance = increment; distance <= 35.0f; distance += increment) - { - float moveX = bot->GetPositionX() - distance * cos(testAngle); - float moveY = bot->GetPositionY() - distance * sin(testAngle); - float moveZ = bot->GetPositionZ(); - - // Check distances and line of sight - float newPuddleDistance = - sqrt(pow(moveX - puddle->GetPositionX(), 2) + pow(moveY - puddle->GetPositionY(), 2)); - float newCenterDistance = sqrt(pow(moveX - ICC_ROTFACE_CENTER_POSITION.GetPositionX(), 2) + - pow(moveY - ICC_ROTFACE_CENTER_POSITION.GetPositionY(), 2)); - - if (newPuddleDistance >= minPuddleDistance && newCenterDistance >= minCenterDistance && - newCenterDistance <= maxCenterDistance && bot->IsWithinLOS(moveX, moveY, moveZ)) - { - // Calculate safety score (favor positions farther from puddle and center) - float safetyScore = newPuddleDistance + newCenterDistance; - if (safetyScore > maxSafetyScore) - { - maxSafetyScore = safetyScore; - bestX = moveX; - bestY = moveY; - bestZ = moveZ; - } - } - } - - // If we are already in a valid position, stop moving - if (maxSafetyScore > 0.0f && bot->GetExactDist2d(bestX, bestY) <= increment) - return false; - } - - // Move to the best position found - if (maxSafetyScore > 0.0f) - { - return MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - - return false; -} - -bool IccRotfaceGroupPositionAction::HandleOozeTargeting() -{ - Unit* smallOoze = AI_VALUE2(Unit*, "find target", "little ooze"); - bool hasMutatedInfection = botAI->HasAura("Mutated Infection", bot); - - if ((smallOoze && smallOoze->GetVictim() == bot) || hasMutatedInfection) - return HandleOozeMemberPositioning(); - - return false; -} - -bool IccRotfaceGroupPositionAction::HandleOozeMemberPositioning() -{ - Unit* bigOoze = AI_VALUE2(Unit*, "find target", "big ooze"); - - // First case: No big ooze exists or is not alive, move to designated position - if (!bigOoze || !bigOoze->IsAlive() || !bigOoze->IsVisible()) - { - if (bot->GetExactDist2d(ICC_ROTFACE_BIG_OOZE_POSITION) > 3.0f) - { - return MoveTo(bot->GetMapId(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionX(), - ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionY(), ICC_ROTFACE_BIG_OOZE_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - } - } - // Second case: Big ooze exists and is alive, move to it for merging - else if (bot->GetExactDist2d(bigOoze) > 2.0f && bigOoze->IsAlive() && bigOoze->IsVisible()) - { - // Move to big ooze for merge in increments of 5 - float dx = bigOoze->GetPositionX() - bot->GetPositionX(); - float dy = bigOoze->GetPositionY() - bot->GetPositionY(); - float dz = bigOoze->GetPositionZ() - bot->GetPositionZ(); - float dist = std::sqrt(dx * dx + dy * dy); - if (dist > 5.0f) - { - dx /= dist; - dy /= dist; - float moveX = bot->GetPositionX() + dx * 5.0f; - float moveY = bot->GetPositionY() + dy * 5.0f; - float moveZ = bot->GetPositionZ() + (dz / dist) * 5.0f; - return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } - return MoveTo(bot->GetMapId(), bigOoze->GetPositionX(), bigOoze->GetPositionY(), bigOoze->GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_COMBAT); - - } - - return false; // Stay at position -} - -bool IccRotfaceGroupPositionAction::PositionRangedAndHealers(Unit* boss,Unit *smallOoze) -{ - // Only for ranged and healers - if (!(botAI->IsRanged(bot) || botAI->IsHeal(bot))) - return false; - - Difficulty diff = bot->GetRaidDifficulty(); - bool isBossCasting = false; - if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && boss->GetCurrentSpell(SPELL_SLIME_SPRAY)) - bool isBossCasting = true; - - bool isHeroic = (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC); - - // Move to the exact same position as the boss during slime spray - if (boss && isBossCasting && !isHeroic) - { - float x = boss->GetPositionX(); - float y = boss->GetPositionY(); - float z = boss->GetPositionZ(); - - // If not already close to the boss's position, move there - if (bot->GetExactDist2d(x, y) > 0.5f) - { - MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, - false); - } - // Otherwise, already at the correct position - return false; - } - - if (!isHeroic && !isBossCasting && boss && !(bot->getClass() == CLASS_HUNTER) && - (bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) < 2.0f || - bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) > 5.0f)) - { - MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), - bot->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - } - - if (!isHeroic) - return false; - - return FindAndMoveFromClosestMember(boss, smallOoze); -} - -bool IccRotfaceGroupPositionAction::FindAndMoveFromClosestMember(Unit* boss, Unit* smallOoze) -{ - - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - Unit* puddle = nullptr; - - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !botAI->HasAura("Ooze Flood", unit)) - continue; - - puddle = unit; - break; - } - - const float safeSpacingRadius = 10.0f; - const float moveIncrement = 2.0f; - const float maxMoveDistance = 12.0f; // Limit maximum movement distance - const float puddleSafeDistance = 30.0f; // Minimum distance to stay away from puddle - const float minCenterDistance = 20.0f; // Minimum distance from center position - const bool isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); - - // Ranged: spread from other members - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - // Calculate a combined vector representing all nearby members' positions - float totalX = 0.0f; - float totalY = 0.0f; - int nearbyCount = 0; - - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot || (smallOoze && smallOoze->GetVictim() == member) || - (member->IsPlayer() && botAI->IsAssistTank(static_cast(member)))) - continue; - - const float distance = bot->GetExactDist2d(member); - if (distance < safeSpacingRadius) - { - // Calculate vector from member to bot - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - - // Weight by inverse distance (closer members have more influence) - float weight = (safeSpacingRadius - distance) / safeSpacingRadius; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - // If we have nearby members, move away in the combined direction - if (nearbyCount > 0) - { - // Normalize the combined vector - float magnitude = std::sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) // Avoid division by zero - { - totalX /= magnitude; - totalY /= magnitude; - - // Calculate move distance based on nearest member - float moveDistance = std::min(moveIncrement, maxMoveDistance); - - // Create target position in the combined direction - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); // Maintain current Z - - // Ensure the target position is at least 30 yards away from the puddle - if (puddle) - { - float puddleDistance = std::sqrt(std::pow(targetX - puddle->GetPositionX(), 2) + - std::pow(targetY - puddle->GetPositionY(), 2)); - if (puddleDistance < puddleSafeDistance) - { - // Adjust the target position to move further away from the puddle - float dx = targetX - puddle->GetPositionX(); - float dy = targetY - puddle->GetPositionY(); - float adjustmentFactor = (puddleSafeDistance - puddleDistance) / puddleDistance; - targetX += dx * adjustmentFactor; - targetY += dy * adjustmentFactor; - } - } - - // Ensure the target position is at least 20 yards away from the center position - const float centerX = ICC_ROTFACE_CENTER_POSITION.GetPositionX(); - const float centerY = ICC_ROTFACE_CENTER_POSITION.GetPositionY(); - float centerDistance = std::sqrt(std::pow(targetX - centerX, 2) + std::pow(targetY - centerY, 2)); - if (centerDistance < minCenterDistance) - { - // Adjust the target position to move further away from the center - float dx = targetX - centerX; - float dy = targetY - centerY; - float adjustmentFactor = (minCenterDistance - centerDistance) / centerDistance; - targetX += dx * adjustmentFactor; - targetY += dy * adjustmentFactor; - } - - // Check if the target position is valid and move there - if (bot->IsWithinLOS(targetX, targetY, targetZ)) - { - Position targetPos(targetX, targetY, targetZ); - MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - else - { - // If los check fails, try shorter distance - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - Position targetPos(targetX, targetY, targetZ); - MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), targetPos.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - } - } - - return false; // Everyone is properly spaced -} - -bool IccRotfaceMoveAwayFromExplosionAction::Execute(Event /*event*/) -{ - // Skip if main tank or ooze flood - if (botAI->IsMainTank(bot)) - return false; - - botAI->Reset(); - - return MoveToRandomSafeLocation(); -} - -bool IccRotfaceMoveAwayFromExplosionAction::MoveToRandomSafeLocation() -{ - // Generate random angle - float angle = frand(0, 2 * M_PI); - - // Calculate initial move position - float moveX = bot->GetPositionX() + 5.0f * cos(angle); - float moveY = bot->GetPositionY() + 5.0f * sin(angle); - float moveZ = bot->GetPositionZ(); - - // Ensure the position is at least 30 yards away from any puddle - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& npc : npcs) - { - Unit* puddle = botAI->GetUnit(npc); - if (!puddle || !botAI->HasAura("Ooze Flood", puddle)) - continue; - - float puddleDistance = - std::sqrt(std::pow(moveX - puddle->GetPositionX(), 2) + std::pow(moveY - puddle->GetPositionY(), 2)); - if (puddleDistance < 30.0f) - { - // Adjust the position to move further away from the puddle - float dx = moveX - puddle->GetPositionX(); - float dy = moveY - puddle->GetPositionY(); - float adjustmentFactor = (30.0f - puddleDistance) / puddleDistance; - moveX += dx * adjustmentFactor; - moveY += dy * adjustmentFactor; - } - } - - // Ensure the position is at least 30 yards away from the center position - const Position centerPosition = ICC_ROTFACE_CENTER_POSITION; - float centerDistance = std::sqrt(std::pow(moveX - centerPosition.GetPositionX(), 2) + - std::pow(moveY - centerPosition.GetPositionY(), 2)); - if (centerDistance < 40.0f) - { - // Adjust the position to move further away from the center - float dx = moveX - centerPosition.GetPositionX(); - float dy = moveY - centerPosition.GetPositionY(); - float adjustmentFactor = (40.0f - centerDistance) / centerDistance; - moveX += dx * adjustmentFactor; - moveY += dy * adjustmentFactor; - } - - // Check line of sight - if (!bot->IsWithinLOS(moveX, moveY, moveZ)) - return false; - - // Move in increments of 5.0f towards the calculated position - float currentX = bot->GetPositionX(); - float currentY = bot->GetPositionY(); - float currentZ = bot->GetPositionZ(); - - float directionX = moveX - currentX; - float directionY = moveY - currentY; - float distance = std::sqrt(directionX * directionX + directionY * directionY); - - if (distance > 5.0f) - { - directionX /= distance; - directionY /= distance; - - moveX = currentX + directionX * 5.0f; - moveY = currentY + directionY * 5.0f; - } - - // Move to the position - return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED); -} - -// Proffesor Putricide -bool IccPutricideGrowingOozePuddleAction::Execute(Event /*event*/) -{ - Unit* closestPuddle = FindClosestThreateningPuddle(); - if (!closestPuddle) - return false; - - Position movePosition = CalculateSafeMovePosition(closestPuddle); - return MoveTo(bot->GetMapId(), movePosition.GetPositionX(), movePosition.GetPositionY(), - movePosition.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); -} - -Unit* IccPutricideGrowingOozePuddleAction::FindClosestThreateningPuddle() -{ - static const float BASE_RADIUS = 2.0f; - static const float STACK_MULTIPLIER = 0.6f; - static const float MIN_DISTANCE = 0.1f; - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - if (npcs.empty()) - return nullptr; - - Unit* closestPuddle = nullptr; - float closestDistance = FLT_MAX; - float closestSafeDistance = BASE_RADIUS; - - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) - continue; - - float currentDistance = std::max(MIN_DISTANCE, bot->GetExactDist(unit)); - float safeDistance = BASE_RADIUS; - - if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) - safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); - - if (currentDistance < safeDistance && currentDistance < closestDistance) - { - closestDistance = currentDistance; - closestSafeDistance = safeDistance; - closestPuddle = unit; - } - } - - return closestPuddle; -} - -Position IccPutricideGrowingOozePuddleAction::CalculateSafeMovePosition(Unit* closestPuddle) -{ - static const float BASE_RADIUS = 2.0f; - static const float STACK_MULTIPLIER = 0.6f; - static const float BUFFER_DISTANCE = 2.0f; - static const float MIN_DISTANCE = 0.1f; - static const int NUM_ANGLES_TO_TEST = 8; - - float botX = bot->GetPositionX(); - float botY = bot->GetPositionY(); - float botZ = bot->GetPositionZ(); - - // Calculate current distance and safe distance - float currentDistance = std::max(MIN_DISTANCE, bot->GetExactDist(closestPuddle)); - float safeDistance = BASE_RADIUS; - if (Aura* grow = closestPuddle->GetAura(SPELL_GROW_AURA)) - safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); - - // Calculate direction vector - float dx = botX - closestPuddle->GetPositionX(); - float dy = botY - closestPuddle->GetPositionY(); - float dist = std::max(MIN_DISTANCE, sqrt(dx * dx + dy * dy)); - - if (dist < MIN_DISTANCE * 2) - { - float randomAngle = float(rand()) / float(RAND_MAX) * 2 * M_PI; - dx = cos(randomAngle); - dy = sin(randomAngle); - } - else - { - dx /= dist; - dy /= dist; - } - - float moveDistance = safeDistance - currentDistance + BUFFER_DISTANCE; - - // Try different angles to find safe position - for (int i = 0; i < NUM_ANGLES_TO_TEST; i++) - { - float angle = (2 * M_PI * i) / NUM_ANGLES_TO_TEST; - float rotatedDx = dx * cos(angle) - dy * sin(angle); - float rotatedDy = dx * sin(angle) + dy * cos(angle); - - float testX = botX + rotatedDx * moveDistance; - float testY = botY + rotatedDy * moveDistance; - - if (!IsPositionTooCloseToOtherPuddles(testX, testY, closestPuddle) && bot->IsWithinLOS(testX, testY, botZ)) - { - // If main tank, add 6f to calculated position in the direction away from the puddle - if (botAI->IsTank(bot)) - { - float awayDx = testX - closestPuddle->GetPositionX(); - float awayDy = testY - closestPuddle->GetPositionY(); - float awayDist = std::sqrt(awayDx * awayDx + awayDy * awayDy); - if (awayDist > 0.001f) - { - awayDx /= awayDist; - awayDy /= awayDist; - testX += awayDx * 6.0f; - testY += awayDy * 6.0f; - } - } - return Position(testX, testY, botZ); - } - } - - // Fallback position if no safe position found - float fallbackX = botX + dx * moveDistance; - float fallbackY = botY + dy * moveDistance; - if (botAI->IsTank(bot)) - { - float awayDx = fallbackX - closestPuddle->GetPositionX(); - float awayDy = fallbackY - closestPuddle->GetPositionY(); - float awayDist = std::sqrt(awayDx * awayDx + awayDy * awayDy); - if (awayDist > 0.001f) - { - awayDx /= awayDist; - awayDy /= awayDist; - fallbackX += awayDx * 6.0f; - fallbackY += awayDy * 6.0f; - } - } - return Position(fallbackX, fallbackY, botZ); -} - -bool IccPutricideGrowingOozePuddleAction::IsPositionTooCloseToOtherPuddles(float x, float y, Unit* ignorePuddle) -{ - static const float BASE_RADIUS = 2.0f; - static const float STACK_MULTIPLIER = 0.6f; - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || unit == ignorePuddle || unit->GetEntry() != NPC_GROWING_OOZE_PUDDLE) - continue; - - float safeDistance = BASE_RADIUS; - if (Aura* grow = unit->GetAura(SPELL_GROW_AURA)) - safeDistance += (grow->GetStackAmount() * STACK_MULTIPLIER); - - float dist = sqrt(pow(x - unit->GetPositionX(), 2) + pow(y - unit->GetPositionY(), 2)); - if (dist < safeDistance) - return true; - } - - return false; -} - -bool IccPutricideVolatileOozeAction::Execute(Event /*event*/) -{ - static const float STACK_DISTANCE = 7.0f; - - Unit* ooze = AI_VALUE2(Unit*, "find target", "volatile ooze"); - if (!ooze) - return false; - - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); - if (!boss) - return false; - - // Main tank handling - if (botAI->IsMainTank(bot) && bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && - !boss->HealthBelowPct(36) && boss->GetVictim() == bot) - return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), - ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); - - // Skip if we have forbidden auras - if (botAI->HasAura("Gaseous Bloat", bot) || botAI->HasAura("Unbound Plague", bot)) - return false; - - // Find all alive oozes - std::vector aliveOozes; - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == ooze->GetEntry()) - aliveOozes.push_back(unit); - } - - // If more than one ooze is alive, kill all but one - if (aliveOozes.size() > 1) - { - for (size_t i = 0; i < aliveOozes.size() - 1; ++i) - { - bot->Kill(bot, aliveOozes[i]); - } - } - - // Mark ooze with skull - MarkOozeWithSkull(ooze); - - // Melee handling (non-tanks) - if (botAI->IsMelee(bot) && !botAI->IsMainTank(bot)) - { - bot->SetTarget(ooze->GetGUID()); - bot->SetFacingToObject(ooze); - if (bot->IsWithinMeleeRange(ooze)) - return Attack(ooze); - } - - // Ranged/healer handling - if (botAI->IsRanged(bot) || botAI->IsHeal(bot)) - { - Unit* stackTarget = FindAuraTarget(); - if (stackTarget && bot->GetDistance2d(stackTarget) > STACK_DISTANCE) - { - return MoveTo(bot->GetMapId(), stackTarget->GetPositionX(), stackTarget->GetPositionY(), - stackTarget->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - - if (ooze && !botAI->IsHeal(bot) && stackTarget && bot->GetDistance2d(stackTarget) <= STACK_DISTANCE) - { - bot->SetTarget(ooze->GetGUID()); - bot->SetFacingToObject(ooze); - if (bot->IsWithinRange(ooze, 25.0f)) - return Attack(ooze); - } - } - - return false; -} - -void IccPutricideVolatileOozeAction::MarkOozeWithSkull(Unit* ooze) -{ - Group* group = bot->GetGroup(); - if (!group) - return; - - constexpr uint8_t skullIconId = 7; - ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); - Unit* markedUnit = botAI->GetUnit(skullGuid); - - // Clear dead marks or marks that are not on ooze - if (markedUnit && (!markedUnit->IsAlive() || (ooze && markedUnit != ooze))) - group->SetTargetIcon(skullIconId, bot->GetGUID(), ObjectGuid::Empty); - - // Mark alive ooze if needed - if (ooze && ooze->IsAlive() && (!skullGuid || !markedUnit)) - group->SetTargetIcon(skullIconId, bot->GetGUID(), ooze->GetGUID()); -} - -Unit* IccPutricideVolatileOozeAction::FindAuraTarget() -{ - Group* group = bot->GetGroup(); - if (!group) - return nullptr; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - if (botAI->HasAura("Volatile Ooze Adhesive", member)) - return member; - } - - return nullptr; -} - -bool IccPutricideGasCloudAction::Execute(Event /*event*/) -{ - Unit* gasCloud = AI_VALUE2(Unit*, "find target", "gas cloud"); - if (!gasCloud) - return false; - - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); - if (!boss) - return false; - - // Tank positioning logic - if (botAI->IsTank(bot) && bot->GetExactDist2d(ICC_PUTRICIDE_TANK_POSITION) > 20.0f && !boss->HealthBelowPct(36) && - boss->GetVictim() == bot) - return MoveTo(bot->GetMapId(), ICC_PUTRICIDE_TANK_POSITION.GetPositionX(), - ICC_PUTRICIDE_TANK_POSITION.GetPositionY(), ICC_PUTRICIDE_TANK_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_COMBAT, true, false); - - if (botAI->IsMainTank(bot)) - return false; - - bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); - Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); - - // Find all alive gasCloud - std::vector aliveGasCloud; - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == gasCloud->GetEntry()) - aliveGasCloud.push_back(unit); - } - - // If more than one GasCloud is alive, kill all but one - if (aliveGasCloud.size() > 1) - { - for (size_t i = 0; i < aliveGasCloud.size() - 1; ++i) - { - bot->Kill(bot, aliveGasCloud[i]); - } - } - - // Skip if we have no aura but ooze exists - if (!hasGaseousBloat && volatileOoze) - return false; - - if (hasGaseousBloat) - return HandleGaseousBloatMovement(gasCloud); - - return HandleGroupAuraSituation(gasCloud); -} - -bool IccPutricideGasCloudAction::HandleGaseousBloatMovement(Unit* gasCloud) -{ - bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); - - if (!hasGaseousBloat) - return false; - - if (hasGaseousBloat && !bot->HasAura(SPELL_NITRO_BOOSTS)) - bot->AddAura(SPELL_NITRO_BOOSTS, - bot); // to make it a bit easier when abo fails to slow or bots take forever to kill oozes - - static const int NUM_ANGLES = 32; // Increased from 16 for better corner escape - static const float MIN_SAFE_DISTANCE = 35.0f; - static const float EMERGENCY_DISTANCE = 15.0f; - static const float GAS_BOMB_SAFE_DIST = 6.0f; - static const float MOVEMENT_INCREMENT = 5.0f; // Fixed movement increment - - Position botPos = bot->GetPosition(); - Position cloudPos = gasCloud->GetPosition(); - float cloudDist = gasCloud->GetExactDist2d(botPos); - - if (cloudDist >= MIN_SAFE_DISTANCE) - return false; - - // Gather all choking gas bombs - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector gasBombs; - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CHOKING_GAS_BOMB) - gasBombs.push_back(unit); - } - - // Calculate direction away from cloud - float dx = botPos.GetPositionX() - cloudPos.GetPositionX(); - float dy = botPos.GetPositionY() - cloudPos.GetPositionY(); - float dist = std::max(0.1f, sqrt(dx * dx + dy * dy)); - - if (dist <= 0) - return false; - - dx /= dist; - dy /= dist; - - // Try to find safe movement position with strict corner avoidance - Position bestPos; - bool foundPath = false; - float bestScore = 0.0f; - - for (int i = 0; i < NUM_ANGLES; i++) - { - float angle = (2 * M_PI * i) / NUM_ANGLES; - float rotatedDx = dx * cos(angle) - dy * sin(angle); - float rotatedDy = dx * sin(angle) + dy * cos(angle); - - // Only test positions at fixed increments of 5.0f - for (float testDist = MOVEMENT_INCREMENT; testDist <= 20.0f; testDist += MOVEMENT_INCREMENT) - { - float testX = botPos.GetPositionX() + rotatedDx * testDist; - float testY = botPos.GetPositionY() + rotatedDy * testDist; - float testZ = botPos.GetPositionZ(); - Position testPos(testX, testY, testZ); - - float newCloudDist = cloudPos.GetExactDist2d(testPos); - - // Check gas bomb distance - float minGasBombDist = FLT_MAX; - for (Unit* bomb : gasBombs) - { - float bombDist = sqrt(pow(testX - bomb->GetPositionX(), 2) + pow(testY - bomb->GetPositionY(), 2)); - if (bombDist < minGasBombDist) - minGasBombDist = bombDist; - } - - if (newCloudDist > cloudDist && minGasBombDist >= GAS_BOMB_SAFE_DIST && - bot->IsWithinLOS(testX, testY, testZ)) - { - // Strict corner avoidance - test movement freedom thoroughly - int freeDirections = 0; - static const int CHECK_DIRS = 16; // More directions for better detection - static const float CHECK_DIST = 8.0f; // Longer distance to detect walls/corners early - - for (int j = 0; j < CHECK_DIRS; j++) - { - float checkAngle = (2 * M_PI * j) / CHECK_DIRS; - float checkX = testX + cos(checkAngle) * CHECK_DIST; - float checkY = testY + sin(checkAngle) * CHECK_DIST; - if (bot->IsWithinLOS(checkX, checkY, testZ)) - freeDirections++; - } - - float freedomScore = (float)freeDirections / CHECK_DIRS; - - // STRICT: Reject any position that looks like a corner or restricted area - // Require at least 80% of directions to be free (13 out of 16) - if (freedomScore < 0.8f) - continue; // Skip this position entirely - - // Also check for "dead ends" - positions that might lead to corners - // Test if we can move further in the same direction - bool canContinueMoving = true; - float continueX = testX + rotatedDx * MOVEMENT_INCREMENT; - float continueY = testY + rotatedDy * MOVEMENT_INCREMENT; - if (!bot->IsWithinLOS(continueX, continueY, testZ)) - canContinueMoving = false; - - // Bonus for positions that allow continued movement in the same direction - float continuityBonus = canContinueMoving ? 5.0f : 0.0f; - - float combinedScore = newCloudDist + (freedomScore * 15.0f) + minGasBombDist + continuityBonus; - - if (!foundPath || combinedScore > bestScore) - { - bestPos = testPos; - bestScore = combinedScore; - foundPath = true; - } - } - } - } - - if (foundPath) - { - botAI->Reset(); - return MoveTo(bot->GetMapId(), bestPos.GetPositionX(), bestPos.GetPositionY(), bestPos.GetPositionZ(), false, - false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - // Emergency movement - only when very close to cloud - if (cloudDist < EMERGENCY_DISTANCE) - { - Position emergencyPos = CalculateEmergencyPosition(botPos, dx, dy); - - // Even for emergency, avoid corners - float minEmergencyGasBombDist = FLT_MAX; - for (Unit* bomb : gasBombs) - { - float bombDist = sqrt(pow(emergencyPos.GetPositionX() - bomb->GetPositionX(), 2) + - pow(emergencyPos.GetPositionY() - bomb->GetPositionY(), 2)); - if (bombDist < minEmergencyGasBombDist) - minEmergencyGasBombDist = bombDist; - } - - if (minEmergencyGasBombDist >= GAS_BOMB_SAFE_DIST && - bot->IsWithinLOS(emergencyPos.GetPositionX(), emergencyPos.GetPositionY(), emergencyPos.GetPositionZ())) - { - botAI->Reset(); - return MoveTo(bot->GetMapId(), emergencyPos.GetPositionX(), emergencyPos.GetPositionY(), - emergencyPos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - } - - return false; -} - -bool IccPutricideGasCloudAction::FindSafeMovementPosition(const Position& botPos, const Position& cloudPos, float dx, - float dy, int numAngles, Position& resultPos) -{ - float bestScore = 0.0f; - bool foundPath = false; - resultPos = botPos; - static const float MOVEMENT_INCREMENT = 5.0f; // Fixed movement increment - - for (int i = 0; i < numAngles; i++) - { - float angle = (2 * M_PI * i) / numAngles; - float rotatedDx = dx * cos(angle) - dy * sin(angle); - float rotatedDy = dx * sin(angle) + dy * cos(angle); - - for (float testDist = MOVEMENT_INCREMENT; testDist <= 20.0f; testDist += MOVEMENT_INCREMENT) - { - Position testPos(botPos.GetPositionX() + rotatedDx * testDist, botPos.GetPositionY() + rotatedDy * testDist, - botPos.GetPositionZ()); - - float newCloudDist = cloudPos.GetExactDist2d(testPos); - if (newCloudDist > cloudPos.GetExactDist2d(botPos) && - bot->IsWithinLOS(testPos.GetPositionX(), testPos.GetPositionY(), testPos.GetPositionZ())) - { - // Strict corner prevention - test movement freedom thoroughly - int freeDirections = 0; - static const int CHECK_DIRECTIONS = 16; - static const float CHECK_DISTANCE = 8.0f; - - for (int j = 0; j < CHECK_DIRECTIONS; j++) - { - float checkAngle = (2 * M_PI * j) / CHECK_DIRECTIONS; - float checkX = testPos.GetPositionX() + cos(checkAngle) * CHECK_DISTANCE; - float checkY = testPos.GetPositionY() + sin(checkAngle) * CHECK_DISTANCE; - if (bot->IsWithinLOS(checkX, checkY, testPos.GetPositionZ())) - freeDirections++; - } - - float freedomScore = (float)freeDirections / CHECK_DIRECTIONS; - - // REJECT positions that could lead to corners - require high freedom - if (freedomScore < 0.8f) // 80% of directions must be free - continue; - - // Check if we can continue moving in the same direction (avoid dead ends) - float continueX = testPos.GetPositionX() + rotatedDx * MOVEMENT_INCREMENT; - float continueY = testPos.GetPositionY() + rotatedDy * MOVEMENT_INCREMENT; - bool canContinue = bot->IsWithinLOS(continueX, continueY, testPos.GetPositionZ()); - - float continuityBonus = canContinue ? 3.0f : 0.0f; - float combinedScore = newCloudDist + (freedomScore * 12.0f) + continuityBonus; - - if (!foundPath || combinedScore > bestScore) - { - resultPos = testPos; - bestScore = combinedScore; - foundPath = true; - } - } - } - } - - return foundPath; -} - -Position IccPutricideGasCloudAction::CalculateEmergencyPosition(const Position& botPos, float dx, float dy) -{ - // For emergency, still try to avoid corners but prioritize getting away from immediate danger - Position bestPos = - Position(botPos.GetPositionX() + dx * 15.0f, botPos.GetPositionY() + dy * 15.0f, botPos.GetPositionZ()); - float bestFreedom = 0.0f; - - // Try fewer directions for emergency but still avoid corners - for (int i = 0; i < 8; i++) - { - float angle = (2 * M_PI * i) / 8; - float rotatedDx = dx * cos(angle) - dy * sin(angle); - float rotatedDy = dx * sin(angle) + dy * cos(angle); - - Position testPos(botPos.GetPositionX() + rotatedDx * 15.0f, botPos.GetPositionY() + rotatedDy * 15.0f, - botPos.GetPositionZ()); - - if (bot->IsWithinLOS(testPos.GetPositionX(), testPos.GetPositionY(), testPos.GetPositionZ())) - { - // Quick freedom check for emergency - int freeDirections = 0; - static const int EMERGENCY_CHECK_DIRS = 8; - static const float EMERGENCY_CHECK_DIST = 6.0f; - - for (int j = 0; j < EMERGENCY_CHECK_DIRS; j++) - { - float checkAngle = (2 * M_PI * j) / EMERGENCY_CHECK_DIRS; - float checkX = testPos.GetPositionX() + cos(checkAngle) * EMERGENCY_CHECK_DIST; - float checkY = testPos.GetPositionY() + sin(checkAngle) * EMERGENCY_CHECK_DIST; - if (bot->IsWithinLOS(checkX, checkY, testPos.GetPositionZ())) - freeDirections++; - } - - float freedom = (float)freeDirections / EMERGENCY_CHECK_DIRS; - - // For emergency, accept positions with at least 60% freedom - if (freedom >= 0.6f && freedom > bestFreedom) - { - bestPos = testPos; - bestFreedom = freedom; - } - } - } - - return bestPos; -} - -bool IccPutricideGasCloudAction::HandleGroupAuraSituation(Unit* gasCloud) -{ - Group* group = bot->GetGroup(); - if (!group || botAI->IsHeal(bot)) - return false; - - // Mark gas cloud with skull if no volatile ooze is present or alive - Unit* volatileOoze = AI_VALUE2(Unit*, "find target", "volatile ooze"); - if ((!volatileOoze || !volatileOoze->IsAlive()) && gasCloud && gasCloud->IsAlive()) - { - Group* group = bot->GetGroup(); - if (group) - { - constexpr uint8_t skullIconId = 7; - ObjectGuid currentSkull = group->GetTargetIcon(skullIconId); - Unit* markedUnit = botAI->GetUnit(currentSkull); - if (!markedUnit || !markedUnit->IsAlive() || markedUnit != gasCloud) - group->SetTargetIcon(skullIconId, bot->GetGUID(), gasCloud->GetGUID()); - } - } - - float currentDist = gasCloud ? bot->GetDistance(gasCloud) : 0.0f; - const float MIN_SAFE_DISTANCE = 15.0f; - - // Always maintain minimum distance when group doesn't have bloat - if (!GroupHasGaseousBloat(group) && gasCloud && currentDist < MIN_SAFE_DISTANCE) - { - // Move away from gas cloud - float angle = bot->GetAngle(gasCloud); - float x = bot->GetPositionX() + cos(angle) * -MIN_SAFE_DISTANCE; - float y = bot->GetPositionY() + sin(angle) * -MIN_SAFE_DISTANCE; - return MoveTo(bot->GetMapId(), x, y, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - - if (GroupHasGaseousBloat(group) && gasCloud) - { - bot->SetTarget(gasCloud->GetGUID()); - bot->SetFacingToObject(gasCloud); - - // Attack logic for group with Gaseous Bloat - if (botAI->IsRanged(bot)) - { - // For ranged attackers, maintain optimal distance (15-25 yards) - if (currentDist > 25.0f) - { - // Move closer if too far - return MoveTo(bot->GetMapId(), gasCloud->GetPositionX(), gasCloud->GetPositionY(), - gasCloud->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - else if (currentDist < MIN_SAFE_DISTANCE) - { - // Move away if too close (but stay closer than normal safe distance since we need to attack) - float angle = bot->GetAngle(gasCloud); - float x = bot->GetPositionX() + cos(angle) * -12.0f; - float y = bot->GetPositionY() + sin(angle) * -12.0f; - return MoveTo(bot->GetMapId(), x, y, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - else - { - // Attack if at good range - return Attack(gasCloud); - } - } - else if (botAI->IsMelee(bot) && !botAI->IsTank(bot)) - { - // For melee attackers, move to attack range (0-5 yards) - if (currentDist > 5.0f) - { - return MoveTo(bot->GetMapId(), gasCloud->GetPositionX(), gasCloud->GetPositionY(), - gasCloud->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - else - return Attack(gasCloud); - } - } - - return false; -} - -bool IccPutricideGasCloudAction::GroupHasGaseousBloat(Group* group) -{ - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && botAI->HasAura("Gaseous Bloat", member)) - return true; - } - return false; -} - -bool IccPutricideAvoidMalleableGooAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); - if (!boss) - return false; - - // Tank handling for choking gas bomb - if (HandleTankPositioning(boss)) - return true; - - // Skip if volatile ooze or gas cloud exists - if (AI_VALUE2(Unit*, "find target", "volatile ooze") || AI_VALUE2(Unit*, "find target", "gas cloud")) - return false; - - // Handle unbound plague movement - if (HandleUnboundPlague(boss)) - return true; - - // Handle ranged/melee positioning - return HandleBossPositioning(boss); -} - -bool IccPutricideAvoidMalleableGooAction::HandleTankPositioning(Unit*) -{ - if (!botAI->IsTank(bot)) - return false; - - Unit* bomb = bot->FindNearestCreature(NPC_CHOKING_GAS_BOMB, 100.0f); - if (!bomb) - return false; - - const float safeDistance = 15.0f; - float currentDistance = bot->GetDistance2d(bomb); - - if (currentDistance < safeDistance) - return MoveAway(bomb, safeDistance - currentDistance); - - return false; -} - -bool IccPutricideAvoidMalleableGooAction::HandleUnboundPlague(Unit* boss) -{ - if (boss && boss->HealthBelowPct(35)) - return false; - - if (!botAI->HasAura("Unbound Plague", bot)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - const float UNBOUND_PLAGUE_DISTANCE = 20.0f; - float closestDistance = UNBOUND_PLAGUE_DISTANCE; - Unit* closestPlayer = nullptr; - - // Find closest player - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - float dist = bot->GetDistance2d(member); - if (dist < closestDistance) - { - closestDistance = dist; - closestPlayer = member; - } - } - - if (!closestPlayer || closestDistance >= UNBOUND_PLAGUE_DISTANCE) - { - bot->Kill(bot, bot); - return true; - } - - // Calculate move away position - float dx = bot->GetPositionX() - closestPlayer->GetPositionX(); - float dy = bot->GetPositionY() - closestPlayer->GetPositionY(); - float dist = sqrt(dx * dx + dy * dy); - - if (dist <= 0) - return false; - - dx /= dist; - dy /= dist; - float moveDistance = UNBOUND_PLAGUE_DISTANCE - closestDistance + 2.0f; - - float moveX = bot->GetPositionX() + dx * moveDistance; - float moveY = bot->GetPositionY() + dy * moveDistance; - - if (bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) - { - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - - return false; -} - -bool IccPutricideAvoidMalleableGooAction::HandleBossPositioning(Unit* boss) -{ - if (botAI->IsTank(bot)) - return false; - - // If boss is close to putricide_bad_position, all non-tank bots should be 1f in front of boss - const float BAD_POS_THRESHOLD = 10.0f; - const float IN_FRONT_DISTANCE = 1.0f; - float bossToBadPos = boss->GetExactDist2d(ICC_PUTRICIDE_BAD_POSITION.GetPositionX(), ICC_PUTRICIDE_BAD_POSITION.GetPositionY()); - - if (bossToBadPos <= BAD_POS_THRESHOLD) - { - // Move to 1f in front of boss - float bossOrientation = boss->GetOrientation(); - float targetX = boss->GetPositionX() + cos(bossOrientation) * IN_FRONT_DISTANCE; - float targetY = boss->GetPositionY() + sin(bossOrientation) * IN_FRONT_DISTANCE; - float targetZ = boss->GetPositionZ(); - - // Only move if not already close enough - if (bot->GetExactDist2d(targetX, targetY) > 0.5f) - { - bot->SetFacingToObject(boss); - return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, botAI->IsRanged(bot), - MovementPriority::MOVEMENT_COMBAT); - } - return false; - } - - float distToBoss = bot->GetExactDist2d(boss); - bool isRanged = botAI->IsRanged(bot); - - // Calculate desired position in front of boss - float desiredDistance = - isRanged ? ((bot->getClass() == CLASS_HUNTER) ? 14.0f : 6.0f) : (distToBoss < 2.0f ? 3.0f : 5.0f); - - // Check if we need to move - if ((std::abs(distToBoss - desiredDistance) > 0.5f || !boss->isInFront(bot)) && - (!isRanged || (isRanged && !botAI->IsTank(bot)))) - { - Position targetPos = CalculateBossPosition(boss, desiredDistance); - - // Check for obstacles - if (HasObstacleBetween(bot->GetPosition(), targetPos)) - { - // Use arc movement to navigate around obstacles - Position arcPoint = CalculateArcPoint(bot->GetPosition(), targetPos, boss->GetPosition()); - - if (bot->GetExactDist2d(arcPoint) > 1.0f) - { - bot->SetFacingToObject(boss); - return MoveTo(bot->GetMapId(), arcPoint.GetPositionX(), arcPoint.GetPositionY(), - arcPoint.GetPositionZ(), false, false, false, isRanged, - MovementPriority::MOVEMENT_COMBAT); - } - } - else - { - // No obstacles, move in increments directly toward target - Position adjustedTarget = CalculateIncrementalMove(bot->GetPosition(), targetPos, 2.0f); - bot->SetFacingToObject(boss); - return MoveTo(bot->GetMapId(), adjustedTarget.GetPositionX(), adjustedTarget.GetPositionY(), - adjustedTarget.GetPositionZ(), false, false, false, isRanged, - MovementPriority::MOVEMENT_COMBAT); - } - } - - return false; -} - -Position IccPutricideAvoidMalleableGooAction::CalculateBossPosition(Unit* boss, float distance) -{ - float bossOrientation = boss->GetOrientation(); - return Position(boss->GetPositionX() + cos(bossOrientation) * distance, - boss->GetPositionY() + sin(bossOrientation) * distance, boss->GetPositionZ()); -} - -bool IccPutricideAvoidMalleableGooAction::HasObstacleBetween(const Position& from, const Position& to) -{ - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - - if (unit->GetEntry() == NPC_GROWING_OOZE_PUDDLE || unit->GetEntry() == NPC_CHOKING_GAS_BOMB) - { - if (IsOnPath(from, to, unit->GetPosition(), 3.0f)) - return true; - } - } - return false; -} - -bool IccPutricideAvoidMalleableGooAction::IsOnPath(const Position& from, const Position& to, const Position& point, - float threshold) -{ - float pathX = to.GetPositionX() - from.GetPositionX(); - float pathY = to.GetPositionY() - from.GetPositionY(); - float pathLen = std::sqrt(pathX * pathX + pathY * pathY); - - if (pathLen < 0.1f) - return false; - - float normX = pathX / pathLen; - float normY = pathY / pathLen; - - float toPointX = point.GetPositionX() - from.GetPositionX(); - float toPointY = point.GetPositionY() - from.GetPositionY(); - float proj = toPointX * normX + toPointY * normY; - - if (proj < 0 || proj > pathLen) - return false; - - float closestX = from.GetPositionX() + normX * proj; - float closestY = from.GetPositionY() + normY * proj; - float distToPath = std::sqrt((point.GetPositionX() - closestX) * (point.GetPositionX() - closestX) + - (point.GetPositionY() - closestY) * (point.GetPositionY() - closestY)); - - return distToPath < threshold; -} - -Position IccPutricideAvoidMalleableGooAction::CalculateArcPoint(const Position& current, const Position& target, const Position& center) -{ - // Calculate vectors from center to current position and target - float currentX = current.GetPositionX() - center.GetPositionX(); - float currentY = current.GetPositionY() - center.GetPositionY(); - float targetX = target.GetPositionX() - center.GetPositionX(); - float targetY = target.GetPositionY() - center.GetPositionY(); - - // Calculate distances - float currentDist = std::sqrt(currentX * currentX + currentY * currentY); - float targetDist = std::sqrt(targetX * targetX + targetY * targetY); - - // Normalize vectors - currentX /= currentDist; - currentY /= currentDist; - targetX /= targetDist; - targetY /= targetDist; - - // Calculate dot product to find the angle between vectors - float dotProduct = currentX * targetX + currentY * targetY; - dotProduct = std::max(-1.0f, std::min(1.0f, dotProduct)); // Clamp to [-1, 1] - float angle = std::acos(dotProduct); - - // Determine rotation direction (clockwise or counterclockwise) - float crossProduct = currentX * targetY - currentY * targetX; - float stepAngle = angle * 0.25f; // Move 25% along the arc - - if (crossProduct < 0) - stepAngle = -stepAngle; // Clockwise - - // Calculate rotation matrix components - float cos_a = std::cos(stepAngle); - float sin_a = std::sin(stepAngle); - - // Rotate current vector - float rotatedX = currentX * cos_a - currentY * sin_a; - float rotatedY = currentX * sin_a + currentY * cos_a; - - // Scale to match the target distance for smoother approach - float desiredDist = currentDist * 0.9f + targetDist * 0.1f; - - // Calculate the new position - return Position(center.GetPositionX() + rotatedX * desiredDist, center.GetPositionY() + rotatedY * desiredDist, - current.GetPositionZ()); -} - -Position IccPutricideAvoidMalleableGooAction::CalculateIncrementalMove(const Position& current, const Position& target, - float maxDistance) -{ - float dx = target.GetPositionX() - current.GetPositionX(); - float dy = target.GetPositionY() - current.GetPositionY(); - float distance = std::sqrt(dx * dx + dy * dy); - - if (distance <= maxDistance) - return target; - - dx /= distance; - dy /= distance; - - return Position(current.GetPositionX() + dx * maxDistance, current.GetPositionY() + dy * maxDistance, - target.GetPositionZ()); -} - -// BPC -bool IccBpcKelesethTankAction::Execute(Event /*event*/) -{ - if (!botAI->IsAssistTank(bot)) - return false; - - // Handle boss positioning - Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); - if (!boss) - return false; - - bool isBossVictim = false; - if (boss && boss->GetVictim() == bot) - isBossVictim = true; - - // If not actively tanking, attack the boss - if (boss->GetVictim() != bot) - { - bot->SetTarget(boss->GetGUID()); - bot->SetFacingToObject(boss); - Attack(boss); - } - - // If tanking boss, check for Dark Nucleus logic - collect any nucleus not targeting us - if (boss->GetVictim() == bot) - { - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - for (auto const& targetGuid : targets) - { - Unit* nucleus = botAI->GetUnit(targetGuid); - if (nucleus && nucleus->IsAlive() && nucleus->GetEntry() == NPC_DARK_NUCLEUS) - { - // Attack nucleus that are NOT targeting us (to collect them) - if (nucleus->GetVictim() != bot) - { - bot->SetTarget(nucleus->GetGUID()); - bot->SetFacingToObject(nucleus); - Attack(nucleus); - // Return early to focus on nucleus collection first - return false; - } - } - } - } - - // Positioning logic - only execute if no nucleus needs collecting - if (botAI->HasAura("Invocation of Blood", boss) && bot->GetExactDist2d(ICC_BPC_MT_POSITION) > 15.0f && isBossVictim) - { - // Calculate direction vector - float dirX = ICC_BPC_MT_POSITION.GetPositionX() - bot->GetPositionX(); - float dirY = ICC_BPC_MT_POSITION.GetPositionY() - bot->GetPositionY(); - - // Calculate distance and normalize - float length = std::sqrt(dirX * dirX + dirY * dirY); - if (length > 0.001f) - { - dirX /= length; - dirY /= length; - - // Calculate movement distance (max 3 yards at a time) - float moveDist = std::min(3.0f, length); - - // Calculate target position - float moveX = bot->GetPositionX() + dirX * moveDist; - float moveY = bot->GetPositionY() + dirY * moveDist; - float moveZ = bot->GetPositionZ(); - - MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } - } - - // Always attack boss when tanking (if no nucleus was prioritized) - if (boss->GetVictim() == bot) - { - bot->SetTarget(boss->GetGUID()); - bot->SetFacingToObject(boss); - Attack(boss); - } - - return false; -} - -bool IccBpcMainTankAction::Execute(Event /*event*/) -{ - // Main tank specific behavior (higher priority) - if (botAI->IsMainTank(bot)) - { - // Get target princes - auto* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - auto* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); - - // Check if we're the target of both princes - bool isVictimOfValanar = false; - if (valanar && valanar->GetVictim() == bot) - isVictimOfValanar = true; - - bool isVictimOfTaldaram = false; - if (taldaram && taldaram->GetVictim() == bot) - isVictimOfTaldaram = true; - - // Move to MT position if targeted by both princes and not already close - if (isVictimOfValanar && isVictimOfTaldaram && bot->GetExactDist2d(ICC_BPC_MT_POSITION) > 15.0f) - { - // Calculate direction vector - float dirX = ICC_BPC_MT_POSITION.GetPositionX() - bot->GetPositionX(); - float dirY = ICC_BPC_MT_POSITION.GetPositionY() - bot->GetPositionY(); - - // Calculate distance and normalize - float length = std::sqrt(dirX * dirX + dirY * dirY); - if (length > 0.001f) - { - dirX /= length; - dirY /= length; - - // Calculate movement distance (max 3 yards at a time) - float moveDist = std::min(3.0f, length); - - // Calculate target position - float moveX = bot->GetPositionX() + dirX * moveDist; - float moveY = bot->GetPositionY() + dirY * moveDist; - float moveZ = bot->GetPositionZ(); - - MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - } - - // Attack prince that's not targeting us - if (valanar && !isVictimOfValanar) - { - bot->SetTarget(valanar->GetGUID()); - bot->SetFacingToObject(valanar); - Attack(valanar); - } - - if (taldaram && !isVictimOfTaldaram) - { - bot->SetTarget(taldaram->GetGUID()); - bot->SetFacingToObject(taldaram); - Attack(taldaram); - } - } - - // Target marking for all tanks, called after main tank priority actions - if (botAI->IsTank(bot)) - MarkEmpoweredPrince(); - - return false; -} - -void IccBpcMainTankAction::MarkEmpoweredPrince() -{ - static constexpr uint8_t SKULL_RAID_ICON = 7; - - // Find empowered prince (Invocation of Blood) - Unit* empoweredPrince = nullptr; - const GuidVector& targets = AI_VALUE(GuidVector, "possible targets"); - - for (auto const& targetGuid : targets) - { - Unit* unit = botAI->GetUnit(targetGuid); - if (!unit || !unit->IsAlive()) - continue; - - if (botAI->HasAura("Invocation of Blood", unit)) - { - const uint32 entry = unit->GetEntry(); - if (entry == NPC_PRINCE_KELESETH || entry == NPC_PRINCE_VALANAR || entry == NPC_PRINCE_TALDARAM) - { - empoweredPrince = unit; - break; - } - } - } - - // Handle marking if we found an empowered prince - if (empoweredPrince && empoweredPrince->IsAlive()) - { - Group* group = bot->GetGroup(); - if (group) - { - const ObjectGuid currentSkullGuid = group->GetTargetIcon(SKULL_RAID_ICON); - Unit* markedUnit = botAI->GetUnit(currentSkullGuid); - - // Clear dead marks or marks that are not on empowered prince - if (markedUnit && (!markedUnit->IsAlive() || markedUnit != empoweredPrince)) - { - group->SetTargetIcon(SKULL_RAID_ICON, bot->GetGUID(), ObjectGuid::Empty); - } - - // Mark alive empowered prince if needed - if (!currentSkullGuid || !markedUnit) - { - group->SetTargetIcon(SKULL_RAID_ICON, bot->GetGUID(), empoweredPrince->GetGUID()); - } - } - } -} - -bool IccBpcEmpoweredVortexAction::Execute(Event /*event*/) -{ - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - if (!valanar) - return false; - - // Check if boss is casting empowered vortex - bool isCastingVortex = false; - if (valanar && valanar->HasUnitState(UNIT_STATE_CASTING) && - (valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) || - valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) || - valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) || - valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4))) - isCastingVortex = true; - - if (isCastingVortex) - { - // Use complex positioning system for empowered vortex - return HandleEmpoweredVortexSpread(); - } - else - { - // Use simple ranged spreading for non-vortex situations - return MaintainRangedSpacing(); - } -} - -bool IccBpcEmpoweredVortexAction::MaintainRangedSpacing() -{ - const float safeSpacingRadius = 7.0f; - const float moveIncrement = 2.0f; - const float maxMoveDistance = 5.0f; - const bool isRanged = botAI->IsRanged(bot) || botAI->IsHeal(bot); - - if (!isRanged) - return false; - - // Get group members - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - // Calculate a combined vector representing all nearby members' positions - float totalX = 0.0f; - float totalY = 0.0f; - int nearbyCount = 0; - - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - { - continue; - } - - const float distance = bot->GetExactDist2d(member); - if (distance < safeSpacingRadius) - { - // Calculate vector from member to bot - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - - // Weight by inverse distance (closer members have more influence) - float weight = (safeSpacingRadius - distance) / safeSpacingRadius; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - // If we have nearby members, move away in the combined direction - if (nearbyCount > 0) - { - // Normalize the combined vector - float magnitude = std::sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) // Avoid division by zero - { - totalX /= magnitude; - totalY /= magnitude; - - // Calculate move distance - float moveDistance = std::min(moveIncrement, maxMoveDistance); - - // Create target position in the combined direction - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); - - // Check if the target position is valid and move there - if (bot->IsWithinLOS(targetX, targetY, targetZ)) - { - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - else - { - // If LOS check fails, try shorter distance - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - } - } - - return false; // Everyone is properly spaced -} - -bool IccBpcEmpoweredVortexAction::HandleEmpoweredVortexSpread() -{ - const float safeSpacingRadius = 13.0f; - const float moveIncrement = 2.0f; - const float maxMoveDistance = 5.0f; - const bool isTank = botAI->IsTank(bot); - - if (isTank) - return false; - - // Get group members - const GuidVector members = AI_VALUE(GuidVector, "group members"); - - // Calculate a combined vector representing all nearby members' positions - float totalX = 0.0f; - float totalY = 0.0f; - int nearbyCount = 0; - - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - { - continue; - } - - const float distance = bot->GetExactDist2d(member); - if (distance < safeSpacingRadius) - { - // Calculate vector from member to bot - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - - // Weight by inverse distance (closer members have more influence) - float weight = (safeSpacingRadius - distance) / safeSpacingRadius; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - // If we have nearby members, move away in the combined direction - if (nearbyCount > 0) - { - // Normalize the combined vector - float magnitude = std::sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) // Avoid division by zero - { - totalX /= magnitude; - totalY /= magnitude; - - // Calculate move distance - float moveDistance = std::min(moveIncrement, maxMoveDistance); - - // Create target position in the combined direction - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); - - // Check if the target position is valid and move there - if (bot->IsWithinLOS(targetX, targetY, targetZ)) - { - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - else - { - // If LOS check fails, try shorter distance - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - } - } - - return false; // Everyone is properly spaced -} - -bool IccBpcKineticBombAction::Execute(Event /*event*/) -{ - // Early exit if not ranged DPS - if (!botAI->IsRangedDps(bot)) - return false; - - // Static constants - static constexpr float MAX_HEIGHT_DIFF = 20.0f; - static constexpr float SAFE_HEIGHT = 371.16473f; - static constexpr float TELEPORT_HEIGHT = 366.16473f; - static constexpr std::array KINETIC_BOMB_ENTRIES = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, - NPC_KINETIC_BOMB3, NPC_KINETIC_BOMB4}; - - // Handle edge case where bot is too high - if (bot->GetPositionZ() > SAFE_HEIGHT) - { - bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TELEPORT_HEIGHT, - bot->GetOrientation()); - } - - // Check current target if valid - if (Unit* currentTarget = AI_VALUE(Unit*, "current target")) - { - if (currentTarget->IsAlive() && std::find(KINETIC_BOMB_ENTRIES.begin(), KINETIC_BOMB_ENTRIES.end(), - currentTarget->GetEntry()) != KINETIC_BOMB_ENTRIES.end()) - { - const float heightDiff = currentTarget->GetPositionZ() - 361.18222f; - if (heightDiff < MAX_HEIGHT_DIFF) - return false; // Continue current attack - } - } - - // Find the best kinetic bomb to attack - if (Unit* bestBomb = FindOptimalKineticBomb()) - { - bot->SetTarget(bestBomb->GetGUID()); - bot->SetFacingToObject(bestBomb); - Attack(bestBomb); - } - - return false; -} - -Unit* IccBpcKineticBombAction::FindOptimalKineticBomb() -{ - static constexpr float MAX_HEIGHT_DIFF = 20.0f; - static constexpr std::array KINETIC_BOMB_ENTRIES = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, - NPC_KINETIC_BOMB3, NPC_KINETIC_BOMB4}; - - const GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - if (targets.empty()) - return nullptr; - - const float botZ = 361.18222f; - Group* group = bot->GetGroup(); - - // Gather all valid kinetic bombs - std::vector kineticBombs; - for (auto const& guid : targets) - { - Unit* unit = botAI->GetUnit(guid); - if (!unit || !unit->IsAlive()) - continue; - if (std::find(KINETIC_BOMB_ENTRIES.begin(), KINETIC_BOMB_ENTRIES.end(), unit->GetEntry()) == - KINETIC_BOMB_ENTRIES.end()) - continue; - kineticBombs.push_back(unit); - } - - if (kineticBombs.empty()) - return nullptr; - - // Sort bombs by Z ascending (lowest first), then by heightDiff ascending (closest to ground) - std::sort(kineticBombs.begin(), kineticBombs.end(), - [botZ](Unit* a, Unit* b) - { - if (a->GetPositionZ() != b->GetPositionZ()) - return a->GetPositionZ() < b->GetPositionZ(); - return std::abs(a->GetPositionZ() - botZ) < std::abs(b->GetPositionZ() - botZ); - }); - - // Assign each ranged DPS to a unique bomb (lowest Z first) - std::vector rangedDps; - if (group) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && botAI->IsRangedDps(member)) - rangedDps.push_back(member); - } - // Sort by GUID for deterministic assignment - std::sort(rangedDps.begin(), rangedDps.end(), [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); - } - else - { - rangedDps.push_back(bot); - } - - // Find this bot's index among ranged DPS - auto it = std::find(rangedDps.begin(), rangedDps.end(), bot); - if (it == rangedDps.end()) - return nullptr; - size_t botIndex = std::distance(rangedDps.begin(), it); - - // Assign bombs in order, skip bombs already handled by other ranged DPS - size_t bombCount = kineticBombs.size(); - for (size_t i = 0, assigned = 0; i < bombCount; ++i) - { - Unit* bomb = kineticBombs[i]; - // Check if bomb is already handled by another ranged DPS closer than this bot - if (IsBombAlreadyHandled(bomb, group)) - continue; - if (assigned == botIndex) - return bomb; - ++assigned; - } - - // Fallback: pick the lowest bomb not already handled - for (Unit* bomb : kineticBombs) - { - if (!IsBombAlreadyHandled(bomb, group)) - return bomb; - } - - return nullptr; -} - -bool IccBpcKineticBombAction::IsBombAlreadyHandled(Unit* bomb, Group* group) -{ - if (!group) - return false; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || member == bot || !member->IsAlive() || !botAI->IsRangedDps(member)) - continue; - - if (member->GetTarget() == bomb->GetGUID() && member->GetDistance(bomb) < bot->GetDistance(bomb)) - return true; - } - - return false; -} - -bool IccBpcBallOfFlameAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "prince taldaram"); - if (!boss) - return false; - - Unit* flame1 = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f); - Unit* flame2 = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f); - - bool ballOfFlame = flame1 && (flame1->GetVictim() == bot); - bool infernoFlame = flame2 && (flame2->GetVictim() == bot); - - if (flame2 && (flame2->GetDistance2d(boss) > 2.0f) && !(flame2->GetDistance2d(boss) > 10.0f) && !infernoFlame && - bot->getClass() != CLASS_HUNTER) - { - if (!botAI->IsTank(bot) && !(flame2->GetVictim() == bot)) - { - float targetX = flame2->GetPositionX(); - float targetY = flame2->GetPositionY(); - float currentX = bot->GetPositionX(); - float currentY = bot->GetPositionY(); - - // Calculate direction vector - float dx = targetX - currentX; - float dy = targetY - currentY; - float distance = sqrt(dx * dx + dy * dy); - - // Normalize and scale to 5 units (or remaining distance if less than 5) - float step = std::min(5.0f, distance); - if (distance > 0.1) - { - dx = dx / distance * step; - dy = dy / distance * step; - } - - // Calculate intermediate position - float newX = currentX + dx; - float newY = currentY + dy; - - MoveTo(flame2->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } - } - - // If victim of ball of flame, keep at least 15f from other party members - if (ballOfFlame || infernoFlame) - { - const float SAFE_DIST = 15.0f; - GuidVector members = AI_VALUE(GuidVector, "group members"); - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || member == bot) - continue; - float dist = bot->GetExactDist2d(member); - if (dist < SAFE_DIST) - { - // Move away from this member - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - float len = std::sqrt(dx * dx + dy * dy); - if (len < 0.01f) - continue; - dx /= len; - dy /= len; - float moveX = bot->GetPositionX() + dx * (SAFE_DIST - dist + 1.0f); - float moveY = bot->GetPositionY() + dy * (SAFE_DIST - dist + 1.0f); - float moveZ = bot->GetPositionZ(); - if (bot->IsWithinLOS(moveX, moveY, moveZ)) - { - MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - } - } - } - } - - return false; -} - -// Blood Queen Lana'thel -bool IccBqlGroupPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); - if (!boss) - return false; - - Aura* frenzyAura = botAI->GetAura("Frenzied Bloodthirst", bot); - Aura* shadowAura = botAI->GetAura("Swarming Shadows", bot); - bool isTank = botAI->IsTank(bot); - // Handle tank positioning - if (isTank && HandleTankPosition(boss, frenzyAura, shadowAura)) - return true; - - // Handle swarming shadows movement - if (shadowAura && HandleShadowsMovement()) - return true; - - // Handle group positioning - if (!frenzyAura && !shadowAura && HandleGroupPosition(boss, frenzyAura, shadowAura)) - return true; - - return false; -} - -bool IccBqlGroupPositionAction::HandleTankPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura) -{ - if (frenzyAura || shadowAura) - return false; - - // Main tank positioning - if (botAI->IsMainTank(bot) && botAI->HasAggro(boss)) - { - if (bot->GetExactDist2d(ICC_BQL_TANK_POSITION) > 3.0f) - { - MoveTo(bot->GetMapId(), ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY(), - ICC_BQL_TANK_POSITION.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT); - } - } - - // Assist tank positioning - if (botAI->IsAssistTank(bot) && !botAI->GetAura("Blood Mirror", bot)) - { - if (Unit* mainTank = AI_VALUE(Unit*, "main tank")) - { - MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_COMBAT); - } - } - - return false; -} - -bool IccBqlGroupPositionAction::HandleShadowsMovement() -{ - const float SAFE_SHADOW_DIST = 4.0f; - const float ARC_STEP = 0.05f; - const float CURVE_SPACING = 15.0f; - const int MAX_CURVES = 3; - const float maxClosestDist = botAI->IsMelee(bot) ? 25.0f : 20.0f; - const Position& center = ICC_BQL_CENTER_POSITION; - const float OUTER_CURVE_PREFERENCE = 200.0f; // Strong preference for outer curves - const float CURVE_SWITCH_PENALTY = 50.0f; // Penalty for switching curves - const float DISTANCE_PENALTY_FACTOR = 100.0f; // Penalty per yard moved from current position - const float MAX_CURVE_JUMP_DIST = 5.0f; // Maximum distance for jumping between curves - - // Track current curve to avoid unnecessary switching - static std::map botCurrentCurve; - int currentCurve = botCurrentCurve.count(bot->GetGUID()) ? botCurrentCurve[bot->GetGUID()] : 0; - - // Find closest wall path - Position lwall[4] = {ICC_BQL_LWALL1_POSITION, AdjustControlPoint(ICC_BQL_LWALL2_POSITION, center, 1.30f), - AdjustControlPoint(ICC_BQL_LWALL3_POSITION, center, 1.30f), ICC_BQL_LRWALL4_POSITION}; - Position rwall[4] = {ICC_BQL_RWALL1_POSITION, AdjustControlPoint(ICC_BQL_RWALL2_POSITION, center, 1.30f), - AdjustControlPoint(ICC_BQL_RWALL3_POSITION, center, 1.30f), ICC_BQL_LRWALL4_POSITION}; - Position* basePath = (bot->GetExactDist2d(lwall[0]) < bot->GetExactDist2d(rwall[0])) ? lwall : rwall; - - // Find all swarming shadows - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - Unit* shadows[100]{}; // Reasonable max estimate - int shadowCount = 0; - for (int i = 0; i < npcs.size() && shadowCount < 100; i++) - { - Unit* unit = botAI->GetUnit(npcs[i]); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) - shadows[shadowCount++] = unit; - } - - // Helper lambda to check if a position is inside a shadow - auto IsPositionInShadow = [&](const Position& pos) -> bool - { - for (int i = 0; i < shadowCount; ++i) - { - if (pos.GetExactDist2d(shadows[i]) < SAFE_SHADOW_DIST) - return true; - } - return false; - }; - - // If bot is at the 4th position (end of the wall), move towards 3rd position or center to avoid getting stuck - float distToL4 = bot->GetExactDist2d(lwall[3]); - float distToR4 = bot->GetExactDist2d(rwall[3]); - const float STUCK_DIST = 2.0f; // within 2 yards is considered stuck at the end - - if (distToL4 < STUCK_DIST || distToR4 < STUCK_DIST) - { - // Move towards 3rd position of the same wall, or towards center if blocked - Position target; - if (distToL4 < distToR4) - { - target = lwall[2]; - } - else - { - target = rwall[2]; - } - - float tx = target.GetPositionX(); - float ty = target.GetPositionY(); - float tz = target.GetPositionZ(); - bot->UpdateAllowedPositionZ(tx, ty, tz); - if (!bot->IsWithinLOS(tx, ty, tz) || IsPositionInShadow(Position(tx, ty, tz))) - { - tx = center.GetPositionX(); - ty = center.GetPositionY(); - tz = center.GetPositionZ(); - } - - if (bot->GetExactDist2d(tx, ty) > 1.0f) - { - MoveTo(bot->GetMapId(), tx, ty, tz, false, false, false, true, MovementPriority::MOVEMENT_FORCED, - true, false); - } - return false; - } - - CurveInfo bestCurve; - bestCurve.foundSafe = false; - bestCurve.score = FLT_MAX; - bool foundCurve = false; - - // Keep track of information about all curves for possible fallback - CurveInfo curveInfos[MAX_CURVES]; - for (int i = 0; i < MAX_CURVES; i++) - { - curveInfos[i].foundSafe = false; - curveInfos[i].score = FLT_MAX; - } - - // Evaluate all curves starting from outermost (lowest index) - for (int curveIdx = 0; curveIdx < MAX_CURVES; curveIdx++) - { - float curveShrink = float(curveIdx) * CURVE_SPACING; - float shrinkFactor = 1.30f - (curveShrink / 30.0f); - if (shrinkFactor < 1.0f) - shrinkFactor = 1.0f; - - Position path[4] = {basePath[0], AdjustControlPoint(basePath[1], center, shrinkFactor / 1.30f), - AdjustControlPoint(basePath[2], center, shrinkFactor / 1.30f), basePath[3]}; - - // Find closest point on curve - float minDist = 9999.0f; - float t_closest = 0.0f; - Position closestPoint = path[0]; - - for (float t = 0.0f; t <= 1.0f; t += ARC_STEP) - { - Position pt = CalculateBezierPoint(t, path); - float dist = bot->GetExactDist2d(pt); - if (dist < minDist) - { - minDist = dist; - t_closest = t; - closestPoint = pt; - } - } - - // Check if the closest point is safe - bool closestIsSafe = !IsPositionInShadow(closestPoint); - - // Find closest safe point by searching in both directions from closest point - Position safeMoveTarget = closestPoint; - float safeMoveTargetDist = FLT_MAX; - bool foundSafe = closestIsSafe; - - // Only search for safe spots if the closest point isn't already safe - if (!closestIsSafe) - { - // Find the nearest safe point along the curve, not by direct distance - // but by distance along the curve from the closest point - - // Search forward on curve from closest point - float forwardT = -1.0f; - Position forwardPt; - for (float t = t_closest + ARC_STEP; t <= 1.0f; t += ARC_STEP) - { - Position pt = CalculateBezierPoint(t, path); - if (!IsPositionInShadow(pt)) - { - forwardT = t; - forwardPt = pt; - break; - } - } - - // Search backward on curve from closest point - float backwardT = -1.0f; - Position backwardPt; - for (float t = t_closest - ARC_STEP; t >= 0.0f; t -= ARC_STEP) - { - Position pt = CalculateBezierPoint(t, path); - if (!IsPositionInShadow(pt)) - { - backwardT = t; - backwardPt = pt; - break; - } - } - - // Choose the closest safe point based on curve distance, not direct distance - if (forwardT >= 0 && backwardT >= 0) - { - // Both directions have safe points, choose the closer one by curve distance - if (std::abs(forwardT - t_closest) < std::abs(backwardT - t_closest)) - { - safeMoveTarget = forwardPt; - foundSafe = true; - } - else - { - safeMoveTarget = backwardPt; - foundSafe = true; - } - } - else if (forwardT >= 0) - { - safeMoveTarget = forwardPt; - foundSafe = true; - } - else if (backwardT >= 0) - { - safeMoveTarget = backwardPt; - foundSafe = true; - } - } - - // Score this curve - float distancePenalty = 0.0f; - float score = 0.0f; - - if (foundSafe) - { - // If we found a safe point, penalize based on travel distance along the curve to reach it - float stepsToCurve = minDist / 2.0f; // Approximate steps to reach the curve - float safeDist = bot->GetExactDist2d(safeMoveTarget); - - // Add distance penalty based on how far we need to move along the curve - distancePenalty = safeDist * (1.0f / DISTANCE_PENALTY_FACTOR); - score = safeDist + distancePenalty; - } - else - { - // No safe point found, assign a high score - distancePenalty = minDist * (1.0f / DISTANCE_PENALTY_FACTOR); - score = minDist + distancePenalty + 1000.0f; // Penalty for unsafe position - } - - // Apply strong penalty for curves that are too far - if (minDist > maxClosestDist) - score += 500.0f; - - // Apply penalty for unsafe curves - if (!foundSafe) - score += 1000.0f; - - // Apply curve index preference (strongly prefer outer curves) - score += curveIdx * OUTER_CURVE_PREFERENCE; - - // Apply curve switching penalty - if (curveIdx != currentCurve && currentCurve != 0) - score += CURVE_SWITCH_PENALTY; - - // MORE IMPORTANT: Apply additional curve switching penalty if the bot is far away - // from the target curve (prevent jumping between curves when far away) - if (curveIdx != currentCurve && minDist > MAX_CURVE_JUMP_DIST) - score += 2000.0f; // Strong penalty to prevent jumping between curves - - // Store this curve's info - curveInfos[curveIdx].moveTarget = foundSafe ? safeMoveTarget : closestPoint; - curveInfos[curveIdx].foundSafe = foundSafe; - curveInfos[curveIdx].minDist = minDist; - curveInfos[curveIdx].curveIdx = curveIdx; - curveInfos[curveIdx].score = score; - curveInfos[curveIdx].closestPoint = closestPoint; - curveInfos[curveIdx].t_closest = t_closest; - - // Only update if this curve is better than our current best - if (!foundCurve || score < bestCurve.score) - { - bestCurve = curveInfos[curveIdx]; - foundCurve = true; - } - } - - // Fallback: If we're trying to switch to a far curve and we're not near any curve, - // find and use the closest curve instead of making a direct beeline - if (foundCurve && bestCurve.minDist > MAX_CURVE_JUMP_DIST && bestCurve.curveIdx != currentCurve) - { - // Look for the closest curve first - float closestDist = FLT_MAX; - int closestCurveIdx = -1; - - for (int i = 0; i < MAX_CURVES; i++) - { - if (curveInfos[i].minDist < closestDist) - { - closestDist = curveInfos[i].minDist; - closestCurveIdx = i; - } - } - - // If we found a closer curve, use that instead - if (closestCurveIdx >= 0 && closestCurveIdx != bestCurve.curveIdx) - { - bestCurve = curveInfos[closestCurveIdx]; - } - } - - // Remember the selected curve for next time - if (foundCurve) - { - botCurrentCurve[bot->GetGUID()] = bestCurve.curveIdx; - } - - // Create a move plan to guide the bot along the curve if necessary - if (foundCurve && bot->GetExactDist2d(bestCurve.moveTarget) > 1.0f) - { - // Final check: ensure we're not moving into a shadow - if (!IsPositionInShadow(bestCurve.moveTarget)) - { - // Get the curve - float curveShrink = float(bestCurve.curveIdx) * CURVE_SPACING; - float shrinkFactor = 1.30f - (curveShrink / 30.0f); - if (shrinkFactor < 1.0f) - shrinkFactor = 1.0f; - - Position path[4] = {basePath[0], AdjustControlPoint(basePath[1], center, shrinkFactor / 1.30f), - AdjustControlPoint(basePath[2], center, shrinkFactor / 1.30f), basePath[3]}; - - // CRITICAL CHANGE: First check if we need to move to the curve - float distToClosestPoint = bot->GetExactDist2d(bestCurve.closestPoint); - - // If we're not on the curve yet, first move to the closest point on the curve - if (distToClosestPoint > 2.0f) - { - botAI->Reset(); - return MoveTo(bot->GetMapId(), bestCurve.closestPoint.GetPositionX(), - bestCurve.closestPoint.GetPositionY(), bestCurve.closestPoint.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - - // Now we know we're on or very close to the curve, so we'll follow it properly - - // Find target point on curve (t_target parameter) - float t_target = 0.0f; - float targetMinDist = 9999.0f; - - for (float t = 0.0f; t <= 1.0f; t += ARC_STEP) - { - Position pt = CalculateBezierPoint(t, path); - float dist = bestCurve.moveTarget.GetExactDist2d(pt); - if (dist < targetMinDist) - { - targetMinDist = dist; - t_target = t; - } - } - - // Find an intermediate point along the curve between closest and target - float t_step = (t_target > bestCurve.t_closest) ? ARC_STEP : -ARC_STEP; - float t_intermediate = bestCurve.t_closest + t_step; - Position intermediateTarget; - bool foundValidIntermediate = false; - - // Limit the distance we move along the curve in one step - const float MAX_CURVE_MOVEMENT = 7.0f; // Max yards to move along curve - float curveDistanceMoved = 0.0f; - Position lastPos = bestCurve.closestPoint; - - while ((t_step > 0 && t_intermediate <= t_target) || (t_step < 0 && t_intermediate >= t_target)) - { - Position pt = CalculateBezierPoint(t_intermediate, path); - - // Check if this point is safe - if (!IsPositionInShadow(pt)) - { - // Calculate distance moved along curve so far - curveDistanceMoved += lastPos.GetExactDist2d(pt); - lastPos = pt; - - // If we've moved the maximum allowed distance, use this position - if (curveDistanceMoved >= MAX_CURVE_MOVEMENT) - { - intermediateTarget = pt; - foundValidIntermediate = true; - break; - } - - // Otherwise, continue moving along the curve - intermediateTarget = pt; - foundValidIntermediate = true; - } - else - { - // We've hit a shadow, stop here - break; - } - - t_intermediate += t_step; - } - - // If we found a valid intermediate point, use it - if (foundValidIntermediate) - { - botAI->Reset(); - MoveTo(bot->GetMapId(), intermediateTarget.GetPositionX(), intermediateTarget.GetPositionY(), - intermediateTarget.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } - - botAI->Reset(); - // Fallback to direct movement to the target point on the curve - MoveTo(bot->GetMapId(), bestCurve.moveTarget.GetPositionX(), bestCurve.moveTarget.GetPositionY(), - bestCurve.moveTarget.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } - } - - return false; -} - -Position IccBqlGroupPositionAction::AdjustControlPoint(const Position& wall, const Position& center, float factor) -{ - float dx = wall.GetPositionX() - center.GetPositionX(); - float dy = wall.GetPositionY() - center.GetPositionY(); - float dz = wall.GetPositionZ() - center.GetPositionZ(); - return Position(center.GetPositionX() + dx * factor, center.GetPositionY() + dy * factor, - center.GetPositionZ() + dz * factor); -} - -Position IccBqlGroupPositionAction::CalculateBezierPoint(float t, const Position path[4]) -{ - float omt = 1 - t; - float omt2 = omt * omt; - float omt3 = omt2 * omt; - float t2 = t * t; - float t3 = t2 * t; - - float x = omt3 * path[0].GetPositionX() + 3 * omt2 * t * path[1].GetPositionX() + - 3 * omt * t2 * path[2].GetPositionX() + t3 * path[3].GetPositionX(); - - float y = omt3 * path[0].GetPositionY() + 3 * omt2 * t * path[1].GetPositionY() + - 3 * omt * t2 * path[2].GetPositionY() + t3 * path[3].GetPositionY(); - - float z = omt3 * path[0].GetPositionZ() + 3 * omt2 * t * path[1].GetPositionZ() + - 3 * omt * t2 * path[2].GetPositionZ() + t3 * path[3].GetPositionZ(); - - return Position(x, y, z); -} - -bool IccBqlGroupPositionAction::HandleGroupPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura) -{ - if (frenzyAura || shadowAura) - return false; - - GuidVector members = AI_VALUE(GuidVector, "group members"); - bool isRanged = botAI->IsRanged(bot); - bool isMelee = botAI->IsMelee(bot); - - if (isRanged && bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) > 35.0f) - MoveTo(boss, 5.0f, MovementPriority::MOVEMENT_FORCED); - - if ((boss->GetExactDist2d(ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY()) > 10.0f) && - isRanged && !((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f) && - (bot->GetExactDist2d(ICC_BQL_CENTER_POSITION.GetPositionX(), ICC_BQL_CENTER_POSITION.GetPositionY()) > 10.0f)) - MoveTo(bot->GetMapId(), ICC_BQL_CENTER_POSITION.GetPositionX(), ICC_BQL_CENTER_POSITION.GetPositionY(), - ICC_BQL_CENTER_POSITION.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - - // --- Ranged bots wall assignment logic --- - if (isRanged) - { - // Gather all ranged and healers, sort by GUID for deterministic assignment - std::vector rangedBots; - std::vector healers; - for (auto const& guid : members) - { - Unit* member = botAI->GetUnit(guid); - if (!member || !member->IsAlive()) - continue; - Player* player = member->ToPlayer(); - if (!player) - continue; - if (botAI->IsRanged(player)) - rangedBots.push_back(player); - if (botAI->IsHeal(player)) - healers.push_back(player); - } - // Remove duplicates (healer can be ranged) - std::sort(rangedBots.begin(), rangedBots.end(), - [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); - std::sort(healers.begin(), healers.end(), [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); - - // Assign at least one healer to each side, then balance the rest - std::vector leftSide, rightSide; - Position leftPos = ICC_BQL_LWALL2_POSITION; - Position rightPos = ICC_BQL_RWALL2_POSITION; - - // Assign healers first - if (!healers.empty()) - { - leftSide.push_back(healers[0]); - if (healers.size() > 1) - rightSide.push_back(healers[1]); - } - // If only one healer, assign to left, right will be filled by ranged DPS - - // Remove assigned healers from rangedBots - for (Player* h : leftSide) - rangedBots.erase(std::remove(rangedBots.begin(), rangedBots.end(), h), rangedBots.end()); - for (Player* h : rightSide) - rangedBots.erase(std::remove(rangedBots.begin(), rangedBots.end(), h), rangedBots.end()); - - // Distribute remaining ranged evenly - size_t totalRanged = leftSide.size() + rightSide.size() + rangedBots.size(); - size_t leftCount = leftSide.size(); - size_t rightCount = rightSide.size(); - for (Player* p : rangedBots) - { - if (leftCount <= rightCount) - { - leftSide.push_back(p); - leftCount++; - } - else - { - rightSide.push_back(p); - rightCount++; - } - } - - // Determine which side this bot is assigned to - bool isLeft = std::find(leftSide.begin(), leftSide.end(), bot) != leftSide.end(); - bool isRight = std::find(rightSide.begin(), rightSide.end(), bot) != rightSide.end(); - - // Move to assigned wall position if not already close - const float MAX_WALL_DIST = 30.0f; - const float MOVE_INCREMENT = 2.0f; - const float MAX_MOVE_DISTANCE = 7.0f; - const float SAFE_SPACING_RADIUS = 7.0f; - const float MIN_CENTER_DISTANCE = 10.0f; - - Position targetWall = isLeft ? leftPos : (isRight ? rightPos : Position()); - if (isLeft || isRight) - { - float distToWall = bot->GetExactDist2d(targetWall.GetPositionX(), targetWall.GetPositionY()); - if (distToWall > MAX_WALL_DIST) - { - // Move in increments toward wall - float dx = targetWall.GetPositionX() - bot->GetPositionX(); - float dy = targetWall.GetPositionY() - bot->GetPositionY(); - float len = std::sqrt(dx * dx + dy * dy); - if (len > 0.001f) - { - dx /= len; - dy /= len; - float moveDist = std::min(MOVE_INCREMENT, distToWall); - float targetX = bot->GetPositionX() + dx * moveDist; - float targetY = bot->GetPositionY() + dy * moveDist; - float targetZ = bot->GetPositionZ(); - if (!bot->IsWithinLOS(targetX, targetY, targetZ)) - { - targetX = bot->GetPositionX() + dx * (moveDist * 0.5f); - targetY = bot->GetPositionY() + dy * (moveDist * 0.5f); - } - botAI->Reset(); - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - // Spread from other assigned members on the same side and from swarming shadows - float totalX = 0.0f, totalY = 0.0f; - int nearbyCount = 0; - const std::vector& mySide = isLeft ? leftSide : rightSide; - for (Player* member : mySide) - { - if (!member || !member->IsAlive() || member == bot) - continue; - float distance = bot->GetExactDist2d(member); - if (distance < SAFE_SPACING_RADIUS) - { - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - // Also spread from swarming shadows - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& npcGuid : npcs) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) - { - float distance = bot->GetExactDist2d(unit); - if (distance < SAFE_SPACING_RADIUS) - { - float dx = bot->GetPositionX() - unit->GetPositionX(); - float dy = bot->GetPositionY() - unit->GetPositionY(); - float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - } - if (nearbyCount > 0) - { - float magnitude = sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) - { - totalX /= magnitude; - totalY /= magnitude; - float moveDistance = std::min(MOVE_INCREMENT, MAX_MOVE_DISTANCE); - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); - if (!bot->IsWithinLOS(targetX, targetY, targetZ)) - { - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - } - botAI->Reset(); - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - // Maintain minimum distance from center position (if too close to center, move out) - float centerX = ICC_BQL_CENTER_POSITION.GetPositionX(); - float centerY = ICC_BQL_CENTER_POSITION.GetPositionY(); - float centerDist = - std::sqrt(std::pow(bot->GetPositionX() - centerX, 2) + std::pow(bot->GetPositionY() - centerY, 2)); - if (centerDist < MIN_CENTER_DISTANCE && !((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f)) - { - float dx = bot->GetPositionX() - centerX; - float dy = bot->GetPositionY() - centerY; - float dist = std::sqrt(dx * dx + dy * dy); - if (dist > 0.001f) - { - dx /= dist; - dy /= dist; - float moveDistance = std::min(MIN_CENTER_DISTANCE - centerDist + 1.0f, MAX_MOVE_DISTANCE); - float targetX = bot->GetPositionX() + dx * moveDistance; - float targetY = bot->GetPositionY() + dy * moveDistance; - float targetZ = bot->GetPositionZ(); - if (!bot->IsWithinLOS(targetX, targetY, targetZ)) - { - targetX = bot->GetPositionX() + dx * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + dy * (moveDistance * 0.5f); - } - botAI->Reset(); - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - } - } - - if (isMelee && ((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f)) - { - const float SAFE_SPACING_RADIUS = 7.0f; - const float MOVE_INCREMENT = 2.0f; - const float MAX_MOVE_DISTANCE = 7.0f; - - float totalX = 0.0f; - float totalY = 0.0f; - int nearbyCount = 0; - - // Find all swarming shadows - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector swarmingShadows; - for (int i = 0; i < npcs.size(); ++i) - { - Unit* unit = botAI->GetUnit(npcs[i]); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SWARMING_SHADOWS) - swarmingShadows.push_back(unit); - } - - for (int i = 0; i < members.size(); i++) - { - Unit* member = botAI->GetUnit(members[i]); - if (!member || !member->IsAlive() || member == bot || botAI->GetAura("Frenzied Bloodthirst", member) || - botAI->GetAura("Uncontrollable Frenzy", member)) - continue; - - float distance = bot->GetExactDist2d(member); - if (distance < SAFE_SPACING_RADIUS) - { - float dx = bot->GetPositionX() - member->GetPositionX(); - float dy = bot->GetPositionY() - member->GetPositionY(); - float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - // Also spread from swarming shadows - for (Unit* shadow : swarmingShadows) - { - float distance = bot->GetExactDist2d(shadow); - if (distance < SAFE_SPACING_RADIUS) - { - float dx = bot->GetPositionX() - shadow->GetPositionX(); - float dy = bot->GetPositionY() - shadow->GetPositionY(); - float weight = (SAFE_SPACING_RADIUS - distance) / SAFE_SPACING_RADIUS; - totalX += dx * weight; - totalY += dy * weight; - nearbyCount++; - } - } - - if (nearbyCount > 0) - { - float magnitude = sqrt(totalX * totalX + totalY * totalY); - if (magnitude > 0.001f) - { - totalX /= magnitude; - totalY /= magnitude; - float moveDistance = MOVE_INCREMENT < MAX_MOVE_DISTANCE ? MOVE_INCREMENT : MAX_MOVE_DISTANCE; - float targetX = bot->GetPositionX() + totalX * moveDistance; - float targetY = bot->GetPositionY() + totalY * moveDistance; - float targetZ = bot->GetPositionZ(); - - if (!bot->IsWithinLOS(targetX, targetY, targetZ)) - { - targetX = bot->GetPositionX() + totalX * (moveDistance * 0.5f); - targetY = bot->GetPositionY() + totalY * (moveDistance * 0.5f); - } - - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - return true; - } - - return false; -} - -bool IccBqlPactOfDarkfallenAction::Execute(Event /*event*/) -{ - // Check if bot has Pact of the Darkfallen - if (!botAI->GetAura("Pact of the Darkfallen", bot)) - return false; - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Find other players with Pact of the Darkfallen - Player* tankWithAura = nullptr; - std::vector playersWithAura; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || member == bot) - continue; - if (botAI->GetAura("Pact of the Darkfallen", member)) - { - playersWithAura.push_back(member); - if (botAI->IsTank(member)) - tankWithAura = member; - } - } - - if (playersWithAura.empty()) - return false; - - // Determine target position - Position targetPos; - if (tankWithAura) - { - // If there's a tank with aura, everyone moves to the tank (including the tank itself for center positioning) - if (botAI->IsTank(bot)) - { - // If current bot is the tank, stay put or move slightly for better positioning - targetPos.Relocate(bot); - } - else - { - // Non-tank bots move to the tank - targetPos.Relocate(tankWithAura); - } - } - else if (playersWithAura.size() >= 2) - { - // Calculate center position of all players with aura (including bot) - CalculateCenterPosition(targetPos, playersWithAura); - } - else if (playersWithAura.size() == 1) - { - // Move to the single other player with aura - targetPos.Relocate(playersWithAura[0]); - } - else - { - // No valid movement case found - return true; - } - - // Move to target position if needed - return MoveToTargetPosition(targetPos, playersWithAura.size() + 1); // +1 to include the bot itself -} - -void IccBqlPactOfDarkfallenAction::CalculateCenterPosition(Position& targetPos, - const std::vector& playersWithAura) -{ - float sumX = bot->GetPositionX(); - float sumY = bot->GetPositionY(); - float sumZ = bot->GetPositionZ(); - - // Add positions of all other players with aura - for (Player* player : playersWithAura) - { - sumX += player->GetPositionX(); - sumY += player->GetPositionY(); - sumZ += player->GetPositionZ(); - } - - // Calculate average position (center) - int totalPlayers = playersWithAura.size() + 1; // +1 for the bot itself - targetPos.Relocate(sumX / totalPlayers, sumY / totalPlayers, sumZ / totalPlayers); -} - -bool IccBqlPactOfDarkfallenAction::MoveToTargetPosition(const Position& targetPos, int auraCount) -{ - const float POSITION_TOLERANCE = 0.1f; - float distance = bot->GetDistance(targetPos); - if (distance <= POSITION_TOLERANCE) - return true; - - // Calculate movement increment - float dx = targetPos.GetPositionX() - bot->GetPositionX(); - float dy = targetPos.GetPositionY() - bot->GetPositionY(); - float dz = targetPos.GetPositionZ() - bot->GetPositionZ(); - float len = sqrt(dx * dx + dy * dy); - - float moveX, moveY, moveZ; - if (len > 5.0f && auraCount <= 2) - { - dx /= len; - dy /= len; - moveX = bot->GetPositionX() + dx * 5.0f; - moveY = bot->GetPositionY() + dy * 5.0f; - moveZ = bot->GetPositionZ() + (dz / distance) * 5.0f; - } - else - { - moveX = targetPos.GetPositionX(); - moveY = targetPos.GetPositionY(); - moveZ = targetPos.GetPositionZ(); - } - - botAI->Reset(); - MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED); - return false; -} - -bool IccBqlVampiricBiteAction::Execute(Event /*event*/) -{ - // Only act when bot has Frenzied Bloodthirst - if (!botAI->GetAura("Frenzied Bloodthirst", bot)) - return false; - - const float BITE_RANGE = 2.0f; - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Find best target - Player* target = FindBestBiteTarget(group); - if (!target) - return false; - - // Handle movement or casting - float x = target->GetPositionX(); - float y = target->GetPositionY(); - float z = target->GetPositionZ(); - - if (bot->GetExactDist2d(target) > BITE_RANGE) - { - return MoveTowardsTarget(target); - } - else if (bot->IsWithinLOS(x, y, z)) - { - return CastVampiricBite(target); - } - - return false; -} - -Player* IccBqlVampiricBiteAction::FindBestBiteTarget(Group* group) -{ - std::set currentlyTargetedPlayers; - std::vector> dpsTargets; - std::vector> healTargets; - - // Get currently targeted players - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; - - if (botAI->GetAura("Frenzied Bloodthirst", member) && member->GetTarget()) - { - currentlyTargetedPlayers.insert(member->GetTarget()); - } - } - - // Evaluate potential targets - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || member == bot || !member->IsAlive()) - continue; - - if (IsInvalidTarget(member) || currentlyTargetedPlayers.count(member->GetGUID())) - continue; - - float distance = bot->GetDistance(member); - if (botAI->IsDps(member)) - dpsTargets.push_back({member, distance}); - else if (botAI->IsHeal(member)) - healTargets.push_back({member, distance}); - } - - // Sort by distance - auto sortByDistance = [](auto const& a, auto const& b) { return a.second < b.second; }; - std::sort(dpsTargets.begin(), dpsTargets.end(), sortByDistance); - std::sort(healTargets.begin(), healTargets.end(), sortByDistance); - - // Return closest valid target - if (!dpsTargets.empty()) - return dpsTargets[0].first; - if (!healTargets.empty()) - return healTargets[0].first; - return nullptr; -} - -bool IccBqlVampiricBiteAction::IsInvalidTarget(Player* player) -{ - return botAI->GetAura("Frenzied Bloodthirst", player) || botAI->GetAura("Essence of the Blood Queen", player) || - botAI->GetAura("Uncontrollable Frenzy", player) || botAI->GetAura("Swarming Shadows", player) || - botAI->IsTank(player); -} - -bool IccBqlVampiricBiteAction::MoveTowardsTarget(Player* target) -{ - if (IsInvalidTarget(target) || !target->IsAlive()) - return false; - - float x = target->GetPositionX(); - float y = target->GetPositionY(); - float z = target->GetPositionZ(); - - if (!bot->IsWithinLOS(x, y, z)) - return false; - - float dx = x - bot->GetPositionX(); - float dy = y - bot->GetPositionY(); - float dz = z - bot->GetPositionZ(); - float len = sqrt(dx * dx + dy * dy); - - float moveX, moveY, moveZ; - if (len > 5.0f) - { - dx /= len; - dy /= len; - moveX = bot->GetPositionX() + dx * 5.0f; - moveY = bot->GetPositionY() + dy * 5.0f; - moveZ = bot->GetPositionZ() + (dz / len) * 5.0f; - } - else - { - moveX = x; - moveY = y; - moveZ = z; - } - - MoveTo(target->GetMapId(), moveX, moveY, moveZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - - return false; -} - -bool IccBqlVampiricBiteAction::CastVampiricBite(Player* target) -{ - if (IsInvalidTarget(target) || !target->IsAlive()) - return false; - - return botAI->CanCastSpell("Vampiric Bite", target) && botAI->CastSpell("Vampiric Bite", target); -} - -// Sister Svalna -bool IccValkyreSpearAction::Execute(Event /*event*/) -{ - // Find the nearest spear - Creature* spear = bot->FindNearestCreature(NPC_SPEAR, 100.0f); - if (!spear) - return false; - - // Move to the spear if not in range - if (!spear->IsWithinDistInMap(bot, INTERACTION_DISTANCE)) - return MoveTo(spear, INTERACTION_DISTANCE); - - // Remove shapeshift forms - botAI->RemoveShapeshift(); - - // Stop movement and click the spear - bot->GetMotionMaster()->Clear(); - bot->StopMoving(); - spear->HandleSpellClick(bot); - - // Dismount if mounted - WorldPacket emptyPacket; - bot->GetSession()->HandleCancelMountAuraOpcode(emptyPacket); - - return false; -} - -bool IccSisterSvalnaAction::Execute(Event /*event*/) -{ - Unit* svalna = AI_VALUE2(Unit*, "find target", "sister svalna"); - if (!svalna || !svalna->HasAura(SPELL_AETHER_SHIELD)) // Check for Aether Shield aura - return false; - - // Check if bot has the spear item - if (!botAI->HasItemInInventory(ITEM_SPEAR)) - return false; - - // Get all items from inventory - std::vector items = botAI->GetInventoryItems(); - for (Item* item : items) - { - if (item->GetEntry() == ITEM_SPEAR) // Spear ID - { - botAI->ImbueItem(item, svalna); // Use spear on Svalna - return false; - } - } - - return false; -} - -// VDW -bool IccValithriaGroupAction::Execute(Event /*event*/) -{ - // Helper lambda to find nearest creature of given entries - auto findNearestCreature = [this](std::initializer_list entries, float range) -> Creature* - { - for (uint32 entry : entries) - { - if (Creature* creature = bot->FindNearestCreature(entry, range)) - { - return creature; - } - } - return nullptr; - }; - - // Find portals and enemies - Creature* portal = findNearestCreature({NPC_DREAM_PORTAL, NPC_DREAM_PORTAL_PRE_EFFECT, NPC_NIGHTMARE_PORTAL, NPC_NIGHTMARE_PORTAL_PRE_EFFECT}, 100.0f); - - Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); - Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); - Creature* manaVoid = bot->FindNearestCreature(NPC_MANA_VOID, 100.0f); - - // Find column of frost units - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector columnOfFrost; - for (ObjectGuid guid : npcs) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->IsAlive() && unit->GetEntry() == NPC_COLUMN_OF_FROST) - { - columnOfFrost.push_back(unit); - } - } - } - - // Tank behavior - if (botAI->IsTank(bot)) - { - for (auto const& targetGuid : AI_VALUE(GuidVector, "possible targets")) - { - if (Unit* unit = botAI->GetUnit(targetGuid)) - { - if (unit->IsAlive() && - (unit->GetEntry() == NPC_GLUTTONOUS_ABOMINATION || unit->GetEntry() == NPC_ROT_WORM)) - { - // Skip if unit is already attacking any tank - if (Unit* victim = unit->GetVictim()) - { - if (victim->IsPlayer() && botAI->IsTank(static_cast(victim))) - { - continue; - } - } - - // Only attack if not already targeting us - if (unit->GetVictim() != bot) - { - bot->SetTarget(unit->GetGUID()); - bot->SetFacingToObject(unit); - Attack(unit); - } - } - } - } - } - - // Healer movement logic - if (botAI->IsHeal(bot) && bot->GetExactDist2d(ICC_VDW_HEAL_POSITION) > 30.0f && !portal) - return MoveTo(bot->GetMapId(), ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY(), - ICC_VDW_HEAL_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_NORMAL); - - // Avoidance behaviors - if (manaVoid && bot->GetExactDist2d(manaVoid) < 10.0f && - !(botAI->GetAura("Twisted Nightmares", bot) || botAI->GetAura("Emerald Vigor", bot))) - { - botAI->Reset(); - FleePosition(manaVoid->GetPosition(), 11.0f, 250U); - } - - for (Unit* column : columnOfFrost) - { - if (column && bot->GetExactDist2d(column) < 7.0f) - { - botAI->Reset(); - FleePosition(column->GetPosition(), 8.0f, 250U); - } - } - - if (worm && worm->IsAlive() && worm->GetVictim() == bot && !botAI->IsTank(bot)) - { - botAI->Reset(); - FleePosition(worm->GetPosition(), 10.0f, 250U); - } - - if (zombie && zombie->IsAlive() && zombie->GetVictim() == bot && !botAI->IsTank(bot) && - bot->GetExactDist2d(zombie) < 20.0f) - { - botAI->Reset(); - FleePosition(zombie->GetPosition(), 21.0f, 250U); - } - - // Crowd control logic - if (zombie && !botAI->IsMainTank(bot) && !botAI->IsHeal(bot) && zombie->GetVictim() != bot) - { - switch (bot->getClass()) - { - case CLASS_MAGE: - if (!botAI->HasAura("Frost Nova", zombie)) - botAI->CastSpell("Frost Nova", zombie); - break; - case CLASS_DRUID: - if (!botAI->HasAura("Entangling Roots", zombie)) - botAI->CastSpell("Entangling Roots", zombie); - break; - case CLASS_PALADIN: - if (!botAI->HasAura("Hammer of Justice", zombie)) - botAI->CastSpell("Hammer of Justice", zombie); - break; - case CLASS_WARRIOR: - if (!botAI->HasAura("Hamstring", zombie)) - botAI->CastSpell("Hamstring", zombie); - break; - case CLASS_HUNTER: - if (!botAI->HasAura("Concussive Shot", zombie)) - botAI->CastSpell("Concussive Shot", zombie); - break; - case CLASS_ROGUE: - if (!botAI->HasAura("Kidney Shot", zombie)) - botAI->CastSpell("Kidney Shot", zombie); - break; - case CLASS_SHAMAN: - if (!botAI->HasAura("Frost Shock", zombie)) - botAI->CastSpell("Frost Shock", zombie); - break; - case CLASS_DEATH_KNIGHT: - if (!botAI->HasAura("Chains of Ice", zombie)) - botAI->CastSpell("Chains of Ice", zombie); - break; - default: - break; - } - } - - // Group assignment and movement logic - Difficulty diff = bot->GetRaidDifficulty(); - Group* group = bot->GetGroup(); - - if (group && (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - return Handle25ManGroupLogic(); - else - return Handle10ManGroupLogic(); -} - -bool IccValithriaGroupAction::MoveTowardsPosition(const Position& pos, float increment) -{ - float dx = pos.GetPositionX() - bot->GetPositionX(); - float dy = pos.GetPositionY() - bot->GetPositionY(); - float dz = pos.GetPositionZ() - bot->GetPositionZ(); - float dist = std::hypot(dx, dy); - - float moveX, moveY, moveZ; - if (dist > increment) - { - dx /= dist; - dy /= dist; - moveX = bot->GetPositionX() + dx * increment; - moveY = bot->GetPositionY() + dy * increment; - moveZ = bot->GetPositionZ() + (dz / dist) * increment; - } - else - { - moveX = pos.GetPositionX(); - moveY = pos.GetPositionY(); - moveZ = pos.GetPositionZ(); - } - - MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, MovementPriority::MOVEMENT_COMBAT, - true, false); - - return false; -} - -bool IccValithriaGroupAction::Handle25ManGroupLogic() -{ - const Position group1Pos = ICC_VDW_GROUP1_POSITION; - const Position group2Pos = ICC_VDW_GROUP2_POSITION; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Collect group members - std::vector tanks, dps; - std::vector> nonHeals; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - if (Player* member = itr->GetSource()) - { - if (member->IsAlive() && !botAI->IsHeal(member)) - { - if (botAI->IsTank(member)) - { - tanks.push_back(member); - } - else - { - dps.push_back(member); - } - nonHeals.emplace_back(member->GetGUID(), member); - } - } - } - - // Sort by GUID for consistent ordering - std::sort(nonHeals.begin(), nonHeals.end(), [](auto const& a, auto const& b) { return a.first < b.first; }); - - // Assign to groups - std::vector group1, group2; - if (!tanks.empty()) - group1.push_back(tanks[0]); - - if (tanks.size() > 1) - group2.push_back(tanks[1]); - else if (tanks.size() == 1 && !dps.empty()) - group2.push_back(dps[0]); - - // Assign remaining DPS - std::set assigned; - for (Player* p : group1) - assigned.insert(p->GetGUID()); - for (Player* p : group2) - assigned.insert(p->GetGUID()); - - for (Player* p : dps) - { - if (assigned.find(p->GetGUID()) == assigned.end()) - { - (group1.size() <= group2.size() ? group1 : group2).push_back(p); - } - } - - // Check which group the bot is in - bool inGroup1 = std::any_of(group1.begin(), group1.end(), [this](Player* p) { return p == bot; }); - bool inGroup2 = std::any_of(group2.begin(), group2.end(), [this](Player* p) { return p == bot; }); - - // Marking logic for tanks and DPS - if (botAI->IsTank(bot) || botAI->IsDps(bot)) - HandleMarkingLogic(inGroup1, inGroup2, group1Pos, group2Pos); - - // Movement logic for non-healers - if (!botAI->IsHeal(bot)) - { - if (inGroup1) - { - float distance = bot->GetDistance(group1Pos); - if (distance > 25.0f) - { - // If far away, move directly to position - MoveTowardsPosition(group1Pos, 5.0f); - } - } - else if (inGroup2) - { - float distance = bot->GetDistance(group2Pos); - if (distance > 25.0f) - { - MoveTowardsPosition(group2Pos, 5.0f); - } - } - } - - return false; -} - -bool IccValithriaGroupAction::HandleMarkingLogic(bool inGroup1, bool inGroup2, const Position& group1Pos, - const Position& group2Pos) -{ - static constexpr uint8_t SKULL_ICON_INDEX = 7; - static constexpr uint8_t CROSS_ICON_INDEX = 6; - static const std::array addPriority = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, - NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, - NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; - - const Position* groupPos = nullptr; - uint8_t iconIndex = 0; - std::string rtiValue; - - if (inGroup1) - { - iconIndex = SKULL_ICON_INDEX; - groupPos = &group1Pos; - rtiValue = "skull"; - } - else if (inGroup2) - { - iconIndex = CROSS_ICON_INDEX; - groupPos = &group2Pos; - rtiValue = "cross"; - } - else - return false; - - context->GetValue("rti")->Set(rtiValue); - - // Find priority target - const GuidVector adds = AI_VALUE(GuidVector, "possible targets"); - Unit* priorityTarget = nullptr; - - for (uint32 entry : addPriority) - { - for (auto const& guid : adds) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->IsAlive() && unit->GetEntry() == entry && - unit->GetExactDist2d(groupPos->GetPositionX(), groupPos->GetPositionY()) <= 40.0f) - { - priorityTarget = unit; - break; - } - } - } - if (priorityTarget) - break; - } - - // Update target icon if needed - if (priorityTarget && bot->GetGroup()) - { - Group* group = bot->GetGroup(); - ObjectGuid currentIcon = group->GetTargetIcon(iconIndex); - Unit* currentIconUnit = botAI->GetUnit(currentIcon); - - // Check if the target already has any raid icon - bool hasOtherIcon = false; - for (uint8 i = 0; i < 8; ++i) - { - if (i == iconIndex) - continue; // Skip our own icon index - if (group->GetTargetIcon(i) == priorityTarget->GetGUID()) - { - hasOtherIcon = true; - break; - } - } - - if (!hasOtherIcon && (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != priorityTarget)) - { - group->SetTargetIcon(iconIndex, bot->GetGUID(), priorityTarget->GetGUID()); - } - } - - return false; -} - -bool IccValithriaGroupAction::Handle10ManGroupLogic() -{ - static constexpr uint8_t DEFAULT_ICON_INDEX = 7; - static const std::array addPriority = {NPC_BLAZING_SKELETON, NPC_SUPPRESSER, - NPC_RISEN_ARCHMAGE, NPC_BLISTERING_ZOMBIE, - NPC_GLUTTONOUS_ABOMINATION, NPC_ROT_WORM}; - - // Marking logic - Group* group = bot->GetGroup(); - if (group) - { - const GuidVector adds = AI_VALUE(GuidVector, "possible targets"); - Unit* priorityTarget = nullptr; - - for (uint32 entry : addPriority) - { - for (auto const& guid : adds) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->IsAlive() && unit->GetEntry() == entry && - unit->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), - ICC_VDW_HEAL_POSITION.GetPositionY()) <= 50.0f) - { - priorityTarget = unit; - break; - } - } - } - if (priorityTarget) - break; - } - - if (priorityTarget) - { - ObjectGuid currentIcon = group->GetTargetIcon(DEFAULT_ICON_INDEX); - Unit* currentIconUnit = botAI->GetUnit(currentIcon); - - if (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != priorityTarget) - { - group->SetTargetIcon(DEFAULT_ICON_INDEX, bot->GetGUID(), priorityTarget->GetGUID()); - } - } - } - - // Movement logic - if (bot->GetExactDist2d(ICC_VDW_HEAL_POSITION.GetPositionX(), ICC_VDW_HEAL_POSITION.GetPositionY()) > 25.0f) - MoveTowardsPosition(ICC_VDW_HEAL_POSITION, 5.0f); - - return false; -} - -bool IccValithriaPortalAction::Execute(Event /*event*/) -{ - // Only healers should take portals, and not if already inside - if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE)) - return false; - - // Gather all portals (pre-effect and real) using nearest npcs - GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); - std::vector preEffectPortals; - std::vector realPortals; - for (auto const& guid : npcs) - { - Creature* c = dynamic_cast(botAI->GetUnit(guid)); - if (!c) - continue; - uint32 entry = c->GetEntry(); - if (entry == NPC_DREAM_PORTAL_PRE_EFFECT || entry == NPC_NIGHTMARE_PORTAL_PRE_EFFECT) - preEffectPortals.push_back(c); - else if (entry == NPC_DREAM_PORTAL || entry == NPC_NIGHTMARE_PORTAL) - realPortals.push_back(c); - } - - if (preEffectPortals.empty() && realPortals.empty()) - return false; - - // Remove duplicates (in case of overlap) - auto sortByGuid = [](Creature* a, Creature* b) { return a->GetGUID() < b->GetGUID(); }; - std::sort(preEffectPortals.begin(), preEffectPortals.end(), sortByGuid); - preEffectPortals.erase(std::unique(preEffectPortals.begin(), preEffectPortals.end()), preEffectPortals.end()); - std::sort(realPortals.begin(), realPortals.end(), sortByGuid); - realPortals.erase(std::unique(realPortals.begin(), realPortals.end()), realPortals.end()); - - // Gather all healers in group, sort by GUID for deterministic assignment - Group* group = bot->GetGroup(); - std::vector healers; - if (group) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && botAI->IsHeal(member)) - healers.push_back(member); - } - std::sort(healers.begin(), healers.end(), [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); - } - else - healers.push_back(bot); - - // Find this bot's index among healers - auto it = std::find(healers.begin(), healers.end(), bot); - if (it == healers.end()) - return false; - size_t healerIndex = std::distance(healers.begin(), it); - - // Assign each healer to a pre-effect portal by index (wrap if more healers than portals) - Creature* assignedPreEffect = nullptr; - if (!preEffectPortals.empty()) - assignedPreEffect = preEffectPortals[healerIndex % preEffectPortals.size()]; - - // Move to assigned pre-effect portal, stand at portal - if (assignedPreEffect) - { - float portalX = assignedPreEffect->GetPositionX(); - float portalY = assignedPreEffect->GetPositionY(); - float portalZ = assignedPreEffect->GetPositionZ(); - float dist = bot->GetDistance2d(portalX, portalY); - - if (dist > 0.5f) - { - // Move directly to the pre-effect portal position - MoveTo(assignedPreEffect->GetMapId(), portalX, portalY, portalZ, false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - // Remove shapeshift forms - botAI->RemoveShapeshift(); - - // Try to click the real portal if it is close enough - Creature* nearestRealPortal = nullptr; - float minDist = 9999.0f; - for (Creature* portal : realPortals) - { - float d = bot->GetDistance2d(portal); - if (d < 3.0f && d < minDist) - { - nearestRealPortal = portal; - minDist = d; - } - } - - if (nearestRealPortal) - { - botAI->RemoveShapeshift(); - bot->GetMotionMaster()->Clear(); - bot->StopMoving(); - bot->SetFacingToObject(nearestRealPortal); - nearestRealPortal->HandleSpellClick(bot); - return true; - } - - // If no real portal is close, wait at the position - return false; - } - - // If no pre-effect portals, try to find a real portal within 3f - Creature* nearestRealPortal = nullptr; - float minDist = 9999.0f; - for (Creature* portal : realPortals) - { - float d = bot->GetDistance2d(portal); - if (d < 3.0f && d < minDist) - { - nearestRealPortal = portal; - minDist = d; - } - } - - if (nearestRealPortal && minDist > 2.0f) - MoveTo(bot->GetMapId(), nearestRealPortal->GetPositionX(), nearestRealPortal->GetPositionY(), - nearestRealPortal->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - - if (nearestRealPortal) - { - botAI->RemoveShapeshift(); - bot->GetMotionMaster()->Clear(); - bot->StopMoving(); - bot->SetFacingToObject(nearestRealPortal); - nearestRealPortal->HandleSpellClick(bot); - return true; - } - - return false; -} - -bool IccValithriaHealAction::Execute(Event /*event*/) -{ - // Early validation checks - if (!botAI->IsHeal(bot) || bot->GetHealthPct() < 50.0f) - return false; - - // Handle movement speed when not in dream state - if (!bot->HasAura(SPELL_DREAM_STATE)) - { - constexpr float NORMAL_SPEED = 1.0f; - bot->SetSpeed(MOVE_RUN, NORMAL_SPEED, true); - bot->SetSpeed(MOVE_WALK, NORMAL_SPEED, true); - bot->SetSpeed(MOVE_FLIGHT, NORMAL_SPEED, true); - } - - // Enforce Z-position limit - constexpr float MAX_Z_POSITION = 367.961f; - constexpr float TARGET_Z_POSITION = 365.0f; - if (bot->GetPositionZ() > MAX_Z_POSITION) - bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), TARGET_Z_POSITION, - bot->GetOrientation()); - - // Find Valithria within range - Creature* valithria = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); - if (!valithria) - return false; - - // Execute class-specific healing logic - switch (bot->getClass()) - { - case CLASS_DRUID: - { - // Druid healing spell constants - constexpr uint32 SPELL_REJUVENATION = 48441; - constexpr uint32 SPELL_REGROWTH = 48443; - constexpr uint32 SPELL_LIFEBLOOM = 48451; - constexpr uint32 SPELL_WILD_GROWTH = 53251; - constexpr uint8 LIFEBLOOM_MAX_STACKS = 3; - - // Apply Rejuvenation if missing - if (!valithria->HasAura(SPELL_REJUVENATION, bot->GetGUID())) - return botAI->CastSpell(SPELL_REJUVENATION, valithria); - - // Apply Regrowth if missing - if (!valithria->HasAura(SPELL_REGROWTH, bot->GetGUID())) - return botAI->CastSpell(SPELL_REGROWTH, valithria); - - // Stack Lifebloom to maximum stacks - Aura* lifebloom = valithria->GetAura(SPELL_LIFEBLOOM, bot->GetGUID()); - if (!lifebloom || lifebloom->GetStackAmount() < LIFEBLOOM_MAX_STACKS) - return botAI->CastSpell(SPELL_LIFEBLOOM, valithria); - - // All HoTs active with full stacks - cast Wild Growth - return botAI->CastSpell(SPELL_WILD_GROWTH, valithria); - } - case CLASS_SHAMAN: - { - constexpr uint32 SPELL_RIPTIDE = 61301; - constexpr uint32 SPELL_HEALING_WAVE = 49273; - - // Cast Healing Wave if Riptide is active, otherwise apply Riptide - return valithria->HasAura(SPELL_RIPTIDE, bot->GetGUID()) ? botAI->CastSpell(SPELL_HEALING_WAVE, valithria) - : botAI->CastSpell(SPELL_RIPTIDE, valithria); - } - case CLASS_PRIEST: - { - constexpr uint32 SPELL_RENEW = 48068; - constexpr uint32 SPELL_GREATER_HEAL = 48063; - - // Cast Greater Heal if Renew is active, otherwise apply Renew - return valithria->HasAura(SPELL_RENEW, bot->GetGUID()) ? botAI->CastSpell(SPELL_GREATER_HEAL, valithria) - : botAI->CastSpell(SPELL_RENEW, valithria); - } - case CLASS_PALADIN: - { - constexpr uint32 SPELL_BEACON_OF_LIGHT = 53563; - constexpr uint32 SPELL_HOLY_LIGHT = 48782; - - // Cast Holy Light if Beacon is active, otherwise apply Beacon of Light - return valithria->HasAura(SPELL_BEACON_OF_LIGHT, bot->GetGUID()) - ? botAI->CastSpell(SPELL_HOLY_LIGHT, valithria) - : botAI->CastSpell(SPELL_BEACON_OF_LIGHT, valithria); - } - default: - return false; - } - - return false; -} - -bool IccValithriaDreamCloudAction::Execute(Event /*event*/) -{ - // Only execute if we're in dream state - if (!bot->HasAura(SPELL_DREAM_STATE)) - return false; - - // Set speed to match players in dream state - bot->SetSpeed(MOVE_RUN, 2.0f, true); - bot->SetSpeed(MOVE_WALK, 2.0f, true); - bot->SetSpeed(MOVE_FLIGHT, 2.0f, true); - - // Gather all group members with dream state - const GuidVector members = AI_VALUE(GuidVector, "group members"); - std::vector dreamBots; - for (auto const& guid : members) - { - Unit* member = botAI->GetUnit(guid); - if (member && member->IsAlive() && member->HasAura(SPELL_DREAM_STATE)) - dreamBots.push_back(member); - } - - if (dreamBots.empty()) - return false; - - // Sort dreamBots by GUID (lowest first) - std::sort(dreamBots.begin(), dreamBots.end(), [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); - - // Find this bot's index in the sorted list - auto it = std::find(dreamBots.begin(), dreamBots.end(), bot); - if (it == dreamBots.end()) - return false; - size_t myIndex = std::distance(dreamBots.begin(), it); - - // Check if all dream bots are stacked within 3f of the current leader (lowest guid) - constexpr float STACK_RADIUS = 2.0f; - Unit* leader = dreamBots.front(); - bool allStacked = true; - for (Unit* member : dreamBots) - { - // Only require stacking for bots, not real players - Player* player = member->ToPlayer(); - if (player && !player->GetSession()) // is a bot - { - if (member->GetDistance(leader) > STACK_RADIUS) - { - allStacked = false; - break; - } - } - } - - // If not all stacked, everyone moves to the leader's position (clouds' position) - constexpr float PORTALSTART_TOLERANCE = 1.0f; - if (!allStacked) - { - if (bot != leader) - { - if (bot->GetDistance(leader) > PORTALSTART_TOLERANCE) - { - bot->TeleportTo(bot->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), leader->GetPositionZ(), - bot->GetOrientation()); - } - } - } - - // All stacked: leader (lowest guid) moves to next cloud, others follow and stack at leader's new position - // Find all dream and nightmare clouds - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector dreamClouds; - std::vector nightmareClouds; - - for (int i = 0; i < npcs.size(); ++i) - { - Unit* unit = botAI->GetUnit(npcs[i]); - if (unit && unit->IsAlive()) - { - if (Creature* creature = unit->ToCreature()) - { - if (creature->GetEntry() == NPC_DREAM_CLOUD) - dreamClouds.push_back(creature); - else if (creature->GetEntry() == NPC_NIGHTMARE_CLOUD) - nightmareClouds.push_back(creature); - } - } - } - - // Sort clouds by distance - std::sort(dreamClouds.begin(), dreamClouds.end(), - [this](Creature* a, Creature* b) { return bot->GetExactDist2d(a) < bot->GetExactDist2d(b); }); - - std::sort(nightmareClouds.begin(), nightmareClouds.end(), - [this](Creature* a, Creature* b) { return bot->GetExactDist2d(a) < bot->GetExactDist2d(b); }); - - // Only the leader moves to the next cloud - if (bot == leader) - { - // Use GUID to determine which cloud type to prefer - bool preferDream = (bot->GetGUID().GetCounter() % 2 == 0); - - // Check if we're close to any cloud - bool atDreamCloud = false; - bool atNightmareCloud = false; - - for (Creature* cloud : dreamClouds) - { - if (bot->GetExactDist2d(cloud) <= 2.0f) - { - atDreamCloud = true; - break; - } - } - - for (Creature* cloud : nightmareClouds) - { - if (bot->GetExactDist2d(cloud) <= 2.0f) - { - atNightmareCloud = true; - break; - } - } - - // If we have emerald vigor, prioritize dream clouds - if (bot->HasAura(SPELL_EMERALD_VIGOR)) - { - // If at dream cloud, move to 2nd closest dream cloud or closest nightmare cloud - if (atDreamCloud) - { - Creature* targetCloud = nullptr; - // Try 2nd closest dream cloud first - if (dreamClouds.size() >= 2 && bot->GetExactDist2d(dreamClouds[1]) > 2.0f) - targetCloud = dreamClouds[1]; - // Otherwise move to closest nightmare cloud - else if (!nightmareClouds.empty() && bot->GetExactDist2d(nightmareClouds[0]) > 2.0f) - targetCloud = nightmareClouds[0]; - - if (targetCloud) - MoveTo(targetCloud->GetMapId(), targetCloud->GetPositionX(), targetCloud->GetPositionY(), - targetCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - // If at nightmare cloud, move to closest dream cloud or 2nd closest nightmare cloud - else if (atNightmareCloud) - { - Creature* targetCloud = nullptr; - // Try closest dream cloud first - if (!dreamClouds.empty() && bot->GetExactDist2d(dreamClouds[0]) > 2.0f) - targetCloud = dreamClouds[0]; - // Otherwise move to 2nd closest nightmare cloud - else if (nightmareClouds.size() >= 2 && bot->GetExactDist2d(nightmareClouds[1]) > 2.0f) - targetCloud = nightmareClouds[1]; - - if (targetCloud) - MoveTo(targetCloud->GetMapId(), targetCloud->GetPositionX(), targetCloud->GetPositionY(), - targetCloud->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_NORMAL); - } - // If not at any cloud, move to closest dream cloud or nightmare cloud - else - { - if (!dreamClouds.empty() && bot->GetExactDist2d(dreamClouds[0]) > 2.0f) - MoveTo(dreamClouds[0]->GetMapId(), dreamClouds[0]->GetPositionX(), dreamClouds[0]->GetPositionY(), - dreamClouds[0]->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - else if (!nightmareClouds.empty() && bot->GetExactDist2d(nightmareClouds[0]) > 2.0f) - MoveTo(nightmareClouds[0]->GetMapId(), nightmareClouds[0]->GetPositionX(), - nightmareClouds[0]->GetPositionY(), nightmareClouds[0]->GetPositionZ(), false, false, false, - true, MovementPriority::MOVEMENT_NORMAL); - } - } - // Otherwise use GUID-based preference - else - { - // If prefer dream clouds based on GUID - if (preferDream) - { - // If at dream cloud, move to 2nd closest dream cloud or closest nightmare cloud - if (atDreamCloud) - { - Creature* targetCloud = nullptr; - // Try 2nd closest dream cloud first - if (dreamClouds.size() >= 2 && bot->GetExactDist2d(dreamClouds[1]) > 2.0f) - targetCloud = dreamClouds[1]; - // Otherwise move to closest nightmare cloud - else if (!nightmareClouds.empty() && bot->GetExactDist2d(nightmareClouds[0]) > 2.0f) - targetCloud = nightmareClouds[0]; - - if (targetCloud) - MoveTo(targetCloud->GetMapId(), targetCloud->GetPositionX(), targetCloud->GetPositionY(), - targetCloud->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - // If at nightmare cloud, move to closest dream cloud or 2nd closest nightmare cloud - else if (atNightmareCloud) - { - Creature* targetCloud = nullptr; - // Try closest dream cloud first - if (!dreamClouds.empty() && bot->GetExactDist2d(dreamClouds[0]) > 2.0f) - targetCloud = dreamClouds[0]; - // Otherwise move to 2nd closest nightmare cloud - else if (nightmareClouds.size() >= 2 && bot->GetExactDist2d(nightmareClouds[1]) > 2.0f) - targetCloud = nightmareClouds[1]; - - if (targetCloud) - MoveTo(targetCloud->GetMapId(), targetCloud->GetPositionX(), targetCloud->GetPositionY(), - targetCloud->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - // If not at any cloud, move to closest dream cloud or nightmare cloud based on preference - else - { - if (!dreamClouds.empty() && bot->GetExactDist2d(dreamClouds[0]) > 2.0f) - MoveTo(dreamClouds[0]->GetMapId(), dreamClouds[0]->GetPositionX(), - dreamClouds[0]->GetPositionY(), dreamClouds[0]->GetPositionZ(), false, false, false, - true, MovementPriority::MOVEMENT_NORMAL); - else if (!nightmareClouds.empty() && bot->GetExactDist2d(nightmareClouds[0]) > 2.0f) - MoveTo(nightmareClouds[0]->GetMapId(), nightmareClouds[0]->GetPositionX(), - nightmareClouds[0]->GetPositionY(), nightmareClouds[0]->GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_NORMAL); - } - } - // If prefer nightmare clouds based on GUID - else - { - // If at nightmare cloud, move to 2nd closest nightmare cloud or closest dream cloud - if (atNightmareCloud) - { - Creature* targetCloud = nullptr; - // Try 2nd closest nightmare cloud first - if (nightmareClouds.size() >= 2 && bot->GetExactDist2d(nightmareClouds[1]) > 2.0f) - targetCloud = nightmareClouds[1]; - // Otherwise move to closest dream cloud - else if (!dreamClouds.empty() && bot->GetExactDist2d(dreamClouds[0]) > 2.0f) - targetCloud = dreamClouds[0]; - - if (targetCloud) - MoveTo(targetCloud->GetMapId(), targetCloud->GetPositionX(), targetCloud->GetPositionY(), - targetCloud->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - // If at dream cloud, move to closest nightmare cloud or 2nd closest dream cloud - else if (atDreamCloud) - { - Creature* targetCloud = nullptr; - // Try closest nightmare cloud first - if (!nightmareClouds.empty() && bot->GetExactDist2d(nightmareClouds[0]) > 2.0f) - targetCloud = nightmareClouds[0]; - // Otherwise move to 2nd closest dream cloud - else if (dreamClouds.size() >= 2 && bot->GetExactDist2d(dreamClouds[1]) > 2.0f) - targetCloud = dreamClouds[1]; - - if (targetCloud) - MoveTo(targetCloud->GetMapId(), targetCloud->GetPositionX(), targetCloud->GetPositionY(), - targetCloud->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_NORMAL); - } - // If not at any cloud, move to closest nightmare cloud or dream cloud based on preference - else - { - if (!nightmareClouds.empty() && bot->GetExactDist2d(nightmareClouds[0]) > 2.0f) - MoveTo(nightmareClouds[0]->GetMapId(), nightmareClouds[0]->GetPositionX(), - nightmareClouds[0]->GetPositionY(), nightmareClouds[0]->GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_NORMAL); - else if (!dreamClouds.empty() && bot->GetExactDist2d(dreamClouds[0]) > 2.0f) - MoveTo(dreamClouds[0]->GetMapId(), dreamClouds[0]->GetPositionX(), - dreamClouds[0]->GetPositionY(), dreamClouds[0]->GetPositionZ(), false, false, false, - true, MovementPriority::MOVEMENT_NORMAL); - } - } - } - } - else - { - // Non-leader bots follow and stack at leader's position - if (bot->GetDistance(leader) > PORTALSTART_TOLERANCE) - { - botAI->Reset(); - bot->TeleportTo(bot->GetMapId(), leader->GetPositionX(), leader->GetPositionY(), leader->GetPositionZ(), - bot->GetOrientation()); - } - } - - return false; -} - -// Sindragosa -bool IccSindragosaGroupPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss || boss->HasUnitMovementFlag(MOVEMENTFLAG_DISABLE_GRAVITY)) - return false; - - Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); - - if (aura && aura->GetStackAmount() >= 6 && botAI->IsMainTank(bot)) - return false; - - if (botAI->IsTank(bot) && boss->GetVictim() == bot) - return HandleTankPositioning(boss); - - if (boss && boss->GetVictim() != bot) - return HandleNonTankPositioning(); - - return false; -} - -bool IccSindragosaGroupPositionAction::HandleTankPositioning(Unit* boss) -{ - float distBossToCenter = boss->GetExactDist2d(ICC_SINDRAGOSA_CENTER_POSITION); - float distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); - float targetOrientation = M_PI / 2; // We want boss to face east - float currentOrientation = boss->GetOrientation(); - - // Normalize both orientations to 0-2π range - currentOrientation = fmod(currentOrientation + 2 * M_PI, 2 * M_PI); - targetOrientation = fmod(targetOrientation + 2 * M_PI, 2 * M_PI); - - float orientationDiff = currentOrientation - targetOrientation; - - // Normalize the difference to be between -PI and PI - while (orientationDiff > M_PI) - orientationDiff -= 2 * M_PI; - while (orientationDiff < -M_PI) - orientationDiff += 2 * M_PI; - - // Stage 1: Move boss to center if too far - if (boss && boss->GetVictim() == bot && distBossToCenter > 16.0f && distToTankPos <= 20.0f) - { - // Calculate direction vector from boss to center - float dirX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() - boss->GetPositionX(); - float dirY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() - boss->GetPositionY(); - - // Move 10 yards beyond center in the same direction - float moveX = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionX() + (dirX / distBossToCenter) * 4.0f; - float moveY = ICC_SINDRAGOSA_CENTER_POSITION.GetPositionY() + (dirY / distBossToCenter) * 4.0f; - - return MoveTo(bot->GetMapId(), moveX, moveY, boss->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); - } - - // Stage 2: Move to tank position if too far - if (boss && boss->GetVictim() == bot && distToTankPos > 10.0f) - { - Position botPos = bot->GetPosition(); - Position tankPos = ICC_SINDRAGOSA_TANK_POSITION; - - float dx = tankPos.GetPositionX() - botPos.GetPositionX(); - float dy = tankPos.GetPositionY() - botPos.GetPositionY(); - - float distance = std::sqrt(dx * dx + dy * dy); - float step = 1.0f; - - // Normalize and scale direction vector - float scale = step / distance; - - float targetX = botPos.GetPositionX() + dx * scale; - float targetY = botPos.GetPositionY() + dy * scale; - - return MoveTo(bot->GetMapId(), targetX, targetY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - - // Stage 3: Adjust orientation when in position - if (boss && boss->GetVictim() == bot && std::abs(orientationDiff) > 0.15f) - { - // Move in an arc (circle) north or south around the boss until the orientation matches - float currentX = bot->GetPositionX(); - float currentY = bot->GetPositionY(); - float centerX = boss->GetPositionX(); - float centerY = boss->GetPositionY(); - float radius = std::max(2.0f, bot->GetExactDist2d(centerX, centerY)); // keep at least 2 yards from boss - - // Calculate current angle from boss to bot - float angle = atan2(currentY - centerY, currentX - centerX); - - // Determine direction: negative diff = move counterclockwise (north), positive = clockwise (south) - float arcStep = 0.125f; // radians per move, adjust for smoothness - if (orientationDiff < 0) - angle += arcStep; // move north (counterclockwise) - else - angle -= arcStep; // move south (clockwise) - - // Calculate new position on the arc - float moveX = centerX + radius * cos(angle); - float moveY = centerY + radius * sin(angle); - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); - } - - // Stage 4: Adjust Y-axis position if too far from tank position - float yDiff = std::abs(bot->GetPositionY() - ICC_SINDRAGOSA_TANK_POSITION.GetPositionY()); - if (boss && boss->GetVictim() == bot && yDiff > 2.0f) - { - Position botPos = bot->GetPosition(); - Position tankPos = ICC_SINDRAGOSA_TANK_POSITION; - - // Only adjust Y position, keep X and Z the same - float newY = botPos.GetPositionY() + (tankPos.GetPositionY() > botPos.GetPositionY() ? 1.0f : -1.0f); - - return MoveTo(bot->GetMapId(), botPos.GetPositionX(), newY, botPos.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); - } - - return false; -} - -bool IccSindragosaGroupPositionAction::HandleNonTankPositioning() -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Collect all alive raid members - std::vector raidMembers; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive()) - continue; - raidMembers.push_back(member); - } - - // Count members without aura 1111 - size_t membersWithoutAura = 0; - for (Player* member : raidMembers) - { - if (!botAI->GetAura("mystic buffet", member)) - membersWithoutAura++; - } - - // Calculate percentage without aura - size_t totalMembers = raidMembers.size(); - if (totalMembers == 0) - return false; - - double percentageWithoutAura = static_cast(membersWithoutAura) / totalMembers; - bool raidClear = (percentageWithoutAura >= 0.6); // 60% or more don't have aura 1111 - - if (raidClear && botAI->IsTank(bot)) - { - static const std::array tombEntries = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; - const GuidVector tombGuids = AI_VALUE(GuidVector, "possible targets no los"); - - Unit* nearestTomb = nullptr; - float minDist = 150.0f; - - for (const auto entry : tombEntries) - { - for (auto const& guid : tombGuids) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->GetEntry() == entry && unit->IsAlive()) - { - float dist = bot->GetDistance(unit); - if (dist < minDist) - { - minDist = dist; - nearestTomb = unit; - } - } - } - } - } - - static constexpr uint8_t SKULL_ICON_INDEX = 7; - - Group* group = bot->GetGroup(); - if (!group) - return false; // Cannot assign icon without group - - Unit* targetToMark = nearestTomb; - - // Fallback: mark boss if no tomb is found - if (!targetToMark) - { - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (boss && boss->IsAlive()) - targetToMark = boss; - } - - if (targetToMark) - { - const ObjectGuid currentSkull = group->GetTargetIcon(SKULL_ICON_INDEX); - Unit* currentSkullUnit = botAI->GetUnit(currentSkull); - - const bool needsUpdate = - !currentSkullUnit || !currentSkullUnit->IsAlive() || currentSkullUnit != targetToMark; - - if (needsUpdate) - group->SetTargetIcon(SKULL_ICON_INDEX, bot->GetGUID(), targetToMark->GetGUID()); - } - } - - context->GetValue("rti")->Set("skull"); - if (botAI->IsRanged(bot)) - { - const float TOLERANCE = 9.0f; - const float MAX_STEP = 5.0f; - - float distToTarget = bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION); - - // Only move if outside tolerance - if (distToTarget > TOLERANCE) - return MoveIncrementallyToPosition(ICC_SINDRAGOSA_RANGED_POSITION, MAX_STEP); - - return false; - } - else - { - const float TOLERANCE = 10.0f; - const float MAX_STEP = 5.0f; - - float distToTarget = bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION); - - // Only move if outside tolerance - if (distToTarget > TOLERANCE) - return MoveIncrementallyToPosition(ICC_SINDRAGOSA_MELEE_POSITION, MAX_STEP); - - return false; - } -} - -bool IccSindragosaGroupPositionAction::MoveIncrementallyToPosition(const Position& targetPos, float maxStep) -{ - // Calculate direction vector to target - float dirX = targetPos.GetPositionX() - bot->GetPositionX(); - float dirY = targetPos.GetPositionY() - bot->GetPositionY(); - - // Normalize direction vector - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; - - // Calculate intermediate point - float distToTarget = bot->GetExactDist2d(targetPos); - float stepSize = std::min(maxStep, distToTarget); - float moveX = bot->GetPositionX() + dirX * stepSize; - float moveY = bot->GetPositionY() + dirY * stepSize; - - return MoveTo(bot->GetMapId(), moveX, moveY, targetPos.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); -} - -bool IccSindragosaTankSwapPositionAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - // Only for assist tank - if (!botAI->IsAssistTank(bot)) - return false; - - float distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); - - // Move to tank position - if (distToTankPos > 3.0f) // Tighter tolerance for tank swap - { - return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_TANK_POSITION.GetPositionX(), - ICC_SINDRAGOSA_TANK_POSITION.GetPositionY(), - ICC_SINDRAGOSA_TANK_POSITION.GetPositionZ(), - false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - - return false; -} - -bool IccSindragosaFrostBeaconAction::Execute(Event /*event*/) -{ - const Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - HandleSupportActions(); - - if (bot->HasAura(FROST_BEACON_AURA_ID)) - { - return HandleBeaconedPlayer(boss); - } - - return HandleNonBeaconedPlayer(boss); -} - -void IccSindragosaFrostBeaconAction::HandleSupportActions() -{ - Group* group = bot->GetGroup(); - - // Tank support - Paladin Hand of Freedom - if (group && bot->getClass() == CLASS_PALADIN) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsTank(member)) - { - continue; - } - - if (botAI->GetAura("Frost Breath", member) && !member->HasAura(HAND_OF_FREEDOM_SPELL_ID)) - { - botAI->CastSpell(HAND_OF_FREEDOM_SPELL_ID, member); - break; - } - } - } - - // Healer support - Apply HoTs to beaconed players - if (botAI->IsHeal(bot) && !bot->HasAura(FROST_BEACON_AURA_ID)) - { - const auto members = AI_VALUE(GuidVector, "group members"); - for (auto const& memberGuid : members) - { - Unit* member = botAI->GetUnit(memberGuid); - if (!member || !member->IsAlive() || !member->HasAura(FROST_BEACON_AURA_ID)) - { - continue; - } - - // Apply class-specific HoT spells - uint32 spellId = 0; - switch (bot->getClass()) - { - case CLASS_PRIEST: - spellId = 48068; - break; // Renew - case CLASS_SHAMAN: - spellId = 61301; - break; // Riptide - case CLASS_DRUID: - spellId = 48441; - break; // Rejuvenation - default: - continue; - } - - if (!member->HasAura(spellId)) - { - botAI->CastSpell(spellId, member); - } - } - } -} - -bool IccSindragosaFrostBeaconAction::HandleBeaconedPlayer(const Unit* boss) -{ - // Phase 3 positioning (below 35% health, not flying) - if (boss->HealthBelowPct(35) && !IsBossFlying(boss)) - { - if (!bot->HasAura(SPELL_NITRO_BOOSTS)) - bot->AddAura(SPELL_NITRO_BOOSTS, bot); - botAI->Reset(); - return MoveToPositionIfNeeded(ICC_SINDRAGOSA_THOMBMB2_POSITION, POSITION_TOLERANCE); - } - - // Regular beacon positioning using tomb spots - Group* group = bot->GetGroup(); - if (!group) - { - return false; - } - - // Collect and sort beaconed players by GUID for deterministic assignment - std::vector beaconedPlayers; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && member->HasAura(FROST_BEACON_AURA_ID)) - { - beaconedPlayers.push_back(member); - } - } - - std::sort(beaconedPlayers.begin(), beaconedPlayers.end(), - [](const Player* a, const Player* b) { return a->GetGUID() < b->GetGUID(); }); - - // Find this bot's index - const auto it = std::find(beaconedPlayers.begin(), beaconedPlayers.end(), bot); - if (it == beaconedPlayers.end()) - { - return false; - } - - const size_t myIndex = std::distance(beaconedPlayers.begin(), it); - const size_t beaconCount = beaconedPlayers.size(); - - // Calculate tomb spot based on beacon count - size_t spot = 0; - switch (beaconCount) - { - case 2: - spot = (myIndex == 0) ? 0 : 2; - break; - case 5: - spot = (myIndex < 2) ? 0 : ((myIndex == 2) ? 1 : 2); - break; - case 6: - spot = myIndex / 2; - break; - default: - spot = myIndex % 3; - break; - } - - // Get tomb position and move if needed - static constexpr std::array tombPositions = { - &ICC_SINDRAGOSA_THOMB1_POSITION, &ICC_SINDRAGOSA_THOMB2_POSITION, &ICC_SINDRAGOSA_THOMB3_POSITION}; - - const Position& tombPosition = *tombPositions[std::min(spot, tombPositions.size() - 1)]; - return MoveToPositionIfNeeded(tombPosition, TOMB_POSITION_TOLERANCE); -} - -bool IccSindragosaFrostBeaconAction::HandleNonBeaconedPlayer(const Unit* boss) -{ - // Collect beaconed players - std::vector beaconedPlayers; - const auto members = AI_VALUE(GuidVector, "group members"); - for (auto const& memberGuid : members) - { - Unit* player = botAI->GetUnit(memberGuid); - if (player && player->GetGUID() != bot->GetGUID() && player->HasAura(FROST_BEACON_AURA_ID)) - { - beaconedPlayers.push_back(player); - } - } - - if (beaconedPlayers.empty()) - { - return false; - } - - // Air phase positioning - if (IsBossFlying(boss)) - { - if (!bot->HasAura(FROST_BEACON_AURA_ID)) - { - const Difficulty diff = bot->GetRaidDifficulty(); - bool is25Man = false; - if (diff && (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - is25Man = true; - - const Position& safePosition = is25Man ? ICC_SINDRAGOSA_FBOMB_POSITION : ICC_SINDRAGOSA_FBOMB10_POSITION; - - const float dist = bot->GetExactDist2d(safePosition.GetPositionX(), safePosition.GetPositionY()); - if (dist > MOVE_TOLERANCE) - { - return MoveToPosition(safePosition); - } - } - return botAI->IsHeal(bot); // Continue for healers, wait for others - } - - // Ground phase - position based on role and avoid beaconed players - const bool isRanged = botAI->IsRanged(bot) || (bot->GetExactDist2d(ICC_SINDRAGOSA_RANGED_POSITION.GetPositionX(),ICC_SINDRAGOSA_RANGED_POSITION.GetPositionY()) < - bot->GetExactDist2d(ICC_SINDRAGOSA_MELEE_POSITION.GetPositionX(),ICC_SINDRAGOSA_MELEE_POSITION.GetPositionY())); - - const Position& targetPosition = isRanged ? ICC_SINDRAGOSA_RANGED_POSITION : ICC_SINDRAGOSA_MELEE_POSITION; - - const float deltaX = std::abs(targetPosition.GetPositionX() - bot->GetPositionX()); - const float deltaY = std::abs(targetPosition.GetPositionY() - bot->GetPositionY()); - if (boss && boss->GetVictim() != bot) - { - if ((deltaX > MOVE_TOLERANCE) || (deltaY > MOVE_TOLERANCE)) - { - if (bot->HasUnitState(UNIT_STATE_CASTING)) - { - botAI->Reset(); - } - return MoveToPosition(targetPosition); - } - } - return false; -} - -bool IccSindragosaFrostBeaconAction::MoveToPositionIfNeeded(const Position& position, float tolerance) -{ - const float distance = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - if (distance > tolerance) - { - return MoveToPosition(position); - } - return distance <= tolerance; -} - -bool IccSindragosaFrostBeaconAction::MoveToPosition(const Position& position) -{ - float posX = position.GetPositionX(); - float posY = position.GetPositionY(); - float posZ = position.GetPositionZ(); - - bot->UpdateAllowedPositionZ(posX, posY, posZ); - - return MoveTo(bot->GetMapId(), posX, posY, posZ, false, false, false, false, MovementPriority::MOVEMENT_FORCED, - true, false); -} - -bool IccSindragosaFrostBeaconAction::IsBossFlying(const Unit* boss) -{ - return boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), - ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f; -} - -bool IccSindragosaBlisteringColdAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - // Only non-tanks should move out - if (botAI->IsMainTank(bot)) - return false; - - float dist = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); - - if (dist >= 33.0f) - return false; - - Position const& targetPos = ICC_SINDRAGOSA_BLISTERING_COLD_POSITION; - - // Only move if we're too close to the boss (< 30 yards) - if (dist < 33.0f) - { - - float const STEP_SIZE = 15.0f; - float distToTarget = bot->GetDistance2d(targetPos.GetPositionX(), targetPos.GetPositionY()); - - if (distToTarget > 0.1f) // Avoid division by zero - { - if (!bot->HasAura(SPELL_NITRO_BOOSTS)) - bot->AddAura(SPELL_NITRO_BOOSTS, bot); - // Calculate direction vector - float dirX = targetPos.GetPositionX() - bot->GetPositionX(); - float dirY = targetPos.GetPositionY() - bot->GetPositionY(); - - // Normalize direction vector - float length = sqrt(dirX * dirX + dirY * dirY); - dirX /= length; - dirY /= length; - - // Move STEP_SIZE yards in that direction - float moveX = bot->GetPositionX() + dirX * STEP_SIZE; - float moveY = bot->GetPositionY() + dirY * STEP_SIZE; - - return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - } - return false; -} - -bool IccSindragosaUnchainedMagicAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true); - if (!aura) - return false; - - Aura* aura1 = botAI->GetAura("Instability", bot, false, true); - - Difficulty diff = bot->GetRaidDifficulty(); - if (aura && (diff == RAID_DIFFICULTY_10MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_NORMAL)) - { - if (aura1 && aura1->GetStackAmount() >= 6) - return true; // Stop casting spells - } - - return false; -} - -bool IccSindragosaChilledToTheBoneAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("Chilled to the Bone", bot, false, true); - if (!aura) - return false; - - if (aura) // Chilled to the Bone - { - if (aura->GetStackAmount() >= 6) - { - botAI->Reset(); - bot->AttackStop(); - return true; - } - } - - return false; -} - -bool IccSindragosaMysticBuffetAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss || !bot || !bot->IsAlive()) - return false; - - // Check if we have Mystic Buffet - Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); - if (!aura) - return false; - - if (boss->GetVictim() == bot) - return false; - - // Skip if we have Frost Beacon - if (bot->HasAura(SPELL_FROST_BEACON)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - static const std::array tombEntries = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; - const GuidVector tombGuids = AI_VALUE(GuidVector, "possible targets no los"); - - Unit* nearestTomb = nullptr; - float minDist = 150.0f; - - for (const auto entry : tombEntries) - { - for (auto const& guid : tombGuids) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->GetEntry() == entry && unit->IsAlive()) - { - float dist = bot->GetDistance(unit); - if (dist < minDist) - { - minDist = dist; - nearestTomb = unit; - } - } - } - } - } - - // Check if anyone in group has Frost Beacon (SPELL_FROST_BEACON) - bool anyoneHasFrostBeacon = false; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->HasAura(SPELL_FROST_BEACON)) - { - anyoneHasFrostBeacon = true; - break; - } - } - - bool tombPresent = nearestTomb != nullptr; - bool atLOS2 = bot->GetExactDist2d(ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), - ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY()) <= 2.0f; - - // Move to LOS2 position if: tomb is present and no one has Frost Beacon - bool shouldMoveLOS2 = tombPresent && !anyoneHasFrostBeacon; - - if (shouldMoveLOS2) - { - // If already at LOS2 and have 3+ stacks, stay still - if (atLOS2 && aura && !botAI->IsHeal(bot)) - { - return true; - } - - botAI->Reset(); - // Move to LOS2 position - return MoveTo(bot->GetMapId(), ICC_SINDRAGOSA_LOS2_POSITION.GetPositionX(), - ICC_SINDRAGOSA_LOS2_POSITION.GetPositionY(), ICC_SINDRAGOSA_LOS2_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_FORCED); - } - return false; -} - -bool IccSindragosaFrostBombAction::Execute(Event /*event*/) -{ - if (!bot || !bot->IsAlive() || bot->HasAura(SPELL_ICE_TOMB)) // Skip if dead or in Ice Tomb - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Find frost bomb marker and tombs - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - const uint32 tombEntries[] = {NPC_TOMB1, NPC_TOMB2, NPC_TOMB3, NPC_TOMB4}; // tomb id's - Unit* marker = nullptr; - std::vector tombs; - std::vector tombGuids; - - // Manually search for units with frost bomb aura (SPELL_FROST_BOMB_VISUAL) using NearestHostileNpcsValue logic - std::list units; - float range = 200.0f; - Acore::AnyUnitInObjectRangeCheck u_check(bot, range); - Acore::UnitListSearcher searcher(bot, units, u_check); - Cell::VisitObjects(bot, searcher, range); - - for (Unit* unit : units) - { - if (!unit || !unit->IsAlive()) - continue; - - if (unit->HasAura(SPELL_FROST_BOMB_VISUAL)) // Frost bomb visual - marker = unit; - - // Check if unit is a tomb - for (uint32 entry : tombEntries) - { - if (unit->GetEntry() == entry) - { - tombs.push_back(unit); - tombGuids.push_back(unit->GetGUID()); - break; - } - } - } - - if (!marker || tombs.empty()) - { - bot->AttackStop(); - return true; - } - - // Get persistent group assignment - use a static map to store assignments - static std::map persistentGroupAssignments; - static std::vector allGroupGuids; // All guids that have ever been in the raid - - // Gather all group members (alive and dead, including those with ice tomb) - std::vector currentGuids; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member) - currentGuids.push_back(member->GetGUID()); - } - - // Add any new GUIDs to our persistent list - for (const ObjectGuid& guid : currentGuids) - { - if (std::find(allGroupGuids.begin(), allGroupGuids.end(), guid) == allGroupGuids.end()) - { - allGroupGuids.push_back(guid); - } - } - - // Sort the complete guid list for consistency - std::sort(allGroupGuids.begin(), allGroupGuids.end()); - - Difficulty diff = bot->GetRaidDifficulty(); - // Determine group count (2 for 10m, 3 for 25m) - int groupCount = (diff == RAID_DIFFICULTY_25MAN_NORMAL || diff == RAID_DIFFICULTY_25MAN_HEROIC) ? 3 : 2; - - // Assign group indices to GUIDs that don't have assignments yet - for (size_t i = 0; i < allGroupGuids.size(); ++i) - { - const ObjectGuid& guid = allGroupGuids[i]; - if (persistentGroupAssignments.find(guid) == persistentGroupAssignments.end()) - { - // Assign to group based on their position in the sorted list - persistentGroupAssignments[guid] = int(i) % groupCount; - } - } - - // Get this bot's group assignment - auto it = persistentGroupAssignments.find(bot->GetGUID()); - if (it == persistentGroupAssignments.end()) - return false; - - int myGroupIndex = it->second; - - // Build group positions based on available tombs - std::vector groupPositions; - for (int i = 0; i < groupCount; ++i) - { - if (i < int(tombs.size())) - { - groupPositions.push_back(tombs[i]->GetPosition()); - } - else - { - groupPositions.push_back(marker->GetPosition()); - } - } - - // PRIORITY 1: Check if there are any tombs near our current position (within 8 yards) - std::vector nearbyTombs; - for (Unit* tomb : tombs) - { - if (tomb->GetExactDist2d(bot) <= 8.0f) - { - nearbyTombs.push_back(tomb); - } - } - - // PRIORITY 2: If no tombs nearby, find tombs near our assigned group position - std::vector groupPositionTombs; - if (nearbyTombs.empty()) - { - for (Unit* tomb : tombs) - { - if (tomb->GetExactDist2d(groupPositions[myGroupIndex]) <= 8.0f) - { - groupPositionTombs.push_back(tomb); - } - } - } - - // Select which tombs to use based on priority - std::vector myTombs; - std::vector myTombGuids; - - if (!nearbyTombs.empty()) - { - // Use tombs near current position (highest priority) - myTombs = nearbyTombs; - for (Unit* tomb : nearbyTombs) - { - myTombGuids.push_back(tomb->GetGUID()); - } - } - else if (!groupPositionTombs.empty()) - { - // Use tombs near group position (medium priority) - myTombs = groupPositionTombs; - for (Unit* tomb : groupPositionTombs) - { - myTombGuids.push_back(tomb->GetGUID()); - } - } - else - { - // Fallback: use closest available tomb (lowest priority) - Unit* closestTomb = nullptr; - float closestDist = 999.0f; - for (Unit* tomb : tombs) - { - float dist = tomb->GetExactDist2d(bot); - if (dist < closestDist) - { - closestDist = dist; - closestTomb = tomb; - } - } - if (closestTomb) - { - myTombs.push_back(closestTomb); - myTombGuids.push_back(closestTomb->GetGUID()); - } - } - - if (myTombs.empty()) - return false; - - // Pick the tomb with highest HP in our selection - size_t bestIdx = 0; - float bestHp = 0.0f; - for (size_t i = 0; i < myTombs.size(); ++i) - { - float hp = myTombs[i]->GetHealthPct(); - if (i == 0 || hp > bestHp) - { - bestHp = hp; - bestIdx = i; - } - } - Unit* losTomb = myTombs[bestIdx]; - ObjectGuid losTombGuid = myTombGuids[bestIdx]; - - // Calculate position for LOS (stand at least 6.5f behind the tomb from the bomb) - float angle = marker->GetAngle(losTomb); - float posX = losTomb->GetPositionX() + cos(angle) * 6.5f; - float posY = losTomb->GetPositionY() + sin(angle) * 6.5f; - float posZ = losTomb->GetPositionZ(); - - // Always move to exact LOS position for safety - float distToLosPos = bot->GetDistance2d(posX, posY); - if (distToLosPos > 0.01f) - { - botAI->Reset(); - bot->AttackStop(); - return MoveTo(bot->GetMapId(), posX, posY, posZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED); - } - - // Check if we are in LOS of the bomb (must be very close to calculated position) - bool inLOS = (distToLosPos <= 0.01f); - - // RTI marker constants - static constexpr uint8_t SKULL_ICON_INDEX = 7; - static constexpr uint8_t CROSS_ICON_INDEX = 6; - static constexpr uint8_t STAR_ICON_INDEX = 0; - - // If in LOS, handle RTI marking for group's tombs - if (inLOS) - { - // Determine RTI marker for this group - uint8_t iconIndex = 0; - std::string rtiValue; - if (myGroupIndex == 0) - { - iconIndex = SKULL_ICON_INDEX; - rtiValue = "skull"; - } - else if (myGroupIndex == 1) - { - iconIndex = CROSS_ICON_INDEX; - rtiValue = "cross"; - } - else if (myGroupIndex == 2) - { - iconIndex = STAR_ICON_INDEX; - rtiValue = "star"; - } - else - return false; - - context->GetValue("rti")->Set(rtiValue); - - // Find a tomb in our group with 45% or more HP to mark - Unit* tombToMark = nullptr; - for (size_t i = 0; i < myTombs.size(); ++i) - { - Unit* tomb = myTombs[i]; - if (tomb->IsAlive() && tomb->HealthAbovePct(45)) - { - tombToMark = tomb; - break; - } - } - - if (tombToMark) - { - // Check if this tomb is already marked with our group's icon - ObjectGuid currentIcon = group->GetTargetIcon(iconIndex); - Unit* currentIconUnit = botAI->GetUnit(currentIcon); - if (!currentIconUnit || !currentIconUnit->IsAlive() || currentIconUnit != tombToMark) - { - // Mark the tomb with our group's target icon - group->SetTargetIcon(iconIndex, bot->GetGUID(), tombToMark->GetGUID()); - } - } - else - { - // No tombs above 45% HP, remove marker if one exists - ObjectGuid currentIcon = group->GetTargetIcon(iconIndex); - if (!currentIcon.IsEmpty()) - { - // Clear the marker for our group's icon - group->SetTargetIcon(iconIndex, bot->GetGUID(), ObjectGuid::Empty); - } - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - bot->AttackStop(); - return true; - } - } - - return false; -} - -// The Lich King -bool IccLichKingShadowTrapAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss || !botAI->IsTank(bot)) - return false; - - Difficulty diff = bot->GetRaidDifficulty(); - - if (sPlayerbotAIConfig.EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - //-------CHEAT------- - if (!bot->HasAura(SPELL_EXPERIENCED)) - bot->AddAura(SPELL_EXPERIENCED, bot); - - if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) - bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); - - if (!bot->HasAura(SPELL_NO_THREAT) && !botAI->IsTank(bot)) - bot->AddAura(SPELL_NO_THREAT, bot); - - if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) - bot->AddAura(SPELL_PAIN_SUPPRESION, bot); - //-------CHEAT------- - } - - // Define ICC_LICH_POSITION and circle parameters - const float X = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(); - const float Y = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY(); - const float Z = ICC_LICH_KING_ASSISTHC_POSITION.GetPositionZ(); - const float CIRCLE_RADIUS = 20.0f; - const float SAFE_DISTANCE = 12.0f; - const int TEST_POSITIONS = 16; - const float ANGLE_STEP = 2 * M_PI / TEST_POSITIONS; - - // Find all nearby shadow traps - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector trapGuids; - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive() || unit->GetEntry() != NPC_SHADOW_TRAP) - continue; - if (bot->GetDistance(unit) < SAFE_DISTANCE + 5.0f) - { - trapGuids.push_back(npc); - } - } - - if (trapGuids.empty()) - return false; - - // Check if current position is already safe - bool currentPositionSafe = true; - for (auto& trapGuid : trapGuids) - { - Unit* trap = botAI->GetUnit(trapGuid); - if (!trap) - continue; - if (bot->GetDistance(trap) < SAFE_DISTANCE) - { - currentPositionSafe = false; - break; - } - } - - // If current position is safe, no need to move - if (currentPositionSafe) - return false; - - // Calculate current angle relative to ICC_LICH_POSITION - float currentX = bot->GetPositionX() - X; - float currentY = bot->GetPositionY() - Y; - float currentAngle = atan2(currentY, currentX); - - // Test clockwise positions first, then opposite position - std::vector testAngles; - // Add clockwise positions - for (int i = 1; i <= TEST_POSITIONS; ++i) - { - testAngles.push_back(currentAngle - (ANGLE_STEP * i)); - } - // Add opposite position as fallback - testAngles.push_back(currentAngle + M_PI); - - // Test all positions - for (float testAngle : testAngles) - { - // Calculate position on circle - float testX = X + cos(testAngle) * CIRCLE_RADIUS; - float testY = Y + sin(testAngle) * CIRCLE_RADIUS; - float testZ = Z; - - // Update Z coordinate for terrain - bot->UpdateAllowedPositionZ(testX, testY, testZ); - - // Check line of sight - if (!bot->IsWithinLOS(testX, testY, testZ)) - continue; - - // Check if this position is safe from all traps - bool isSafe = true; - for (auto& trapGuid : trapGuids) - { - Unit* trap = botAI->GetUnit(trapGuid); - if (!trap) - continue; - float distToTrap = sqrt(pow(testX - trap->GetPositionX(), 2) + pow(testY - trap->GetPositionY(), 2)); - if (distToTrap < SAFE_DISTANCE) - { - isSafe = false; - break; - } - } - - // Found a safe spot - move there - if (isSafe) - { - // Remove botAI->Reset() as it might interfere with movement - MoveTo(bot->GetMapId(), testX, testY, testZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, - true, false); - } - } - - // No safe position found - return false; -} - -bool IccLichKingNecroticPlagueAction::Execute(Event /*event*/) -{ - bool hasPlague = botAI->HasAura("Necrotic Plague", bot); - // Only execute if we have the plague - if (!hasPlague) - return false; - - // Find closest shambling horror - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - Unit* closestHorror = nullptr; - float minHorrorDist = 100.0f; - - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - - uint32 entry = unit->GetEntry(); - if (entry == NPC_SHAMBLING_HORROR1 || entry == NPC_SHAMBLING_HORROR2 || - entry == NPC_SHAMBLING_HORROR3 || entry == NPC_SHAMBLING_HORROR4) - { - float distance = bot->GetDistance(unit); - if (distance < minHorrorDist) - { - minHorrorDist = distance; - closestHorror = unit; - } - } - } - - // If we found a shambling horror, handle movement - if (closestHorror) - { - // If we're close enough, stop and return success - if (minHorrorDist <= 2.0f) - { - bot->StopMoving(); - return true; - } - - // We need to move to the horror - botAI->Reset(); - MoveTo(closestHorror, 2.0f, MovementPriority::MOVEMENT_FORCED); - - return false; // Still moving, not finished yet - } - - // No shambling horror found, but we have plague - this shouldn't happen normally - return false; -} - -bool IccLichKingWinterAction::Execute(Event /*event*/) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return false; - - Unit* iceSphere = AI_VALUE2(Unit*, "find target", "ice sphere"); - - bool isVictim = false; - if (iceSphere && iceSphere->GetVictim() == bot && !botAI->IsTank(bot)) - isVictim = true; - - // First priority: Get out of Defile if we're in one - if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) - { - // Find nearest safe position (use tank position as fallback) - const Position* safePos = botAI->IsTank(bot) ? GetMainTankPosition() : GetMainTankRangedPosition(); - TryMoveToPosition(safePos->GetPositionX(), safePos->GetPositionY(), 840.857f, true); - return true; - } - - float currentDistance = bot->GetDistance2d(boss); - - Difficulty diff = bot->GetRaidDifficulty(); - - if (sPlayerbotAIConfig.EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - //------CHEAT------- - if (!bot->HasAura(SPELL_EXPERIENCED)) - bot->AddAura(SPELL_EXPERIENCED, bot); - - if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) - bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); - - if (!bot->HasAura(SPELL_NO_THREAT) && !botAI->IsTank(bot)) - bot->AddAura(SPELL_NO_THREAT, bot); - - if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) - bot->AddAura(SPELL_PAIN_SUPPRESION, bot); - //------CHEAT------- - } - - if (currentDistance < 35.0f && !bot->HasAura(SPELL_NITRO_BOOSTS)) - bot->AddAura(SPELL_NITRO_BOOSTS, bot); - - // Handle group target management - if (Group* group = bot->GetGroup()) - { - const ObjectGuid currentSkullTarget = group->GetTargetIcon(7); - if (!currentSkullTarget.IsEmpty()) - { - Unit* skullTarget = ObjectAccessor::GetUnit(*bot, currentSkullTarget); - group->SetTargetIcon(7, bot->GetGUID(), ObjectGuid::Empty); - } - } - - if (isVictim) - MoveFromGroup(6.0f); - - if (!isVictim) - { - HandlePositionCorrection(); - - // Handle tank positioning and add management FIRST - HandleTankPositioning(); // New method that handles both main and assist tanks - - // Then handle other roles - HandleMeleePositioning(); - HandleRangedPositioning(); - } - - return false; -} - -void IccLichKingWinterAction::HandlePositionCorrection() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - - // Fix underground bug - if (abs(bot->GetPositionZ() - 840.857f) > 1.0f) - bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 840.857f, bot->GetOrientation()); - - // Reset targeting for specific conditions - if (currentTarget && boss && currentTarget == boss) - botAI->Reset(); - - if (botAI->IsTank(bot) && currentTarget && - ((currentTarget->GetEntry() == NPC_ICE_SPHERE1 || currentTarget->GetEntry() == NPC_ICE_SPHERE2 || - currentTarget->GetEntry() == NPC_ICE_SPHERE3 || currentTarget->GetEntry() == NPC_ICE_SPHERE4))) - botAI->Reset(); -} - -const Position* IccLichKingWinterAction::GetMainTankPosition() -{ - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - if (!mainTank) - { - // FIXED: When no main tank, use the bot with lowest GUID to determine position - // This ensures ALL bots make the same decision collectively - - Unit* referenceBot = nullptr; - ObjectGuid lowestGuid; - - // Find the bot with lowest GUID in the group - Group* group = bot->GetGroup(); - if (group) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && member->IsInWorld()) - { - if (lowestGuid.IsEmpty() || member->GetGUID() < lowestGuid) - { - lowestGuid = member->GetGUID(); - referenceBot = member; - } - } - } - } - - // If no group or reference bot found, fall back to current bot - if (!referenceBot) - referenceBot = bot; - - // Use the reference bot's position to determine closest tank position - float dist1 = - referenceBot->GetDistance2d(ICC_LK_FROST1_POSITION.GetPositionX(), ICC_LK_FROST1_POSITION.GetPositionY()); - float dist2 = - referenceBot->GetDistance2d(ICC_LK_FROST2_POSITION.GetPositionX(), ICC_LK_FROST2_POSITION.GetPositionY()); - float dist3 = - referenceBot->GetDistance2d(ICC_LK_FROST3_POSITION.GetPositionX(), ICC_LK_FROST3_POSITION.GetPositionY()); - - if (dist2 < dist1 && dist2 < dist3) - return &ICC_LK_FROST2_POSITION; - else if (dist3 < dist1 && dist3 < dist2) - return &ICC_LK_FROST3_POSITION; - else - return &ICC_LK_FROST1_POSITION; - } - - // Calculate which position the main tank is closest to - float dist1 = mainTank->GetDistance2d(ICC_LK_FROST1_POSITION.GetPositionX(), ICC_LK_FROST1_POSITION.GetPositionY()); - float dist2 = mainTank->GetDistance2d(ICC_LK_FROST2_POSITION.GetPositionX(), ICC_LK_FROST2_POSITION.GetPositionY()); - float dist3 = mainTank->GetDistance2d(ICC_LK_FROST3_POSITION.GetPositionX(), ICC_LK_FROST3_POSITION.GetPositionY()); - - if (dist2 < dist1 && dist2 < dist3) - return &ICC_LK_FROST2_POSITION; - else if (dist3 < dist1 && dist3 < dist2) - return &ICC_LK_FROST3_POSITION; - else - return &ICC_LK_FROST1_POSITION; -} - -const Position* IccLichKingWinterAction::GetMainTankRangedPosition() -{ - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - if (!mainTank) - { - // FIXED: When no main tank, use the bot with lowest GUID to determine position - // This ensures ALL bots make the same decision collectively - - Unit* referenceBot = nullptr; - ObjectGuid lowestGuid; - - // Find the bot with lowest GUID in the group - Group* group = bot->GetGroup(); - if (group) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && member->IsInWorld()) - { - if (lowestGuid.IsEmpty() || member->GetGUID() < lowestGuid) - { - lowestGuid = member->GetGUID(); - referenceBot = member; - } - } - } - } - - // If no group or reference bot found, fall back to current bot - if (!referenceBot) - referenceBot = bot; - - // Use the reference bot's position to determine closest ranged position - float dist1 = - referenceBot->GetDistance2d(ICC_LK_FROSTR1_POSITION.GetPositionX(), ICC_LK_FROSTR1_POSITION.GetPositionY()); - float dist2 = - referenceBot->GetDistance2d(ICC_LK_FROSTR2_POSITION.GetPositionX(), ICC_LK_FROSTR2_POSITION.GetPositionY()); - float dist3 = - referenceBot->GetDistance2d(ICC_LK_FROSTR3_POSITION.GetPositionX(), ICC_LK_FROSTR3_POSITION.GetPositionY()); - - if (dist2 < dist1 && dist2 < dist3) - return &ICC_LK_FROSTR2_POSITION; - else if (dist3 < dist1 && dist3 < dist2) - return &ICC_LK_FROSTR3_POSITION; - else - return &ICC_LK_FROSTR1_POSITION; - } - - // Map main tank's melee position to corresponding ranged position - const Position* tankMeleePos = GetMainTankPosition(); - - if (tankMeleePos == &ICC_LK_FROST1_POSITION) - return &ICC_LK_FROSTR1_POSITION; - else if (tankMeleePos == &ICC_LK_FROST2_POSITION) - return &ICC_LK_FROSTR2_POSITION; - else - return &ICC_LK_FROSTR3_POSITION; -} - -bool IccLichKingWinterAction::IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return true; // No boss, assume safe - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - const float BASE_RADIUS = 6.0f; - const float SAFETY_MARGIN = 3.0f; - - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) - { - // Calculate current defile radius including growth - float currentRadius = BASE_RADIUS; - Aura* growAura = nullptr; - - // Find growth aura (you'll need to define DEFILE_AURAS array) - for (size_t i = 0; i < DEFILE_AURA_COUNT; i++) - { - growAura = unit->GetAura(DEFILE_AURAS[i]); - if (growAura) - break; - } - - if (growAura) - { - uint8 stacks = growAura->GetStackAmount(); - float growthMultiplier = (bot->GetRaidDifficulty() == RAID_DIFFICULTY_10MAN_HEROIC || - bot->GetRaidDifficulty() == RAID_DIFFICULTY_10MAN_NORMAL) - ? 1.4f - : 0.95f; - currentRadius = BASE_RADIUS + (stacks * growthMultiplier); - } - - float dx = x - unit->GetPositionX(); - float dy = y - unit->GetPositionY(); - float distance = sqrt(dx * dx + dy * dy); - - if (distance < (currentRadius + SAFETY_MARGIN + minSafeDistance)) - return false; - } - } - return true; -} - -bool IccLichKingWinterAction::TryMoveToPosition(float targetX, float targetY, float targetZ, bool isForced) -{ - float currentX = bot->GetPositionX(); - float currentY = bot->GetPositionY(); - float currentZ = bot->GetPositionZ(); - - float dx = targetX - currentX; - float dy = targetY - currentY; - float dz = targetZ - currentZ; - float distance = sqrtf(dx * dx + dy * dy + dz * dz); - - if (distance < 0.1f) - return true; // Already at the position - - dx /= distance; - dy /= distance; - - // First check if direct path is safe - if (bot->IsWithinLOS(targetX, targetY, targetZ) && IsPositionSafeFromDefile(targetX, targetY, targetZ, 3.0f)) - { - if (isForced) - botAI->Reset(); - - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, - true, false); - return true; - } - - // If direct path isn't safe, try to find a safe path around defiles - const int MAX_ATTEMPTS = 8; - const float ANGLE_STEP = M_PI / 4.0f; - float attemptDistance = std::min(10.0f, distance); - - for (int i = 0; i < MAX_ATTEMPTS; i++) - { - float angle = i * ANGLE_STEP; - float offsetX = attemptDistance * cos(angle); - float offsetY = attemptDistance * sin(angle); - - // Try positions clockwise and counter-clockwise - for (int direction = -1; direction <= 1; direction += 2) - { - if (i == 0 && direction == 1) - continue; // Skip duplicate first attempt - - float testX = currentX + dx * attemptDistance + offsetX * direction; - float testY = currentY + dy * attemptDistance + offsetY * direction; - float testZ = targetZ; - - if (bot->IsWithinLOS(testX, testY, testZ) && IsPositionSafeFromDefile(testX, testY, testZ, 3.0f)) - { - if (isForced) - botAI->Reset(); - - MoveTo(bot->GetMapId(), testX, testY, testZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - return false; // Not at final position yet - } - } - } - - // If no safe path found, just move directly (better than standing in defile) - if (isForced) - botAI->Reset(); - - MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, - true, false); - return false; -} - -// Helper function to check if a unit is a valid collectible add -bool IccLichKingWinterAction::IsValidCollectibleAdd(Unit* unit) -{ - if (!unit || !unit->IsAlive()) - return false; - - uint32 entry = unit->GetEntry(); - - // Only spirits, shambling horrors, and ghouls are valid collectible adds - return (entry == NPC_SHAMBLING_HORROR1 || entry == NPC_SHAMBLING_HORROR2 || entry == NPC_SHAMBLING_HORROR3 || - entry == NPC_SHAMBLING_HORROR4 || entry == NPC_RAGING_SPIRIT1 || entry == NPC_RAGING_SPIRIT2 || - entry == NPC_RAGING_SPIRIT3 || entry == NPC_RAGING_SPIRIT4 || entry == NPC_DRUDGE_GHOUL1 || - entry == NPC_DRUDGE_GHOUL2 || entry == NPC_DRUDGE_GHOUL3 || entry == NPC_DRUDGE_GHOUL4); -} - -// FIXED HandleTankPositioning method -void IccLichKingWinterAction::HandleTankPositioning() -{ - if (!botAI->IsTank(bot)) - return; - - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return; - - // Get the target position based on main tank's choice - const Position* targetPos = GetMainTankPosition(); - - // First check if current position is safe - if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) - { - // If in defile, prioritize getting out - TryMoveToPosition(targetPos->GetPositionX(), targetPos->GetPositionY(), 840.857f, true); - return; - } - - float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); - - // MAIN TANK: Always stay at tank position - if (botAI->IsMainTank(bot)) - { - // Main tank should always maintain position at tank spot - if (distToTarget > 2.0f) - { - float targetX = targetPos->GetPositionX(); - float targetY = targetPos->GetPositionY(); - float targetZ = 840.857f; - - TryMoveToPosition(targetX, targetY, targetZ, true); - return; // Don't do add management until in position - } - - // Once in position, handle add management from tank position - HandleMainTankAddManagement(boss, targetPos); - } - // ASSIST TANK: More flexible positioning based on add collection - else if (botAI->IsAssistTank(bot)) - { - // First ensure we're reasonably close to tank area - if (distToTarget > 15.0f) - { - float targetX = targetPos->GetPositionX() + 3.0f; // Slight offset from main tank - float targetY = targetPos->GetPositionY() + 2.0f; - float targetZ = 840.857f; - - TryMoveToPosition(targetX, targetY, targetZ, true); - return; - } - - // Handle assist tank add collection and positioning - HandleAssistTankAddManagement(boss, targetPos); - } -} - -// Updated HandleMeleePositioning method - only for non-tanks -void IccLichKingWinterAction::HandleMeleePositioning() -{ - // Skip if this is a tank - they have their own positioning logic - if (botAI->IsTank(bot)) - return; - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - // Handle melee positioning behind target (for DPS only) - if (currentTarget && !botAI->IsRanged(bot) && currentTarget->isInFront(bot) && currentTarget->IsAlive() && - currentTarget->GetEntry() != NPC_THE_LICH_KING && currentTarget->GetEntry() != NPC_ICE_SPHERE1 && - currentTarget->GetEntry() != NPC_ICE_SPHERE2 && currentTarget->GetEntry() != NPC_ICE_SPHERE3 && currentTarget->GetEntry() != NPC_ICE_SPHERE4) - { - // Calculate desired position (4.0f behind the target) - float orientation = currentTarget->GetOrientation() + M_PI + M_PI / 8; - float x = currentTarget->GetPositionX(); - float y = currentTarget->GetPositionY(); - float z = bot->GetPositionZ(); - float targetX = x + cos(orientation) * 4.0f; - float targetY = y + sin(orientation) * 4.0f; - Position botPos = bot->GetPosition(); - float dx = targetX - botPos.GetPositionX(); - float dy = targetY - botPos.GetPositionY(); - float distance = sqrt(dx * dx + dy * dy); - - if (distance <= 1.0f) - return; - - // Move in increments of 2 yards toward the target position - float moveDistance = std::min(2.0f, distance); - float normalizedDx = dx / distance; - float normalizedDy = dy / distance; - - float newX = botPos.GetPositionX() + normalizedDx * moveDistance; - float newY = botPos.GetPositionY() + normalizedDy * moveDistance; - - TryMoveToPosition(newX, newY, z, false); - } - // Handle non-ranged DPS positioning - USE MAIN TANK'S POSITION - if (!botAI->IsRanged(bot)) - { - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - const Position* targetPos = GetMainTankPosition(); - float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); - if (distToTarget > 8.0f) - { - float targetX = targetPos->GetPositionX() - 5.0f; - float targetY = targetPos->GetPositionY() + 5.0f; - float targetZ = 840.857f; - if (boss && !boss->HealthAbovePct(50)) - { - targetX = targetPos->GetPositionX(); - targetY = targetPos->GetPositionY(); - targetZ = 840.857f; - } - - // Move in increments of 2 yards toward the tank position - Position botPos = bot->GetPosition(); - float dx = targetX - botPos.GetPositionX(); - float dy = targetY - botPos.GetPositionY(); - float distance = sqrt(dx * dx + dy * dy); - - if (distance > 2.0f) - { - float normalizedDx = dx / distance; - float normalizedDy = dy / distance; - - float newX = botPos.GetPositionX() + normalizedDx * 2.0f; - float newY = botPos.GetPositionY() + normalizedDy * 2.0f; - - TryMoveToPosition(newX, newY, targetZ); - } - else - { - TryMoveToPosition(targetX, targetY, targetZ); - } - } - } -} - -// Updated HandleRangedPositioning method -void IccLichKingWinterAction::HandleRangedPositioning() -{ - if (!botAI->IsRanged(bot)) - return; - - // Get the ranged position based on main tank's choice - const Position* targetPos = GetMainTankRangedPosition(); - - // First check if current position is safe - if (!IsPositionSafeFromDefile(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), 3.0f)) - { - // If in defile, prioritize getting out - TryMoveToPosition(targetPos->GetPositionX(), targetPos->GetPositionY(), 840.857f, true); - return; - } - - float distToTarget = bot->GetDistance2d(targetPos->GetPositionX(), targetPos->GetPositionY()); - - if (distToTarget > 2.0f) - { - float targetX = targetPos->GetPositionX(); - float targetY = targetPos->GetPositionY(); - float targetZ = 840.857f; - - TryMoveToPosition(targetX, targetY, targetZ); - } - - // Handle sphere targeting for ranged DPS - if (botAI->IsRangedDps(bot)) - { - bool hasHunter = false; - Group* group = bot->GetGroup(); - if (group) - { - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && member->IsAlive() && member->IsInWorld() && member->getClass() == CLASS_HUNTER) - { - hasHunter = true; - break; - } - } - } - - if (bot->getClass() == CLASS_HUNTER || !hasHunter) - { - Unit* currentTarget = bot->GetVictim(); - if (currentTarget && currentTarget->IsAlive()) - { - uint32 entry = currentTarget->GetEntry(); - if (entry == NPC_ICE_SPHERE1 || entry == NPC_ICE_SPHERE2 || entry == NPC_ICE_SPHERE3 || entry == NPC_ICE_SPHERE4) - { - bot->SetFacingToObject(currentTarget); - Attack(currentTarget); - return; - } - } - - Unit* closestSphere = nullptr; - float closestDist = 100.0f; - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - uint32 entry = unit->GetEntry(); - if (entry == NPC_ICE_SPHERE1 || entry == NPC_ICE_SPHERE2 || entry == NPC_ICE_SPHERE3 || entry == NPC_ICE_SPHERE4) - { - float dist = bot->GetDistance(unit); - if (!closestSphere || dist < closestDist) - { - closestSphere = unit; - closestDist = dist; - } - } - } - if (closestSphere) - { - bot->SetTarget(closestSphere->GetGUID()); - bot->SetFacingToObject(closestSphere); - Attack(closestSphere); - } - } - } -} - -void IccLichKingWinterAction::HandleMainTankAddManagement(Unit* boss, const Position* tankPos) -{ - if (!botAI->IsMainTank(bot)) - return; - - // First, ensure we're at the correct tank position - float distToTankPos = bot->GetDistance2d(tankPos->GetPositionX(), tankPos->GetPositionY()); - if (distToTankPos > 3.0f) - { - TryMoveToPosition(tankPos->GetPositionX(), tankPos->GetPositionY(), 840.857f, true); - return; // Wait until we're in position - } - - // Get all valid adds in the encounter area - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - std::vector validAdds; - Unit* currentTarget = bot->GetVictim(); - - // Collect all valid adds - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (!IsValidCollectibleAdd(unit)) - continue; - - validAdds.push_back(unit); - } - - // If we have no adds, clear target if needed - if (validAdds.empty()) - { - if (currentTarget && !IsValidCollectibleAdd(currentTarget)) - { - bot->SetTarget(ObjectGuid::Empty); - } - return; - } - - // Strategy for add management: - // 1. First priority: Adds attacking non-tanks - // 2. Second priority: Adds not attacking us - // 3. Third priority: All other valid adds - Unit* priorityAdd = nullptr; - Unit* secondaryAdd = nullptr; - Unit* otherAdd = nullptr; - - for (Unit* add : validAdds) - { - Unit* addVictim = add->GetVictim(); - - // Highest priority: Adds attacking non-tanks - if (addVictim && addVictim->IsPlayer() && !botAI->IsTank(addVictim->ToPlayer())) - { - if (!priorityAdd || bot->GetDistance(add) < bot->GetDistance(priorityAdd)) - { - priorityAdd = add; - } - continue; - } - - // Medium priority: Adds not attacking us - if (addVictim != bot) - { - if (!secondaryAdd || bot->GetDistance(add) < bot->GetDistance(secondaryAdd)) - { - secondaryAdd = add; - } - continue; - } - - // Lowest priority: All other valid adds - if (!otherAdd || bot->GetDistance(add) < bot->GetDistance(otherAdd)) - { - otherAdd = add; - } - } - - // Select the highest priority add available - Unit* targetAdd = priorityAdd ? priorityAdd : (secondaryAdd ? secondaryAdd : otherAdd); - - if (targetAdd) - { - float addDist = bot->GetDistance(targetAdd); - - // If add is close enough (within melee range), attack it - if (addDist < 10.0f) - { - // If we're not already attacking this add, switch to it - if (currentTarget != targetAdd) - { - bot->SetTarget(targetAdd->GetGUID()); - bot->SetFacingToObject(targetAdd); - Attack(targetAdd); - } - } - else - { - // Add is too far - move toward it while staying near tank position - float moveX = targetAdd->GetPositionX(); - float moveY = targetAdd->GetPositionY(); - - // Don't move too far from tank position (max 15 yards) - float maxPullDistance = 15.0f; - float pullRatio = std::min(1.0f, maxPullDistance / addDist); - - float adjustedX = tankPos->GetPositionX() + (moveX - tankPos->GetPositionX()) * pullRatio; - float adjustedY = tankPos->GetPositionY() + (moveY - tankPos->GetPositionY()) * pullRatio; - - TryMoveToPosition(adjustedX, adjustedY, 840.857f, false); - - // Still try to attack while moving - bot->SetTarget(targetAdd->GetGUID()); - bot->SetFacingToObject(targetAdd); - Attack(targetAdd); - } - } - else if (currentTarget && !IsValidCollectibleAdd(currentTarget)) - { - // Clear invalid target - bot->SetTarget(ObjectGuid::Empty); - } -} - -void IccLichKingWinterAction::HandleAssistTankAddManagement(Unit* boss, const Position* tankPos) -{ - if (!botAI->IsAssistTank(bot)) - return; - - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - if (!mainTank) - return; - - // Look for priority adds that need to be collected - Unit* targetAdd = nullptr; - float closestDist = FLT_MAX; - bool foundPriorityAdd = false; - - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - - // Priority 1: Adds attacking non-tanks (players/healers) - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (!IsValidCollectibleAdd(unit)) - continue; - - Unit* addVictim = unit->GetVictim(); - if (addVictim && addVictim->IsPlayer() && !botAI->IsTank(addVictim->ToPlayer())) - { - float addDist = bot->GetDistance(unit); - if (addDist < closestDist) - { - targetAdd = unit; - closestDist = addDist; - foundPriorityAdd = true; - } - } - } - - // Priority 2: Adds not attacking main tank (if no priority adds found) - if (!foundPriorityAdd) - { - closestDist = FLT_MAX; - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (!IsValidCollectibleAdd(unit)) - continue; - - // Only target adds that are NOT attacking the main tank - if (unit->GetVictim() != mainTank) - { - float addDist = bot->GetDistance(unit); - if (addDist < closestDist) - { - targetAdd = unit; - closestDist = addDist; - } - } - } - } - - if (targetAdd) - { - // Calculate position between add and main tank - float pullX = (targetAdd->GetPositionX() + tankPos->GetPositionX()) / 2; - float pullY = (targetAdd->GetPositionY() + tankPos->GetPositionY()) / 2; - - // Move to intercept position if add is far from main tank - if (targetAdd->GetDistance2d(tankPos->GetPositionX(), tankPos->GetPositionY()) > 10.0f) - { - if (bot->GetDistance2d(pullX, pullY) > 3.0f) - { - TryMoveToPosition(pullX, pullY, 840.857f, false); - } - } - // Otherwise move toward the add - else if (closestDist > 5.0f) - { - TryMoveToPosition(targetAdd->GetPositionX(), targetAdd->GetPositionY(), 840.857f, false); - } - - // Attack the add - bot->SetTarget(targetAdd->GetGUID()); - bot->SetFacingToObject(targetAdd); - Attack(targetAdd); - } - else - { - // No adds to collect, position near main tank - float distToTankPos = bot->GetDistance2d(tankPos->GetPositionX(), tankPos->GetPositionY()); - if (distToTankPos > 2.0f) - { - float X = mainTank->GetPositionX(); - float Y = mainTank->GetPositionY(); - TryMoveToPosition(X, Y, 840.857f, false); - } - - // Check current target validity - Unit* currentTarget = bot->GetVictim(); - if (currentTarget && !IsValidCollectibleAdd(currentTarget)) - { - bot->SetTarget(ObjectGuid::Empty); - } - } -} - -bool IccLichKingAddsAction::Execute(Event /*event*/) -{ - if (bot->HasAura(SPELL_HARVEST_SOUL_VALKYR)) // Don't process actions if bot is picked up by Val'kyr - return false; - - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - - Difficulty diff = bot->GetRaidDifficulty(); - - if (sPlayerbotAIConfig.EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - //------CHEAT------- - if (!bot->HasAura(SPELL_EXPERIENCED)) - bot->AddAura(SPELL_EXPERIENCED, bot); - - if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) - bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); - - if (boss && boss->HealthBelowPct(60) && boss->HealthAbovePct(40) && !bot->HasAura(SPELL_EMPOWERED_BLOOD)) - bot->AddAura(SPELL_EMPOWERED_BLOOD, bot); - - if (!bot->HasAura(SPELL_NO_THREAT) && !botAI->IsTank(bot)) - bot->AddAura(SPELL_NO_THREAT, bot); - - if (!bot->HasAura(SPELL_PAIN_SUPPRESION)) - bot->AddAura(SPELL_PAIN_SUPPRESION, bot); - //------CHEAT------- - } - - Unit* spiritWarden = AI_VALUE2(Unit*, "find target", "spirit warden"); - bool hasPlague = botAI->HasAura("Necrotic Plague", bot); - Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); - - Group* group = bot->GetGroup(); - if (group && boss && boss->HealthAbovePct(71)) - { - constexpr uint8_t skullIconId = 7; - ObjectGuid skullGuid = group->GetTargetIcon(skullIconId); - if (skullGuid != boss->GetGUID()) - group->SetTargetIcon(skullIconId, bot->GetGUID(), boss->GetGUID()); - } - - //-----------Valkyr bot suicide if group fails to kill Valkyr in time------------- comment out if you dont want it - if (bot->HasAura(30440)) // Random aura tracking whether bot has fallen off edge / been thrown by Val'kyr - { - if (bot->GetPositionZ() > 779.0f) - return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 740.01f); - else - { - bot->Kill(bot, bot); // If bot has jumped past the kill Z (780), **Now it is fixed and bots will actually die instead of beeing frozen** - return true; - } - } - - bool hasWinterAura = false; - if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || - boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4))) - hasWinterAura = true; - - bool hasWinter2Aura = false; - if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || - boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8))) - hasWinter2Aura = true; - - if (boss && boss->GetHealthPct() < 70 && boss->GetHealthPct() > 40 && !hasWinterAura && - !hasWinter2Aura) // If boss is in p2, check if bot has been thrown off platform - { - float dx = bot->GetPositionX() - 503.0f; - float dy = bot->GetPositionY() - (-2124.0f); - float distance = sqrt(dx * dx + dy * dy); // Calculate distance from the center of the platform - - if (distance > 52.0f && distance < 70.0f && - bot->GetPositionZ() > 844) // If bot has fallen off edge, distance is over 52 - { - bot->AddAura(30440, bot); // Apply random 30 sec aura to track that we've initiated a jump - return JumpTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), - 740.01f); // Start jumping to the abyss - } - } - //-----------Valkyr bot suicide if group fails to kill Valkyr in time------------- comment out if you dont want it - - // Handle teleportation fixes - HandleTeleportationFixes(diff, terenasMenethilHC); - - // Handle heroic mode spirit bomb avoidance for main tank - if (HandleSpiritBombAvoidance(diff, terenasMenethilHC)) - return true; - - // Handle non-main tank positioning in heroic mode - HandleHeroicNonTankPositioning(diff, terenasMenethilHC); - - // Handle spirit marking and targeting in heroic mode - HandleSpiritMarkingAndTargeting(diff, terenasMenethilHC); - - if (terenasMenethilHC) - return false; - - // Handle quake mechanics - if (HandleQuakeMechanics(boss)) - return true; - - // Handle shambling horror interactions - HandleShamblingHorrors(boss, hasPlague); - - // Handle assist tank add management - if (HandleAssistTankAddManagement(boss, diff)) - return true; - - // Handle melee positioning - HandleMeleePositioning(boss, hasPlague, diff); - - // Handle main tank targeting in heroic - HandleMainTankTargeting(boss, diff); - - // Handle non-tank positioning in heroic - HandleNonTankHeroicPositioning(boss, diff, hasPlague); - - // Handle ranged positioning - HandleRangedPositioning(boss, hasPlague, diff); - - // Handle defile mechanics - HandleDefileMechanics(boss, diff); - - // Handle Val'kyr mechanics - HandleValkyrMechanics(diff); - - // Handle vile spirit mechanics - HandleVileSpiritMechanics(); - - return false; -} - -void IccLichKingAddsAction::HandleTeleportationFixes(Difficulty diff, Unit* terenasMenethilHC) -{ - // temp soultion for bots when they get teleport far away into another dimension (they are unable to attack spirit - // warden) in heroic they will be in safe spot while player is surviving vile spirits - if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) && - abs(bot->GetPositionY() - -2095.7915f) > 200.0f) - { - bot->TeleportTo(bot->GetMapId(), ICC_LICH_KING_ADDS_POSITION.GetPositionX(), - ICC_LICH_KING_ADDS_POSITION.GetPositionY(), ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), - bot->GetOrientation()); - } - - // temp solution for bots going underground due to buggy ice platfroms and adds that go underground - if (abs(bot->GetPositionZ() - 840.857f) > 1.0f && !botAI->GetAura("Harvest Soul", bot, false, false) && - !botAI->GetAura("Harvest Souls", bot, false, false)) - bot->TeleportTo(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), 840.857f, bot->GetOrientation()); - - if (abs(bot->GetPositionZ() - 1049.865f) > 5.0f && botAI->GetAura("Harvest Soul", bot, false, false) && - terenasMenethilHC) - bot->TeleportTo(bot->GetMapId(), terenasMenethilHC->GetPositionX(), terenasMenethilHC->GetPositionY(), 1049.865f, - bot->GetOrientation()); -} - -bool IccLichKingAddsAction::HandleSpiritBombAvoidance(Difficulty diff, Unit* terenasMenethilHC) -{ - if (!botAI->IsMainTank(bot) || !terenasMenethilHC || !diff || - !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - return false; - - std::map spiritBombs; - - // Gather all spirit bombs using their GUIDs for reliable tracking - GuidVector npcs1 = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npcGuid : npcs1) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_SPIRIT_BOMB) - { - spiritBombs[npcGuid] = unit; - } - } - - // Only proceed if there are actually spirit bombs present - if (spiritBombs.empty()) - return false; - - const float SAFE_DISTANCE = 14.0f; // Minimum safe horizontal distance - const float SAFE_HEIGHT_DIFF = 12.0f; // Safe if Z difference is greater than this - const float BOMB_DENSITY_RADIUS = 10.0f; // Radius for counting nearby bombs - const float BOMB_COUNT_PENALTY = 10.0f; // Score penalty per bomb in vicinity - const float MAX_HEIGHT_DIFF = 8.0f; // Increased from 5.0f for more flexibility - - // First check if current position is already safe - bool currentPositionSafe = true; - float minDistanceToAnyBomb = std::numeric_limits::max(); - - // Clean up invalid bombs and check current position safety - auto it = spiritBombs.begin(); - while (it != spiritBombs.end()) - { - Unit* verifiedBomb = botAI->GetUnit(it->first); - if (!verifiedBomb || !verifiedBomb->IsAlive() || verifiedBomb->GetEntry() != NPC_SPIRIT_BOMB) - { - it = spiritBombs.erase(it); - continue; - } - - float dx = bot->GetPositionX() - verifiedBomb->GetPositionX(); - float dy = bot->GetPositionY() - verifiedBomb->GetPositionY(); - float dz = bot->GetPositionZ() - verifiedBomb->GetPositionZ(); - float horizontalDistance = sqrt(dx * dx + dy * dy); - float verticalDistance = fabs(dz); - - minDistanceToAnyBomb = std::min(minDistanceToAnyBomb, horizontalDistance); - - // Position is dangerous if horizontally close AND not high enough above/below - if (horizontalDistance < SAFE_DISTANCE && verticalDistance <= SAFE_HEIGHT_DIFF) - { - currentPositionSafe = false; - } - ++it; - } - - // If no valid bombs remain after cleanup, exit early - if (spiritBombs.empty()) - { - return false; - } - - // Only move if current position is unsafe - if (!currentPositionSafe) - { - float bestScore = -std::numeric_limits::max(); - float bestX = 0, bestY = 0, bestZ = 0; - bool foundSafePosition = false; - - // Multi-distance search to avoid getting stuck - std::vector searchDistances = {6.0f, 10.0f, 15.0f, 20.0f, 25.0f}; - - for (float searchDistance : searchDistances) - { - // Try 36 different angles for thorough coverage - for (int i = 0; i < 36; i++) - { - float testAngle = i * 2 * M_PI / 36; - - float testX = bot->GetPositionX() + searchDistance * cos(testAngle); - float testY = bot->GetPositionY() + searchDistance * sin(testAngle); - float testZ = bot->GetPositionZ(); - - bot->UpdateAllowedPositionZ(testX, testY, testZ); - - // More lenient LOS and height checks - bool validPosition = true; - float heightDiff = fabs(testZ - bot->GetPositionZ()); - - // Skip positions that are too high/low or not in LOS - if (heightDiff >= MAX_HEIGHT_DIFF) - { - validPosition = false; - } - - // Only check LOS if height difference is reasonable - if (validPosition && !bot->IsWithinLOS(testX, testY, testZ)) - { - validPosition = false; - } - - if (!validPosition) - continue; - - // Check position safety and bomb density - bool positionSafe = true; - float minDistAtPos = std::numeric_limits::max(); - int bombCountInVicinity = 0; - - for (auto const& bombPair : spiritBombs) - { - Unit* bomb = bombPair.second; - if (!bomb || !bomb->IsAlive()) - continue; - - float dx = testX - bomb->GetPositionX(); - float dy = testY - bomb->GetPositionY(); - float dz = testZ - bomb->GetPositionZ(); - float horizontalDist = sqrt(dx * dx + dy * dy); - float verticalDist = fabs(dz); - - // Track minimum distance to any bomb - minDistAtPos = std::min(minDistAtPos, horizontalDist); - - // Count bombs within density radius - if (horizontalDist < BOMB_DENSITY_RADIUS) - { - bombCountInVicinity++; - } - - // Check safety condition - if (horizontalDist < SAFE_DISTANCE && verticalDist <= SAFE_HEIGHT_DIFF) - { - positionSafe = false; - break; - } - } - - // Skip unsafe positions - if (!positionSafe) - continue; - - // Calculate composite score with distance bonus to prefer closer safe positions - float distanceBonus = std::max(0.0f, 30.0f - searchDistance); // Prefer closer positions - float score = minDistAtPos - (bombCountInVicinity * BOMB_COUNT_PENALTY) + distanceBonus; - - // Update best position if this one is better - if (score > bestScore) - { - bestScore = score; - bestX = testX; - bestY = testY; - bestZ = testZ; - foundSafePosition = true; - } - } - - // If we found a safe position at this distance, use it (prefer closer positions) - if (foundSafePosition && searchDistance <= 15.0f) - break; - } - - // Move to the best position found - if (foundSafePosition) - { - // Final validation before moving - if (bot->IsWithinLOS(bestX, bestY, bestZ) && fabs(bestZ - bot->GetPositionZ()) <= MAX_HEIGHT_DIFF) - { - MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - } - } - } - return true; -} - -void IccLichKingAddsAction::HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenasMenethilHC) -{ - if (!terenasMenethilHC || botAI->IsMainTank(bot) || !diff || - !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - return; - - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - // Only move if significantly far from main tank (increased threshold to reduce jittery movement) - if (mainTank && bot->GetExactDist2d(mainTank->GetPositionX(), mainTank->GetPositionY()) > 2.0f) - { - MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), mainTank->GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_FORCED); - } -} - -void IccLichKingAddsAction::HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenasMenethilHC) -{ - if (!terenasMenethilHC || botAI->IsMainTank(bot) || !diff || - !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - return; - - Group* group = bot->GetGroup(); - if (!group) - return; - - static constexpr uint8_t STAR_ICON_INDEX = 0; - static constexpr float MAX_Z_DIFF = 20.0f; - - // Check if current marked target is still valid and threatening - Unit* currentMarkedTarget = botAI->GetUnit(group->GetTargetIcon(STAR_ICON_INDEX)); - bool needNewMark = !currentMarkedTarget || !currentMarkedTarget->IsAlive(); - - // Check if current marked spirit is targeting a group member - bool currentTargetingGroupMember = false; - if (currentMarkedTarget && currentMarkedTarget->IsAlive()) - { - Unit* spiritTarget = currentMarkedTarget->GetVictim(); - if (spiritTarget && spiritTarget->IsPlayer()) - { - if (Group* spiritTargetGroup = spiritTarget->ToPlayer()->GetGroup()) - { - if (spiritTargetGroup->GetGUID() == group->GetGUID()) - { - currentTargetingGroupMember = true; - } - } - } - } - - // Only search for new target if we need to mark OR if we can find a higher priority target - if (needNewMark || !currentTargetingGroupMember) - { - Unit* prioritySpirit = nullptr; // Spirit targeting group member - Unit* nearestSpirit = nullptr; // Fallback: nearest spirit - float priorityDist = 100.0f; - float nearestDist = 100.0f; - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive() || !unit->isTargetableForAttack()) - continue; - - uint32 entry = unit->GetEntry(); - if (entry == NPC_WICKED_SPIRIT1 || entry == NPC_WICKED_SPIRIT2 || entry == NPC_WICKED_SPIRIT3 || - entry == NPC_WICKED_SPIRIT4) - { - // Check Z-axis difference first - float zDiff = std::abs(unit->GetPositionZ() - bot->GetPositionZ()); - if (zDiff <= MAX_Z_DIFF) - { - float dist = bot->GetDistance(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()); - - // Check if this spirit is targeting a group member - bool targetingGroupMember = false; - Unit* spiritTarget = unit->GetVictim(); - if (spiritTarget && spiritTarget->IsPlayer()) - { - if (Group* spiritTargetGroup = spiritTarget->ToPlayer()->GetGroup()) - { - if (spiritTargetGroup->GetGUID() == group->GetGUID()) - { - targetingGroupMember = true; - } - } - } - - // Priority: spirits targeting group members - if (targetingGroupMember) - { - if (!prioritySpirit || dist < priorityDist) - { - prioritySpirit = unit; - priorityDist = dist; - } - } - - // Fallback: track nearest spirit regardless of target - if (!nearestSpirit || dist < nearestDist) - { - nearestSpirit = unit; - nearestDist = dist; - } - } - } - } - - // Mark priority spirit if found, otherwise fall back to nearest - Unit* spiritToMark = prioritySpirit ? prioritySpirit : nearestSpirit; - - // Only mark if we found a better target or need a new mark - if (spiritToMark && (needNewMark || (prioritySpirit && !currentTargetingGroupMember))) - { - group->SetTargetIcon(STAR_ICON_INDEX, bot->GetGUID(), spiritToMark->GetGUID()); - } - } - - // Only ranged DPS use star for RTI - if (botAI->IsRangedDps(bot)) - { - context->GetValue("rti")->Set("star"); - Unit* starTarget = botAI->GetUnit(group->GetTargetIcon(STAR_ICON_INDEX)); - if (starTarget && starTarget->IsAlive()) - { - bot->SetTarget(starTarget->GetGUID()); - bot->SetFacingToObject(starTarget); - Attack(starTarget); - bot->Kill(bot, starTarget); //temp solution since bots struggle to kill spirits in time, they have to follow main tank closely so that they do not get hit by bomb, thus making them have very limited time to react - } - } -} - -bool IccLichKingAddsAction::HandleQuakeMechanics(Unit* boss) -{ - if (!boss || !boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_QUAKE)) - return false; - - float currentDistance = bot->GetExactDist2d(boss); - - // If already at ideal distance (40f), no need to move - if (currentDistance >= 35.0f && currentDistance <= 45.0f) - return false; - - if (bot->HasUnitState(UNIT_STATE_CASTING)) - botAI->Reset(); - - float botX = bot->GetPositionX(); - float botY = bot->GetPositionY(); - float targetX, targetY; - - if (!botAI->IsTank(bot)) - { - // Non-tanks: use offset position as direction guide - float offsetX = boss->GetPositionX() + 15.0f; - float offsetY = boss->GetPositionY() + 15.0f; - - // Calculate direction towards offset position - float dx = offsetX - botX; - float dy = offsetY - botY; - float distance = sqrt(dx * dx + dy * dy); - - if (distance > 0.0f) - { - // Move 10f towards offset position - float ratio = 10.0f / distance; - targetX = botX + dx * ratio; - targetY = botY + dy * ratio; - } - else - { - targetX = botX; - targetY = botY; - } - } - else - { - // Tanks: use offset position as direction guide - float offsetX = boss->GetPositionX() - 15.0f; - float offsetY = boss->GetPositionY() - 15.0f; - - // Calculate direction towards offset position - float dx = offsetX - botX; - float dy = offsetY - botY; - float distance = sqrt(dx * dx + dy * dy); - - if (distance > 0.0f) - { - // Move 10f towards offset position - float ratio = 10.0f / distance; - targetX = botX + dx * ratio; - targetY = botY + dy * ratio; - } - else - { - targetX = botX; - targetY = botY; - } - } - - MoveTo(bot->GetMapId(), targetX, targetY, boss->GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - - return false; -} - -void IccLichKingAddsAction::HandleShamblingHorrors(Unit* boss, bool hasPlague) -{ - // Find closest shambling horror - GuidVector npcs2 = AI_VALUE(GuidVector, "nearest hostile npcs"); - Unit* closestHorror = nullptr; - float minHorrorDistance = std::numeric_limits::max(); - - for (auto& npc : npcs2) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && - (unit->GetEntry() == NPC_SHAMBLING_HORROR1 || unit->GetEntry() == NPC_SHAMBLING_HORROR2 || unit->GetEntry() == NPC_SHAMBLING_HORROR3 || - unit->GetEntry() == NPC_SHAMBLING_HORROR4)) // Shambling horror entries - { - float distance = bot->GetDistance(unit); - if (distance < minHorrorDistance) - { - minHorrorDistance = distance; - closestHorror = unit; - } - } - } - - /* - if (!closestHorror || hasPlague) - { - } - else if (!hasPlague && closestHorror->isInFront(bot) && closestHorror->IsAlive() && !botAI->IsTank(bot) && - bot->GetDistance2d(closestHorror) < 3.0f) - return FleePosition(closestHorror->GetPosition(), 2.0f, 250U); - */ - - // If bot is hunter and shambling is enraged, use Tranquilizing Shot - if (bot->getClass() == CLASS_HUNTER && closestHorror && botAI->HasAura("Enrage", closestHorror)) - botAI->CastSpell("Tranquilizing Shot", closestHorror); -} - -bool IccLichKingAddsAction::HandleAssistTankAddManagement(Unit* boss, Difficulty diff) -{ - if (!botAI->IsAssistTank(bot) || !boss || boss->HealthBelowPct(71)) - return false; - - // Find all adds and categorize them by targeting status - GuidVector targets = AI_VALUE(GuidVector, "possible targets"); - std::vector addsNotTargetingUs; - std::vector addsTargetingUs; - - for (auto i = targets.begin(); i != targets.end(); ++i) - { - Unit* unit = botAI->GetUnit(*i); - if (unit && unit->IsAlive() && - (unit->GetEntry() == NPC_SHAMBLING_HORROR1 || unit->GetEntry() == NPC_SHAMBLING_HORROR2 || - unit->GetEntry() == NPC_SHAMBLING_HORROR3 || - unit->GetEntry() == NPC_SHAMBLING_HORROR4 || // Shambling entry - unit->GetEntry() == NPC_RAGING_SPIRIT1 || unit->GetEntry() == NPC_RAGING_SPIRIT2 || - unit->GetEntry() == NPC_RAGING_SPIRIT3 || unit->GetEntry() == NPC_RAGING_SPIRIT4 || // Spirits entry - unit->GetEntry() == NPC_DRUDGE_GHOUL1 || unit->GetEntry() == NPC_DRUDGE_GHOUL2 || - unit->GetEntry() == NPC_DRUDGE_GHOUL3 || unit->GetEntry() == NPC_DRUDGE_GHOUL4)) // Drudge Ghouls entry - { - if (unit->GetVictim() == bot) - { - addsTargetingUs.push_back(unit->GetGUID()); - } - else - { - addsNotTargetingUs.push_back(unit->GetGUID()); - } - } - } - - // If there are adds not targeting us, we need to collect them all - if (!addsNotTargetingUs.empty()) - { - // Find the highest priority target (Shamblings first, then closest) - Unit* priorityTarget = nullptr; - Unit* closestAdd = nullptr; - float closestDist = 999.0f; - - for (const ObjectGuid& addGuid : addsNotTargetingUs) - { - Unit* add = botAI->GetUnit(addGuid); - if (add && add->IsAlive()) - { - // Shambling takes absolute priority regardless of distance - if (add->GetEntry() == NPC_SHAMBLING_HORROR1 || add->GetEntry() == NPC_SHAMBLING_HORROR2 || - add->GetEntry() == NPC_SHAMBLING_HORROR3 || add->GetEntry() == NPC_SHAMBLING_HORROR4) - { - priorityTarget = add; - break; // Found shambling, stop looking - } - - // Track closest add as backup - float dist = bot->GetExactDist2d(add); - if (dist < closestDist) - { - closestDist = dist; - closestAdd = add; - } - } - } - - // Choose target: Shambling first, then closest - Unit* targetToAttack = priorityTarget ? priorityTarget : closestAdd; - - if (targetToAttack) - { - // Generate threat on ALL adds not targeting us using ranged abilities - for (const ObjectGuid& addGuid : addsNotTargetingUs) - { - Unit* add = botAI->GetUnit(addGuid); - if (add && add->IsAlive()) - { - float dist = bot->GetExactDist2d(add); - - // Use ranged threat generation if within range - if (dist <= 30.0f) - { - // Try taunt first if available - if (botAI->CastSpell("taunt", add)) - { - continue; - } - // Fall back to ranged attack - else if (botAI->CastSpell("shoot", add) || botAI->CastSpell("throw", add)) - { - continue; - } - // Last resort - basic attack state update - else - { - bot->AttackerStateUpdate(add); - } - } - } - } - - // Move towards and attack the priority target - float distToTarget = bot->GetExactDist2d(targetToAttack); - - // If we're too far from our priority target, move closer - if (distToTarget > 5.0f) - { - MoveTo(bot->GetMapId(), targetToAttack->GetPositionX(), targetToAttack->GetPositionY(), - targetToAttack->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_FORCED, - true, false); - } - else - { - // We're close enough, set target and attack - bot->SetTarget(targetToAttack->GetGUID()); - bot->SetFacingToObject(targetToAttack); - Attack(targetToAttack); - } - } - } - // If all adds are targeting us or there are no adds, maintain position based on difficulty - else - { - // In heroic mode, stay at melee position - if (diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - - if (bot->GetExactDist2d(ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(), - ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY()) > 2.0f) - { - MoveTo(bot->GetMapId(), ICC_LICH_KING_ASSISTHC_POSITION.GetPositionX(), - ICC_LICH_KING_ASSISTHC_POSITION.GetPositionY(), ICC_LICH_KING_ASSISTHC_POSITION.GetPositionZ(), - false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - } - // In normal mode, stay at adds position - else - { - if (bot->GetExactDist2d(ICC_LICH_KING_ADDS_POSITION) > 2.0f) - { - MoveTo(bot->GetMapId(), ICC_LICH_KING_ADDS_POSITION.GetPositionX(), - ICC_LICH_KING_ADDS_POSITION.GetPositionY(), ICC_LICH_KING_ADDS_POSITION.GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - } - - // If we have adds targeting us, attack them with stable target selection - if (!addsTargetingUs.empty()) - { - Unit* currentTarget = bot->GetVictim(); - bool needNewTarget = true; - - // Check if current target is still valid (alive and attacking us) - if (currentTarget && currentTarget->IsAlive()) - { - for (const ObjectGuid& addGuid : addsTargetingUs) - { - if (addGuid == currentTarget->GetGUID()) - { - needNewTarget = false; - break; - } - } - } - - // Only pick new target if current one is invalid - if (needNewTarget) - { - currentTarget = nullptr; - - // Priority 1: Shambling Horror - for (const ObjectGuid& addGuid : addsTargetingUs) - { - Unit* add = botAI->GetUnit(addGuid); - if (add && add->IsAlive()) - { - if (add->GetEntry() == NPC_SHAMBLING_HORROR1 || add->GetEntry() == NPC_SHAMBLING_HORROR2 || - add->GetEntry() == NPC_SHAMBLING_HORROR3 || add->GetEntry() == NPC_SHAMBLING_HORROR4) - { - currentTarget = add; - break; - } - } - } - - // Priority 2: Any other add if no Shambling Horror - if (!currentTarget) - { - for (const ObjectGuid& addGuid : addsTargetingUs) - { - Unit* add = botAI->GetUnit(addGuid); - if (add && add->IsAlive()) - { - currentTarget = add; - break; - } - } - } - } - - if (currentTarget) - { - bot->SetTarget(currentTarget->GetGUID()); - bot->SetFacingToObject(currentTarget); - Attack(currentTarget); - } - } - } - return false; -} - -void IccLichKingAddsAction::HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff) -{ - if (!boss || !botAI->IsMelee(bot) || botAI->IsAssistTank(bot) || boss->HealthBelowPct(71) || hasPlague) - return; - - if (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) - return; - - float currentDist = bot->GetDistance(ICC_LICH_KING_MELEE_POSITION); - - if (currentDist > 6.0f && !botAI->IsMainTank(bot)) - { - MoveTo(bot->GetMapId(), ICC_LICH_KING_MELEE_POSITION.GetPositionX(), - ICC_LICH_KING_MELEE_POSITION.GetPositionY(), ICC_LICH_KING_MELEE_POSITION.GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - - if (currentDist > 6.0f && botAI->IsMainTank(bot) && boss && boss->GetVictim() == bot) - { - Position currentPos = bot->GetPosition(); - Position targetPos = ICC_LICH_KING_MELEE_POSITION; - - // Calculate direction vector - float dx = targetPos.GetPositionX() - currentPos.GetPositionX(); - float dy = targetPos.GetPositionY() - currentPos.GetPositionY(); - - // Calculate distance and normalize direction - float distance = sqrt(dx * dx + dy * dy); - if (distance > 0.1) - { - dx /= distance; - dy /= distance; - } - - // Calculate intermediate position (3f towards target) - float step = std::min(3.0f, distance - 1.0f); // Don't overshoot the target - if (step > 0) - { - float intermediateX = currentPos.GetPositionX() + dx * step; - float intermediateY = currentPos.GetPositionY() + dy * step; - - MoveTo(bot->GetMapId(), intermediateX, intermediateY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } - else - { - // If we're within 1.0f + 3.0f of the target, move directly to it - MoveTo(bot->GetMapId(), targetPos.GetPositionX(), targetPos.GetPositionY(), bot->GetPositionZ(), false, - false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } - } -} - -void IccLichKingAddsAction::HandleMainTankTargeting(Unit* boss, Difficulty diff) -{ - if (!botAI->IsMainTank(bot) || !boss) - return; - - if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - return; - - if (boss->HealthBelowPct(71) || boss->GetVictim() == bot) - return; - - bot->SetTarget(boss->GetGUID()); - bot->SetFacingToObject(boss); - Attack(boss); -} - -void IccLichKingAddsAction::HandleNonTankHeroicPositioning(Unit* boss, Difficulty diff, bool hasPlague) -{ - if (botAI->IsTank(bot) || !boss) - return; - - if (!(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - return; - - if (boss->HealthBelowPct(71) || hasPlague) - return; - - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - if (!mainTank) - return; - - if (bot->GetDistance2d(mainTank->GetPositionX(), mainTank->GetPositionY()) > 20.0f && (bot->getClass() == CLASS_HUNTER)) - { - botAI->Reset(); - - // Calculate direction vector to main tank - float dx = mainTank->GetPositionX() - bot->GetPositionX(); - float dy = mainTank->GetPositionY() - bot->GetPositionY(); - - // Normalize and scale to 2f increments - float distance = sqrt(dx * dx + dy * dy); - if (distance > 0) - { - dx = dx / distance * 2.0f; - dy = dy / distance * 2.0f; - - // Calculate new position (2f closer to main tank) - float newX = bot->GetPositionX() + dx; - float newY = bot->GetPositionY() + dy; - - MoveTo(bot->GetMapId(), newX, newY, bot->GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_FORCED, true, false); - } - } - - if (bot->GetDistance2d(mainTank->GetPositionX(), mainTank->GetPositionY()) > 1.0f && (bot->getClass() != CLASS_HUNTER)) - { - botAI->Reset(); - MoveTo(bot->GetMapId(), mainTank->GetPositionX(), mainTank->GetPositionY(), bot->GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } -} - -void IccLichKingAddsAction::HandleRangedPositioning(Unit* boss, bool hasPlague, Difficulty diff) -{ - if (!boss || !botAI->IsRanged(bot) || boss->HealthBelowPct(71) || hasPlague) - return; - - if (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) - return; - - float currentDist = bot->GetDistance(ICC_LICH_KING_RANGED_POSITION); - if (currentDist > 2.0f) - { - MoveTo(bot->GetMapId(), ICC_LICH_KING_RANGED_POSITION.GetPositionX(), - ICC_LICH_KING_RANGED_POSITION.GetPositionY(), ICC_LICH_KING_RANGED_POSITION.GetPositionZ(), false, false, - false, true, MovementPriority::MOVEMENT_FORCED, true, false); - } -} - -void IccLichKingAddsAction::HandleDefileMechanics(Unit* boss, Difficulty diff) -{ - if (!boss) - return; - - // Constants - const float BASE_RADIUS = 6.0f; - const float SAFETY_MARGIN = 3.0f; - const float MOVE_DISTANCE = 5.0f; - const float SPREAD_DISTANCE = 12.0f; - const float FIXED_Z = 840.857f; - const float MAX_HEIGHT_DIFF = 5.0f; - const float MIN_PLAYER_SPACING = 5.0f; - const float MAX_BOSS_DISTANCE = 40.0f; - const int ANGLE_TESTS = 16; - const int MAX_ANGLE_OFFSETS = 8; - - // Gather all defile units - std::vector defiles; - Unit* closestDefile = nullptr; - float closestDistance = std::numeric_limits::max(); - - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) - { - defiles.push_back(unit); - float dist = bot->GetDistance(unit); - if (dist < closestDistance) - { - closestDistance = dist; - closestDefile = unit; - } - } - } - - // Only process defile avoidance if defiles exist - if (!defiles.empty()) - { - // Check if we need to move away from defiles - bool needToMove = false; - float botX = bot->GetPositionX(); - float botY = bot->GetPositionY(); - - for (Unit* defile : defiles) - { - if (!defile || !defile->IsAlive()) - continue; - - // Calculate current defile radius including growth - float currentRadius = BASE_RADIUS; - Aura* growAura = nullptr; - - // Find growth aura - for (size_t i = 0; i < DEFILE_AURA_COUNT; i++) - { - growAura = defile->GetAura(DEFILE_AURAS[i]); - if (growAura) - break; - } - - if (growAura) - { - uint8 stacks = growAura->GetStackAmount(); - float growthMultiplier = - (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) ? 1.4f : 0.95f; - currentRadius = BASE_RADIUS + (stacks * growthMultiplier); - } - - // Check if bot is too close to this defile - float dx = botX - defile->GetPositionX(); - float dy = botY - defile->GetPositionY(); - float distanceToCenter = sqrt(dx * dx + dy * dy); - - if (distanceToCenter < (currentRadius + SAFETY_MARGIN)) - { - needToMove = true; - break; - } - } - - // Move away from defiles if needed - if (needToMove) - { - float bestAngle = 0.0f; - float maxSafetyScore = 0.0f; - bool foundSafePosition = false; - - // Test multiple angles to find safest escape route - for (int i = 0; i < ANGLE_TESTS; i++) - { - float testAngle = i * M_PI / 8; - float testX = botX + MOVE_DISTANCE * cos(testAngle); - float testY = botY + MOVE_DISTANCE * sin(testAngle); - float testZ = FIXED_Z; - - bot->UpdateAllowedPositionZ(testX, testY, testZ); - - // Skip invalid positions (LOS and height check) - if (!bot->IsWithinLOS(testX, testY, testZ) || fabs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) - continue; - - // Calculate minimum distance to any defile from this position - float minDefileDistance = std::numeric_limits::max(); - for (Unit* defile : defiles) - { - if (!defile || !defile->IsAlive()) - continue; - - float dx = testX - defile->GetPositionX(); - float dy = testY - defile->GetPositionY(); - float dist = sqrt(dx * dx + dy * dy); - minDefileDistance = std::min(minDefileDistance, dist); - } - - // Calculate scoring (safety + boss proximity) - float distanceToBoss = boss->GetDistance2d(testX, testY); - float safetyScore = minDefileDistance; - float bossScore = 100.0f - std::min(100.0f, distanceToBoss); - float totalScore = safetyScore + (bossScore * 0.5f); - - if (totalScore > maxSafetyScore) - { - maxSafetyScore = totalScore; - bestAngle = testAngle; - foundSafePosition = true; - } - } - - // Execute movement if safe position found - if (foundSafePosition && maxSafetyScore > 0) - { - float moveX = botX + MOVE_DISTANCE * cos(bestAngle); - float moveY = botY + MOVE_DISTANCE * sin(bestAngle); - float moveZ = FIXED_Z; - - if (bot->HasUnitState(UNIT_STATE_CASTING)) - botAI->Reset(); - - bot->UpdateAllowedPositionZ(moveX, moveY, moveZ); - MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, true, - MovementPriority::MOVEMENT_FORCED); - } - } - } - - // Handle Defile cast - spread positioning - if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(DEFILE_CAST_ID)) - { - // Count players and determine bot's index - uint32 playerCount = 0; - uint32 botIndex = 0; - uint32 currentIndex = 0; - - float botX = bot->GetPositionX(); - float botY = bot->GetPositionY(); - - Map::PlayerList const& players = bot->GetMap()->GetPlayers(); - for (Map::PlayerList::const_iterator itr = players.begin(); itr != players.end(); ++itr) - { - Player* player = itr->GetSource(); - if (!player || !player->IsAlive()) - continue; - - if (player == bot) - botIndex = currentIndex; - - currentIndex++; - playerCount++; - } - - // Calculate preferred spread angle based on bot index - float preferredAngle = (float(botIndex) / float(playerCount)) * 2 * M_PI; - bool foundSafeSpot = false; - float bestSpreadAngle = preferredAngle; - - // Try positions starting from preferred angle, expanding outward - for (int offset = 0; offset <= MAX_ANGLE_OFFSETS && !foundSafeSpot; offset++) - { - for (int direction = -1; direction <= 1; direction += 2) - { - if (offset == 0 && direction > 0) // Skip duplicate check of preferred angle - continue; - - float testAngle = preferredAngle + (direction * offset * M_PI / 16); - float testX = botX + SPREAD_DISTANCE * cos(testAngle); - float testY = botY + SPREAD_DISTANCE * sin(testAngle); - float testZ = FIXED_Z; - - bot->UpdateAllowedPositionZ(testX, testY, testZ); - - // Validate position basics (LOS and height) - if (!bot->IsWithinLOS(testX, testY, testZ) || fabs(testZ - bot->GetPositionZ()) >= MAX_HEIGHT_DIFF) - continue; - - // Check boss distance - if (boss->GetDistance2d(testX, testY) > MAX_BOSS_DISTANCE) - continue; - - // Check safety from all defiles (only if defiles exist) - bool safeFromDefiles = true; - for (Unit* defile : defiles) - { - if (!defile || !defile->IsAlive()) - continue; - - // Calculate current defile radius including growth - float currentRadius = BASE_RADIUS; - Aura* growAura = nullptr; - - // Find growth aura - for (size_t i = 0; i < DEFILE_AURA_COUNT; i++) - { - growAura = defile->GetAura(DEFILE_AURAS[i]); - if (growAura) - break; - } - - if (growAura) - { - uint8 stacks = growAura->GetStackAmount(); - float growthMultiplier = - (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_10MAN_NORMAL) ? 1.4f - : 0.95f; - currentRadius = BASE_RADIUS + (stacks * growthMultiplier); - } - - float dx = testX - defile->GetPositionX(); - float dy = testY - defile->GetPositionY(); - float distToDefile = sqrt(dx * dx + dy * dy); - - if (distToDefile < (currentRadius + SAFETY_MARGIN)) - { - safeFromDefiles = false; - break; - } - } - - if (!safeFromDefiles) - continue; - - // Check spacing from other players - bool tooCloseToPlayers = false; - for (Map::PlayerList::const_iterator playerItr = players.begin(); playerItr != players.end(); - ++playerItr) - { - Player* player = playerItr->GetSource(); - if (!player || !player->IsAlive() || player == bot) - continue; - - float dx = testX - player->GetPositionX(); - float dy = testY - player->GetPositionY(); - float dist = sqrt(dx * dx + dy * dy); - - if (dist < MIN_PLAYER_SPACING) - { - tooCloseToPlayers = true; - break; - } - } - - if (tooCloseToPlayers) - continue; - - // Found valid position - bestSpreadAngle = testAngle; - foundSafeSpot = true; - break; - } - } - - // Execute spread movement if safe spot found - if (foundSafeSpot) - { - float spreadX = botX + SPREAD_DISTANCE * cos(bestSpreadAngle); - float spreadY = botY + SPREAD_DISTANCE * sin(bestSpreadAngle); - float spreadZ = FIXED_Z; - - bot->UpdateAllowedPositionZ(spreadX, spreadY, spreadZ); - MoveTo(bot->GetMapId(), spreadX, spreadY, spreadZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT); - } - } -} - -void IccLichKingAddsAction::HandleValkyrMechanics(Difficulty diff) -{ - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector grabbingValkyrs; - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - - // Find grabbing Val'kyrs - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - - if (unit->GetEntry() == NPC_VALKYR_SHADOWGUARD1 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD2 || - unit->GetEntry() == NPC_VALKYR_SHADOWGUARD3 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD4) - { - bool isGrabbing = false; - - if (diff && !(diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - if (unit->HasAura(SPELL_HARVEST_SOUL_VALKYR)) - isGrabbing = true; - } - else - { - if (unit->HasAura(SPELL_HARVEST_SOUL_VALKYR) && unit->HealthAbovePct(49)) - isGrabbing = true; - } - - if (isGrabbing) - grabbingValkyrs.push_back(unit); - } - } - - Group* group = bot->GetGroup(); - if (!group) - return; - - // If no grabbing Val'kyrs, mark the Lich King with skull - if (grabbingValkyrs.empty() || (boss && boss->HealthBelowPct(43))) - { - // Find the Lich King - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_THE_LICH_KING && unit->HealthBelowPct(68) && unit->HealthAbovePct(40)) - { - ObjectGuid currentSkull = group->GetTargetIcon(7); // Skull icon - if (currentSkull != unit->GetGUID()) - { - group->SetTargetIcon(7, bot->GetGUID(), unit->GetGUID()); - } - break; - } - } - return; - } - - if (botAI->IsMainTank(bot)) - return; - - // Filter out dead Val'kyrs to ensure accurate group calculation - std::vector aliveGrabbingValkyrs; - for (Unit* valkyr : grabbingValkyrs) - { - if (valkyr && valkyr->IsAlive()) - aliveGrabbingValkyrs.push_back(valkyr); - } - - if (aliveGrabbingValkyrs.empty()) - return; - - HandleValkyrMarking(aliveGrabbingValkyrs, diff); - HandleValkyrAssignment(aliveGrabbingValkyrs); -} - -void IccLichKingAddsAction::HandleValkyrMarking(const std::vector& grabbingValkyrs, Difficulty diff) -{ - Group* group = bot->GetGroup(); - if (!group) - return; - - // Sort Val'kyrs by their GUID to ensure consistent ordering - std::vector sortedValkyrs = grabbingValkyrs; - std::sort(sortedValkyrs.begin(), sortedValkyrs.end(), [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); - - static constexpr uint8_t ICON_INDICES[] = {7, 6, 0}; // Skull, Cross, Star - static constexpr const char* ICON_NAMES[] = {"skull", "cross", "star"}; - - // In heroic mode, clean up invalid markers for all possible icons - if (diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - for (size_t i = 0; i < 3; ++i) - { - ObjectGuid currentIcon = group->GetTargetIcon(ICON_INDICES[i]); - Unit* currentIconUnit = botAI->GetUnit(currentIcon); - - if (currentIconUnit && IsValkyr(currentIconUnit)) - { - bool shouldRemoveMarker = !currentIconUnit->HasAura(SPELL_HARVEST_SOUL_VALKYR) || - std::abs(currentIconUnit->GetPositionZ() - bot->GetPositionZ()) > 5.0f; - - if (shouldRemoveMarker) - group->SetTargetIcon(ICON_INDICES[i], bot->GetGUID(), ObjectGuid::Empty); - } - } - } - - // Clear unused markers if we have fewer Val'kyrs than icons - for (size_t i = sortedValkyrs.size(); i < 3; ++i) - { - ObjectGuid currentIcon = group->GetTargetIcon(ICON_INDICES[i]); - if (!currentIcon.IsEmpty()) - { - group->SetTargetIcon(ICON_INDICES[i], bot->GetGUID(), ObjectGuid::Empty); - } - } - - // Mark each alive Val'kyr with appropriate icon - for (size_t i = 0; i < sortedValkyrs.size() && i < 3; ++i) - { - ObjectGuid currentIcon = group->GetTargetIcon(ICON_INDICES[i]); - Unit* currentIconUnit = botAI->GetUnit(currentIcon); - - if (!currentIconUnit || currentIconUnit != sortedValkyrs[i]) - { - group->SetTargetIcon(ICON_INDICES[i], bot->GetGUID(), sortedValkyrs[i]->GetGUID()); - } - } -} - -void IccLichKingAddsAction::HandleValkyrAssignment(const std::vector& grabbingValkyrs) -{ - Group* group = bot->GetGroup(); - if (!group) - return; - - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (boss && boss->HealthBelowPct(40)) - return; - - // Double-check that all Val'kyrs in the list are actually alive and valid targets - std::vector validValkyrs; - for (Unit* valkyr : grabbingValkyrs) - { - if (valkyr && valkyr->IsAlive() && valkyr->HasAura(SPELL_HARVEST_SOUL_VALKYR)) - { - validValkyrs.push_back(valkyr); - } - } - - if (validValkyrs.empty()) - return; - - // Sort valid Val'kyrs for consistent assignment - std::sort(validValkyrs.begin(), validValkyrs.end(), [](Unit* a, Unit* b) { return a->GetGUID() < b->GetGUID(); }); - - // Get all non-main-tank members (DPS, healers, and off-tanks) - std::vector assistMembers; - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (member && !botAI->IsMainTank(member)) - assistMembers.push_back(member); - } - - if (assistMembers.empty()) - return; - - // Sort assist members by GUID for consistent assignment - std::sort(assistMembers.begin(), assistMembers.end(), - [](Player* a, Player* b) { return a->GetGUID() < b->GetGUID(); }); - - // Find our position among assist members - auto it = std::find(assistMembers.begin(), assistMembers.end(), bot); - if (it == assistMembers.end()) - return; // We're main tank, shouldn't handle Val'kyrs - - size_t myAssistIndex = std::distance(assistMembers.begin(), it); - size_t totalAssist = assistMembers.size(); - size_t aliveValkyrs = validValkyrs.size(); - - // Calculate balanced group sizes - std::vector groupSizes = CalculateBalancedGroupSizes(totalAssist, aliveValkyrs); - - // Determine which Val'kyr this bot should target - size_t assignedValkyrIndex = GetAssignedValkyrIndex(myAssistIndex, groupSizes); - - if (assignedValkyrIndex < validValkyrs.size()) - { - Unit* myValkyr = validValkyrs[assignedValkyrIndex]; - - // Set RTI context based on assignment - std::string rtiValue = GetRTIValueForValkyr(assignedValkyrIndex); - context->GetValue("rti")->Set(rtiValue); - - // Attack and apply CC - bot->SetTarget(myValkyr->GetGUID()); - bot->SetFacingToObject(myValkyr); - Difficulty diff = bot->GetRaidDifficulty(); - - if (sPlayerbotAIConfig.EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - //---------CHEAT--------- - if (!myValkyr->HasAura(SPELL_HAMMER_OF_JUSTICE)) - bot->AddAura(SPELL_HAMMER_OF_JUSTICE, myValkyr); - //---------CHEAT--------- - } - ApplyCCToValkyr(myValkyr); - } -} - -std::vector IccLichKingAddsAction::CalculateBalancedGroupSizes(size_t totalAssist, size_t numValkyrs) -{ - std::vector groupSizes(numValkyrs, 0); - - if (numValkyrs == 0) - return groupSizes; - - // Base size for each group - size_t baseSize = totalAssist / numValkyrs; - size_t remainder = totalAssist % numValkyrs; - - // Distribute assist members as evenly as possible - for (size_t i = 0; i < numValkyrs; ++i) - { - groupSizes[i] = baseSize; - if (i < remainder) - groupSizes[i]++; // Add extra member to first 'remainder' groups - } - - return groupSizes; -} - -size_t IccLichKingAddsAction::GetAssignedValkyrIndex(size_t assistIndex, const std::vector& groupSizes) -{ - size_t currentIndex = 0; - - for (size_t valkyrIndex = 0; valkyrIndex < groupSizes.size(); ++valkyrIndex) - { - if (assistIndex < currentIndex + groupSizes[valkyrIndex]) - return valkyrIndex; - - currentIndex += groupSizes[valkyrIndex]; - } - - // Fallback - should not happen with correct logic - return 0; -} - -std::string IccLichKingAddsAction::GetRTIValueForValkyr(size_t valkyrIndex) -{ - switch (valkyrIndex) - { - case 0: - return "skull"; - case 1: - return "cross"; - case 2: - return "star"; - default: - return "skull"; // Fallback - } -} - -void IccLichKingAddsAction::ApplyCCToValkyr(Unit* valkyr) -{ - switch (bot->getClass()) - { - case CLASS_MAGE: - if (!botAI->HasAura("Frost Nova", valkyr)) - botAI->CastSpell("Frost Nova", valkyr); - break; - case CLASS_DRUID: - if (!botAI->HasAura("Entangling Roots", valkyr)) - botAI->CastSpell("Entangling Roots", valkyr); - break; - case CLASS_PALADIN: - if (!botAI->HasAura("Hammer of Justice", valkyr)) - botAI->CastSpell("Hammer of Justice", valkyr); - break; - case CLASS_WARRIOR: - if (!botAI->HasAura("Hamstring", valkyr)) - botAI->CastSpell("Hamstring", valkyr); - break; - case CLASS_HUNTER: - if (!botAI->HasAura("Concussive Shot", valkyr)) - botAI->CastSpell("Concussive Shot", valkyr); - break; - case CLASS_ROGUE: - if (!botAI->HasAura("Kidney Shot", valkyr)) - botAI->CastSpell("Kidney Shot", valkyr); - break; - case CLASS_SHAMAN: - if (!botAI->HasAura("Frost Shock", valkyr)) - botAI->CastSpell("Frost Shock", valkyr); - break; - case CLASS_DEATH_KNIGHT: - if (!botAI->HasAura("Chains of Ice", valkyr)) - botAI->CastSpell("Chains of Ice", valkyr); - break; - case CLASS_PRIEST: - if (!botAI->HasAura("Psychic Scream", valkyr)) - botAI->CastSpell("Psychic Scream", valkyr); - break; - case CLASS_WARLOCK: - if (!botAI->HasAura("Fear", valkyr)) - botAI->CastSpell("Fear", valkyr); - break; - default: - break; - } -} - -bool IccLichKingAddsAction::IsValkyr(Unit* unit) -{ - return unit->GetEntry() == NPC_VALKYR_SHADOWGUARD1 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD2 || - unit->GetEntry() == NPC_VALKYR_SHADOWGUARD3 || unit->GetEntry() == NPC_VALKYR_SHADOWGUARD4; -} - -void IccLichKingAddsAction::HandleVileSpiritMechanics() -{ - const float radiusVile = 12.0f; - - GuidVector npcs3 = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto& npc : npcs3) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || (unit->GetEntry() != NPC_VILE_SPIRIT1 && unit->GetEntry() != NPC_VILE_SPIRIT2 && unit->GetEntry() != NPC_VILE_SPIRIT3 && - unit->GetEntry() != NPC_VILE_SPIRIT4)) - continue; - - // Only run away if the spirit is targeting us - if (unit->GetVictim() && unit->GetVictim()->GetGUID() == bot->GetGUID()) - { - float currentDistance = bot->GetDistance2d(unit); - - if (currentDistance < radiusVile) - { - botAI->Reset(); - MoveAway(unit, radiusVile - currentDistance); - } - } - } -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.h b/modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.h deleted file mode 100644 index 9ac2a48a0..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Action/RaidIccActions.h +++ /dev/null @@ -1,669 +0,0 @@ -#ifndef _PLAYERBOT_RAIDICCACTIONS_H -#define _PLAYERBOT_RAIDICCACTIONS_H - -#include "Action.h" -#include "MovementActions.h" -#include "PlayerbotAI.h" -#include "Playerbots.h" -#include "AttackAction.h" -#include "LastMovementValue.h" -#include "ObjectGuid.h" -#include "PlayerbotAIConfig.h" -#include "RaidIccStrategy.h" -#include "ScriptedCreature.h" -#include "SharedDefines.h" -#include "Trigger.h" -#include "CellImpl.h" -#include "GridNotifiers.h" -#include "GridNotifiersImpl.h" -#include "Vehicle.h" -#include "RaidIccTriggers.h" - -const Position ICC_LM_TANK_POSITION = Position(-391.0f, 2259.0f, 42.0f); -const Position ICC_DARK_RECKONING_SAFE_POSITION = Position(-523.33386f, 2211.2031f, 62.823116f); -const Position ICC_LDW_TANK_POSTION = Position(-570.1f, 2211.2456f, 49.476616f); //-590.0f -const Position ICC_ROTTING_FROST_GIANT_TANK_POSITION = Position(-328.5085f, 2225.5142f, 199.97298f); -const Position ICC_GUNSHIP_TELEPORT_ALLY = Position (-370.04645f, 1993.3536f, 466.65656f); -const Position ICC_GUNSHIP_TELEPORT_ALLY2 = Position (-392.66208f, 2064.893f, 466.5672f, 5.058196f); -const Position ICC_GUNSHIP_TELEPORT_HORDE = Position (-449.5343f, 2477.2024f, 470.17648f); -const Position ICC_GUNSHIP_TELEPORT_HORDE2 = Position (-429.81586f, 2400.6804f, 471.56537f); -const Position ICC_DBS_TANK_POSITION = Position(-494.26517f, 2211.549f, 541.11414f); -const Position ICC_FESTERGUT_TANK_POSITION = Position(4269.1772f, 3144.7673f, 360.38577f); -const Position ICC_FESTERGUT_RANGED_SPORE = Position(4261.143f, 3109.4146f, 360.38605f); -const Position ICC_FESTERGUT_MELEE_SPORE = Position(4269.1772f, 3144.7673f, 360.38577f); -const Position ICC_ROTFACE_TANK_POSITION = Position(4447.061f, 3150.9758f, 360.38568f); -const Position ICC_ROTFACE_BIG_OOZE_POSITION = Position(4432.687f, 3142.3035f, 360.38623f); -const Position ICC_ROTFACE_SAFE_POSITION = Position(4446.557f, 3065.6594f, 360.51843f); -const Position ICC_ROTFACE_CENTER_POSITION = Position(4446.0547f, 3144.8677f, 360.38593f); //actual center 4.74089 4445.6616f, 3137.1526f, 360.38608 -const Position ICC_PUTRICIDE_TANK_POSITION = Position(4373.227f, 3222.058f, 389.4029f); -const Position ICC_PUTRICIDE_GREEN_POSITION = Position(4423.4126f, 3194.2715f, 389.37683f); -const Position ICC_PUTRICIDE_BAD_POSITION = Position(4356.1724f, 3261.5232f, 389.3985f); -//const Position ICC_PUTRICIDE_GAS3_POSITION = Position(4367.753f, 3177.5894f, 389.39575f); -//const Position ICC_PUTRICIDE_GAS4_POSITION = Position(4321.8486f, 3206.464f, 389.3982f); -const Position ICC_BPC_OT_POSITION = Position(4649.2236f, 2796.0972f, 361.1815f); -const Position ICC_BPC_MT_POSITION = Position(4648.5674f, 2744.847f, 361.18222f); -const Position ICC_BQL_CENTER_POSITION = Position(4595.0f, 2769.0f, 400.0f); -const Position ICC_BQL_LWALL1_POSITION = Position(4624.685f, 2789.4895f, 400.13834f); -const Position ICC_BQL_LWALL2_POSITION = Position(4600.749f, 2805.7568f, 400.1374f); -const Position ICC_BQL_LWALL3_POSITION = Position(4572.857f, 2797.3872f, 400.1374f); -const Position ICC_BQL_RWALL1_POSITION = Position(4625.724f, 2748.9917f, 400.13693f); -const Position ICC_BQL_RWALL2_POSITION = Position(4608.3774f, 2735.7466f, 400.13693f); -const Position ICC_BQL_RWALL3_POSITION = Position(4576.813f, 2739.6067f, 400.13693f); -const Position ICC_BQL_LRWALL4_POSITION = Position(4539.345f, 2769.3853f, 403.7267f); -const Position ICC_BQL_TANK_POSITION = Position(4629.746f, 2769.6396f, 401.7479f); //old just in front of stairs 4616.102f, 2768.9167f, 400.13797f -const Position ICC_VDW_HEAL_POSITION = Position(4203.752f, 2483.4343f, 364.87274f); -const Position ICC_VDW_GROUP1_POSITION = Position(4203.585f, 2464.422f, 364.86887f); -const Position ICC_VDW_GROUP2_POSITION = Position(4203.5806f, 2505.2383f, 364.87677f); -const Position ICC_VDW_PORTALSTART_POSITION = Position(4202.637f, 2488.171f, 375.00256f); -const Position ICC_SINDRAGOSA_TANK_POSITION = Position(4408.016f, 2508.0647f, 203.37955f); -const Position ICC_SINDRAGOSA_FLYING_POSITION = Position(4525.6f, 2485.15f, 245.082f); -const Position ICC_SINDRAGOSA_RANGED_POSITION = Position(4441.572f, 2484.482f, 203.37836f); -const Position ICC_SINDRAGOSA_MELEE_POSITION = Position(4423.4546f, 2491.7175f, 203.37686f); -const Position ICC_SINDRAGOSA_BLISTERING_COLD_POSITION = Position(4473.6616f, 2484.8489f, 203.38258f); -const Position ICC_SINDRAGOSA_THOMB1_POSITION = Position(4433.6484f, 2469.4133f, 203.3806f); -const Position ICC_SINDRAGOSA_THOMB2_POSITION = Position(4434.143f, 2486.201f, 203.37473f); -const Position ICC_SINDRAGOSA_THOMB3_POSITION = Position(4436.1147f, 2501.464f, 203.38266f); -const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC1_POSITION = Position(4444.9707f, 2455.7322f, 203.38701f); -const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC2_POSITION = Position(4461.3945f, 2463.5513f, 203.38727f); -const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC3_POSITION = Position(4473.6616f, 2484.8489f, 203.38258f); -const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC4_POSITION = Position(4459.9336f, 2507.409f, 203.38606f); -const Position ICC_SINDRAGOSA_UNCHAINEDMAGIC5_POSITION = Position(4442.3096f, 2512.4688f, 203.38647f); -const Position ICC_SINDRAGOSA_CENTER_POSITION = Position(4408.0464f, 2484.478f, 203.37529f); -const Position ICC_SINDRAGOSA_THOMBMB2_POSITION = Position(4436.895f, 2498.1401f, 203.38133f); -const Position ICC_SINDRAGOSA_FBOMB_POSITION = Position(4449.3647f, 2486.4524f, 203.379f); -const Position ICC_SINDRAGOSA_FBOMB10_POSITION = Position(4449.3647f, 2486.4524f, 203.379f); -const Position ICC_SINDRAGOSA_LOS2_POSITION = Position(4441.8286f, 2501.946f, 203.38435f); -const Position ICC_LICH_KING_ADDS_POSITION = Position(476.7332f, -2095.3894f, 840.857f); // old 486.63647f, -2095.7915f, 840.857f -const Position ICC_LICH_KING_MELEE_POSITION = Position(503.5546f, -2106.8213f, 840.857f); -const Position ICC_LICH_KING_RANGED_POSITION = Position(501.3563f, -2085.1816f, 840.857f); -const Position ICC_LICH_KING_ASSISTHC_POSITION = Position(517.2145f, -2125.0674f, 840.857f); -const Position ICC_LK_FROST1_POSITION = Position(503.96548f, -2183.216f, 840.857f); -const Position ICC_LK_FROST2_POSITION = Position(563.07166f, -2125.7578f, 840.857f); -const Position ICC_LK_FROST3_POSITION = Position(503.40182f, -2067.3435f, 840.857f); -const Position ICC_LK_FROSTR1_POSITION = Position(481.168f, -2177.8723f, 840.857f); -const Position ICC_LK_FROSTR2_POSITION = Position(562.20807f, -2100.2393f, 840.857f); -const Position ICC_LK_FROSTR3_POSITION = Position(526.35297f, -2071.0317f, 840.857f); - -//Lord Marrogwar -class IccLmTankPositionAction : public AttackAction -{ -public: - IccLmTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc lm tank position") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool MoveTowardPosition(const Position& position, float incrementSize); -}; - -class IccSpikeAction : public AttackAction -{ -public: - IccSpikeAction(PlayerbotAI* botAI) : AttackAction(botAI, "icc spike") {} - bool Execute(Event event) override; - - bool HandleSpikeTargeting(Unit* boss); - bool MoveTowardPosition(const Position& position, float incrementSize); - void UpdateRaidTargetIcon(Unit* target); -}; - -//Lady Deathwhisper -class IccDarkReckoningAction : public MovementAction -{ -public: - IccDarkReckoningAction(PlayerbotAI* botAI, std::string const name = "icc dark reckoning") - : MovementAction(botAI, name) {} - bool Execute(Event event) override; -}; - -class IccRangedPositionLadyDeathwhisperAction : public AttackAction -{ -public: - IccRangedPositionLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc ranged position lady deathwhisper") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool MaintainRangedSpacing(); -}; - -class IccAddsLadyDeathwhisperAction : public AttackAction -{ -public: - IccAddsLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc adds lady deathwhisper") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool IsTargetedByShade(uint32 shadeEntry); - bool MoveTowardPosition(const Position& position, float incrementSize); - bool HandleAddTargeting(Unit* boss); - void UpdateRaidTargetIcon(Unit* target); - -}; - -class IccShadeLadyDeathwhisperAction : public MovementAction -{ -public: - IccShadeLadyDeathwhisperAction(PlayerbotAI* botAI, std::string const name = "icc shade lady deathwhisper") - : MovementAction(botAI, name) {} - bool Execute(Event event) override; -}; - -//Gunship Battle -class IccRottingFrostGiantTankPositionAction : public AttackAction -{ -public: - IccRottingFrostGiantTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotting frost giant tank position") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; -}; - -class IccCannonFireAction : public Action -{ -public: - IccCannonFireAction(PlayerbotAI* botAI, std::string const name = "icc cannon fire") - : Action(botAI, name) {} - bool Execute(Event event) override; - - Unit* FindValidCannonTarget(); - bool TryCastCannonSpell(uint32 spellId, Unit* target, Unit* vehicleBase); -}; - -class IccGunshipEnterCannonAction : public MovementAction -{ -public: - IccGunshipEnterCannonAction(PlayerbotAI* botAI, std::string const name = "icc gunship enter cannon") - : MovementAction(botAI, name) {} - bool Execute(Event event) override; - - bool EnterVehicle(Unit* vehicleBase, bool moveIfFar); - Unit* FindBestAvailableCannon(); - bool IsValidCannon(Unit* vehicle, const uint32 validEntries[]); -}; - -class IccGunshipTeleportAllyAction : public AttackAction -{ -public: - IccGunshipTeleportAllyAction(PlayerbotAI* botAI, std::string const name = "icc gunship teleport ally") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool TeleportTo(const Position& position); - void CleanupSkullIcon(uint8_t SKULL_ICON_INDEX); - void UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX); -}; - -class IccGunshipTeleportHordeAction : public AttackAction -{ -public: - IccGunshipTeleportHordeAction(PlayerbotAI* botAI, std::string const name = "icc gunship teleport horde") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool TeleportTo(const Position& position); - void CleanupSkullIcon(uint8_t SKULL_ICON_INDEX); - void UpdateBossSkullIcon(Unit* boss, uint8_t SKULL_ICON_INDEX); -}; - -//DBS -class IccDbsTankPositionAction : public AttackAction -{ -public: - IccDbsTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc dbs tank position") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool CrowdControlBloodBeasts(); - bool EvadeBloodBeasts(); - bool PositionInRangedFormation(); -}; - -class IccAddsDbsAction : public AttackAction -{ -public: - IccAddsDbsAction(PlayerbotAI* botAI, std::string const name = "icc adds dbs") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - Unit* FindPriorityTarget(Unit* boss); - void UpdateSkullMarker(Unit* priorityTarget); -}; - -//FESTERGUT -class IccFestergutGroupPositionAction : public AttackAction -{ -public: - IccFestergutGroupPositionAction(PlayerbotAI* botAI, std::string const name = "icc festergut group position") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool HasSporesInGroup(); - bool PositionNonTankMembers(); - int CalculatePositionIndex(Group* group); -}; - -class IccFestergutSporeAction : public AttackAction -{ -public: - IccFestergutSporeAction(PlayerbotAI* botAI, std::string const name = "icc festergut spore") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - Position CalculateSpreadPosition(); - struct SporeInfo - { - std::vector sporedPlayers; - ObjectGuid lowestGuid; - bool hasLowestGuid = false; - }; - SporeInfo FindSporedPlayers(); - Position DetermineTargetPosition(bool hasSpore, const SporeInfo& sporeInfo, const Position& spreadRangedPos); - bool CheckMainTankSpore(); -}; - -//Rotface -class IccRotfaceTankPositionAction : public AttackAction -{ -public: - IccRotfaceTankPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotface tank position") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - void MarkBossWithSkull(Unit* boss); - bool PositionMainTankAndMelee(Unit *boss); - bool HandleAssistTankPositioning(Unit* boss); - bool HandleBigOozePositioning(Unit* boss); -}; - -class IccRotfaceGroupPositionAction : public AttackAction -{ -public: - IccRotfaceGroupPositionAction(PlayerbotAI* botAI, std::string const name = "icc rotface group position") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - //bool MoveAwayFromBigOoze(Unit* bigOoze); - bool HandlePuddleAvoidance(Unit* boss); - bool MoveAwayFromPuddle(Unit* boss, Unit* puddle, float puddleDistance); - bool HandleOozeTargeting(); - bool HandleOozeMemberPositioning(); - bool PositionRangedAndHealers(Unit* boss,Unit* smallOoze); - bool FindAndMoveFromClosestMember(Unit* boss, Unit* smallOoze); -}; - -class IccRotfaceMoveAwayFromExplosionAction : public MovementAction -{ -public: - IccRotfaceMoveAwayFromExplosionAction(PlayerbotAI* botAI, std::string const name = "icc rotface move away from explosion") - : MovementAction(botAI, name) {} - bool Execute(Event event) override; - - bool MoveToRandomSafeLocation(); - -}; - -//PP -class IccPutricideGrowingOozePuddleAction : public AttackAction -{ -public: - IccPutricideGrowingOozePuddleAction(PlayerbotAI* botAI, std::string const name = "icc putricide growing ooze puddle") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - Unit* FindClosestThreateningPuddle(); - Position CalculateSafeMovePosition(Unit* closestPuddle); - bool IsPositionTooCloseToOtherPuddles(float x, float y, Unit* ignorePuddle); -}; - -class IccPutricideVolatileOozeAction : public AttackAction -{ -public: - IccPutricideVolatileOozeAction(PlayerbotAI* botAI, std::string const name = "icc putricide volatile ooze") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - void MarkOozeWithSkull(Unit* ooze); - Unit* FindAuraTarget(); -}; - -class IccPutricideGasCloudAction : public AttackAction -{ -public: - IccPutricideGasCloudAction(PlayerbotAI* botAI, std::string const name = "icc putricide gas cloud") - : AttackAction(botAI, name) {} - bool Execute(Event event) override; - - bool HandleGaseousBloatMovement(Unit* gasCloud); - Position CalculateEmergencyPosition(const Position& botPos, float dx, float dy); - bool FindSafeMovementPosition(const Position& botPos, const Position& cloudPos, float dx, float dy, int numAngles, - Position& resultPos); - bool HandleGroupAuraSituation(Unit* gasCloud); - bool GroupHasGaseousBloat(Group* group); -}; - -class IccPutricideAvoidMalleableGooAction : public MovementAction -{ -public: - IccPutricideAvoidMalleableGooAction(PlayerbotAI* botAI, std::string const name = "icc putricide avoid malleable goo") - : MovementAction(botAI, name) {} - bool Execute(Event event) override; - - bool HandleTankPositioning(Unit* boss); - bool HandleUnboundPlague(Unit* boss); - bool HandleBossPositioning(Unit* boss); - Position CalculateBossPosition(Unit* boss, float distance); - bool HasObstacleBetween(const Position& from, const Position& to); - bool IsOnPath(const Position& from, const Position& to, const Position& point, float threshold); - Position CalculateArcPoint(const Position& current, const Position& target, const Position& center); - Position CalculateIncrementalMove(const Position& current, const Position& target, float maxDistance); -}; - -//BPC -class IccBpcKelesethTankAction : public AttackAction -{ -public: - IccBpcKelesethTankAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc bpc keleseth tank") {} - bool Execute(Event event) override; -}; - -class IccBpcMainTankAction : public AttackAction -{ -public: - IccBpcMainTankAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc bpc main tank") {} - bool Execute(Event event) override; - - void MarkEmpoweredPrince(); -}; - -class IccBpcEmpoweredVortexAction : public MovementAction -{ -public: - IccBpcEmpoweredVortexAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc bpc empowered vortex") {} - bool Execute(Event event) override; - - bool MaintainRangedSpacing(); - bool HandleEmpoweredVortexSpread(); -}; - -class IccBpcKineticBombAction : public AttackAction -{ -public: - IccBpcKineticBombAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc bpc kinetic bomb") {} - bool Execute(Event event) override; - - Unit* FindOptimalKineticBomb(); - bool IsBombAlreadyHandled(Unit* bomb, Group* group); -}; - -class IccBpcBallOfFlameAction : public MovementAction -{ -public: - IccBpcBallOfFlameAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc bpc ball of flame") {} - bool Execute(Event event) override; -}; - -//Blood Queen Lana'thel -class IccBqlGroupPositionAction : public AttackAction -{ -public: - IccBqlGroupPositionAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc group tank position") {} - bool Execute(Event event) override; - - bool HandleTankPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura); - bool HandleShadowsMovement(); - Position AdjustControlPoint(const Position& wall, const Position& center, float factor); - Position CalculateBezierPoint(float t, const Position path[4]); - bool HandleGroupPosition(Unit* boss, Aura* frenzyAura, Aura* shadowAura); - -private: - // Evaluate curves - struct CurveInfo - { - Position moveTarget; - int curveIdx = 0; - bool foundSafe = false; - float minDist = 0.0f; - float score = 0.0f; - Position closestPoint; - float t_closest = 0.0f; - }; -}; - -class IccBqlPactOfDarkfallenAction : public MovementAction -{ -public: - IccBqlPactOfDarkfallenAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc bql pact of darkfallen") {} - bool Execute(Event event) override; - - void CalculateCenterPosition(Position& targetPos, const std::vector& playersWithAura); - bool MoveToTargetPosition(const Position& targetPos, int auraCount); -}; - -class IccBqlVampiricBiteAction : public AttackAction -{ -public: - IccBqlVampiricBiteAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc bql vampiric bite") {} - bool Execute(Event event) override; - - Player* FindBestBiteTarget(Group* group); - bool IsInvalidTarget(Player* player); - bool MoveTowardsTarget(Player* target); - bool CastVampiricBite(Player* target); -}; - -// Sister Svalna -class IccValkyreSpearAction : public AttackAction -{ -public: - IccValkyreSpearAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc valkyre spear") {} - bool Execute(Event event) override; -}; - -class IccSisterSvalnaAction : public AttackAction -{ -public: - IccSisterSvalnaAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc sister svalna") {} - bool Execute(Event event) override; -}; - -// Valithria Dreamwalker - -class IccValithriaGroupAction : public AttackAction -{ -public: - IccValithriaGroupAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc valithria group") {} - bool Execute(Event event) override; - - bool MoveTowardsPosition(const Position& pos, float increment); - bool Handle25ManGroupLogic(); - bool HandleMarkingLogic(bool inGroup1, bool inGroup2, const Position& group1Pos, const Position& group2Pos); - bool Handle10ManGroupLogic(); -}; - -class IccValithriaPortalAction : public MovementAction -{ -public: - IccValithriaPortalAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc valithria portal") {} - bool Execute(Event event) override; -}; - -class IccValithriaHealAction : public AttackAction -{ -public: - IccValithriaHealAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc valithria heal") {} - bool Execute(Event event) override; -}; - -class IccValithriaDreamCloudAction : public MovementAction -{ -public: - IccValithriaDreamCloudAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc valithria dream cloud") {} - bool Execute(Event event) override; -}; - -//Sindragosa -class IccSindragosaGroupPositionAction : public AttackAction -{ -public: - IccSindragosaGroupPositionAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc sindragosa group position") {} - bool Execute(Event event) override; - - bool HandleTankPositioning(Unit* boss); - bool HandleNonTankPositioning(); - bool MoveIncrementallyToPosition(const Position& targetPos, float maxStep); -}; - -class IccSindragosaFrostBeaconAction : public MovementAction -{ -public: - IccSindragosaFrostBeaconAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc sindragosa frost beacon") {} - bool Execute(Event event) override; - - void HandleSupportActions(); - bool HandleBeaconedPlayer(const Unit* boss); - bool HandleNonBeaconedPlayer(const Unit* boss); - bool MoveToPositionIfNeeded(const Position& position, float tolerance); - bool MoveToPosition(const Position& position); - bool IsBossFlying(const Unit* boss); - - private: - static constexpr uint32 FROST_BEACON_AURA_ID = SPELL_FROST_BEACON; - static constexpr uint32 HAND_OF_FREEDOM_SPELL_ID = 1044; - static constexpr float POSITION_TOLERANCE = 1.0f; - static constexpr float TOMB_POSITION_TOLERANCE = 0.5f; - static constexpr float MIN_SAFE_DISTANCE = 13.0f; - static constexpr float MOVE_TOLERANCE = 2.0f; -}; - -class IccSindragosaBlisteringColdAction : public MovementAction -{ -public: - IccSindragosaBlisteringColdAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc sindragosa blistering cold") {} - bool Execute(Event event) override; -}; - -class IccSindragosaUnchainedMagicAction : public AttackAction -{ -public: - IccSindragosaUnchainedMagicAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc sindragosa unchained magic") {} - bool Execute(Event event) override; -}; - -class IccSindragosaChilledToTheBoneAction : public AttackAction -{ -public: - IccSindragosaChilledToTheBoneAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc sindragosa chilled to the bone") {} - bool Execute(Event event) override; -}; - -class IccSindragosaMysticBuffetAction : public MovementAction -{ -public: - IccSindragosaMysticBuffetAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc sindragosa mystic buffet") {} - bool Execute(Event event) override; -}; - -class IccSindragosaFrostBombAction : public MovementAction -{ -public: - IccSindragosaFrostBombAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc sindragosa frost bomb") {} - bool Execute(Event event) override; -}; - -class IccSindragosaTankSwapPositionAction : public AttackAction -{ - public: - IccSindragosaTankSwapPositionAction(PlayerbotAI* botAI) - : AttackAction(botAI, "sindragosa tank swap position") {} - bool Execute(Event event) override; -}; - -//LK -class IccLichKingShadowTrapAction : public MovementAction -{ - public: - IccLichKingShadowTrapAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc lich king shadow trap") {} - bool Execute(Event event) override; -}; - -class IccLichKingNecroticPlagueAction : public MovementAction -{ - public: - IccLichKingNecroticPlagueAction(PlayerbotAI* botAI) - : MovementAction(botAI, "icc lich king necrotic plague") {} - bool Execute(Event event) override; -}; - -class IccLichKingWinterAction : public AttackAction -{ - public: - IccLichKingWinterAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc lich king winter") {} - bool Execute(Event event) override; - - void HandlePositionCorrection(); - bool IsValidCollectibleAdd(Unit* unit); - bool IsPositionSafeFromDefile(float x, float y, float z, float minSafeDistance); - void HandleTankPositioning(); - void HandleMeleePositioning(); - void HandleRangedPositioning(); - void HandleMainTankAddManagement(Unit* boss, const Position* tankPos); - void HandleAssistTankAddManagement(Unit* boss, const Position* tankPos); - - private: - const Position* GetMainTankPosition(); - const Position* GetMainTankRangedPosition(); - bool TryMoveToPosition(float targetX, float targetY, float targetZ, bool isForced = true); -}; - -class IccLichKingAddsAction : public AttackAction -{ - public: - IccLichKingAddsAction(PlayerbotAI* botAI) - : AttackAction(botAI, "icc lich king adds") {} - bool Execute(Event event) override; - - void HandleTeleportationFixes(Difficulty diff, Unit* terenasMenethilHC); - bool HandleSpiritBombAvoidance(Difficulty diff, Unit* terenasMenethilHC); - void HandleHeroicNonTankPositioning(Difficulty diff, Unit* terenasMenethilHC); - void HandleSpiritMarkingAndTargeting(Difficulty diff, Unit* terenasMenethilHC); - bool HandleQuakeMechanics(Unit* boss); - void HandleShamblingHorrors(Unit* boss, bool hasPlague); - bool HandleAssistTankAddManagement(Unit* boss, Difficulty diff); - void HandleMeleePositioning(Unit* boss, bool hasPlague, Difficulty diff); - void HandleMainTankTargeting(Unit* boss, Difficulty diff); - void HandleNonTankHeroicPositioning(Unit* boss, Difficulty diff, bool hasPlague); - void HandleRangedPositioning(Unit* boss, bool hasPlague, Difficulty diff); - void HandleDefileMechanics(Unit* boss, Difficulty diff); - void HandleValkyrMechanics(Difficulty diff); - std::vector CalculateBalancedGroupSizes(size_t totalAssist, size_t numValkyrs); - size_t GetAssignedValkyrIndex(size_t assistIndex, const std::vector& groupSizes); - std::string GetRTIValueForValkyr(size_t valkyrIndex); - void HandleValkyrMarking(const std::vector& grabbingValkyrs, Difficulty diff); - void HandleValkyrAssignment(const std::vector& grabbingValkyrs); - void ApplyCCToValkyr(Unit* valkyr); - bool IsValkyr(Unit* unit); - void HandleVileSpiritMechanics(); -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp deleted file mode 100644 index c6a6fdee5..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Multiplier/RaidIccMultipliers.cpp +++ /dev/null @@ -1,874 +0,0 @@ -#include "RaidIccMultipliers.h" - -#include "ChooseTargetActions.h" -#include "DKActions.h" -#include "DruidActions.h" -#include "DruidBearActions.h" -#include "FollowActions.h" -#include "GenericActions.h" -#include "GenericSpellActions.h" -#include "HunterActions.h" -#include "MageActions.h" -#include "MovementActions.h" -#include "PaladinActions.h" -#include "PriestActions.h" -#include "RaidIccActions.h" -#include "ReachTargetActions.h" -#include "RogueActions.h" -#include "ShamanActions.h" -#include "UseMeetingStoneAction.h" -#include "WarriorActions.h" -#include "PlayerbotAI.h" -#include "RaidIccTriggers.h" - -// LK global variables -namespace -{ -uint32 g_lastPlagueTime = 0; -bool g_plagueAllowedToCure = false; -std::map g_plagueTimes; -std::map g_allowCure; -std::mutex g_plagueMutex; // Lock before accessing shared variables -} - -// Lady Deathwhisper -float IccLadyDeathwhisperMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); - if (!boss) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - static constexpr uint32 VENGEFUL_SHADE_ID = NPC_SHADE; - - // Get the nearest hostile NPCs - const GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - // Allow the IccShadeLadyDeathwhisperAction to run - if (dynamic_cast(action)) - return 1.0f; - - for (auto const& npcGuid : npcs) - { - Unit* shade = botAI->GetUnit(npcGuid); - - if (!shade || shade->GetEntry() != VENGEFUL_SHADE_ID) - continue; - - if (!shade->GetVictim() || shade->GetVictim()->GetGUID() != bot->GetGUID()) - continue; - - return 0.0f; // Cancel all other actions when we need to handle Vengeful Shade - } - - return 1.0f; -} - -// dbs -float IccAddsDbsMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); - if (!boss) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - - if (botAI->IsRanged(bot)) - if (dynamic_cast(action)) - return 0.0f; - - if (botAI->IsMainTank(bot)) - { - Aura* aura = botAI->GetAura("rune of blood", bot); - if (aura) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - } - - return 1.0f; -} - -// dogs -float IccDogsMultiplier::GetValue(Action* action) -{ - bool bossPresent = false; - if (AI_VALUE2(Unit*, "find target", "stinky") || AI_VALUE2(Unit*, "find target", "precious")) - bossPresent = true; - - if (!bossPresent) - return 1.0f; - - if (botAI->IsMainTank(bot)) - { - Aura* aura = botAI->GetAura("mortal wound", bot, false, true); - if (aura && aura->GetStackAmount() >= 8) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - } - return 1.0f; -} - -// Festergut -float IccFestergutMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (botAI->IsMainTank(bot)) - { - Aura* aura = botAI->GetAura("gastric bloat", bot, false, true); - if (aura && aura->GetStackAmount() >= 6) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - } - - if (dynamic_cast(action)) - return 1.0f; - - if (bot->HasAura(SPELL_GAS_SPORE)) - return 0.0f; - - return 1.0f; -} - -// Rotface -float IccRotfaceMultiplier::GetValue(Action* action) -{ - Unit* boss1 = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss1) - return 1.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action) && !(bot->getClass() == CLASS_HUNTER)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (botAI->IsAssistTank(bot) && (dynamic_cast(action) || dynamic_cast(action))) - return 0.0f; - - Unit* boss = AI_VALUE2(Unit*, "find target", "big ooze"); - if (!boss) - return 1.0f; - - static std::map lastExplosionTimes; - static std::map hasMoved; - - ObjectGuid botGuid = bot->GetGUID(); - - // When cast starts, record the time - if (boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION)) - { - if (lastExplosionTimes[botGuid] == 0) // Only set if not already set - { - lastExplosionTimes[botGuid] = time(nullptr); - hasMoved[botGuid] = false; - } - } - - // If explosion cast is no longer active, reset the timers - if (!boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION)) - { - if (lastExplosionTimes[botGuid] > 0 && time(nullptr) - lastExplosionTimes[botGuid] >= 16) - { - lastExplosionTimes[botGuid] = 0; - hasMoved[botGuid] = false; - return 1.0f; // Allow normal actions to resume - } - } - - // If 9 seconds have passed since cast start and we haven't moved yet - if (lastExplosionTimes[botGuid] > 0 && !hasMoved[botGuid] && time(nullptr) - lastExplosionTimes[botGuid] >= 9) - { - if (dynamic_cast(action) - && !dynamic_cast(action)) - { - return 0.0f; // Block other movement actions - } - hasMoved[botGuid] = true; // Mark that we've initiated movement - } - - // Continue blocking other movements for 7 seconds after moving - if (hasMoved[botGuid] && time(nullptr) - lastExplosionTimes[botGuid] < 16 // 9 seconds wait + 7 seconds stay - && dynamic_cast(action) - && !dynamic_cast(action)) - return 0.0f; - - return 1.0f; -} - -// pp -float IccAddsPutricideMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); - if (!boss) - return 1.0f; - - bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); - bool hasUnboundPlague = botAI->HasAura("Unbound Plague", bot); - - if (!(bot->getClass() == CLASS_HUNTER) && dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (botAI->IsMainTank(bot)) - { - Aura* aura = botAI->GetAura("mutated plague", bot, false, true); - if (aura && aura->GetStackAmount() >= 4) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - } - - if (hasGaseousBloat) - { - if (dynamic_cast(action)) - return 1.0f; - - if (dynamic_cast(action)) - return 1.0f; - - if (botAI->IsHeal(bot)) - return 1.0f; - else - return 0.0f; // Cancel all other actions when we need to handle Gaseous Bloat - } - - if (hasUnboundPlague && boss && !boss->HealthBelowPct(35)) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; // Cancel all other actions when we need to handle Unbound Plague - } - - if (dynamic_cast(action)) - { - if (dynamic_cast(action)) - return 0.0f; - if (dynamic_cast(action) && !botAI->IsMainTank(bot)) - return 0.0f; - //if (dynamic_cast(action) && !hasGaseousBloat) - //return 0.0f; - } - - return 1.0f; -} - -// bpc -float IccBpcAssistMultiplier::GetValue(Action* action) -{ - Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - if (!keleseth) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); - if (aura) - { - if (aura->GetStackAmount() > 18 && botAI->IsTank(bot)) - { - if (dynamic_cast(action)) - return 0.0f; - } - - if (aura->GetStackAmount() > 12 && !botAI->IsTank(bot)) - { - if (dynamic_cast(action)) - return 0.0f; - } - } - - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - if (!valanar) - return 1.0f; - - if (valanar && valanar->HasUnitState(UNIT_STATE_CASTING) && - (valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX1) || - valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX2) || - valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX3) || - valanar->FindCurrentSpellBySpellId(SPELL_EMPOWERED_SHOCK_VORTEX4))) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 1.0f; - else - return 0.0f; // Cancel all other actions when we need to handle Empowered Vortex - } - - Unit* flame1 = bot->FindNearestCreature(NPC_BALL_OF_FLAME, 100.0f); - Unit* flame2 = bot->FindNearestCreature(NPC_BALL_OF_INFERNO_FLAME, 100.0f); - bool ballOfFlame = flame1 && flame1->GetVictim() == bot; - bool infernoFlame = flame2 && flame2->GetVictim() == bot; - - if (flame2) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 1.0f; - } - - if (ballOfFlame || infernoFlame) - { - // If bot is tank, do nothing special - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; // Cancel all other actions when we need to handle Ball of Flame - } - - static const std::array bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, - NPC_KINETIC_BOMB4}; - const GuidVector bombs = AI_VALUE(GuidVector, "possible targets no los"); - - bool bombFound = false; - - for (const auto entry : bombEntries) - { - for (auto const& guid : bombs) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->GetEntry() == entry) - { - // Check if bomb is within valid Z-axis range - if (unit->GetPositionZ() - bot->GetPositionZ() < 25.0f) - { - bombFound = true; - break; - } - } - } - } - if (bombFound) - break; - } - - if (bombFound && !(aura && aura->GetStackAmount() > 12) && !botAI->IsTank(bot)) - { - // If kinetic bomb action is active, disable these actions - if (dynamic_cast(action)) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } - - // For assist tank during BPC fight - if (botAI->IsAssistTank(bot) && !(aura && aura->GetStackAmount() > 18)) - { - // Allow BPC-specific actions - if (dynamic_cast(action)) - return 1.0f; - - // Disable normal assist behavior - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - - } - - return 1.0f; -} - -//BQL -float IccBqlMultiplier::GetValue(Action* action) -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); - if (!boss) - return 1.0f; - - Aura* aura2 = botAI->GetAura("Swarming Shadows", bot); - Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); - - if (botAI->IsRanged(bot)) - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - // If bot has Pact of Darkfallen aura, return 0 for all other actions - if (bot->HasAura(SPELL_PACT_OF_THE_DARKFALLEN)) - { - if (dynamic_cast(action)) - return 1.0f; // Allow Pact of Darkfallen action - else - return 0.0f; // Cancel all other actions when we need to handle Pact of Darkfallen - } - - if (botAI->IsMelee(bot) && ((boss->GetPositionZ() - ICC_BQL_CENTER_POSITION.GetPositionZ()) > 5.0f) && !aura) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - - // If bot has frenzied bloodthirst, allow highest priority for bite action - if (aura) // If bot has frenzied bloodthirst - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - - if (aura2 && !aura) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; // Cancel all other actions when we need to handle Swarming Shadows - } - - if ((boss->GetExactDist2d(ICC_BQL_TANK_POSITION.GetPositionX(), ICC_BQL_TANK_POSITION.GetPositionY()) > 10.0f) && - botAI->IsRanged(bot) && !((boss->GetPositionZ() - bot->GetPositionZ()) > 5.0f)) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} - -//VDW -float IccValithriaDreamCloudMultiplier::GetValue(Action* action) -{ - Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); - - Aura* twistedNightmares = botAI->GetAura("Twisted Nightmares", bot); - Aura* emeraldVigor = botAI->GetAura("Emerald Vigor", bot); - - if (!boss && !bot->HasAura(SPELL_DREAM_STATE)) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - if (botAI->IsTank(bot)) - { - if (dynamic_cast(action)) - return 0.0f; - } - - if (botAI->IsHeal(bot) && (twistedNightmares || emeraldVigor)) - if (dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - if (bot->HasAura(SPELL_DREAM_STATE) && !bot->HealthBelowPct(50)) - { - if (dynamic_cast(action)) - return 1.0f; // Allow Dream Cloud action - else - return 0.0f; // Cancel all other actions when we need to handle Dream Cloud - } - - return 1.0f; - -} - -//SINDRAGOSA - -float IccSindragosaMultiplier::GetValue(Action* action) -{ - Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); - if (!boss) - return 1.0f; - Aura* aura = botAI->GetAura("Unchained Magic", bot, false, true); - - Difficulty diff = bot->GetRaidDifficulty(); - - if (boss->HealthBelowPct(95)) - { - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - } - - if (aura && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC) && - !dynamic_cast(action)) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - - // Check if boss is casting blistering cold (using both normal and heroic spell IDs) - if (boss->HasUnitState(UNIT_STATE_CASTING) && - (boss->FindCurrentSpellBySpellId(70123) || boss->FindCurrentSpellBySpellId(71047) || - boss->FindCurrentSpellBySpellId(71048) || boss->FindCurrentSpellBySpellId(71049))) - { - // If this is the blistering cold action, give it highest priority - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 1.0f; - - // Disable all other actions while blistering cold is casting - return 0.0f; - } - - // Highest priority if we have beacon - if (bot->HasAura(SPELL_FROST_BEACON)) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - - Group* group = bot->GetGroup(); - // Check if anyone in group has Frost Beacon (SPELL_FROST_BEACON) - bool anyoneHasFrostBeacon = false; - - if (group) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->HasAura(SPELL_FROST_BEACON)) - { - anyoneHasFrostBeacon = true; - break; - } - } - } - - if (anyoneHasFrostBeacon && boss && - boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), - ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && - !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - - if (anyoneHasFrostBeacon && !botAI->IsMainTank(bot)) - { - if (dynamic_cast(action)) - return 0.0f; - } - - if (botAI->IsMainTank(bot)) - { - Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); - if (aura && aura->GetStackAmount() >= 6) - { - if (dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - } - - if (!botAI->IsTank(bot) && boss && boss->HealthBelowPct(35)) - { - if (dynamic_cast(action)) - return 0.0f; - } - - if (boss && botAI->IsTank(bot)) - { - if (boss->HealthBelowPct(35)) - { - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - } - - if (boss && boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 30.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) - { - if (dynamic_cast(action)) - return 1.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} - -float IccLichKingAddsMultiplier::GetValue(Action* action) -{ - Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); - - if (!terenasMenethilHC) - if (dynamic_cast(action)) - return 0.0f; - - if (terenasMenethilHC) - { - Unit* mainTank = AI_VALUE(Unit*, "main tank"); - - if (!botAI->IsMainTank(bot) && mainTank && bot->GetExactDist2d(mainTank->GetPositionX(), mainTank->GetPositionY()) < 2.0f) - { - if (dynamic_cast(action)) - return 0.0f; - } - - if (botAI->IsMelee(bot) || (bot->getClass() == CLASS_WARLOCK)) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 1.0f; - else - return 0.0f; - } - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - } - - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return 1.0f; - - // Handle cure actions - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - { - Group* group = bot->GetGroup(); - if (!group) - return 1.0f; - - // Check if any bot in the group has plague - bool anyBotHasPlague = false; - ObjectGuid plaguedPlayerGuid; // Track who has plague - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - if (Player* member = ref->GetSource()) - { - if (botAI->HasAura("Necrotic Plague", member)) - { - anyBotHasPlague = true; - plaguedPlayerGuid = member->GetGUID(); // Changed from GetObjectGuid() - break; - } - } - } - - uint32 currentTime = getMSTime(); - - // Reset state if no one has plague - if (!anyBotHasPlague) - { - std::lock_guard lock(g_plagueMutex); // Properly initialized - g_plagueTimes.clear(); - g_allowCure.clear(); - return 1.0f; - } - - { // New scope for lock_guard - std::lock_guard lock(g_plagueMutex); // Properly initialized - - // Start timer if this is a new plague - if (g_plagueTimes.find(plaguedPlayerGuid) == g_plagueTimes.end()) - { - g_plagueTimes[plaguedPlayerGuid] = currentTime; - g_allowCure[plaguedPlayerGuid] = false; - return 0.0f; - } - - // Once we allow cure, keep allowing it until plague is gone - if (g_allowCure[plaguedPlayerGuid]) - { - return 1.0f; - } - - // Check if enough time has passed (2,5 seconds) - if (currentTime - g_plagueTimes[plaguedPlayerGuid] >= 2500) - { - g_allowCure[plaguedPlayerGuid] = true; - return 1.0f; - } - } // lock_guard is automatically released here - - return 0.0f; - } - - if (dynamic_cast(action) && (bot->getClass() != CLASS_HUNTER)) - return 0.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - - if (boss && !boss->HealthBelowPct(71)) - { - if (!botAI->IsTank(bot)) - if (dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) - return 0.0f; - } - - Unit* currentTarget = AI_VALUE(Unit*, "current target"); - - bool hasWinterAura = false; - if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || - boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4))) - hasWinterAura = true; - - bool hasWinter2Aura = false; - if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || - boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8))) - hasWinter2Aura = true; - - bool isCasting = false; - if (boss && boss->HasUnitState(UNIT_STATE_CASTING)) - isCasting = true; - - bool isWinter = false; - if (boss && (boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER1) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER2) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER5) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER6) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER3) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER4) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER7) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER8))) - isWinter = true; - - if (hasWinterAura || hasWinter2Aura || (isCasting && isWinter)) - { - if (dynamic_cast(action) || dynamic_cast(action)) - return 1.0f; - - if (botAI->IsAssistTank(bot) && dynamic_cast(action)) - return 0.0f; - - if (dynamic_cast(action)) - return 0.0f; - - if (currentTarget && boss && bot->GetDistance2d(boss->GetPositionX(), boss->GetPositionY()) > 50.0f && currentTarget == boss) - { - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } - - if (currentTarget && (currentTarget->GetEntry() == NPC_ICE_SPHERE1 || currentTarget->GetEntry() == NPC_ICE_SPHERE2 || - currentTarget->GetEntry() == NPC_ICE_SPHERE3 || currentTarget->GetEntry() == NPC_ICE_SPHERE4)) - { - if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } - - } - - if (botAI->IsRanged(bot) && !botAI->GetAura("Harvest Soul", bot, false, false)) - { - // Check for defile presence - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - bool defilePresent = false; - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->IsAlive() && unit->GetEntry() == DEFILE_NPC_ID) // Defile entry - { - defilePresent = true; - break; - } - } - - // Only disable movement if defile is present - if (defilePresent && ( - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action))) - { - return 0.0f; - } - } - - if (botAI->IsAssistTank(bot) && boss && !boss->HealthBelowPct(71) && currentTarget == boss) - { - if (dynamic_cast(action)) - return 0.0f; - } - - return 1.0f; -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp deleted file mode 100644 index 75989ba1c..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Trigger/RaidIccTriggers.cpp +++ /dev/null @@ -1,1189 +0,0 @@ -#include "RaidIccTriggers.h" -#include "RaidIccActions.h" -#include "NearestNpcsValue.h" -#include "PlayerbotAIConfig.h" -#include "ObjectAccessor.h" -#include "GenericTriggers.h" -#include "Playerbots.h" -#include "Trigger.h" -#include "GridNotifiers.h" -#include "Vehicle.h" - -//Lord Marrogwar -bool IccLmTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lord marrowgar"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -//Lady Deathwhisper -bool IccDarkReckoningTrigger::IsActive() -{ - if (bot->HasAura(SPELL_DARK_RECKONING)) - return true; - - return false; -} - -bool IccLadyDeathwhisperTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "lady deathwhisper"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -//Gunship Battle -bool IccRottingFrostGiantTankPositionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "rotting frost giant"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccInCannonTrigger::IsActive() -{ - Unit* vehicleBase = bot->GetVehicleBase(); - Vehicle* vehicle = bot->GetVehicle(); - if (!vehicleBase || !vehicle) - return false; - - uint32 entry = vehicleBase->GetEntry(); - return entry == NPC_CANNONA || entry == NPC_CANNONH; -} - -bool IccGunshipCannonNearTrigger::IsActive() -{ - if (bot->GetVehicle()) - return false; - - Unit* mount1 = bot->FindNearestCreature(NPC_CANNONA, 100.0f); - - Unit* mount2 = bot->FindNearestCreature(NPC_CANNONH, 100.0f); - - if (!mount1 && !mount2) - return false; - - if (!botAI->IsDps(bot)) - return false; - // Player* master = botAI->GetMaster(); - // if (!master) - // return false; - - // if (!master->GetVehicle()) - // return false; - - return true; -} - -bool IccGunshipTeleportAllyTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_HIGH_OVERLORD_SAURFANG, 100.0f); - if (!boss || !boss->IsInWorld() || boss->IsDuringRemoveFromWorld()) - return false; - - if (!boss->IsAlive()) - return false; - - if (!boss->IsHostileTo(bot)) - return false; - - return true; -} - -bool IccGunshipTeleportHordeTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_MURADIN_BRONZEBEARD, 100.0f); - if (!boss || !boss->IsInWorld() || boss->IsDuringRemoveFromWorld()) - return false; - - if (!boss->IsAlive()) - return false; - - if (!boss->IsHostileTo(bot)) - return false; - - return true; -} - -//DBS -bool IccDbsTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccDbsMainTankRuneOfBloodTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "deathbringer saurfang"); - - if (!boss) - return false; - - if (!botAI->IsAssistTankOfIndex(bot, 0)) - return false; - - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) - return false; - - Aura* aura = botAI->GetAura("rune of blood", mt); - if (!aura) - return false; - - return true; -} - -//DOGS -bool IccStinkyPreciousMainTankMortalWoundTrigger::IsActive() -{ - bool bossPresent = false; - if (AI_VALUE2(Unit*, "find target", "stinky") || AI_VALUE2(Unit*, "find target", "precious")) - bossPresent = true; - - if (!bossPresent) - return false; - - if (!botAI->IsAssistTankOfIndex(bot, 0)) - return false; - - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) - return false; - - Aura* aura = botAI->GetAura("mortal wound", mt, false, true); - if (!aura || aura->GetStackAmount() < 8) - return false; - - return true; -} - -//FESTERGUT -bool IccFestergutGroupPositionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccFestergutMainTankGastricBloatTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss) - { - return false; - } - if (!botAI->IsAssistTankOfIndex(bot, 0)) - { - return false; - } - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) - { - return false; - } - Aura* aura = botAI->GetAura("Gastric Bloat", mt, false, true); - if (!aura || aura->GetStackAmount() < 6) - { - return false; - } - return true; -} - -bool IccFestergutSporeTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "festergut"); - if (!boss || botAI->IsTank(bot)) - return false; - - // Check for spore aura (ID: 69279) on any bot in the group - Group* group = bot->GetGroup(); - if (!group) - return false; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member) - continue; - - if (member->HasAura(SPELL_GAS_SPORE)) - return true; - } - - return false; -} - -//ROTFACE -bool IccRotfaceTankPositionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss || !(botAI->IsTank(bot) || botAI->IsMelee(bot))) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccRotfaceGroupPositionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "rotface"); - if (!boss) - return false; - - return true; -} - -bool IccRotfaceMoveAwayFromExplosionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "big ooze"); - if (!boss) - return false; - - return boss->HasUnitState(UNIT_STATE_CASTING) && boss->FindCurrentSpellBySpellId(SPELL_UNSTABLE_OOZE_EXPLOSION); -} - -//PP - -bool IccPutricideGrowingOozePuddleTrigger::IsActive() -{ - // Early return if boss doesn't exist - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); - if (!boss) - return false; - - Difficulty diff = bot->GetRaidDifficulty(); - - if (sPlayerbotAIConfig.EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - //-------CHEAT------- - if (!bot->HasAura(SPELL_EXPERIENCED)) - bot->AddAura(SPELL_EXPERIENCED, bot); - - if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) - bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); - - if (!bot->HasAura(SPELL_NO_THREAT) && botAI->HasAggro(boss) && !botAI->IsTank(bot)) - bot->AddAura(SPELL_NO_THREAT, bot); - - if (botAI->IsMainTank(bot) && !bot->HasAura(SPELL_SPITEFULL_FURY) && boss->GetVictim() != bot) - bot->AddAura(SPELL_SPITEFULL_FURY, bot); - //-------CHEAT------- - } - - const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - for (auto const& npc : npcs) - { - if (Unit* unit = botAI->GetUnit(npc)) - { - const uint32 entry = unit->GetEntry(); - if (entry == NPC_GROWING_OOZE_PUDDLE) - return true; - } - } - - return false; -} - -bool IccPutricideVolatileOozeTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "volatile ooze"); - if (!boss) - return false; - - if (botAI->HasAura("Gaseous Bloat", bot)) - return false; - - return true; -} - -bool IccPutricideGasCloudTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "gas cloud"); - if (!boss) - return false; - - Unit* boss1 = AI_VALUE2(Unit*, "find target", "volatile ooze"); - - bool hasGaseousBloat = botAI->HasAura("Gaseous Bloat", bot); - - if (hasGaseousBloat && boss1) - return true; - - if (boss1) - return false; - - return true; -} - -bool IccPutricideMainTankMutatedPlagueTrigger::IsActive() -{ - bool bossPresent = false; - if (AI_VALUE2(Unit*, "find target", "professor putricide")) - bossPresent = true; - - if (!bossPresent) - return false; - - if (!botAI->IsAssistTankOfIndex(bot, 0)) - { - return false; - } - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) - { - return false; - } - Aura* aura = botAI->GetAura("Mutated Plague", mt, false, true); - if (!aura || aura->GetStackAmount() < 4) - { - return false; - } - return true; -} - -bool IccPutricideMalleableGooTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "professor putricide"); - if (!boss) - return false; - - if (botAI->IsTank(bot)) - return true; - - Unit* boss1 = AI_VALUE2(Unit*, "find target", "volatile ooze"); - if (boss1) - return false; - - Unit* boss2 = AI_VALUE2(Unit*, "find target", "gas cloud"); - if (boss2) - return false; - - return true; -} - -//BPC -bool IccBpcKelesethTankTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "prince keleseth"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - if (!botAI->IsAssistTank(bot)) - return false; - - Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); - if (aura) - if (aura->GetStackAmount() > 18) - return false; - - return true; -} - -bool IccBpcMainTankTrigger::IsActive() -{ - if (!botAI->IsTank(bot)) - return false; - - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); - Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - - if (!(valanar || taldaram || keleseth)) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccBpcEmpoweredVortexTrigger::IsActive() -{ - // Tanks should ignore this mechanic - if (botAI->IsMainTank(bot) || botAI->IsAssistTank(bot)) - return false; - - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - if (!valanar) - return false; - - Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true); - if (aura) - if (aura->GetStackAmount() > 12) - return false; - - Aura* auraValanar = botAI->GetAura("Invocation of Blood", valanar); - if (!auraValanar) - return false; - - return true; -} - -bool IccBpcKineticBombTrigger::IsActive() -{ - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); - Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - - if (!(valanar || taldaram || keleseth)) - return false; - - if (!botAI->IsRanged(bot) || botAI->IsHeal(bot)) - return false; - - // Early exit condition - if Shadow Prison has too many stacks - if (Aura* aura = botAI->GetAura("Shadow Prison", bot, false, true)) - { - if (aura->GetStackAmount() > 12) - return false; - } - - static const std::array bombEntries = {NPC_KINETIC_BOMB1, NPC_KINETIC_BOMB2, NPC_KINETIC_BOMB3, - NPC_KINETIC_BOMB4}; - const GuidVector bombs = AI_VALUE(GuidVector, "possible targets no los"); - - bool bombFound = false; - - for (const auto entry : bombEntries) - { - for (auto const& guid : bombs) - { - if (Unit* unit = botAI->GetUnit(guid)) - { - if (unit->GetEntry() == entry) - { - // Check if bomb is within valid Z-axis range - if (unit->GetPositionZ() - bot->GetPositionZ() < 25.0f) - { - bombFound = true; - break; - } - } - } - } - if (bombFound) - break; - } - - return botAI->IsRangedDps(bot) && bombFound; -} - -bool IccBpcBallOfFlameTrigger::IsActive() -{ - Unit* valanar = AI_VALUE2(Unit*, "find target", "prince valanar"); - Unit* taldaram = AI_VALUE2(Unit*, "find target", "prince taldaram"); - Unit* keleseth = AI_VALUE2(Unit*, "find target", "prince keleseth"); - - if (!(valanar || taldaram || keleseth)) - return false; - - Aura* auraTaldaram = botAI->GetAura("Invocation of Blood", taldaram); - if (!auraTaldaram) - return false; - - return true; -} - -//BQL -bool IccBqlGroupPositionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccBqlPactOfDarkfallenTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("Pact of the Darkfallen", bot); - if (!aura) - return false; - - return true; -} - -bool IccBqlVampiricBiteTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "blood-queen lana'thel"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("Frenzied Bloodthirst", bot); - if (!aura) - return false; - - return true; -} - -// Sister Svalna -bool IccValkyreSpearTrigger::IsActive() -{ - // Check if there's a spear nearby - if (Creature* spear = bot->FindNearestCreature(NPC_SPEAR, 100.0f)) - return true; - - return false; -} - -bool IccSisterSvalnaTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sister svalna"); - if (!boss) - return false; - - if (bot->GetExactDist2d(boss) > 30.0f) - return false; - - return true; -} - -// VDW -bool IccValithriaGroupTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); - if (!boss) - return false; - - if (bot->HasAura(SPELL_EXPERIENCED)) - bot->RemoveAura(SPELL_EXPERIENCED); - - return true; -} - -bool IccValithriaPortalTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("Twisted Nightmares", bot, false, true); - if (aura && aura->GetStackAmount() >= 25) - return false; - - // Only healers should use portals - if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE)) - return false; - - Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); - Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); - - if ((worm && worm->GetVictim() == bot) || (zombie && zombie->GetVictim() == bot)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Collect healer GUIDs and check for druids - std::vector healerGuids; - std::vector druidGuids; - int healerCount = 0; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || botAI->IsRealPlayer()) - continue; - - if (botAI->IsHeal(member) && !botAI->IsRealPlayer()) - { - healerCount++; - healerGuids.push_back(member->GetGUID()); - // Check if druid (class 11) - if (member->getClass() == CLASS_DRUID) - druidGuids.push_back(member->GetGUID()); - } - } - - // Sort GUIDs to ensure consistent ordering - std::sort(healerGuids.begin(), healerGuids.end()); - std::sort(druidGuids.begin(), druidGuids.end()); - - // Find position of current bot's GUID in the sorted list - auto botGuidPos = std::find(healerGuids.begin(), healerGuids.end(), bot->GetGUID()); - if (botGuidPos == healerGuids.end()) - return false; - - int healerIndex = std::distance(healerGuids.begin(), botGuidPos); - - // Find if this bot is a druid - bool isDruid = (bot->getClass() == CLASS_DRUID); - - // Determine raid healer assignment - bool shouldHealRaid = false; - int druidCount = druidGuids.size(); - - if (druidCount > 0) - { - // If we have druids, they should heal raid - if (isDruid) - { - // If there are more druids than raid healers needed, extra druids can heal boss - int raidHealersNeeded = healerCount > 3 ? 2 : 1; - int druidIndex = - std::distance(druidGuids.begin(), std::find(druidGuids.begin(), druidGuids.end(), bot->GetGUID())); - if (druidIndex < raidHealersNeeded) - shouldHealRaid = true; - else - shouldHealRaid = false; - } - else if (healerCount > 3 && druidCount == 1) - { - // If only 1 druid and need 2 raid healers, pick the last non-druid healer as well - if (healerIndex == (healerCount - 1) && !isDruid) - shouldHealRaid = true; - } - } - else - { - // No druids, assign raid healers as before - if (healerCount > 3) - shouldHealRaid = (healerIndex >= (healerCount - 2)); - else - shouldHealRaid = (healerIndex == (healerCount - 1)); - } - - // Raid healers should not use portals - if (shouldHealRaid) - return false; - - // Find the nearest portal creature - Creature* portal1 = bot->FindNearestCreature(NPC_DREAM_PORTAL, 100.0f); - if (!portal1) - portal1 = bot->FindNearestCreature(NPC_DREAM_PORTAL_PRE_EFFECT, 100.0f); - - Creature* portal2 = bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 100.0f); - if (!portal2) - portal2 = bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL_PRE_EFFECT, 100.0f); - - return portal1 || portal2; -} - -bool IccValithriaHealTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_VALITHRIA_DREAMWALKER, 100.0f); - if (!boss) - return false; - - // Only healers should use healing - if (!botAI->IsHeal(bot) || bot->HasAura(SPELL_DREAM_STATE) || bot->HealthBelowPct(50)) - return false; - - Creature* worm = bot->FindNearestCreature(NPC_ROT_WORM, 100.0f); - Creature* zombie = bot->FindNearestCreature(NPC_BLISTERING_ZOMBIE, 100.0f); - - if ((worm && worm->GetVictim() == bot) || (zombie && zombie->GetVictim() == bot)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - // Collect healer GUIDs and check for druids - std::vector healerGuids; - std::vector druidGuids; - int healerCount = 0; - - for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) - { - Player* member = itr->GetSource(); - if (!member || !member->IsAlive() || botAI->IsRealPlayer()) - continue; - - if (botAI->IsHeal(member) && !botAI->IsRealPlayer()) - { - healerCount++; - healerGuids.push_back(member->GetGUID()); - // Check if druid (class 11) - if (member->getClass() == CLASS_DRUID) - druidGuids.push_back(member->GetGUID()); - } - } - - // Sort GUIDs to ensure consistent ordering - std::sort(healerGuids.begin(), healerGuids.end()); - std::sort(druidGuids.begin(), druidGuids.end()); - - // Find position of current bot's GUID in the sorted list - auto botGuidPos = std::find(healerGuids.begin(), healerGuids.end(), bot->GetGUID()); - if (botGuidPos == healerGuids.end()) - return false; - - int healerIndex = std::distance(healerGuids.begin(), botGuidPos); - - // Find if this bot is a druid - bool isDruid = (bot->getClass() == CLASS_DRUID); - - // Determine raid healer assignment - bool shouldHealRaid = false; - int druidCount = druidGuids.size(); - - if (druidCount > 0) - { - // If we have druids, they should heal raid - if (isDruid) - { - // If there are more druids than raid healers needed, extra druids can heal boss - int raidHealersNeeded = healerCount > 3 ? 2 : 1; - int druidIndex = - std::distance(druidGuids.begin(), std::find(druidGuids.begin(), druidGuids.end(), bot->GetGUID())); - if (druidIndex < raidHealersNeeded) - shouldHealRaid = true; - else - shouldHealRaid = false; // extra druids can heal boss - } - else if (healerCount > 3 && druidCount == 1) - { - // If only 1 druid and need 2 raid healers, pick the last non-druid healer as well - if (healerIndex == (healerCount - 1) && !isDruid) - shouldHealRaid = true; - } - } - else - { - // No druids, assign raid healers as before - if (healerCount > 3) - shouldHealRaid = (healerIndex >= (healerCount - 2)); - else - shouldHealRaid = (healerIndex == (healerCount - 1)); - } - - // If assigned to raid healing, return false to not heal Valithria - if (shouldHealRaid) - return false; - - if (bot->GetHealthPct() < 50.0f) - return false; - - // For Valithria healers, check portal logic - // If no portal is found within 100 yards, we should heal - if (!bot->FindNearestCreature(NPC_DREAM_PORTAL, 100.0f) && !bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 100.0f)) - return true; - - if (bot->FindNearestCreature(NPC_DREAM_PORTAL, 10.0f) || bot->FindNearestCreature(NPC_NIGHTMARE_PORTAL, 10.0f)) - return false; - - // If portal is far but within 100 yards, heal while moving to it - return true; -} - -bool IccValithriaDreamCloudTrigger::IsActive() -{ - // Only active if we're in dream state - if (!bot->HasAura(SPELL_DREAM_STATE) || bot->HealthBelowPct(50)) - return false; - - // Find nearest cloud of either type - Creature* dreamCloud = bot->FindNearestCreature(NPC_DREAM_CLOUD, 100.0f); - Creature* nightmareCloud = bot->FindNearestCreature(NPC_NIGHTMARE_CLOUD, 100.0f); - - return (dreamCloud || nightmareCloud); -} - -//SINDRAGOSA -bool IccSindragosaGroupPositionTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); // sindra - if (!boss) - return false; - - Difficulty diff = bot->GetRaidDifficulty(); - - if (sPlayerbotAIConfig.EnableICCBuffs && diff && (diff == RAID_DIFFICULTY_10MAN_HEROIC || diff == RAID_DIFFICULTY_25MAN_HEROIC)) - { - //-------CHEAT------- - if (!bot->HasAura(SPELL_EXPERIENCED)) - bot->AddAura(SPELL_EXPERIENCED, bot); - - if (!bot->HasAura(SPELL_AGEIS_OF_DALARAN)) - bot->AddAura(SPELL_AGEIS_OF_DALARAN, bot); - - if (!bot->HasAura(SPELL_NO_THREAT) && botAI->HasAggro(boss) && !botAI->IsTank(bot)) - bot->AddAura(SPELL_NO_THREAT, bot); - - if (botAI->IsMainTank(bot) && !bot->HasAura(SPELL_SPITEFULL_FURY) && boss->GetVictim() != bot) - bot->AddAura(SPELL_SPITEFULL_FURY, bot); - //-------CHEAT------- - } - - if (!boss || bot->HasAura(SPELL_FROST_BEACON) /*|| bot->HasAura(69762)*/ || boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f) - return false; - - return true; -} - -bool IccSindragosaFrostBeaconTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - if (bot->HasAura(SPELL_FROST_BEACON)) - return true; - - Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); - for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); ++itr) - { - Player* member = ObjectAccessor::FindPlayer(itr->guid); - if (!member || !member->IsAlive()) - continue; - - if (member->HasAura(SPELL_FROST_BEACON)) // If any player has Frost Beacon, keep trigger active - return true; - } - - return false; -} - -bool IccSindragosaBlisteringColdTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - if (botAI->IsMainTank(bot)) - return false; - - // Don't move if any bot in group has ice tomb - Group* group = bot->GetGroup(); - if (!group) - return false; - - float dist = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); - - if (dist >= 33.0f) - return false; - - bool isCasting = false; - if (boss && boss->HasUnitState(UNIT_STATE_CASTING)) - isCasting = true; - - bool isBlisteringCold = false; - if (boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4))) - isBlisteringCold = true; - - return isCasting && isBlisteringCold; -} - -bool IccSindragosaUnchainedMagicTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("unchained magic", bot, false, false); - if (!aura) - return false; - - bool isBlisteringCold = false; - if (boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4))) - isBlisteringCold = true; - - if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && isBlisteringCold) - return false; - - return true; -} - -bool IccSindragosaChilledToTheBoneTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("Chilled to the Bone", bot, false, false); - if (!aura) - return false; - - bool isBlisteringCold = false; - if (boss && (boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD1) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD2) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD3) || - boss->FindCurrentSpellBySpellId(SPELL_BLISTERING_COLD4))) - isBlisteringCold = true; - - if (boss && boss->HasUnitState(UNIT_STATE_CASTING) && isBlisteringCold) - return false; - - return true; -} - -bool IccSindragosaMysticBuffetTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - if (boss->GetVictim() == bot) - return false; - - Aura* aura = botAI->GetAura("mystic buffet", bot, false, true); - if (!aura) - return false; - - if (bot->HasAura(SPELL_FROST_BEACON)) - return false; - - if (aura->GetStackAmount() >= 1) - return true; - - return false; -} - -bool IccSindragosaMainTankMysticBuffetTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - Aura* aura = botAI->GetAura("mystic buffet", bot, false, false); - if (botAI->IsTank(bot) && aura) //main tank will delete mystic buffet until I find a better way to swap tanks, atm it is not great since while swapping they will wipe group 7/10 times. - bot->RemoveAura(aura->GetId()); - - if (botAI->IsTank(bot) && boss->GetVictim() == bot) - return false; - - // Only for assist tank - if (!botAI->IsAssistTankOfIndex(bot, 0)) - return false; - - // Don't swap if we have frost beacon - if (bot->HasAura(SPELL_FROST_BEACON)) // Frost Beacon - return false; - - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) - return false; - - // Check main tank stacks - Aura* mtAura = botAI->GetAura("mystic buffet", mt, false, true); - if (!mtAura || mtAura->GetStackAmount() < 6) - return false; - - // Check our own stacks - don't taunt if we have too many - Aura* selfAura = botAI->GetAura("mystic buffet", bot, false, true); - if (selfAura && selfAura->GetStackAmount() > 6) - return false; - - // Only taunt if we're in position - float distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); - if (distToTankPos > 3.0f) - return false; - - return true; -} - -bool IccSindragosaTankSwapPositionTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "sindragosa"); - if (!boss) - return false; - - if (boss && boss->GetVictim() == bot) - return false; - - // Only for assist tank - if (!botAI->IsAssistTankOfIndex(bot, 0)) - return false; - - // Don't move to position if we have frost beacon - if (bot->HasAura(SPELL_FROST_BEACON)) - return false; - - // Check our own stacks - don't try to tank if we have too many - Aura* selfAura = botAI->GetAura("mystic buffet", bot, false, true); - if (selfAura && selfAura->GetStackAmount() > 6) - return false; - - // Check if main tank has high stacks - Unit* mt = AI_VALUE(Unit*, "main tank"); - if (!mt) - return false; - - Aura* mtAura = botAI->GetAura("mystic buffet", mt, false, true); - if (!mtAura) - return false; - - uint32 mtStacks = mtAura->GetStackAmount(); - if (mtStacks < 6) // Only start moving when MT has 5+ stacks - return false; - - // Check if we're already in position - float distToTankPos = bot->GetExactDist2d(ICC_SINDRAGOSA_TANK_POSITION); - if (distToTankPos <= 3.0f) - return false; - - return true; // Move to position if all conditions are met -} - -bool IccSindragosaFrostBombTrigger::IsActive() -{ - Unit* boss = bot->FindNearestCreature(NPC_SINDRAGOSA, 200.0f); - if (!boss) - return false; - - if (!bot->IsAlive() || bot->HasAura(SPELL_ICE_TOMB)) // Skip if dead or in Ice Tomb - return false; - - if (boss->GetExactDist2d(ICC_SINDRAGOSA_FLYING_POSITION.GetPositionX(), ICC_SINDRAGOSA_FLYING_POSITION.GetPositionY()) < 50.0f && !boss->HealthBelowPct(25) && !boss->HealthAbovePct(99)) - return true; - - return false; -} - -//LK - -bool IccLichKingShadowTrapTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return false; - - bool hasPlague = botAI->HasAura("Necrotic Plague", bot); - if (hasPlague) - return false; - - if (!botAI->IsMainTank(bot)) - return false; - - if (boss->HealthBelowPct(65)) - return false; - - // search for all nearby traps - GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - std::vector nearbyTraps; - bool needToMove = false; - - for (auto& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (!unit || !unit->IsAlive()) - continue; - - if (unit->GetEntry() == NPC_SHADOW_TRAP && bot->GetExactDist2d(unit) < 11.0f) // shadow trap - return true; - } - - return false; -} - -bool IccLichKingNecroticPlagueTrigger::IsActive() -{ - bool hasPlague = botAI->HasAura("Necrotic Plague", bot); - - return hasPlague; -} - -bool IccLichKingWinterTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - if (!boss) - return false; - - // Check for either Remorseless Winter - bool hasWinterAura = false; - if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER1) || boss->HasAura(SPELL_REMORSELESS_WINTER2) || - boss->HasAura(SPELL_REMORSELESS_WINTER3) || boss->HasAura(SPELL_REMORSELESS_WINTER4))) - hasWinterAura = true; - - bool hasWinter2Aura = false; - if (boss && (boss->HasAura(SPELL_REMORSELESS_WINTER5) || boss->HasAura(SPELL_REMORSELESS_WINTER6) || - boss->HasAura(SPELL_REMORSELESS_WINTER7) || boss->HasAura(SPELL_REMORSELESS_WINTER8))) - hasWinter2Aura = true; - - bool isCasting = false; - if (boss && boss->HasUnitState(UNIT_STATE_CASTING)) - isCasting = true; - - bool isWinter = false; - if (boss && boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER1) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER2) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER5) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER6) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER3) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER4) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER7) || - boss->FindCurrentSpellBySpellId(SPELL_REMORSELESS_WINTER8)) - isWinter = true; - - if (hasWinterAura || hasWinter2Aura) - return true; - - if (isCasting && isWinter) - return true; - - return false; -} - -bool IccLichKingAddsTrigger::IsActive() -{ - Unit* boss = AI_VALUE2(Unit*, "find target", "the lich king"); - - bool hasPlague = botAI->HasAura("Necrotic Plague", bot); - if (hasPlague) - return false; - - Unit* terenasMenethilHC = bot->FindNearestCreature(NPC_TERENAS_MENETHIL_HC, 55.0f); - Unit* terenasMenethil = bot->FindNearestCreature(NPC_TERENAS_MENETHIL, 55.0f); - - if (terenasMenethilHC) - return true; - - if (terenasMenethil) - return true; - - if (!boss) - return false; - - return true; -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h b/modules/mod-playerbots/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h deleted file mode 100644 index e2fc782ca..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Icecrown/Util/RaidIccScripts.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _PLAYERBOT_RAIDICCSCRIPTS_H -#define _PLAYERBOT_RAIDICCSCRIPTS_H - -#include "../../../../src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.h" - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/RaidKarazhanActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraActionContext.h similarity index 51% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/RaidKarazhanActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraActionContext.h index e555cd14d..f1b3e2488 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/RaidKarazhanActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraActionContext.h @@ -1,7 +1,13 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H -#define _PLAYERBOT_RAIDKARAZHANACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidKarazhanActions.h" +#ifndef PLAYERBOTS_KARAACTIONCONTEXT_H +#define PLAYERBOTS_KARAACTIONCONTEXT_H + +#include "KaraActions.h" #include "NamedObjectContext.h" class RaidKarazhanActionContext : public NamedObjectContext @@ -136,128 +142,141 @@ public: private: // Trash - static Action* mana_warp_stun_creature_before_warp_breach( - PlayerbotAI* botAI) { return new ManaWarpStunCreatureBeforeWarpBreachAction(botAI); } + static Action* mana_warp_stun_creature_before_warp_breach(PlayerbotAI* botAI) { + return new ManaWarpStunCreatureBeforeWarpBreachAction(botAI); + } // Attumen the Huntsman - static Action* attumen_the_huntsman_mark_target( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanMarkTargetAction(botAI); } - - static Action* attumen_the_huntsman_split_bosses( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanSplitBossesAction(botAI); } - - static Action* attumen_the_huntsman_stack_behind( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanStackBehindAction(botAI); } - - static Action* attumen_the_huntsman_manage_dps_timer( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanManageDpsTimerAction(botAI); } + static Action* attumen_the_huntsman_mark_target(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanMarkTargetAction(botAI); + } + static Action* attumen_the_huntsman_split_bosses(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanSplitBossesAction(botAI); + } + static Action* attumen_the_huntsman_stack_behind(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanStackBehindAction(botAI); + } + static Action* attumen_the_huntsman_manage_dps_timer(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanManageDpsTimerAction(botAI); + } // Moroes - static Action* moroes_main_tank_attack_boss( - PlayerbotAI* botAI) { return new MoroesMainTankAttackBossAction(botAI); } - - static Action* moroes_mark_target( - PlayerbotAI* botAI) { return new MoroesMarkTargetAction(botAI); } + static Action* moroes_main_tank_attack_boss(PlayerbotAI* botAI) { + return new MoroesMainTankAttackBossAction(botAI); + } + static Action* moroes_mark_target(PlayerbotAI* botAI) { + return new MoroesMarkTargetAction(botAI); + } // Maiden of Virtue - static Action* maiden_of_virtue_move_boss_to_healer( - PlayerbotAI* botAI) { return new MaidenOfVirtueMoveBossToHealerAction(botAI); } - - static Action* maiden_of_virtue_position_ranged( - PlayerbotAI* botAI) { return new MaidenOfVirtuePositionRangedAction(botAI); } + static Action* maiden_of_virtue_move_boss_to_healer(PlayerbotAI* botAI) { + return new MaidenOfVirtueMoveBossToHealerAction(botAI); + } + static Action* maiden_of_virtue_position_ranged(PlayerbotAI* botAI) { + return new MaidenOfVirtuePositionRangedAction(botAI); + } // The Big Bad Wolf - static Action* big_bad_wolf_position_boss( - PlayerbotAI* botAI) { return new BigBadWolfPositionBossAction(botAI); } - - static Action* big_bad_wolf_run_away_from_boss( - PlayerbotAI* botAI) { return new BigBadWolfRunAwayFromBossAction(botAI); } + static Action* big_bad_wolf_position_boss(PlayerbotAI* botAI) { + return new BigBadWolfPositionBossAction(botAI); + } + static Action* big_bad_wolf_run_away_from_boss(PlayerbotAI* botAI) { + return new BigBadWolfRunAwayFromBossAction(botAI); + } // Romulo and Julianne - static Action* romulo_and_julianne_mark_target( - PlayerbotAI* botAI) { return new RomuloAndJulianneMarkTargetAction(botAI); } + static Action* romulo_and_julianne_mark_target(PlayerbotAI* botAI) { + return new RomuloAndJulianneMarkTargetAction(botAI); + } // The Wizard of Oz - static Action* wizard_of_oz_mark_target( - PlayerbotAI* botAI) { return new WizardOfOzMarkTargetAction(botAI); } - - static Action* wizard_of_oz_scorch_strawman( - PlayerbotAI* botAI) { return new WizardOfOzScorchStrawmanAction(botAI); } + static Action* wizard_of_oz_mark_target(PlayerbotAI* botAI) { + return new WizardOfOzMarkTargetAction(botAI); + } + static Action* wizard_of_oz_scorch_strawman(PlayerbotAI* botAI) { + return new WizardOfOzScorchStrawmanAction(botAI); + } // The Curator - static Action* the_curator_mark_astral_flare( - PlayerbotAI* botAI) { return new TheCuratorMarkAstralFlareAction(botAI); } - - static Action* the_curator_position_boss( - PlayerbotAI* botAI) { return new TheCuratorPositionBossAction(botAI); } - - static Action* the_curator_spread_ranged( - PlayerbotAI* botAI) { return new TheCuratorSpreadRangedAction(botAI); } + static Action* the_curator_mark_astral_flare(PlayerbotAI* botAI) { + return new TheCuratorMarkAstralFlareAction(botAI); + } + static Action* the_curator_position_boss(PlayerbotAI* botAI) { + return new TheCuratorPositionBossAction(botAI); + } + static Action* the_curator_spread_ranged(PlayerbotAI* botAI) { + return new TheCuratorSpreadRangedAction(botAI); + } // Terestian Illhoof - static Action* terestian_illhoof_mark_target( - PlayerbotAI* botAI) { return new TerestianIllhoofMarkTargetAction(botAI); } + static Action* terestian_illhoof_mark_target(PlayerbotAI* botAI) { + return new TerestianIllhoofMarkTargetAction(botAI); + } // Shade of Aran - static Action* shade_of_aran_run_away_from_arcane_explosion( - PlayerbotAI* botAI) { return new ShadeOfAranRunAwayFromArcaneExplosionAction(botAI); } - - static Action* shade_of_aran_stop_moving_during_flame_wreath( - PlayerbotAI* botAI) { return new ShadeOfAranStopMovingDuringFlameWreathAction(botAI); } - - static Action* shade_of_aran_mark_conjured_elemental( - PlayerbotAI* botAI) { return new ShadeOfAranMarkConjuredElementalAction(botAI); } - - static Action* shade_of_aran_ranged_maintain_distance( - PlayerbotAI* botAI) { return new ShadeOfAranRangedMaintainDistanceAction(botAI); } + static Action* shade_of_aran_run_away_from_arcane_explosion(PlayerbotAI* botAI) { + return new ShadeOfAranRunAwayFromArcaneExplosionAction(botAI); + } + static Action* shade_of_aran_stop_moving_during_flame_wreath(PlayerbotAI* botAI) { + return new ShadeOfAranStopMovingDuringFlameWreathAction(botAI); + } + static Action* shade_of_aran_mark_conjured_elemental(PlayerbotAI* botAI) { + return new ShadeOfAranMarkConjuredElementalAction(botAI); + } + static Action* shade_of_aran_ranged_maintain_distance(PlayerbotAI* botAI) { + return new ShadeOfAranRangedMaintainDistanceAction(botAI); + } // Netherspite - static Action* netherspite_block_red_beam( - PlayerbotAI* botAI) { return new NetherspiteBlockRedBeamAction(botAI); } - - static Action* netherspite_block_blue_beam( - PlayerbotAI* botAI) { return new NetherspiteBlockBlueBeamAction(botAI); } - - static Action* netherspite_block_green_beam( - PlayerbotAI* botAI) { return new NetherspiteBlockGreenBeamAction(botAI); } - - static Action* netherspite_avoid_beam_and_void_zone( - PlayerbotAI* botAI) { return new NetherspiteAvoidBeamAndVoidZoneAction(botAI); } - - static Action* netherspite_banish_phase_avoid_void_zone( - PlayerbotAI* botAI) { return new NetherspiteBanishPhaseAvoidVoidZoneAction(botAI); } - - static Action* netherspite_manage_timers_and_trackers( - PlayerbotAI* botAI) { return new NetherspiteManageTimersAndTrackersAction(botAI); } + static Action* netherspite_block_red_beam(PlayerbotAI* botAI) { + return new NetherspiteBlockRedBeamAction(botAI); + } + static Action* netherspite_block_blue_beam(PlayerbotAI* botAI) { + return new NetherspiteBlockBlueBeamAction(botAI); + } + static Action* netherspite_block_green_beam(PlayerbotAI* botAI) { + return new NetherspiteBlockGreenBeamAction(botAI); + } + static Action* netherspite_avoid_beam_and_void_zone(PlayerbotAI* botAI) { + return new NetherspiteAvoidBeamAndVoidZoneAction(botAI); + } + static Action* netherspite_banish_phase_avoid_void_zone(PlayerbotAI* botAI) { + return new NetherspiteBanishPhaseAvoidVoidZoneAction(botAI); + } + static Action* netherspite_manage_timers_and_trackers(PlayerbotAI* botAI) { + return new NetherspiteManageTimersAndTrackersAction(botAI); + } // Prince Malchezaar - static Action* prince_malchezaar_enfeebled_avoid_hazard( - PlayerbotAI* botAI) { return new PrinceMalchezaarEnfeebledAvoidHazardAction(botAI); } - - static Action* prince_malchezaar_non_tank_avoid_infernal( - PlayerbotAI* botAI) { return new PrinceMalchezaarNonTankAvoidInfernalAction(botAI); } - - static Action* prince_malchezaar_main_tank_movement( - PlayerbotAI* botAI) { return new PrinceMalchezaarMainTankMovementAction(botAI); } + static Action* prince_malchezaar_enfeebled_avoid_hazard(PlayerbotAI* botAI) { + return new PrinceMalchezaarEnfeebledAvoidHazardAction(botAI); + } + static Action* prince_malchezaar_non_tank_avoid_infernal(PlayerbotAI* botAI) { + return new PrinceMalchezaarNonTankAvoidInfernalAction(botAI); + } + static Action* prince_malchezaar_main_tank_movement(PlayerbotAI* botAI) { + return new PrinceMalchezaarMainTankMovementAction(botAI); + } // Nightbane - static Action* nightbane_ground_phase_position_boss( - PlayerbotAI* botAI) { return new NightbaneGroundPhasePositionBossAction(botAI); } - - static Action* nightbane_ground_phase_rotate_ranged_positions( - PlayerbotAI* botAI) { return new NightbaneGroundPhaseRotateRangedPositionsAction(botAI); } - - static Action* nightbane_cast_fear_ward_on_main_tank( - PlayerbotAI* botAI) { return new NightbaneCastFearWardOnMainTankAction(botAI); } - - static Action* nightbane_control_pet_aggression( - PlayerbotAI* botAI) { return new NightbaneControlPetAggressionAction(botAI); } - - static Action* nightbane_flight_phase_movement( - PlayerbotAI* botAI) { return new NightbaneFlightPhaseMovementAction(botAI); } - - static Action* nightbane_manage_timers_and_trackers( - PlayerbotAI* botAI) { return new NightbaneManageTimersAndTrackersAction(botAI); } + static Action* nightbane_ground_phase_position_boss(PlayerbotAI* botAI) { + return new NightbaneGroundPhasePositionBossAction(botAI); + } + static Action* nightbane_ground_phase_rotate_ranged_positions(PlayerbotAI* botAI) { + return new NightbaneGroundPhaseRotateRangedPositionsAction(botAI); + } + static Action* nightbane_cast_fear_ward_on_main_tank(PlayerbotAI* botAI) { + return new NightbaneCastFearWardOnMainTankAction(botAI); + } + static Action* nightbane_control_pet_aggression(PlayerbotAI* botAI) { + return new NightbaneControlPetAggressionAction(botAI); + } + static Action* nightbane_flight_phase_movement(PlayerbotAI* botAI) { + return new NightbaneFlightPhaseMovementAction(botAI); + } + static Action* nightbane_manage_timers_and_trackers(PlayerbotAI* botAI) { + return new NightbaneManageTimersAndTrackersAction(botAI); + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraActions.cpp index b04e09d0d..7e612a0cf 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraActions.cpp @@ -1,5 +1,11 @@ -#include "RaidKarazhanActions.h" -#include "RaidKarazhanHelpers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "KaraActions.h" +#include "KaraHelpers.h" #include "Playerbots.h" #include "PlayerbotTextMgr.h" #include "RaidBossHelpers.h" @@ -16,14 +22,13 @@ bool ManaWarpStunCreatureBeforeWarpBreachAction::Execute(Event /*event*/) if (!manaWarp) return false; - static const std::array spells = + static const std::array spells = { "bash", "concussion blow", "hammer of justice", "kidney shot", "maim", - "revenge stun", "shadowfury", "shockwave" }; @@ -45,31 +50,32 @@ bool AttumenTheHuntsmanMarkTargetAction::Execute(Event /*event*/) Unit* attumenMounted = GetFirstAliveUnitByEntry(botAI, NPC_ATTUMEN_THE_HUNTSMAN_MOUNTED); if (attumenMounted) { - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) - MarkTargetWithStar(bot, attumenMounted); + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID) && + MarkTargetWithStar(bot, attumenMounted)) + { + return true; + } SetRtiTarget(botAI, "star", attumenMounted); - if (bot->GetTarget() != attumenMounted->GetGUID()) - { - bot->SetTarget(attumenMounted->GetGUID()); + if (AI_VALUE(Unit*, "current target") != attumenMounted) return Attack(attumenMounted); - } + } else if (Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight")) { - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) - MarkTargetWithStar(bot, midnight); + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID) && + MarkTargetWithStar(bot, midnight)) + { + return true; + } if (!botAI->IsAssistTankOfIndex(bot, 0)) { SetRtiTarget(botAI, "star", midnight); - if (bot->GetTarget() != midnight->GetGUID()) - { - bot->SetTarget(midnight->GetGUID()); + if (AI_VALUE(Unit*, "current target") != midnight) return Attack(midnight); - } } } @@ -87,16 +93,18 @@ bool AttumenTheHuntsmanSplitBossesAction::Execute(Event /*event*/) if (!attumen) return false; - MarkTargetWithSquare(bot, attumen); + if (MarkTargetWithSquare(bot, attumen)) + return true; + SetRtiTarget(botAI, "square", attumen); - if (bot->GetVictim() != attumen) + if (AI_VALUE(Unit*, "current target") != attumen) return Attack(attumen); if (attumen->GetVictim() == bot && midnight->GetVictim() != bot) { const float safeDistance = 6.0f; - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); + Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance); if (nearestPlayer && attumen->GetExactDist2d(nearestPlayer) < safeDistance) return MoveFromGroup(safeDistance + 2.0f); } @@ -116,9 +124,9 @@ bool AttumenTheHuntsmanStackBehindAction::Execute(Event /*event*/) float rearX = attumenMounted->GetPositionX() + std::cos(orientation) * distanceBehind; float rearY = attumenMounted->GetPositionY() + std::sin(orientation) * distanceBehind; - if (bot->GetExactDist2d(rearX, rearY) > 1.0f) + if (bot->GetDistance2d(rearX, rearY) > 1.0f) { - return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, attumenMounted->GetPositionZ(), false, false, false, false, + return MoveTo(KARAZHAN_MAP_ID, rearX, rearY, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); } @@ -159,10 +167,12 @@ bool MoroesMainTankAttackBossAction::Execute(Event /*event*/) if (!moroes) return false; - MarkTargetWithCircle(bot, moroes); + if (MarkTargetWithCircle(bot, moroes)) + return true; + SetRtiTarget(botAI, "circle", moroes); - if (bot->GetVictim() != moroes) + if (AI_VALUE(Unit*, "current target") != moroes) return Attack(moroes); return false; @@ -171,20 +181,20 @@ bool MoroesMainTankAttackBossAction::Execute(Event /*event*/) // Mark targets with skull in the recommended kill order bool MoroesMarkTargetAction::Execute(Event /*event*/) { - Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); - Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); - Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); - Unit* rafe = AI_VALUE2(Unit*, "find target", "baron rafe dreuger"); - Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); - Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - Unit* target = GetFirstAliveUnit({dorothea, catriona, keira, rafe, robin, crispin}); - - if (target) + static const std::array moroesGuests = { - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) - MarkTargetWithSkull(bot, target); + "baroness dorothea millstipe", + "lady catriona von'indi", + "lady keira berrybuck", + "baron rafe dreuger", + "lord robin daris", + "lord crispin ference" + }; - SetRtiTarget(botAI, "skull", target); + for (const char* name : moroesGuests) + { + if (Unit* guest = AI_VALUE2(Unit*, "find target", name)) + return MarkTargetWithSkull(bot, guest); } return false; @@ -200,7 +210,7 @@ bool MaidenOfVirtueMoveBossToHealerAction::Execute(Event /*event*/) if (!maiden) return false; - if (bot->GetVictim() != maiden) + if (AI_VALUE(Unit*, "current target") != maiden) return Attack(maiden); Unit* healer = nullptr; @@ -294,7 +304,7 @@ bool BigBadWolfPositionBossAction::Execute(Event /*event*/) if (!wolf) return false; - if (bot->GetVictim() != wolf) + if (AI_VALUE(Unit*, "current target") != wolf) return Attack(wolf); if (wolf->GetVictim() == bot) @@ -363,7 +373,7 @@ bool RomuloAndJulianneMarkTargetAction::Execute(Event /*event*/) target = (romulo->GetHealthPct() >= julianne->GetHealthPct()) ? romulo : julianne; if (target) - MarkTargetWithSkull(bot, target); + return MarkTargetWithSkull(bot, target); return false; } @@ -373,16 +383,11 @@ bool RomuloAndJulianneMarkTargetAction::Execute(Event /*event*/) // Mark targets with skull in the recommended kill order bool WizardOfOzMarkTargetAction::Execute(Event /*event*/) { - Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); - Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); - Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); - Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); - Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); - Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); - Unit* target = GetFirstAliveUnit({dorothee, tito, roar, strawman, tinhead, crone}); - - if (target) - MarkTargetWithSkull(bot, target); + for (const char* name : GetOzTargets()) + { + if (Unit* target = AI_VALUE2(Unit*, "find target", name)) + return MarkTargetWithSkull(bot, target); + } return false; } @@ -399,17 +404,10 @@ bool WizardOfOzScorchStrawmanAction::Execute(Event /*event*/) // The Curator -// Prioritize destroying Astral Flares bool TheCuratorMarkAstralFlareAction::Execute(Event /*event*/) { - Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); - if (!flare) - return false; - - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) - MarkTargetWithSkull(bot, flare); - - SetRtiTarget(botAI, "skull", flare); + if (Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare")) + return MarkTargetWithSkull(bot, flare); return false; } @@ -422,10 +420,12 @@ bool TheCuratorPositionBossAction::Execute(Event /*event*/) if (!curator) return false; - MarkTargetWithCircle(bot, curator); + if (MarkTargetWithCircle(bot, curator)) + return true; + SetRtiTarget(botAI, "circle", curator); - if (bot->GetVictim() != curator) + if (AI_VALUE(Unit*, "current target") != curator) return Attack(curator); if (curator->GetVictim() == bot) @@ -453,7 +453,7 @@ bool TheCuratorPositionBossAction::Execute(Event /*event*/) bool TheCuratorSpreadRangedAction::Execute(Event /*event*/) { const float minDistance = 5.0f; - Unit* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance); + Player* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance); if (nearestPlayer) { @@ -470,13 +470,18 @@ bool TheCuratorSpreadRangedAction::Execute(Event /*event*/) // Prioritize (1) Demon Chains, (2) Kil'rek, (3) Illhoof bool TerestianIllhoofMarkTargetAction::Execute(Event /*event*/) { - Unit* demonChains = GetFirstAliveUnitByEntry(botAI, NPC_DEMON_CHAINS); - Unit* kilrek = GetFirstAliveUnitByEntry(botAI, NPC_KILREK); - Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); + static const std::array illhoofTargets = + { + "demon chains", + "kil'rek", + "terestian illhoof" + }; - Unit* target = GetFirstAliveUnit({demonChains, kilrek, illhoof}); - if (target) - MarkTargetWithSkull(bot, target); + for (const char* name : illhoofTargets) + { + if (Unit* target = AI_VALUE2(Unit*, "find target", name)) + return MarkTargetWithSkull(bot, target); + } return false; } @@ -520,10 +525,8 @@ bool ShadeOfAranStopMovingDuringFlameWreathAction::Execute(Event /*event*/) // Mark Conjured Elementals with skull so DPS can burn them down bool ShadeOfAranMarkConjuredElementalAction::Execute(Event /*event*/) { - Unit* elemental = GetFirstAliveUnitByEntry(botAI, NPC_CONJURED_ELEMENTAL); - - if (elemental) - MarkTargetWithSkull(bot, elemental); + if (Unit* elemental = GetFirstAliveUnitByEntry(botAI, NPC_CONJURED_ELEMENTAL)) + return MarkTargetWithSkull(bot, elemental); return false; } @@ -744,7 +747,7 @@ bool NetherspiteBlockBlueBeamAction::Execute(Event /*event*/) float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) + if (!IsSafePosition(candidateX, candidateY, voidZones, 4.0f)) continue; float distToIdeal = fabs(dist - idealDistance); @@ -836,7 +839,7 @@ bool NetherspiteBlockGreenBeamAction::Execute(Event /*event*/) float candidateX = bx + dx * dist; float candidateY = by + dy * dist; float candidateZ = bz; - if (!IsSafePosition(candidateX, candidateY, candidateZ, voidZones, 4.0f)) + if (!IsSafePosition(candidateX, candidateY, voidZones, 4.0f)) continue; float distToIdeal = fabs(dist - 18.0f); @@ -870,11 +873,10 @@ bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event /*event*/) if (!netherspite) return false; - auto [redBlocker, greenBlocker, blueBlocker] = GetCurrentBeamBlockers(botAI, bot); std::vector voidZones = GetAllVoidZones(botAI, bot); bool nearVoidZone = !IsSafePosition(bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), voidZones, 4.0f); + voidZones, 4.0f); std::vector beams; Unit* redPortal = bot->FindNearestCreature(NPC_RED_PORTAL, 150.0f); @@ -923,7 +925,7 @@ bool NetherspiteAvoidBeamAndVoidZoneAction::Execute(Event /*event*/) float cy = botY + std::sin(angle) * dist; float cz = netherspiteZ; - if (!IsSafePosition(cx, cy, cz, voidZones, 4.0f) || + if (!IsSafePosition(cx, cy, voidZones, 4.0f) || !IsAwayFromBeams(cx, cy, beams, netherspite)) continue; @@ -1008,7 +1010,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event /*event*/) if (netherspite->GetHealth() == netherspite->GetMaxHealth() && !netherspite->HasAura(SPELL_GREEN_BEAM_HEAL)) { - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) netherspiteDpsWaitTimer.insert_or_assign(instanceId, now); if (botAI->IsTank(bot) && !bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1019,7 +1021,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event /*event*/) } else if (netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) netherspiteDpsWaitTimer.erase(instanceId); if (botAI->IsTank(bot)) @@ -1030,7 +1032,7 @@ bool NetherspiteManageTimersAndTrackersAction::Execute(Event /*event*/) } else if (!netherspite->HasAura(SPELL_NETHERSPITE_BANISHED)) { - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) netherspiteDpsWaitTimer.try_emplace(instanceId, now); if (botAI->IsTank(bot) && bot->HasAura(SPELL_RED_BEAM_DEBUFF)) @@ -1179,7 +1181,7 @@ bool PrinceMalchezaarNonTankAvoidInfernalAction::Execute(Event /*event*/) bot->AttackStop(); bot->InterruptNonMeleeSpells(true); return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); + MovementPriority::MOVEMENT_COMBAT, true, false); } } @@ -1194,7 +1196,7 @@ bool PrinceMalchezaarMainTankMovementAction::Execute(Event /*event*/) if (!malchezaar) return false; - if (bot->GetVictim() != malchezaar) + if (AI_VALUE(Unit*, "current target") != malchezaar) return Attack(malchezaar); std::vector infernals = GetSpawnedInfernals(botAI); @@ -1245,7 +1247,7 @@ bool PrinceMalchezaarMainTankMovementAction::Execute(Event /*event*/) { bot->AttackStop(); return MoveTo(KARAZHAN_MAP_ID, bestDestX, bestDestY, bestDestZ, false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); + MovementPriority::MOVEMENT_COMBAT, true, true); } } @@ -1260,9 +1262,10 @@ bool NightbaneGroundPhasePositionBossAction::Execute(Event /*event*/) if (!nightbane) return false; - MarkTargetWithSkull(bot, nightbane); + if (MarkTargetWithSkull(bot, nightbane)) + return true; - if (bot->GetVictim() != nightbane) + if (AI_VALUE(Unit*, "current target") != nightbane) return Attack(nightbane); const ObjectGuid botGuid = bot->GetGUID(); @@ -1399,10 +1402,10 @@ bool NightbaneFlightPhaseMovementAction::Execute(Event /*event*/) if (!nightbane || nightbane->GetPositionZ() <= NIGHTBANE_FLIGHT_Z) return false; - MarkTargetWithMoon(bot, nightbane); + if (MarkTargetWithMoon(bot, nightbane)) + return true; - Unit* botTarget = botAI->GetUnit(bot->GetTarget()); - if (botTarget && botTarget == nightbane) + if (AI_VALUE(Unit*, "current target") == nightbane) { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); @@ -1458,7 +1461,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event /*event*/) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) nightbaneDpsWaitTimer.erase(instanceId); } // Erase flight phase timer and Rain of Bones tracker on ground phase and start DPS wait timer @@ -1466,7 +1469,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event /*event*/) { nightbaneRainOfBonesHit.erase(botGuid); - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) { nightbaneFlightPhaseStartTimer.erase(instanceId); nightbaneDpsWaitTimer.try_emplace(instanceId, now); @@ -1482,7 +1485,7 @@ bool NightbaneManageTimersAndTrackersAction::Execute(Event /*event*/) if (botAI->IsRanged(bot)) nightbaneRangedStep.erase(botGuid); - if (IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) { nightbaneDpsWaitTimer.erase(instanceId); nightbaneFlightPhaseStartTimer.try_emplace(instanceId, now); diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraActions.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraActions.h index 6fca1bcbb..d7b3c05c8 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Action/RaidKarazhanActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANACTIONS_H -#define _PLAYERBOT_RAIDKARAZHANACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_KARAACTIONS_H +#define PLAYERBOTS_KARAACTIONS_H #include "Action.h" #include "AttackAction.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraHelpers.cpp similarity index 95% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraHelpers.cpp index 513920e11..14244b291 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraHelpers.cpp @@ -1,4 +1,10 @@ -#include "RaidKarazhanHelpers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "KaraHelpers.h" #include "Playerbots.h" namespace KarazhanHelpers @@ -50,15 +56,20 @@ namespace KarazhanHelpers const Position NIGHTBANE_FLIGHT_STACK_POSITION = { -11159.555f, -1893.526f, 91.473f }; // Broken Barrel const Position NIGHTBANE_RAIN_OF_BONES_POSITION = { -11165.233f, -1911.123f, 91.473f }; - Unit* GetFirstAliveUnit(const std::vector& units) - { - for (Unit* unit : units) - { - if (unit && unit->IsAlive()) - return unit; - } + // Wizard of Oz - return nullptr; + std::array const& GetOzTargets() + { + static std::array const targets = + { + "dorothee", + "tito", + "roar", + "strawman", + "tinhead", + }; + + return targets; } bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot) @@ -272,7 +283,7 @@ namespace KarazhanHelpers return voidZones; } - bool IsSafePosition(float x, float y, float z, const std::vector& hazards, float hazardRadius) + bool IsSafePosition(float x, float y, const std::vector& hazards, float hazardRadius) { for (Unit* hazard : hazards) { @@ -351,7 +362,7 @@ namespace KarazhanHelpers destX, destY, destZ, true)) continue; - if (!IsSafePosition(destX, destY, destZ, hazards, safeDistance)) + if (!IsSafePosition(destX, destY, hazards, safeDistance)) continue; if (requireSafePath) diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraHelpers.h similarity index 91% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraHelpers.h index ad7e8c3d5..c7cb0cd09 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Util/RaidKarazhanHelpers.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraHelpers.h @@ -1,6 +1,13 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANHELPERS_H_ -#define _PLAYERBOT_RAIDKARAZHANHELPERS_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ +#ifndef PLAYERBOTS_KARAHELPERS_H +#define PLAYERBOTS_KARAHELPERS_H + +#include #include #include @@ -110,14 +117,14 @@ namespace KarazhanHelpers extern const Position NIGHTBANE_FLIGHT_STACK_POSITION; extern const Position NIGHTBANE_RAIN_OF_BONES_POSITION; - Unit* GetFirstAliveUnit(const std::vector& units); + std::array const& GetOzTargets(); bool IsFlameWreathActive(PlayerbotAI* botAI, Player* bot); std::vector GetRedBlockers(PlayerbotAI* botAI, Player* bot); std::vector GetBlueBlockers(PlayerbotAI* botAI, Player* bot); std::vector GetGreenBlockers(PlayerbotAI* botAI, Player* bot); std::tuple GetCurrentBeamBlockers(PlayerbotAI* botAI, Player* bot); std::vector GetAllVoidZones(PlayerbotAI *botAI, Player* bot); - bool IsSafePosition (float x, float y, float z, const std::vector& hazards, float hazardRadius); + bool IsSafePosition (float x, float y, const std::vector& hazards, float hazardRadius); std::vector GetSpawnedInfernals(PlayerbotAI* botAI); bool IsStraightPathSafe( const Position& start, const Position& target, diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraMultipliers.cpp similarity index 90% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraMultipliers.cpp index 06a3335c3..69d345965 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraMultipliers.cpp @@ -1,6 +1,12 @@ -#include "RaidKarazhanMultipliers.h" -#include "RaidKarazhanActions.h" -#include "RaidKarazhanHelpers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "KaraMultipliers.h" +#include "KaraActions.h" +#include "KaraHelpers.h" #include "AttackAction.h" #include "ChooseTargetActions.h" #include "DruidActions.h" @@ -80,6 +86,19 @@ float AttumenTheHuntsmanWaitForDpsMultiplier::GetValue(Action* action) return 1.0f; } +// Disables co +disperse and co +tank face +float MaidenOfVirtueDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "maiden of virtue")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + // The assist tank should stay on the boss to be 2nd on aggro and tank Hateful Bolts float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) { @@ -93,6 +112,19 @@ float TheCuratorDisableTankAssistMultiplier::GetValue(Action* action) return 1.0f; } +// Disables co +disperse and co +tank face +float TheCuratorDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "the curator")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + // Save Bloodlust/Heroism for Evocation (100% increased damage) float TheCuratorDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) { @@ -350,17 +382,11 @@ float NightbaneDisableMovementMultiplier::GetValue(Action* action) if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - - // Disable CombatFormationMoveAction for all bots except: - // (1) main tank and (2) only during the ground phase, other melee - if (botAI->IsRanged(bot) || - (botAI->IsMelee(bot) && !botAI->IsMainTank(bot) && - nightbane->GetPositionZ() > NIGHTBANE_FLIGHT_Z)) + dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) { - if (dynamic_cast(action)) - return 0.0f; + return 0.0f; } return 1.0f; diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraMultipliers.h similarity index 82% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraMultipliers.h index 8ad5a1d4e..f9c56c453 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Multiplier/RaidKarazhanMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANMULTIPLIERS_H -#define _PLAYERBOT_RAIDKARAZHANMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_KARAMULTIPLIERS_H +#define PLAYERBOTS_KARAMULTIPLIERS_H #include "Multiplier.h" @@ -27,6 +33,14 @@ public: virtual float GetValue(Action* action); }; +class MaidenOfVirtueDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + MaidenOfVirtueDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "maiden of virtue disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + class TheCuratorDisableTankAssistMultiplier : public Multiplier { public: @@ -35,6 +49,14 @@ public: virtual float GetValue(Action* action); }; +class TheCuratorDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + TheCuratorDisableCombatFormationMoveMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "the curator disable combat formation move multiplier") {} + virtual float GetValue(Action* action); +}; + class TheCuratorDelayBloodlustAndHeroismMultiplier : public Multiplier { public: diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraStrategy.cpp similarity index 94% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraStrategy.cpp index 04578f083..65a18ca67 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraStrategy.cpp @@ -1,5 +1,11 @@ -#include "RaidKarazhanStrategy.h" -#include "RaidKarazhanMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "KaraStrategy.h" +#include "KaraMultipliers.h" void RaidKarazhanStrategy::InitTriggers(std::vector& triggers) { @@ -146,7 +152,9 @@ void RaidKarazhanStrategy::InitMultipliers(std::vector& multipliers multipliers.push_back(new AttumenTheHuntsmanDisableTankAssistMultiplier(botAI)); multipliers.push_back(new AttumenTheHuntsmanStayStackedMultiplier(botAI)); multipliers.push_back(new AttumenTheHuntsmanWaitForDpsMultiplier(botAI)); + multipliers.push_back(new MaidenOfVirtueDisableCombatFormationMoveMultiplier(botAI)); multipliers.push_back(new TheCuratorDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new TheCuratorDisableCombatFormationMoveMultiplier(botAI)); multipliers.push_back(new TheCuratorDelayBloodlustAndHeroismMultiplier(botAI)); multipliers.push_back(new ShadeOfAranArcaneExplosionDisableChargeMultiplier(botAI)); multipliers.push_back(new ShadeOfAranFlameWreathDisableMovementMultiplier(botAI)); diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraStrategy.h similarity index 53% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraStrategy.h index 7d6b16dee..4c4614615 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Strategy/RaidKarazhanStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraStrategy.h @@ -1,8 +1,13 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANSTRATEGY_H_ -#define _PLAYERBOT_RAIDKARAZHANSTRATEGY_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_KARASTRATEGY_H +#define PLAYERBOTS_KARASTRATEGY_H #include "Strategy.h" -#include "Multiplier.h" class RaidKarazhanStrategy : public Strategy { diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggerContext.h similarity index 55% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggerContext.h index e3f606c94..a5da69cb2 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggerContext.h @@ -1,8 +1,14 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDKARAZHANTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidKarazhanTriggers.h" -#include "AiObjectContext.h" +#ifndef PLAYERBOTS_KARATRIGGERCONTEXT_H +#define PLAYERBOTS_KARATRIGGERCONTEXT_H + +#include "KaraTriggers.h" +#include "NamedObjectContext.h" class RaidKarazhanTriggerContext : public NamedObjectContext { @@ -136,128 +142,141 @@ public: private: // Trash - static Trigger* mana_warp_is_about_to_explode( - PlayerbotAI* botAI) { return new ManaWarpIsAboutToExplodeTrigger(botAI); } + static Trigger* mana_warp_is_about_to_explode(PlayerbotAI* botAI) { + return new ManaWarpIsAboutToExplodeTrigger(botAI); + } // Attumen the Huntsman - static Trigger* attumen_the_huntsman_need_target_priority( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanNeedTargetPriorityTrigger(botAI); } - - static Trigger* attumen_the_huntsman_attumen_spawned( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenSpawnedTrigger(botAI); } - - static Trigger* attumen_the_huntsman_attumen_is_mounted( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanAttumenIsMountedTrigger(botAI); } - - static Trigger* attumen_the_huntsman_boss_wipes_aggro_when_mounting( - PlayerbotAI* botAI) { return new AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger(botAI); } + static Trigger* attumen_the_huntsman_need_target_priority(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanNeedTargetPriorityTrigger(botAI); + } + static Trigger* attumen_the_huntsman_attumen_spawned(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanAttumenSpawnedTrigger(botAI); + } + static Trigger* attumen_the_huntsman_attumen_is_mounted(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanAttumenIsMountedTrigger(botAI); + } + static Trigger* attumen_the_huntsman_boss_wipes_aggro_when_mounting(PlayerbotAI* botAI) { + return new AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger(botAI); + } // Moroes - static Trigger* moroes_boss_engaged_by_main_tank( - PlayerbotAI* botAI) { return new MoroesBossEngagedByMainTankTrigger(botAI); } - - static Trigger* moroes_need_target_priority( - PlayerbotAI* botAI) { return new MoroesNeedTargetPriorityTrigger(botAI); } + static Trigger* moroes_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new MoroesBossEngagedByMainTankTrigger(botAI); + } + static Trigger* moroes_need_target_priority(PlayerbotAI* botAI) { + return new MoroesNeedTargetPriorityTrigger(botAI); + } // Maiden of Virtue - static Trigger* maiden_of_virtue_healers_are_stunned_by_repentance( - PlayerbotAI* botAI) { return new MaidenOfVirtueHealersAreStunnedByRepentanceTrigger(botAI); } - - static Trigger* maiden_of_virtue_holy_wrath_deals_chain_damage( - PlayerbotAI* botAI) { return new MaidenOfVirtueHolyWrathDealsChainDamageTrigger(botAI); } + static Trigger* maiden_of_virtue_healers_are_stunned_by_repentance(PlayerbotAI* botAI) { + return new MaidenOfVirtueHealersAreStunnedByRepentanceTrigger(botAI); + } + static Trigger* maiden_of_virtue_holy_wrath_deals_chain_damage(PlayerbotAI* botAI) { + return new MaidenOfVirtueHolyWrathDealsChainDamageTrigger(botAI); + } // The Big Bad Wolf - static Trigger* big_bad_wolf_boss_engaged_by_tank( - PlayerbotAI* botAI) { return new BigBadWolfBossEngagedByTankTrigger(botAI); } - - static Trigger* big_bad_wolf_boss_is_chasing_little_red_riding_hood( - PlayerbotAI* botAI) { return new BigBadWolfBossIsChasingLittleRedRidingHoodTrigger(botAI); } + static Trigger* big_bad_wolf_boss_engaged_by_tank(PlayerbotAI* botAI) { + return new BigBadWolfBossEngagedByTankTrigger(botAI); + } + static Trigger* big_bad_wolf_boss_is_chasing_little_red_riding_hood(PlayerbotAI* botAI) { + return new BigBadWolfBossIsChasingLittleRedRidingHoodTrigger(botAI); + } // Romulo and Julianne - static Trigger* romulo_and_julianne_both_bosses_revived( - PlayerbotAI* botAI) { return new RomuloAndJulianneBothBossesRevivedTrigger(botAI); } + static Trigger* romulo_and_julianne_both_bosses_revived(PlayerbotAI* botAI) { + return new RomuloAndJulianneBothBossesRevivedTrigger(botAI); + } // The Wizard of Oz - static Trigger* wizard_of_oz_need_target_priority( - PlayerbotAI* botAI) { return new WizardOfOzNeedTargetPriorityTrigger(botAI); } - - static Trigger* wizard_of_oz_strawman_is_vulnerable_to_fire( - PlayerbotAI* botAI) { return new WizardOfOzStrawmanIsVulnerableToFireTrigger(botAI); } + static Trigger* wizard_of_oz_need_target_priority(PlayerbotAI* botAI) { + return new WizardOfOzNeedTargetPriorityTrigger(botAI); + } + static Trigger* wizard_of_oz_strawman_is_vulnerable_to_fire(PlayerbotAI* botAI) { + return new WizardOfOzStrawmanIsVulnerableToFireTrigger(botAI); + } // The Curator - static Trigger* the_curator_astral_flare_spawned( - PlayerbotAI* botAI) { return new TheCuratorAstralFlareSpawnedTrigger(botAI); } - - static Trigger* the_curator_boss_engaged_by_tanks( - PlayerbotAI* botAI) { return new TheCuratorBossEngagedByTanksTrigger(botAI); } - - static Trigger* the_curator_astral_flares_cast_arcing_sear( - PlayerbotAI* botAI) { return new TheCuratorBossAstralFlaresCastArcingSearTrigger(botAI); } + static Trigger* the_curator_astral_flare_spawned(PlayerbotAI* botAI) { + return new TheCuratorAstralFlareSpawnedTrigger(botAI); + } + static Trigger* the_curator_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new TheCuratorBossEngagedByTanksTrigger(botAI); + } + static Trigger* the_curator_astral_flares_cast_arcing_sear(PlayerbotAI* botAI) { + return new TheCuratorBossAstralFlaresCastArcingSearTrigger(botAI); + } // Terestian Illhoof - static Trigger* terestian_illhoof_need_target_priority( - PlayerbotAI* botAI) { return new TerestianIllhoofNeedTargetPriorityTrigger(botAI); } + static Trigger* terestian_illhoof_need_target_priority(PlayerbotAI* botAI) { + return new TerestianIllhoofNeedTargetPriorityTrigger(botAI); + } // Shade of Aran - static Trigger* shade_of_aran_arcane_explosion_is_casting( - PlayerbotAI* botAI) { return new ShadeOfAranArcaneExplosionIsCastingTrigger(botAI); } - - static Trigger* shade_of_aran_flame_wreath_is_active( - PlayerbotAI* botAI) { return new ShadeOfAranFlameWreathIsActiveTrigger(botAI); } - - static Trigger* shade_of_aran_conjured_elementals_summoned( - PlayerbotAI* botAI) { return new ShadeOfAranConjuredElementalsSummonedTrigger(botAI); } - - static Trigger* shade_of_aran_boss_uses_counterspell_and_blizzard( - PlayerbotAI* botAI) { return new ShadeOfAranBossUsesCounterspellAndBlizzardTrigger(botAI); } + static Trigger* shade_of_aran_arcane_explosion_is_casting(PlayerbotAI* botAI) { + return new ShadeOfAranArcaneExplosionIsCastingTrigger(botAI); + } + static Trigger* shade_of_aran_flame_wreath_is_active(PlayerbotAI* botAI) { + return new ShadeOfAranFlameWreathIsActiveTrigger(botAI); + } + static Trigger* shade_of_aran_conjured_elementals_summoned(PlayerbotAI* botAI) { + return new ShadeOfAranConjuredElementalsSummonedTrigger(botAI); + } + static Trigger* shade_of_aran_boss_uses_counterspell_and_blizzard(PlayerbotAI* botAI) { + return new ShadeOfAranBossUsesCounterspellAndBlizzardTrigger(botAI); + } // Netherspite - static Trigger* netherspite_red_beam_is_active( - PlayerbotAI* botAI) { return new NetherspiteRedBeamIsActiveTrigger(botAI); } - - static Trigger* netherspite_blue_beam_is_active( - PlayerbotAI* botAI) { return new NetherspiteBlueBeamIsActiveTrigger(botAI); } - - static Trigger* netherspite_green_beam_is_active( - PlayerbotAI* botAI) { return new NetherspiteGreenBeamIsActiveTrigger(botAI); } - - static Trigger* netherspite_bot_is_not_beam_blocker( - PlayerbotAI* botAI) { return new NetherspiteBotIsNotBeamBlockerTrigger(botAI); } - - static Trigger* netherspite_boss_is_banished( - PlayerbotAI* botAI) { return new NetherspiteBossIsBanishedTrigger(botAI); } - - static Trigger* netherspite_need_to_manage_timers_and_trackers( - PlayerbotAI* botAI) { return new NetherspiteNeedToManageTimersAndTrackersTrigger(botAI); } + static Trigger* netherspite_red_beam_is_active(PlayerbotAI* botAI) { + return new NetherspiteRedBeamIsActiveTrigger(botAI); + } + static Trigger* netherspite_blue_beam_is_active(PlayerbotAI* botAI) { + return new NetherspiteBlueBeamIsActiveTrigger(botAI); + } + static Trigger* netherspite_green_beam_is_active(PlayerbotAI* botAI) { + return new NetherspiteGreenBeamIsActiveTrigger(botAI); + } + static Trigger* netherspite_bot_is_not_beam_blocker(PlayerbotAI* botAI) { + return new NetherspiteBotIsNotBeamBlockerTrigger(botAI); + } + static Trigger* netherspite_boss_is_banished(PlayerbotAI* botAI) { + return new NetherspiteBossIsBanishedTrigger(botAI); + } + static Trigger* netherspite_need_to_manage_timers_and_trackers(PlayerbotAI* botAI) { + return new NetherspiteNeedToManageTimersAndTrackersTrigger(botAI); + } // Prince Malchezaar - static Trigger* prince_malchezaar_bot_is_enfeebled( - PlayerbotAI* botAI) { return new PrinceMalchezaarBotIsEnfeebledTrigger(botAI); } - - static Trigger* prince_malchezaar_infernals_are_spawned( - PlayerbotAI* botAI) { return new PrinceMalchezaarInfernalsAreSpawnedTrigger(botAI); } - - static Trigger* prince_malchezaar_boss_engaged_by_main_tank( - PlayerbotAI* botAI) { return new PrinceMalchezaarBossEngagedByMainTankTrigger(botAI); } + static Trigger* prince_malchezaar_bot_is_enfeebled(PlayerbotAI* botAI) { + return new PrinceMalchezaarBotIsEnfeebledTrigger(botAI); + } + static Trigger* prince_malchezaar_infernals_are_spawned(PlayerbotAI* botAI) { + return new PrinceMalchezaarInfernalsAreSpawnedTrigger(botAI); + } + static Trigger* prince_malchezaar_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new PrinceMalchezaarBossEngagedByMainTankTrigger(botAI); + } // Nightbane - static Trigger* nightbane_boss_engaged_by_main_tank( - PlayerbotAI* botAI) { return new NightbaneBossEngagedByMainTankTrigger(botAI); } - - static Trigger* nightbane_ranged_bots_are_in_charred_earth( - PlayerbotAI* botAI) { return new NightbaneRangedBotsAreInCharredEarthTrigger(botAI); } - - static Trigger* nightbane_main_tank_is_susceptible_to_fear( - PlayerbotAI* botAI) { return new NightbaneMainTankIsSusceptibleToFearTrigger(botAI); } - - static Trigger* nightbane_pets_ignore_collision_to_chase_flying_boss( - PlayerbotAI* botAI) { return new NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger(botAI); } - - static Trigger* nightbane_boss_is_flying( - PlayerbotAI* botAI) { return new NightbaneBossIsFlyingTrigger(botAI); } - - static Trigger* nightbane_need_to_manage_timers_and_trackers( - PlayerbotAI* botAI) { return new NightbaneNeedToManageTimersAndTrackersTrigger(botAI); } + static Trigger* nightbane_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new NightbaneBossEngagedByMainTankTrigger(botAI); + } + static Trigger* nightbane_ranged_bots_are_in_charred_earth(PlayerbotAI* botAI) { + return new NightbaneRangedBotsAreInCharredEarthTrigger(botAI); + } + static Trigger* nightbane_main_tank_is_susceptible_to_fear(PlayerbotAI* botAI) { + return new NightbaneMainTankIsSusceptibleToFearTrigger(botAI); + } + static Trigger* nightbane_pets_ignore_collision_to_chase_flying_boss(PlayerbotAI* botAI) { + return new NightbanePetsIgnoreCollisionToChaseFlyingBossTrigger(botAI); + } + static Trigger* nightbane_boss_is_flying(PlayerbotAI* botAI) { + return new NightbaneBossIsFlyingTrigger(botAI); + } + static Trigger* nightbane_need_to_manage_timers_and_trackers(PlayerbotAI* botAI) { + return new NightbaneNeedToManageTimersAndTrackersTrigger(botAI); + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggers.cpp similarity index 82% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggers.cpp index 3c43aa898..2928f24aa 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggers.cpp @@ -1,6 +1,12 @@ -#include "RaidKarazhanTriggers.h" -#include "RaidKarazhanHelpers.h" -#include "RaidKarazhanActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "KaraTriggers.h" +#include "KaraHelpers.h" +#include "KaraActions.h" #include "Playerbots.h" #include "RaidBossHelpers.h" @@ -14,11 +20,7 @@ bool ManaWarpIsAboutToExplodeTrigger::IsActive() bool AttumenTheHuntsmanNeedTargetPriorityTrigger::IsActive() { - if (botAI->IsHeal(bot)) - return false; - - Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); - return midnight != nullptr; + return AI_VALUE2(Unit*, "find target", "midnight"); } bool AttumenTheHuntsmanAttumenSpawnedTrigger::IsActive() @@ -41,11 +43,8 @@ bool AttumenTheHuntsmanAttumenIsMountedTrigger::IsActive() bool AttumenTheHuntsmanBossWipesAggroWhenMountingTrigger::IsActive() { - if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) - return false; - - Unit* midnight = AI_VALUE2(Unit*, "find target", "midnight"); - return midnight != nullptr; + return IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID) && + AI_VALUE2(Unit*, "find target", "midnight"); } bool MoroesBossEngagedByMainTankTrigger::IsActive() @@ -59,18 +58,8 @@ bool MoroesBossEngagedByMainTankTrigger::IsActive() bool MoroesNeedTargetPriorityTrigger::IsActive() { - if (!botAI->IsDps(bot)) - return false; - - Unit* dorothea = AI_VALUE2(Unit*, "find target", "baroness dorothea millstipe"); - Unit* catriona = AI_VALUE2(Unit*, "find target", "lady catriona von'indi"); - Unit* keira = AI_VALUE2(Unit*, "find target", "lady keira berrybuck"); - Unit* rafe = AI_VALUE2(Unit*, "find target", "baron rafe dreuger"); - Unit* robin = AI_VALUE2(Unit*, "find target", "lord robin daris"); - Unit* crispin = AI_VALUE2(Unit*, "find target", "lord crispin ference"); - - Unit* target = GetFirstAliveUnit({ dorothea, catriona, keira, rafe, robin, crispin }); - return target != nullptr; + return IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID) && + AI_VALUE2(Unit*, "find target", "moroes"); } bool MaidenOfVirtueHealersAreStunnedByRepentanceTrigger::IsActive() @@ -111,7 +100,7 @@ bool BigBadWolfBossIsChasingLittleRedRidingHoodTrigger::IsActive() bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() { - if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (!IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) return false; Unit* romulo = AI_VALUE2(Unit*, "find target", "romulo"); @@ -127,18 +116,16 @@ bool RomuloAndJulianneBothBossesRevivedTrigger::IsActive() bool WizardOfOzNeedTargetPriorityTrigger::IsActive() { - if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (!IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) return false; - Unit* dorothee = AI_VALUE2(Unit*, "find target", "dorothee"); - Unit* tito = AI_VALUE2(Unit*, "find target", "tito"); - Unit* roar = AI_VALUE2(Unit*, "find target", "roar"); - Unit* strawman = AI_VALUE2(Unit*, "find target", "strawman"); - Unit* tinhead = AI_VALUE2(Unit*, "find target", "tinhead"); - Unit* crone = AI_VALUE2(Unit*, "find target", "the crone"); + for (const char* name : GetOzTargets()) + { + if (AI_VALUE2(Unit*, "find target", name)) + return true; + } - Unit* target = GetFirstAliveUnit({ dorothee, tito, roar, strawman, tinhead, crone }); - return target != nullptr; + return false; } bool WizardOfOzStrawmanIsVulnerableToFireTrigger::IsActive() @@ -152,11 +139,8 @@ bool WizardOfOzStrawmanIsVulnerableToFireTrigger::IsActive() bool TheCuratorAstralFlareSpawnedTrigger::IsActive() { - if (!botAI->IsDps(bot)) - return false; - - Unit* flare = AI_VALUE2(Unit*, "find target", "astral flare"); - return flare != nullptr; + return IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID) && + AI_VALUE2(Unit*, "find target", "astral flare"); } bool TheCuratorBossEngagedByTanksTrigger::IsActive() @@ -179,7 +163,7 @@ bool TheCuratorBossAstralFlaresCastArcingSearTrigger::IsActive() bool TerestianIllhoofNeedTargetPriorityTrigger::IsActive() { - if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (!IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) return false; Unit* illhoof = AI_VALUE2(Unit*, "find target", "terestian illhoof"); @@ -203,7 +187,7 @@ bool ShadeOfAranFlameWreathIsActiveTrigger::IsActive() // Exclusion of Banish is so the player may Banish elementals if they wish bool ShadeOfAranConjuredElementalsSummonedTrigger::IsActive() { - if (!IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (!IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) return false; Unit* elemental = AI_VALUE2(Unit*, "find target", "conjured elemental"); @@ -280,7 +264,7 @@ bool NetherspiteBossIsBanishedTrigger::IsActive() bool NetherspiteNeedToManageTimersAndTrackersTrigger::IsActive() { - if (!botAI->IsTank(bot) && !IsMechanicTrackerBot(botAI, bot, KARAZHAN_MAP_ID, nullptr)) + if (!botAI->IsTank(bot) && !IsMechanicTrackerBot(bot, KARAZHAN_MAP_ID)) return false; Unit* netherspite = AI_VALUE2(Unit*, "find target", "netherspite"); diff --git a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggers.h similarity index 96% rename from modules/mod-playerbots/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggers.h index e3465ef1e..0edb8d753 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Karazhan/Trigger/RaidKarazhanTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/Kara/KaraTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDKARAZHANTRIGGERS_H -#define _PLAYERBOT_RAIDKARAZHANTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_KARATRIGGERS_H +#define PLAYERBOTS_KARATRIGGERS_H #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/RaidMcActionContext.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCActionContext.h similarity index 87% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/RaidMcActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/MC/MCActionContext.h index aaccb80d3..4e1e936e6 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/RaidMcActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCActionContext.h @@ -1,10 +1,16 @@ -#ifndef _PLAYERBOT_RAIDMCACTIONCONTEXT_H -#define _PLAYERBOT_RAIDMCACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCACTIONCONTEXT_H +#define PLAYERBOTS_MCACTIONCONTEXT_H #include "Action.h" #include "BossAuraActions.h" #include "NamedObjectContext.h" -#include "RaidMcActions.h" +#include "MCActions.h" class RaidMcActionContext : public NamedObjectContext { @@ -26,6 +32,7 @@ public: creators["mc golemagg assist tank attack core rager"] = &RaidMcActionContext::golemagg_assist_tank_attack_core_rager; creators["mc majordomo shadow resistance"] = &RaidMcActionContext::majordomo_shadow_resistance; creators["mc ragnaros fire resistance"] = &RaidMcActionContext::ragnaros_fire_resistance; + creators["mc core hound mark"] = &RaidMcActionContext::core_hound_mark; } private: @@ -44,6 +51,7 @@ private: static Action* golemagg_assist_tank_attack_core_rager(PlayerbotAI* botAI) { return new McGolemaggAssistTankAttackCoreRagerAction(botAI); } static Action* majordomo_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "majordomo executus"); } static Action* ragnaros_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "ragnaros"); } + static Action* core_hound_mark(PlayerbotAI* botAI) { return new McCoreHoundMarkAction(botAI); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Action/RaidMcActions.cpp b/modules/mod-playerbots/src/Ai/Raid/MC/MCActions.cpp similarity index 82% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Action/RaidMcActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/MC/MCActions.cpp index d2bc2edc1..63753e105 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Action/RaidMcActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCActions.cpp @@ -1,8 +1,14 @@ -#include "RaidMcActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MCActions.h" #include "Playerbots.h" #include "RtiTargetValue.h" -#include "RaidMcHelpers.h" +#include "MCHelpers.h" static constexpr float LIVING_BOMB_DISTANCE = 20.0f; static constexpr float INFERNO_DISTANCE = 20.0f; @@ -212,3 +218,45 @@ bool McGolemaggAssistTankAttackCoreRagerAction::Execute(Event event) return false; } + +Unit* McCoreHoundMarkAction::GetTarget() +{ + Unit* highestHealthHound = nullptr; + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) + { + Unit* unit = ref->GetOwner(); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CORE_HOUND) + { + if (!highestHealthHound || unit->GetHealth() > highestHealthHound->GetHealth()) + highestHealthHound = unit; + } + } + + if (!highestHealthHound) + return nullptr; + + Group* group = bot->GetGroup(); + ObjectGuid currentSkullGuid = group ? group->GetTargetIcon(RtiTargetValue::skullIndex) : ObjectGuid::Empty; + if (!currentSkullGuid.IsEmpty() && currentSkullGuid != highestHealthHound->GetGUID()) + { + // Only switch skull if the new target has meaningfully more health (10% buffer) to prevent rapid re-marking + if (Unit* currentSkullUnit = botAI->GetUnit(currentSkullGuid)) + if (currentSkullUnit->IsAlive() && highestHealthHound->GetHealth() <= currentSkullUnit->GetHealth() * 1.10f) + return nullptr; + } + + if (currentSkullGuid.IsEmpty() || currentSkullGuid != highestHealthHound->GetGUID()) + return highestHealthHound; + + return nullptr; +} + +bool McCoreHoundMarkAction::Execute(Event /*event*/) +{ + Unit* target = GetTarget(); + if (!target) + return false; + + bot->GetGroup()->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), target->GetGUID()); + return true; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Action/RaidMcActions.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCActions.h similarity index 78% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Action/RaidMcActions.h rename to modules/mod-playerbots/src/Ai/Raid/MC/MCActions.h index 680b311d3..8bd58e09b 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Action/RaidMcActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDMCACTIONS_H -#define _PLAYERBOT_RAIDMCACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCACTIONS_H +#define PLAYERBOTS_MCACTIONS_H #include "AttackAction.h" #include "MovementActions.h" @@ -64,4 +70,13 @@ public: bool Execute(Event event) override; }; +class McCoreHoundMarkAction : public Action +{ +public: + McCoreHoundMarkAction(PlayerbotAI* botAI, std::string const name = "mc core hound mark") + : Action(botAI, name) {}; + Unit* GetTarget() override; + bool Execute(Event event) override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/MC/MCHelpers.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCHelpers.h new file mode 100644 index 000000000..bdd97f32a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCHelpers.h @@ -0,0 +1,31 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCHELPERS_H +#define PLAYERBOTS_MCHELPERS_H + +namespace MoltenCoreHelpers +{ +enum MoltenCoreNPCs +{ + // Golemagg + NPC_CORE_RAGER = 11672, + + // Core Hound (trash) + NPC_CORE_HOUND = 11671, +}; +enum MoltenCoreSpells +{ + // Baron Geddon + SPELL_INFERNO = 19695, + SPELL_LIVING_BOMB = 20475, + + // Golemagg + SPELL_GOLEMAGGS_TRUST = 20553, +}; +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Multiplier/RaidMcMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/MC/MCMultipliers.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Multiplier/RaidMcMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Raid/MC/MCMultipliers.cpp index d1ee936b0..523afca22 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Multiplier/RaidMcMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCMultipliers.cpp @@ -1,4 +1,10 @@ -#include "RaidMcMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MCMultipliers.h" #include "Playerbots.h" #include "ChooseTargetActions.h" @@ -9,8 +15,8 @@ #include "ShamanActions.h" #include "WarriorActions.h" #include "DKActions.h" -#include "RaidMcActions.h" -#include "RaidMcHelpers.h" +#include "MCActions.h" +#include "MCHelpers.h" using namespace MoltenCoreHelpers; diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Multiplier/RaidMcMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCMultipliers.h similarity index 67% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Multiplier/RaidMcMultipliers.h rename to modules/mod-playerbots/src/Ai/Raid/MC/MCMultipliers.h index 56dc7a5f8..7dee76811 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Multiplier/RaidMcMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDMCMULTIPLIERS_H -#define _PLAYERBOT_RAIDMCMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCMULTIPLIERS_H +#define PLAYERBOTS_MCMULTIPLIERS_H #include "Multiplier.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/MC/MCStrategy.cpp similarity index 87% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/MC/MCStrategy.cpp index 3eddb76a7..74d5fe1d3 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCStrategy.cpp @@ -1,6 +1,12 @@ -#include "RaidMcStrategy.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidMcMultipliers.h" +#include "MCStrategy.h" + +#include "MCMultipliers.h" #include "Strategy.h" void RaidMcStrategy::InitTriggers(std::vector& triggers) @@ -71,6 +77,11 @@ void RaidMcStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("mc ragnaros fire resistance", { NextAction("mc ragnaros fire resistance", ACTION_RAID) })); + + // Trash + triggers.push_back( + new TriggerNode("mc core hound mark", + { NextAction("mc core hound mark", ACTION_RAID) })); } void RaidMcStrategy::InitMultipliers(std::vector& multipliers) diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCStrategy.h similarity index 52% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/MC/MCStrategy.h index 45b503e93..af1acdd0d 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Strategy/RaidMcStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCStrategy.h @@ -1,8 +1,12 @@ -#ifndef _PLAYERBOT_RAIDMCSTRATEGY_H -#define _PLAYERBOT_RAIDMCSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCSTRATEGY_H +#define PLAYERBOTS_MCSTRATEGY_H -#include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidMcStrategy : public Strategy diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCTriggerContext.h similarity index 86% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/MC/MCTriggerContext.h index a62d851dc..dfd6e3ac5 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/RaidMcTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCTriggerContext.h @@ -1,10 +1,15 @@ -#ifndef _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCTRIGGERCONTEXT_H +#define PLAYERBOTS_MCTRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "BossAuraTriggers.h" #include "NamedObjectContext.h" -#include "RaidMcTriggers.h" +#include "MCTriggers.h" class RaidMcTriggerContext : public NamedObjectContext { @@ -26,6 +31,7 @@ public: creators["mc golemagg is assist tank"] = &RaidMcTriggerContext::golemagg_is_assist_tank; creators["mc majordomo shadow resistance"] = &RaidMcTriggerContext::majordomo_shadow_resistance; creators["mc ragnaros fire resistance"] = &RaidMcTriggerContext::ragnaros_fire_resistance; + creators["mc core hound mark"] = &RaidMcTriggerContext::core_hound_mark; } private: @@ -44,6 +50,7 @@ private: static Trigger* golemagg_is_assist_tank(PlayerbotAI* botAI) { return new McGolemaggIsAssistTankTrigger(botAI); } static Trigger* majordomo_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "majordomo executus"); } static Trigger* ragnaros_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "ragnaros"); } + static Trigger* core_hound_mark(PlayerbotAI* botAI) { return new McCoreHoundMarkTrigger(botAI); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Trigger/RaidMcTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/MC/MCTriggers.cpp similarity index 70% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Trigger/RaidMcTriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/MC/MCTriggers.cpp index 834d703d3..f9877a86c 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Trigger/RaidMcTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCTriggers.cpp @@ -1,7 +1,13 @@ -#include "RaidMcTriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MCTriggers.h" #include "SharedDefines.h" -#include "RaidMcHelpers.h" +#include "MCHelpers.h" using namespace MoltenCoreHelpers; @@ -38,3 +44,8 @@ bool McGolemaggIsAssistTankTrigger::IsActive() { return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsAssistTank(bot); } + +bool McCoreHoundMarkTrigger::IsActive() +{ + return PlayerbotAI::IsMainTank(bot) && AI_VALUE2(Unit*, "find target", "core hound"); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Trigger/RaidMcTriggers.h b/modules/mod-playerbots/src/Ai/Raid/MC/MCTriggers.h similarity index 71% rename from modules/mod-playerbots/src/Ai/Raid/MoltenCore/Trigger/RaidMcTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/MC/MCTriggers.h index 4cd84a2df..670ebf098 100644 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Trigger/RaidMcTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/MC/MCTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDMCTRIGGERS_H -#define _PLAYERBOT_RAIDMCTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MCTRIGGERS_H +#define PLAYERBOTS_MCTRIGGERS_H #include "PlayerbotAI.h" #include "Playerbots.h" @@ -47,4 +53,11 @@ public: bool IsActive() override; }; +class McCoreHoundMarkTrigger : public Trigger +{ +public: + McCoreHoundMarkTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc core hound mark") {} + bool IsActive() override; +}; + #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Mag/MagActionContext.h new file mode 100644 index 000000000..c0a4bda1a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagActionContext.h @@ -0,0 +1,94 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGACTIONCONTEXT_H +#define PLAYERBOTS_MAGACTIONCONTEXT_H + +#include "MagActions.h" +#include "NamedObjectContext.h" + +class RaidMagtheridonActionContext : public NamedObjectContext +{ +public: + RaidMagtheridonActionContext() + { + creators["magtheridon main tank attack first three channelers"] = + &RaidMagtheridonActionContext::magtheridon_main_tank_attack_first_three_channelers; + + creators["magtheridon first assist tank attack nw channeler"] = + &RaidMagtheridonActionContext::magtheridon_first_assist_tank_attack_nw_channeler; + + creators["magtheridon second assist tank attack ne channeler"] = + &RaidMagtheridonActionContext::magtheridon_second_assist_tank_attack_ne_channeler; + + creators["magtheridon misdirect hellfire channelers to main tank"] = + &RaidMagtheridonActionContext::magtheridon_misdirect_hellfire_channelers_to_main_tank; + + creators["magtheridon assign dps priority"] = + &RaidMagtheridonActionContext::magtheridon_assign_dps_priority; + + creators["magtheridon warlock cc burning abyssal"] = + &RaidMagtheridonActionContext::magtheridon_warlock_cc_burning_abyssal; + + creators["magtheridon main tank position boss"] = + &RaidMagtheridonActionContext::magtheridon_main_tank_position_boss; + + creators["magtheridon spread ranged"] = + &RaidMagtheridonActionContext::magtheridon_spread_ranged; + + creators["magtheridon move out of debris"] = + &RaidMagtheridonActionContext::magtheridon_move_out_of_debris; + + creators["magtheridon use manticron cube"] = + &RaidMagtheridonActionContext::magtheridon_use_manticron_cube; + + creators["magtheridon manage timers and assignments"] = + &RaidMagtheridonActionContext::magtheridon_manage_timers_and_assignments; + + creators["magtheridon erase timers and trackers"] = + &RaidMagtheridonActionContext::magtheridon_erase_timers_and_trackers; + } + +private: + static Action* magtheridon_main_tank_attack_first_three_channelers(PlayerbotAI* botAI) { + return new MagtheridonMainTankAttackFirstThreeChannelersAction(botAI); + } + static Action* magtheridon_first_assist_tank_attack_nw_channeler(PlayerbotAI* botAI) { + return new MagtheridonFirstAssistTankAttackNWChannelerAction(botAI); + } + static Action* magtheridon_second_assist_tank_attack_ne_channeler(PlayerbotAI* botAI) { + return new MagtheridonSecondAssistTankAttackNEChannelerAction(botAI); + } + static Action* magtheridon_misdirect_hellfire_channelers_to_main_tank(PlayerbotAI* botAI) { + return new MagtheridonMisdirectHellfireChannelersToMainTankAction(botAI); + } + static Action* magtheridon_assign_dps_priority(PlayerbotAI* botAI) { + return new MagtheridonAssignDpsPriorityAction(botAI); + } + static Action* magtheridon_warlock_cc_burning_abyssal(PlayerbotAI* botAI) { + return new MagtheridonWarlockCcBurningAbyssalAction(botAI); + } + static Action* magtheridon_main_tank_position_boss(PlayerbotAI* botAI) { + return new MagtheridonMainTankPositionBossAction(botAI); + } + static Action* magtheridon_spread_ranged(PlayerbotAI* botAI) { + return new MagtheridonSpreadRangedAction(botAI); + } + static Action* magtheridon_move_out_of_debris(PlayerbotAI* botAI) { + return new MagtheridonMoveOutOfDebrisAction(botAI); + } + static Action* magtheridon_use_manticron_cube(PlayerbotAI* botAI) { + return new MagtheridonUseManticronCubeAction(botAI); + } + static Action* magtheridon_manage_timers_and_assignments(PlayerbotAI* botAI) { + return new MagtheridonManageTimersAndAssignmentsAction(botAI); + } + static Action* magtheridon_erase_timers_and_trackers(PlayerbotAI* botAI) { + return new MagtheridonEraseTimersAndTrackersAction(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.cpp new file mode 100644 index 000000000..7080c82e9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.cpp @@ -0,0 +1,757 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MagActions.h" +#include "MagHelpers.h" +#include "Creature.h" +#include "ObjectAccessor.h" +#include "ObjectGuid.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace MagtheridonHelpers; + +bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event /*event*/) +{ + Creature* channelerTarget = nullptr; + if (Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER)) + { + channelerTarget = channelerSquare; + if (MarkTargetWithSquare(bot, channelerSquare)) + return true; + + SetRtiTarget(botAI, "square", channelerSquare); + } + else if (Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER)) + { + channelerTarget = channelerStar; + if (MarkTargetWithStar(bot, channelerStar)) + return true; + + SetRtiTarget(botAI, "star", channelerStar); + } + else if (Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER)) + { + channelerTarget = channelerCircle; + if (MarkTargetWithCircle(bot, channelerCircle)) + return true; + + SetRtiTarget(botAI, "circle", channelerCircle); + } + + if (channelerTarget && AI_VALUE(Unit*, "current target") != channelerTarget) + return Attack(channelerTarget); + + // After first three channelers are dead, wait for Magtheridon to activate + if (!channelerTarget) + { + const Position& position = WAITING_FOR_MAGTHERIDON_POSITION; + if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + { + return MoveTo(MAGTHERIDON_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + bot->SetFacingTo(position.GetOrientation()); + return true; + } + + return false; +} + +bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event /*event*/) +{ + Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); + if (!channelerDiamond) + return false; + + if (MarkTargetWithDiamond(bot, channelerDiamond)) + return true; + + SetRtiTarget(botAI, "diamond", channelerDiamond); + + if (AI_VALUE(Unit*, "current target") != channelerDiamond) + return Attack(channelerDiamond); + + if (channelerDiamond->GetVictim() == bot) + { + const Position& position = NW_CHANNELER_TANK_POSITION; + const float distanceToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distanceToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(distanceToPosition, 3.5f); + const float moveX = bot->GetPositionX() + (dX / distanceToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distanceToPosition) * moveDist; + + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event /*event*/) +{ + Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); + if (!channelerTriangle) + return false; + + if (MarkTargetWithTriangle(bot, channelerTriangle)) + return true; + + SetRtiTarget(botAI, "triangle", channelerTriangle); + + if (AI_VALUE(Unit*, "current target") != channelerTriangle) + return Attack(channelerTriangle); + + if (channelerTriangle->GetVictim() == bot) + { + const Position& position = NE_CHANNELER_TANK_POSITION; + const float distanceToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distanceToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(distanceToPosition, 3.5f); + const float moveX = bot->GetPositionX() + (dX / distanceToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distanceToPosition) * moveDist; + + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// Misdirect West & East Channelers to Main Tank +bool MagtheridonMisdirectHellfireChannelersToMainTankAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && + GET_PLAYERBOT_AI(member)) + { + hunters.push_back(member); + } + } + + int hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + + Player* mainTank = GetGroupMainTank(botAI, bot); + + Creature* targetChanneler = nullptr; + if (hunterIndex == 0) + targetChanneler = GetChanneler(bot, WEST_CHANNELER); + else if (hunterIndex == 1) + targetChanneler = GetChanneler(bot, EAST_CHANNELER); + + if (mainTank && targetChanneler) + { + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (!bot->HasAura(static_cast(MagtheridonSpells::SPELL_MISDIRECTION))) + return false; + + if (botAI->CanCastSpell("steady shot", targetChanneler)) + return botAI->CastSpell("steady shot", targetChanneler); + } + + return false; +} + +bool MagtheridonAssignDpsPriorityAction::Execute(Event /*event*/) +{ + // Listed in order of priority + if (Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER)) + { + if (MarkTargetWithSquare(bot, channelerSquare)) + return true; + + SetRtiTarget(botAI, "square", channelerSquare); + + if (AI_VALUE(Unit*, "current target") != channelerSquare) + return Attack(channelerSquare); + } + else if (Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER)) + { + if (MarkTargetWithStar(bot, channelerStar)) + return true; + + SetRtiTarget(botAI, "star", channelerStar); + + if (AI_VALUE(Unit*, "current target") != channelerStar) + return Attack(channelerStar); + } + else if (Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER)) + { + if (MarkTargetWithCircle(bot, channelerCircle)) + return true; + + SetRtiTarget(botAI, "circle", channelerCircle); + + if (AI_VALUE(Unit*, "current target") != channelerCircle) + return Attack(channelerCircle); + } + else if (Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER)) + { + if (MarkTargetWithDiamond(bot, channelerDiamond)) + return true; + + SetRtiTarget(botAI, "diamond", channelerDiamond); + + if (AI_VALUE(Unit*, "current target") != channelerDiamond) + return Attack(channelerDiamond); + } + else if (Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER)) + { + if (MarkTargetWithTriangle(bot, channelerTriangle)) + return true; + + SetRtiTarget(botAI, "triangle", channelerTriangle); + + if (AI_VALUE(Unit*, "current target") != channelerTriangle) + return Attack(channelerTriangle); + } + else if (Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon")) + { + SetRtiTarget(botAI, "cross", magtheridon); + + if (AI_VALUE(Unit*, "current target") != magtheridon) + return Attack(magtheridon); + } + + return false; +} + +// Assign Burning Abyssals to Warlocks to Banish +// Burning Abyssals in excess of Warlocks in party will be Feared +bool MagtheridonWarlockCcBurningAbyssalAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector abyssals; + std::list creatureList; + constexpr float searchRadius = 100.0f; + + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(MagtheridonNpcs::NPC_BURNING_ABYSSAL), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + abyssals.push_back(creature); + } + + std::vector warlocks; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_WARLOCK && + GET_PLAYERBOT_AI(member)) + { + warlocks.push_back(member); + } + } + + int warlockIndex = -1; + for (size_t i = 0; i < warlocks.size(); ++i) + { + if (warlocks[i] == bot) + { + warlockIndex = static_cast(i); + break; + } + } + + if (warlockIndex >= 0 && warlockIndex < abyssals.size()) + { + Unit* assignedAbyssal = abyssals[warlockIndex]; + if (!botAI->HasAura("banish", assignedAbyssal) && + botAI->CanCastSpell("banish", assignedAbyssal)) + { + return botAI->CastSpell("banish", assignedAbyssal); + } + } + + for (size_t i = warlocks.size(); i < abyssals.size(); ++i) + { + Unit* excessAbyssal = abyssals[i]; + if (!botAI->HasAura("banish", excessAbyssal) && + !botAI->HasAura("fear", excessAbyssal) && + botAI->CanCastSpell("fear", excessAbyssal)) + { + return botAI->CastSpell("fear", excessAbyssal); + } + } + + return false; +} + +// Main tank will back up to the Eastern point of the room +bool MagtheridonMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + if (MarkTargetWithCross(bot, magtheridon)) + return true; + + SetRtiTarget(botAI, "cross", magtheridon); + + if (AI_VALUE(Unit*, "current target") != magtheridon) + return Attack(magtheridon); + + if (magtheridon->GetVictim() == bot && bot->GetHealthPct() > 50.0f) + { + const Position& position = MAGTHERIDON_TANK_POSITION; + const float distanceToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distanceToPosition > 3.0f) + { + const float dX = position.GetPositionX() - bot->GetPositionX(); + const float dY = position.GetPositionY() - bot->GetPositionY(); + const float moveDist = std::min(2.25f, distanceToPosition); + const float moveX = bot->GetPositionX() + (dX / distanceToPosition) * moveDist; + const float moveY = bot->GetPositionY() + (dY / distanceToPosition) * moveDist; + + return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +// Ranged stay away from Magtheridon and other players +// Magtheridon's CombatReach is 12 yards and BoundingRadius is 4 yards +bool MagtheridonSpreadRangedAction::Execute(Event /*event*/) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + if (IsCubeClicker(bot)) + { + auto timerIt = blastNovaTimer.find(magtheridon->GetMap()->GetInstanceId()); + if (timerIt != blastNovaTimer.end() && + time(nullptr) - timerIt->second >= BLAST_NOVA_INTERIM_SECONDS) + { + return false; + } + } + + constexpr float safeDistFromBoss = 10.0f; + const float currentDistance = bot->GetDistance2d(magtheridon); + if (currentDistance < safeDistFromBoss) + return MoveAway(magtheridon, safeDistFromBoss - currentDistance); + + constexpr float safeDistFromPlayer = 6.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistFromPlayer)) + return FleePosition(nearestPlayer->GetPosition(), safeDistFromPlayer, minInterval); + + return false; +} + +// For bots that are assigned to click cubes +// Magtheridon casts Blast Nova every 54.35 to 55.40s, with a 2s cast time +bool MagtheridonUseManticronCubeAction::Execute(Event /*event*/) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + CubeInfo const* cubeInfo = GetAssignedCube(); + if (!cubeInfo) + return false; + + GameObject* cube = botAI->GetGameObject(cubeInfo->guid); + if (!cube) + return false; + + // Release cubes after Blast Nova is interrupted + if (HandleCubeRelease(magtheridon)) + return true; + + // Handle active cube logic based on Blast Nova casting state + const bool blastNovaActive = + magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId( + static_cast(MagtheridonSpells::SPELL_BLAST_NOVA)); + + // If Blast Nova is actively casting, always try to click the cube + if (blastNovaActive) + return HandleCubeInteraction(*cubeInfo, cube); + + // Otherwise, if Blast Nova is coming soon, move to and wait near the cube + return HandleWaitingPhase(*cubeInfo); +} + +CubeInfo const* MagtheridonUseManticronCubeAction::GetAssignedCube() +{ + auto mapIt = botToCubeAssignments.find(bot->GetMap()->GetInstanceId()); + if (mapIt == botToCubeAssignments.end()) + return nullptr; + + auto it = mapIt->second.find(bot->GetGUID()); + return it != mapIt->second.end() ? &it->second : nullptr; +} + +bool MagtheridonUseManticronCubeAction::HandleCubeRelease(Unit* magtheridon) +{ + if (bot->HasAura(static_cast(MagtheridonSpells::SPELL_SHADOW_GRASP)) && + !(magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId( + static_cast(MagtheridonSpells::SPELL_BLAST_NOVA)))) + { + uint32 delay = urand(200, 3000); + botAI->AddTimedEvent( + [this] + { + botAI->Reset(); + }, + delay); + botAI->SetNextCheckDelay(delay + 50); + return true; + } + + return false; +} + +bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeInfo) +{ + auto timerIt = blastNovaTimer.find(bot->GetMap()->GetInstanceId()); + if (timerIt == blastNovaTimer.end() || + time(nullptr) - timerIt->second < BLAST_NOVA_INTERIM_SECONDS) + { + return false; + } + + constexpr float safeWaitDistance = 8.0f; + + if (fabs(bot->GetDistance2d(cubeInfo.x, cubeInfo.y) - safeWaitDistance) <= 1.0f) + return true; + + if (Position safePos; FindSafePositionNearCube(cubeInfo, safeWaitDistance, safePos)) + { + botAI->InterruptSpell(); + return MoveTo(MAGTHERIDON_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool MagtheridonUseManticronCubeAction::FindSafePositionNearCube( + const CubeInfo& cubeInfo, float preferredDistance, Position& outPos) +{ + constexpr float angleStep = M_PI / 8.0f; + + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float angle = 0.0f; angle < 2.0f * M_PI; angle += angleStep) + { + const float x = cubeInfo.x + std::cos(angle) * preferredDistance; + const float y = cubeInfo.y + std::sin(angle) * preferredDistance; + + if (IsPositionInActiveDebris(bot->GetMap()->GetInstanceId(), x, y)) + continue; + + if (IsPositionInActiveConflagration(botAI, bot, x, y)) + continue; + + const float moveDistance = bot->GetExactDist2d(x, y); + if (moveDistance < minMoveDistance) + { + outPos = Position(x, y, bot->GetPositionZ()); + minMoveDistance = moveDistance; + foundSafe = true; + } + } + + return foundSafe; +} + +bool MagtheridonUseManticronCubeAction::HandleCubeInteraction( + const CubeInfo& cubeInfo, GameObject* cube) +{ + constexpr float interactDistance = 1.0f; + const float cubeDist = bot->GetDistance2d(cubeInfo.x, cubeInfo.y); + + if (cubeDist < interactDistance + 1.0f) + { + uint32 delay = urand(200, 1500); + botAI->AddTimedEvent( + [this, cube] + { + bot->StopMoving(); + cube->Use(bot); + }, + delay); + botAI->SetNextCheckDelay(delay + 50); + return true; + } + + botAI->InterruptSpell(); + return MoveTo(cube, interactDistance, MovementPriority::MOVEMENT_FORCED); +} + +bool MagtheridonMoveOutOfDebrisAction::Execute(Event /*event*/) +{ + if (Position safePos; FindSafePosition(safePos)) + { + botAI->InterruptSpell(); + return MoveTo(MAGTHERIDON_MAP_ID, safePos.GetPositionX(), safePos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool MagtheridonMoveOutOfDebrisAction::FindSafePosition(Position& outPos) +{ + constexpr float maxSearchRadius = 20.0f; + constexpr float distanceStep = 1.0f; + constexpr float angleStep = M_PI / 12.0f; + + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + uint32 const instanceId = bot->GetMap()->GetInstanceId(); + + for (float distance = 2.0f; distance <= maxSearchRadius; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2.0f * M_PI; angle += angleStep) + { + float const x = bot->GetPositionX() + distance * std::cos(angle); + float const y = bot->GetPositionY() + distance * std::sin(angle); + + if (IsPositionInActiveDebris(instanceId, x, y)) + continue; + + if (IsPositionInActiveConflagration(botAI, bot, x, y)) + continue; + + float const moveDistance = bot->GetExactDist2d(x, y); + + if (!foundSafe || moveDistance < minMoveDistance) + { + outPos = Position(x, y, bot->GetPositionZ()); + minMoveDistance = moveDistance; + foundSafe = true; + } + } + } + + return foundSafe; +} + +bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event /*event*/) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return false; + + const uint32 instanceId = magtheridon->GetMap()->GetInstanceId(); + const time_t now = time(nullptr); + + const bool blastNovaActive = + magtheridon->HasUnitState(UNIT_STATE_CASTING) && + magtheridon->FindCurrentSpellBySpellId( + static_cast(MagtheridonSpells::SPELL_BLAST_NOVA)); + + if (!lastBlastNovaState[instanceId] && blastNovaActive) + blastNovaTimer[instanceId] = now; + + lastBlastNovaState[instanceId] = blastNovaActive; + + bool updated = false; + + if (IsMagtheridonActive(magtheridon)) + { + if (blastNovaTimer.try_emplace(instanceId, now).second) + updated = true; + + if (dpsWaitTimer.try_emplace(instanceId, now).second) + updated = true; + + if (magtheridon->GetHealthPct() < 30.0f && !ceilingCollapseApplied[instanceId]) + { + blastNovaTimer[instanceId] += 18; + ceilingCollapseApplied[instanceId] = true; + updated = true; + } + + if (NeedsCubeReassignment(instanceId) && AssignCubeClickers()) + updated = true; + } + else + { + if (blastNovaTimer.erase(instanceId) > 0) + updated = true; + + if (dpsWaitTimer.erase(instanceId) > 0) + updated = true; + + if (botToCubeAssignments.erase(instanceId) > 0) + updated = true; + + if (ceilingCollapseApplied.erase(instanceId) > 0) + updated = true; + + if (lastBlastNovaState.erase(instanceId) > 0) + updated = true; + } + + return updated; +} + +bool MagtheridonManageTimersAndAssignmentsAction::AssignCubeClickers() +{ + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + std::vector cubes = GetAllCubeInfosByDbGuids(bot->GetMap(), MANTICRON_CUBE_DB_GUIDS); + + auto& assignment = botToCubeAssignments[instanceId]; + Group* group = bot->GetGroup(); + if (!group || cubes.empty()) + { + assignment.clear(); + return true; + } + + // Prune dead or absent players from the existing assignment + for (auto it = assignment.begin(); it != assignment.end(); ) + { + Player* player = ObjectAccessor::FindPlayer(it->first); + if (!player || !player->IsAlive() || player->GetMapId() != MAGTHERIDON_MAP_ID) + it = assignment.erase(it); + else + ++it; + } + + // Fill unassigned cubes + for (CubeInfo const& cube : cubes) + { + bool alreadyAssigned = false; + for (auto const& pair : assignment) + { + if (pair.second.guid == cube.guid) + { + alreadyAssigned = true; + break; + } + } + if (alreadyAssigned) + continue; + + Player* candidate = nullptr; + + // Pass 1: ranged DPS excluding warlocks + for (GroupReference* ref = group->GetFirstMember(); + ref && !candidate; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsRangedDps(member) || + !GET_PLAYERBOT_AI(member) || member->getClass() == CLASS_WARLOCK) + continue; + + if (assignment.find(member->GetGUID()) != assignment.end()) + continue; + + candidate = member; + } + + // Pass 2: any non-tank bot + if (!candidate) + { + for (GroupReference* ref = group->GetFirstMember(); + ref && !candidate; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || botAI->IsTank(member) || + !GET_PLAYERBOT_AI(member)) + continue; + + if (assignment.find(member->GetGUID()) != assignment.end()) + continue; + + candidate = member; + } + } + + if (candidate) + assignment[candidate->GetGUID()] = cube; + } + + return true; +} + +bool MagtheridonManageTimersAndAssignmentsAction::NeedsCubeReassignment(const uint32 instanceId) +{ + auto mapIt = botToCubeAssignments.find(instanceId); + if (mapIt == botToCubeAssignments.end() || mapIt->second.empty()) + return true; + + for (auto const& pair : mapIt->second) + { + Player* assigned = ObjectAccessor::FindPlayer(pair.first); + if (!assigned || !assigned->IsAlive()) + return true; + } + + return false; +} + +bool MagtheridonEraseTimersAndTrackersAction::Execute(Event /*event*/) +{ + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + bool erased = false; + + if (blastNovaTimer.erase(instanceId) > 0) + erased = true; + if (dpsWaitTimer.erase(instanceId) > 0) + erased = true; + if (ceilingCollapseApplied.erase(instanceId) > 0) + erased = true; + if (lastBlastNovaState.erase(instanceId) > 0) + erased = true; + if (botToCubeAssignments.erase(instanceId) > 0) + erased = true; + if (activeDebrisPositions.erase(instanceId) > 0) + erased = true; + + return erased; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.h b/modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.h new file mode 100644 index 000000000..f3787fbbf --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagActions.h @@ -0,0 +1,125 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGACTIONS_H +#define PLAYERBOTS_MAGACTIONS_H + +#include "MagHelpers.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +class MagtheridonMainTankAttackFirstThreeChannelersAction : public AttackAction +{ +public: + MagtheridonMainTankAttackFirstThreeChannelersAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon main tank attack first three channelers") {} + bool Execute(Event event) override; +}; + +class MagtheridonFirstAssistTankAttackNWChannelerAction : public AttackAction +{ +public: + MagtheridonFirstAssistTankAttackNWChannelerAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon first assist tank attack nw channeler") {} + bool Execute(Event event) override; +}; + +class MagtheridonSecondAssistTankAttackNEChannelerAction : public AttackAction +{ +public: + MagtheridonSecondAssistTankAttackNEChannelerAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon second assist tank attack ne channeler") {} + bool Execute(Event event) override; +}; + +class MagtheridonMisdirectHellfireChannelersToMainTankAction : public AttackAction +{ +public: + MagtheridonMisdirectHellfireChannelersToMainTankAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon misdirect hellfire channelers to main tank") {} + bool Execute(Event event) override; +}; + +class MagtheridonAssignDpsPriorityAction : public AttackAction +{ +public: + MagtheridonAssignDpsPriorityAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon assign dps priority") {} + bool Execute(Event event) override; +}; + +class MagtheridonWarlockCcBurningAbyssalAction : public AttackAction +{ +public: + MagtheridonWarlockCcBurningAbyssalAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon warlock cc burning abyssal") {} + bool Execute(Event event) override; +}; + +class MagtheridonMainTankPositionBossAction : public AttackAction +{ +public: + MagtheridonMainTankPositionBossAction( + PlayerbotAI* botAI) : AttackAction(botAI, "magtheridon main tank position boss") {} + bool Execute(Event event) override; +}; + +class MagtheridonSpreadRangedAction : public MovementAction +{ +public: + MagtheridonSpreadRangedAction( + PlayerbotAI* botAI) : MovementAction(botAI, "magtheridon spread ranged") {} + bool Execute(Event event) override; +}; + +class MagtheridonMoveOutOfDebrisAction : public MovementAction +{ +public: + MagtheridonMoveOutOfDebrisAction( + PlayerbotAI* botAI) : MovementAction(botAI, "magtheridon move out of debris") {} + bool Execute(Event event) override; + +private: + bool FindSafePosition(Position& outPos); +}; + +class MagtheridonUseManticronCubeAction : public MovementAction +{ +public: + MagtheridonUseManticronCubeAction( + PlayerbotAI* botAI) : MovementAction(botAI, "magtheridon use manticron cube") {} + bool Execute(Event event) override; + +private: + MagtheridonHelpers::CubeInfo const* GetAssignedCube(); + bool HandleCubeRelease(Unit* magtheridon); + bool HandleWaitingPhase(const MagtheridonHelpers::CubeInfo& cubeInfo); + bool HandleCubeInteraction(const MagtheridonHelpers::CubeInfo& cubeInfo, GameObject* cube); + bool FindSafePositionNearCube(const MagtheridonHelpers::CubeInfo& cubeInfo, float preferredDistance, Position& outPos); +}; + +class MagtheridonManageTimersAndAssignmentsAction : public Action +{ +public: + MagtheridonManageTimersAndAssignmentsAction( + PlayerbotAI* botAI) : Action(botAI, "magtheridon manage timers and assignments") {} + bool Execute(Event event) override; + +private: + bool AssignCubeClickers(); + bool NeedsCubeReassignment(uint32 instanceId); +}; + +class MagtheridonEraseTimersAndTrackersAction : public Action +{ +public: + MagtheridonEraseTimersAndTrackersAction( + PlayerbotAI* botAI, std::string const name = "magtheridon erase timers and trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.cpp new file mode 100644 index 000000000..f5b3a3843 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.cpp @@ -0,0 +1,141 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include + +#include "MagMultipliers.h" +#include "MagActions.h" +#include "MagHelpers.h" +#include "DruidBearActions.h" +#include "DKActions.h" +#include "ChooseTargetActions.h" +#include "FollowActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "MovementActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "ReachTargetActions.h" +#include "WarriorActions.h" +#include "WipeAction.h" + +using namespace MagtheridonHelpers; + +// When a cube clicker is in the handling phase (waiting near cube or moving +// to use), suppress movement actions that would pull them away from the cube +float MagtheridonUseManticronCubeMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "magtheridon")) + return 1.0f; + + if (!IsCubeClicker(bot)) + return 1.0f; + + auto timerIt = blastNovaTimer.find(bot->GetMap()->GetInstanceId()); + if (timerIt == blastNovaTimer.end() || + time(nullptr) - timerIt->second < BLAST_NOVA_INTERIM_SECONDS) + { + return 1.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +// Wait for 6 seconds after Magtheridon becomes attackable before engaging +float MagtheridonWaitToAttackMultiplier::GetValue(Action* action) +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon || !IsMagtheridonActive(magtheridon)) + return 1.0f; + + if (botAI->IsHeal(bot) || botAI->IsMainTank(bot)) + return 1.0f; + + constexpr uint8 dpsWaitSeconds = 6; + auto it = dpsWaitTimer.find(magtheridon->GetMap()->GetInstanceId()); + if (it != dpsWaitTimer.end() && time(nullptr) - it->second > dpsWaitSeconds) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float MagtheridonControlTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || bot->GetVictim() == nullptr) + return 1.0f; + + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + if (!botAI->IsMainTank(bot) && magtheridon->GetVictim() != bot) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (IsMagtheridonActive(magtheridon) || GetChanneler(bot, SOUTH_CHANNELER) || + GetChanneler(bot, WEST_CHANNELER) || GetChanneler(bot, EAST_CHANNELER)) + { + return 1.0f; + } + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + + return 1.0f; +} + +float MagtheridonDebrisDangerMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "magtheridon") || + dynamic_cast(action) || + dynamic_cast(action)) + { + return 1.0f; + } + + // 15y is wider than the default hazard zone but is needed to + // keep the multiplier in effect + if (IsPositionInActiveDebris( + bot->GetMap()->GetInstanceId(), bot->GetPositionX(), + bot->GetPositionY(), 15.0f)) + { + return 0.0f; + } + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.h new file mode 100644 index 000000000..816aed291 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagMultipliers.h @@ -0,0 +1,44 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGMULTIPLIERS_H +#define PLAYERBOTS_MAGMULTIPLIERS_H + +#include "Multiplier.h" + +class MagtheridonUseManticronCubeMultiplier : public Multiplier +{ +public: + MagtheridonUseManticronCubeMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon use manticron cube multiplier") {} + float GetValue(Action* action) override; +}; + +class MagtheridonWaitToAttackMultiplier : public Multiplier +{ +public: + MagtheridonWaitToAttackMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon wait to attack multiplier") {} + float GetValue(Action* action) override; +}; + +class MagtheridonControlTankActionsMultiplier : public Multiplier +{ +public: + MagtheridonControlTankActionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon control tank actions multiplier") {} + float GetValue(Action* action) override; +}; + +class MagtheridonDebrisDangerMultiplier : public Multiplier +{ +public: + MagtheridonDebrisDangerMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon debris danger multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Mag/MagStrategy.cpp similarity index 68% rename from modules/mod-playerbots/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/Mag/MagStrategy.cpp index 73d240825..ea90530ec 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagStrategy.cpp @@ -1,26 +1,14 @@ -#include "RaidMagtheridonStrategy.h" -#include "RaidMagtheridonMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MagStrategy.h" +#include "MagMultipliers.h" void RaidMagtheridonStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("magtheridon incoming blast nova", { - NextAction("magtheridon use manticron cube", ACTION_EMERGENCY + 10) })); - - triggers.push_back(new TriggerNode("magtheridon need to manage timers and assignments", { - NextAction("magtheridon manage timers and assignments", ACTION_EMERGENCY + 1) })); - - triggers.push_back(new TriggerNode("magtheridon burning abyssal spawned", { - NextAction("magtheridon warlock cc burning abyssal", ACTION_RAID + 3) })); - - triggers.push_back(new TriggerNode("magtheridon boss engaged by ranged", { - NextAction("magtheridon spread ranged", ACTION_RAID + 2) })); - - triggers.push_back(new TriggerNode("magtheridon pulling west and east channelers", { - NextAction("magtheridon misdirect hellfire channelers", ACTION_RAID + 2) })); - - triggers.push_back(new TriggerNode("magtheridon boss engaged by main tank", { - NextAction("magtheridon main tank position boss", ACTION_RAID + 2) })); - triggers.push_back(new TriggerNode("magtheridon first three channelers engaged by main tank", { NextAction("magtheridon main tank attack first three channelers", ACTION_RAID + 1) })); @@ -30,13 +18,38 @@ void RaidMagtheridonStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("magtheridon ne channeler engaged by second assist tank", { NextAction("magtheridon second assist tank attack ne channeler", ACTION_RAID + 1) })); + triggers.push_back(new TriggerNode("magtheridon pulling west and east channelers", { + NextAction("magtheridon misdirect hellfire channelers to main tank", ACTION_RAID + 2) })); + triggers.push_back(new TriggerNode("magtheridon determining kill order", { NextAction("magtheridon assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("magtheridon burning abyssal spawned", { + NextAction("magtheridon warlock cc burning abyssal", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("magtheridon boss engaged by main tank", { + NextAction("magtheridon main tank position boss", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("magtheridon boss engaged by ranged", { + NextAction("magtheridon spread ranged", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("magtheridon standing in debris", { + NextAction("magtheridon move out of debris", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("magtheridon incoming blast nova", { + NextAction("magtheridon use manticron cube", ACTION_EMERGENCY + 9) })); + + triggers.push_back(new TriggerNode("magtheridon need to manage timers and assignments", { + NextAction("magtheridon manage timers and assignments", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("magtheridon bot is not in combat", { + NextAction("magtheridon erase timers and trackers", ACTION_EMERGENCY + 12) })); } void RaidMagtheridonStrategy::InitMultipliers(std::vector& multipliers) { multipliers.push_back(new MagtheridonUseManticronCubeMultiplier(botAI)); multipliers.push_back(new MagtheridonWaitToAttackMultiplier(botAI)); - multipliers.push_back(new MagtheridonDisableOffTankAssistMultiplier(botAI)); + multipliers.push_back(new MagtheridonControlTankActionsMultiplier(botAI)); + multipliers.push_back(new MagtheridonDebrisDangerMultiplier(botAI)); } diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Mag/MagStrategy.h similarity index 54% rename from modules/mod-playerbots/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/Mag/MagStrategy.h index 7b8ab8f9b..4432f07e0 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Strategy/RaidMagtheridonStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagStrategy.h @@ -1,8 +1,13 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H -#define _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGSTRATEGY_H +#define PLAYERBOTS_MAGSTRATEGY_H #include "Strategy.h" -#include "Multiplier.h" class RaidMagtheridonStrategy : public Strategy { diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggerContext.h new file mode 100644 index 000000000..899ac52b2 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggerContext.h @@ -0,0 +1,94 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGTRIGGERCONTEXT_H +#define PLAYERBOTS_MAGTRIGGERCONTEXT_H + +#include "MagTriggers.h" +#include "NamedObjectContext.h" + +class RaidMagtheridonTriggerContext : public NamedObjectContext +{ +public: + RaidMagtheridonTriggerContext() : NamedObjectContext() + { + creators["magtheridon first three channelers engaged by main tank"] = + &RaidMagtheridonTriggerContext::magtheridon_first_three_channelers_engaged_by_main_tank; + + creators["magtheridon nw channeler engaged by first assist tank"] = + &RaidMagtheridonTriggerContext::magtheridon_nw_channeler_engaged_by_first_assist_tank; + + creators["magtheridon ne channeler engaged by second assist tank"] = + &RaidMagtheridonTriggerContext::magtheridon_ne_channeler_engaged_by_second_assist_tank; + + creators["magtheridon pulling west and east channelers"] = + &RaidMagtheridonTriggerContext::magtheridon_pull_west_and_east_channelers; + + creators["magtheridon determining kill order"] = + &RaidMagtheridonTriggerContext::magtheridon_determining_kill_order; + + creators["magtheridon burning abyssal spawned"] = + &RaidMagtheridonTriggerContext::magtheridon_burning_abyssal_spawned; + + creators["magtheridon boss engaged by main tank"] = + &RaidMagtheridonTriggerContext::magtheridon_boss_engaged_by_main_tank; + + creators["magtheridon boss engaged by ranged"] = + &RaidMagtheridonTriggerContext::magtheridon_boss_engaged_by_ranged; + + creators["magtheridon standing in debris"] = + &RaidMagtheridonTriggerContext::magtheridon_standing_in_debris; + + creators["magtheridon incoming blast nova"] = + &RaidMagtheridonTriggerContext::magtheridon_incoming_blast_nova; + + creators["magtheridon need to manage timers and assignments"] = + &RaidMagtheridonTriggerContext::magtheridon_need_to_manage_timers_and_assignments; + + creators["magtheridon bot is not in combat"] = + &RaidMagtheridonTriggerContext::magtheridon_bot_is_not_in_combat; + } + +private: + static Trigger* magtheridon_first_three_channelers_engaged_by_main_tank(PlayerbotAI* botAI) { + return new MagtheridonFirstThreeChannelersEngagedByMainTankTrigger(botAI); + } + static Trigger* magtheridon_nw_channeler_engaged_by_first_assist_tank(PlayerbotAI* botAI) { + return new MagtheridonNWChannelerEngagedByFirstAssistTankTrigger(botAI); + } + static Trigger* magtheridon_ne_channeler_engaged_by_second_assist_tank(PlayerbotAI* botAI) { + return new MagtheridonNEChannelerEngagedBySecondAssistTankTrigger(botAI); + } + static Trigger* magtheridon_pull_west_and_east_channelers(PlayerbotAI* botAI) { + return new MagtheridonPullingWestAndEastChannelersTrigger(botAI); + } + static Trigger* magtheridon_determining_kill_order(PlayerbotAI* botAI) { + return new MagtheridonDeterminingKillOrderTrigger(botAI); + } + static Trigger* magtheridon_burning_abyssal_spawned(PlayerbotAI* botAI) { + return new MagtheridonBurningAbyssalSpawnedTrigger(botAI); + } + static Trigger* magtheridon_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new MagtheridonBossEngagedByMainTankTrigger(botAI); + } + static Trigger* magtheridon_boss_engaged_by_ranged(PlayerbotAI* botAI) { + return new MagtheridonBossEngagedByRangedTrigger(botAI); + } + static Trigger* magtheridon_standing_in_debris(PlayerbotAI* botAI) { + return new MagtheridonStandingInDebrisTrigger(botAI); + } + static Trigger* magtheridon_incoming_blast_nova(PlayerbotAI* botAI) { + return new MagtheridonIncomingBlastNovaTrigger(botAI); + } + static Trigger* magtheridon_need_to_manage_timers_and_assignments(PlayerbotAI* botAI) { + return new MagtheridonNeedToManageTimersAndAssignmentsTrigger(botAI); + } + static Trigger* magtheridon_bot_is_not_in_combat(PlayerbotAI* botAI) { + return new MagtheridonBotIsNotInCombatTrigger(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.cpp new file mode 100644 index 000000000..8b82ee647 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.cpp @@ -0,0 +1,128 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MagTriggers.h" +#include "MagHelpers.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace MagtheridonHelpers; + +bool MagtheridonFirstThreeChannelersEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + return magtheridon && !IsMagtheridonActive(magtheridon); +} + +bool MagtheridonNWChannelerEngagedByFirstAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, false)) + return false; + + return AI_VALUE2(Unit*, "find target", "magtheridon") && + GetChanneler(bot, NORTHWEST_CHANNELER); +} + +bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + return AI_VALUE2(Unit*, "find target", "magtheridon") && + GetChanneler(bot, NORTHEAST_CHANNELER); +} + +bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + if (!AI_VALUE2(Unit*, "find target", "magtheridon")) + return false; + + return GetChanneler(bot, WEST_CHANNELER) || GetChanneler(bot, EAST_CHANNELER); +} + +bool MagtheridonDeterminingKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot) || !AI_VALUE2(Unit*, "find target", "magtheridon")) + return false; + + Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); + Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); + + return !botAI->IsMainTank(bot) && + !(botAI->IsAssistTankOfIndex(bot, 0, false) && channelerDiamond) && + !(botAI->IsAssistTankOfIndex(bot, 1, true) && channelerTriangle); +} + +bool MagtheridonBurningAbyssalSpawnedTrigger::IsActive() +{ + return bot->getClass() == CLASS_WARLOCK && + AI_VALUE2(Unit*, "find target", "burning abyssal"); +} + +bool MagtheridonBossEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon || !IsMagtheridonActive(magtheridon)) + return false; + + // Include an assist tank that pulls aggro + return botAI->IsMainTank(bot) || magtheridon->GetVictim() == bot; +} + +bool MagtheridonBossEngagedByRangedTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + if (!magtheridon || !IsMagtheridonActive(magtheridon)) + return false; + + constexpr uint8 dpsWaitSeconds = 6; + auto it = dpsWaitTimer.find(magtheridon->GetMap()->GetInstanceId()); + if (it == dpsWaitTimer.end() || (time(nullptr) - it->second) < dpsWaitSeconds) + return false; + + return magtheridon->GetVictim() != bot; +} + +bool MagtheridonStandingInDebrisTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "magtheridon")) + return false; + + return IsPositionInActiveDebris( + bot->GetMap()->GetInstanceId(), bot->GetPositionX(), bot->GetPositionY()); +} + +bool MagtheridonIncomingBlastNovaTrigger::IsActive() +{ + Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); + return magtheridon && IsMagtheridonActive(magtheridon) && IsCubeClicker(bot); +} + +bool MagtheridonNeedToManageTimersAndAssignmentsTrigger::IsActive() +{ + return IsMechanicTrackerBot(bot, MAGTHERIDON_MAP_ID) && + AI_VALUE2(Unit*, "find target", "magtheridon"); +} + +bool MagtheridonBotIsNotInCombatTrigger::IsActive() +{ + return bot->GetMapId() == MAGTHERIDON_MAP_ID && + !AI_VALUE2(bool, "combat", "self target") && + !AI_VALUE2(Unit*, "find target", "magtheridon") && + !AI_VALUE2(Unit*, "find target", "hellfire channeler"); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.h new file mode 100644 index 000000000..4eca530d3 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/MagTriggers.h @@ -0,0 +1,108 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGTRIGGERS_H +#define PLAYERBOTS_MAGTRIGGERS_H + +#include "Trigger.h" +#include "PlayerbotAI.h" + +class MagtheridonFirstThreeChannelersEngagedByMainTankTrigger : public Trigger +{ +public: + MagtheridonFirstThreeChannelersEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon first three channelers engaged by main tank") {} + bool IsActive() override; +}; + +class MagtheridonNWChannelerEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + MagtheridonNWChannelerEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon nw channeler engaged by first assist tank") {} + bool IsActive() override; +}; + +class MagtheridonNEChannelerEngagedBySecondAssistTankTrigger : public Trigger +{ +public: + MagtheridonNEChannelerEngagedBySecondAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon ne channeler engaged by second assist tank") {} + bool IsActive() override; +}; + +class MagtheridonPullingWestAndEastChannelersTrigger : public Trigger +{ +public: + MagtheridonPullingWestAndEastChannelersTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon pulling west and east channelers") {} + bool IsActive() override; +}; + +class MagtheridonDeterminingKillOrderTrigger : public Trigger +{ +public: + MagtheridonDeterminingKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon determining kill order") {} + bool IsActive() override; +}; + +class MagtheridonBurningAbyssalSpawnedTrigger : public Trigger +{ +public: + MagtheridonBurningAbyssalSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon burning abyssal spawned") {} + bool IsActive() override; +}; + +class MagtheridonBossEngagedByMainTankTrigger : public Trigger +{ +public: + MagtheridonBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon boss engaged by main tank") {} + bool IsActive() override; +}; + +class MagtheridonBossEngagedByRangedTrigger : public Trigger +{ +public: + MagtheridonBossEngagedByRangedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon boss engaged by ranged") {} + bool IsActive() override; +}; + +class MagtheridonStandingInDebrisTrigger : public Trigger +{ +public: + MagtheridonStandingInDebrisTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon standing in debris") {} + bool IsActive() override; +}; + +class MagtheridonIncomingBlastNovaTrigger : public Trigger +{ +public: + MagtheridonIncomingBlastNovaTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon incoming blast nova") {} + bool IsActive() override; +}; + +class MagtheridonNeedToManageTimersAndAssignmentsTrigger : public Trigger +{ +public: + MagtheridonNeedToManageTimersAndAssignmentsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "magtheridon need to manage timers and assignments") {} + bool IsActive() override; +}; + +class MagtheridonBotIsNotInCombatTrigger : public Trigger +{ +public: + MagtheridonBotIsNotInCombatTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon bot is not in combat") {}; + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.cpp new file mode 100644 index 000000000..0b94c6c5f --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.cpp @@ -0,0 +1,139 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MagHelpers.h" +#include "Creature.h" +#include "GameObject.h" +#include "Map.h" +#include "ObjectGuid.h" +#include "Playerbots.h" + +namespace MagtheridonHelpers +{ + +const Position WAITING_FOR_MAGTHERIDON_POSITION = { -31.962f, -8.514f, -0.304f, 0.657f }; +const Position MAGTHERIDON_TANK_POSITION = { -6.147f, -37.812f, -0.411f, 0.0f }; +const Position NW_CHANNELER_TANK_POSITION = { -11.764f, 30.818f, -0.411f, 0.0f }; +const Position NE_CHANNELER_TANK_POSITION = { -12.490f, -26.211f, -0.411f, 0.0f }; +const Position RANGED_SPREAD_POSITION = { -14.890f, 1.995f, -0.406f, 0.0f }; +const Position HEALER_SPREAD_POSITION = { -2.265f, 1.874f, -0.404f, 0.0f }; + +std::unordered_map blastNovaTimer; +std::unordered_map dpsWaitTimer; +std::unordered_map ceilingCollapseApplied; +std::unordered_map lastBlastNovaState; +std::unordered_map> + botToCubeAssignments; +std::unordered_map> activeDebrisPositions; + +// Identify channelers by their database GUIDs +Creature* GetChanneler(Player* bot, uint32 dbGuid) +{ + Map* map = bot->GetMap(); + if (!map) + return nullptr; + + auto it = map->GetCreatureBySpawnIdStore().find(dbGuid); + if (it == map->GetCreatureBySpawnIdStore().end()) + return nullptr; + + Creature* channeler = it->second; + if (!channeler->IsAlive()) + return nullptr; + + return channeler; +} + +bool IsMagtheridonActive(Unit* magtheridon) +{ + return magtheridon && !magtheridon->HasAura( + static_cast(MagtheridonSpells::SPELL_SHADOW_CAGE)); +} + +const std::vector MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 }; + +// Get the positions of all Manticron Cubes by their database GUIDs +std::vector GetAllCubeInfosByDbGuids( + Map* map, const std::vector& cubeDbGuids) +{ + std::vector cubes; + if (!map) + return cubes; + + for (uint32 dbGuid : cubeDbGuids) + { + auto bounds = map->GetGameObjectBySpawnIdStore().equal_range(dbGuid); + if (bounds.first == bounds.second) + continue; + + GameObject* go = bounds.first->second; + if (!go) + continue; + + CubeInfo info; + info.guid = go->GetGUID(); + info.x = go->GetPositionX(); + info.y = go->GetPositionY(); + info.z = go->GetPositionZ(); + cubes.push_back(info); + } + + return cubes; +} + +bool IsCubeClicker(Player* bot) +{ + auto mapIt = botToCubeAssignments.find(bot->GetMap()->GetInstanceId()); + return mapIt != botToCubeAssignments.end() && + mapIt->second.find(bot->GetGUID()) != mapIt->second.end(); +} + +bool IsPositionInActiveDebris(uint32 instanceId, float x, float y, float radius) +{ + constexpr uint32 maxAgeMs = 8000; + + auto it = activeDebrisPositions.find(instanceId); + if (it == activeDebrisPositions.end()) + return false; + + const uint32 now = getMSTime(); + for (DebrisData const& debris : it->second) + { + if (getMSTimeDiff(debris.spawnTime, now) > maxAgeMs) + continue; + + float dx = x - debris.position.GetPositionX(); + float dy = y - debris.position.GetPositionY(); + if ((dx * dx + dy * dy) < (radius * radius)) + return true; + } + + return false; +} + +bool IsPositionInActiveConflagration(PlayerbotAI* botAI, Player* bot, float x, float y) +{ + constexpr float conflagrationHazardRadius = 5.0f; + GuidVector const& gameObjects = + botAI->GetAiObjectContext()->GetValue("nearest game objects")->Get(); + for (auto const& goGuid : gameObjects) + { + GameObject* go = botAI->GetGameObject(goGuid); + if (!go || !go->isSpawned() || + go->GetEntry() != + static_cast(MagtheridonHelpers::MagtheridonObjects::GO_BLAZE)) + { + continue; + } + + if (go->GetDistance2d(x, y) < conflagrationHazardRadius) + return true; + } + + return false; +} + +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.h b/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.h new file mode 100644 index 000000000..45fe099f7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagHelpers.h @@ -0,0 +1,91 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_MAGHELPERS_H +#define PLAYERBOTS_MAGHELPERS_H + +#include +#include +#include + +#include "Group.h" +#include "ObjectGuid.h" +#include "PlayerbotAI.h" + +namespace MagtheridonHelpers +{ + +enum class MagtheridonSpells : uint32 +{ + // Magtheridon + SPELL_SHADOW_CAGE = 30205, + SPELL_SHADOW_GRASP = 30410, + SPELL_BLAST_NOVA = 30616, + SPELL_DEBRIS_SPAWN = 30630, + SPELL_QUAKE = 30657, + + // Hunter + SPELL_MISDIRECTION = 35079, +}; + +enum class MagtheridonNpcs : uint32 +{ + NPC_BURNING_ABYSSAL = 17454, +}; + +enum class MagtheridonObjects : uint32 +{ + GO_BLAZE = 181832, +}; + +struct CubeInfo +{ + ObjectGuid guid; + float x, y, z; +}; + +struct DebrisData +{ + Position position; + uint32 spawnTime; +}; + +constexpr uint32 MAGTHERIDON_MAP_ID = 544; +constexpr uint32 SOUTH_CHANNELER = 90978; +constexpr uint32 WEST_CHANNELER = 90979; +constexpr uint32 NORTHWEST_CHANNELER = 90980; +constexpr uint32 EAST_CHANNELER = 90982; +constexpr uint32 NORTHEAST_CHANNELER = 90981; +constexpr uint8 BLAST_NOVA_INTERIM_SECONDS = 45; + +extern const Position WAITING_FOR_MAGTHERIDON_POSITION; +extern const Position MAGTHERIDON_TANK_POSITION; +extern const Position NW_CHANNELER_TANK_POSITION; +extern const Position NE_CHANNELER_TANK_POSITION; +extern const Position RANGED_SPREAD_POSITION; +extern const Position HEALER_SPREAD_POSITION; + +extern std::unordered_map dpsWaitTimer; +extern std::unordered_map blastNovaTimer; +extern std::unordered_map ceilingCollapseApplied; +extern std::unordered_map lastBlastNovaState; +extern std::unordered_map> + botToCubeAssignments; +extern std::unordered_map> activeDebrisPositions; + +extern const std::vector MANTICRON_CUBE_DB_GUIDS; +std::vector GetAllCubeInfosByDbGuids( + Map* map, const std::vector& cubeDbGuids); +Creature* GetChanneler(Player* bot, uint32 dbGuid); +bool IsMagtheridonActive(Unit* magtheridon); +bool IsCubeClicker(Player* bot); +bool IsPositionInActiveDebris(uint32 instanceId, float x, float y, float radius = 10.0f); +bool IsPositionInActiveConflagration( + PlayerbotAI* botAI, Player* bot, float x, float y); + +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagScripts.cpp b/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagScripts.cpp new file mode 100644 index 000000000..e6b79f35f --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Mag/Util/MagScripts.cpp @@ -0,0 +1,75 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "MagHelpers.h" +#include "AllSpellScript.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "Playerbots.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "Timer.h" + +using namespace MagtheridonHelpers; + +class MagtheridonBotSpellScript : public AllSpellScript +{ +public: + MagtheridonBotSpellScript() : AllSpellScript("MagtheridonBotSpellScript") {} + + void OnSpellCast( + Spell* /*spell*/, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (spellInfo->Id == static_cast(MagtheridonSpells::SPELL_DEBRIS_SPAWN)) + { + // Debris is a one-shot that has no prior warning other than a visual effect, + // which necessitates this spell hook to track debris spawn positions + const uint32 instanceId = caster->GetMap()->GetInstanceId(); + const uint32 now = getMSTime(); + + activeDebrisPositions[instanceId].push_back({ caster->GetPosition(), now }); + + constexpr uint32 debrisLifetimeMs = 10000; + auto& positions = activeDebrisPositions[instanceId]; + positions.erase(std::remove_if(positions.begin(), positions.end(), + [now](DebrisData const& d) { + return getMSTimeDiff(d.spawnTime, now) > debrisLifetimeMs; + }), + positions.end()); + + // Interrupt casts for bots that could be standing in incoming debris + Map::PlayerList const& players = caster->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive()) + continue; + + PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); + if (!botAI || !botAI->HasStrategy("magtheridon", BOT_STATE_COMBAT)) + continue; + + if (IsPositionInActiveDebris( + instanceId, player->GetPositionX(), player->GetPositionY())) + { + botAI->RequestSpellInterrupt(); + } + } + } + else if (spellInfo->Id == static_cast(MagtheridonSpells::SPELL_QUAKE)) + { + // To account for Blast Nova delay caused by Quake's DelayAll(6999ms) + auto it = blastNovaTimer.find(caster->GetMap()->GetInstanceId()); + if (it != blastNovaTimer.end()) + it->second += 7; + } + } +}; + +void AddSC_MagtheridonBotScripts() +{ + new MagtheridonBotSpellScript(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp deleted file mode 100644 index ffe446965..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.cpp +++ /dev/null @@ -1,673 +0,0 @@ -#include "RaidMagtheridonActions.h" -#include "RaidMagtheridonHelpers.h" -#include "Creature.h" -#include "ObjectAccessor.h" -#include "ObjectGuid.h" -#include "Playerbots.h" -#include "RaidBossHelpers.h" - -using namespace MagtheridonHelpers; - -bool MagtheridonMainTankAttackFirstThreeChannelersAction::Execute(Event /*event*/) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return false; - - Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare) - MarkTargetWithSquare(bot, channelerSquare); - - Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar) - MarkTargetWithStar(bot, channelerStar); - - Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle) - MarkTargetWithCircle(bot, channelerCircle); - - // After first three channelers are dead, wait for Magtheridon to activate - if (!channelerSquare && !channelerStar && !channelerCircle) - { - const Position& position = WAITING_FOR_MAGTHERIDON_POSITION; - if (!bot->IsWithinDist2d(position.GetPositionX(), position.GetPositionY(), 2.0f)) - { - return MoveTo(MAGTHERIDON_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - - bot->SetFacingTo(position.GetOrientation()); - return true; - } - - Creature* currentTarget = nullptr; - std::string rtiName; - if (channelerSquare) - { - currentTarget = channelerSquare; - rtiName = "square"; - } - else if (channelerStar) - { - currentTarget = channelerStar; - rtiName = "star"; - } - else if (channelerCircle) - { - currentTarget = channelerCircle; - rtiName = "circle"; - } - - SetRtiTarget(botAI, rtiName, currentTarget); - - if (currentTarget && bot->GetVictim() != currentTarget) - return Attack(currentTarget); - - return false; -} - -bool MagtheridonFirstAssistTankAttackNWChannelerAction::Execute(Event /*event*/) -{ - Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (!channelerDiamond) - return false; - - MarkTargetWithDiamond(bot, channelerDiamond); - SetRtiTarget(botAI, "diamond", channelerDiamond); - - if (bot->GetVictim() != channelerDiamond) - return Attack(channelerDiamond); - - if (channelerDiamond->GetVictim() == bot) - { - const Position& position = NW_CHANNELER_TANK_POSITION; - const float maxDistance = 3.0f; - float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceToPosition > maxDistance) - { - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - - return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -bool MagtheridonSecondAssistTankAttackNEChannelerAction::Execute(Event /*event*/) -{ - Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (!channelerTriangle) - return false; - - MarkTargetWithTriangle(bot, channelerTriangle); - SetRtiTarget(botAI, "triangle", channelerTriangle); - - if (bot->GetVictim() != channelerTriangle) - return Attack(channelerTriangle); - - if (channelerTriangle->GetVictim() == bot) - { - const Position& position = NE_CHANNELER_TANK_POSITION; - const float maxDistance = 3.0f; - float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceToPosition > maxDistance) - { - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - - return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -// Misdirect West & East Channelers to Main Tank -bool MagtheridonMisdirectHellfireChannelers::Execute(Event /*event*/) -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - std::vector hunters; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) - hunters.push_back(member); - } - - int hunterIndex = -1; - for (size_t i = 0; i < hunters.size(); ++i) - { - if (hunters[i] == bot) - { - hunterIndex = static_cast(i); - break; - } - } - - Player* mainTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - - Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - - switch (hunterIndex) - { - case 0: - if (mainTank && channelerStar && - channelerStar->GetVictim() != mainTank) - { - if (botAI->CanCastSpell("misdirection", mainTank)) - return botAI->CastSpell("misdirection", mainTank); - - if (!bot->HasAura(SPELL_MISDIRECTION)) - return false; - - if (botAI->CanCastSpell("steady shot", channelerStar)) - return botAI->CastSpell("steady shot", channelerStar); - } - break; - - case 1: - if (mainTank && channelerCircle && - channelerCircle->GetVictim() != mainTank) - { - if (botAI->CanCastSpell("misdirection", mainTank)) - return botAI->CastSpell("misdirection", mainTank); - - if (!bot->HasAura(SPELL_MISDIRECTION)) - return false; - - if (botAI->CanCastSpell("steady shot", channelerCircle)) - return botAI->CastSpell("steady shot", channelerCircle); - } - break; - - default: - break; - } - - return false; -} - -bool MagtheridonAssignDPSPriorityAction::Execute(Event /*event*/) -{ - // Listed in order of priority - Creature* channelerSquare = GetChanneler(bot, SOUTH_CHANNELER); - if (channelerSquare) - { - SetRtiTarget(botAI, "square", channelerSquare); - - if (bot->GetTarget() != channelerSquare->GetGUID()) - return Attack(channelerSquare); - - return false; - } - - Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - if (channelerStar) - { - SetRtiTarget(botAI, "star", channelerStar); - - if (bot->GetTarget() != channelerStar->GetGUID()) - return Attack(channelerStar); - - return false; - } - - Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - if (channelerCircle) - { - SetRtiTarget(botAI, "circle", channelerCircle); - - if (bot->GetTarget() != channelerCircle->GetGUID()) - return Attack(channelerCircle); - - return false; - } - - Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - if (channelerDiamond) - { - SetRtiTarget(botAI, "diamond", channelerDiamond); - - if (bot->GetTarget() != channelerDiamond->GetGUID()) - return Attack(channelerDiamond); - - return false; - } - - Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - if (channelerTriangle) - { - SetRtiTarget(botAI, "triangle", channelerTriangle); - - if (bot->GetTarget() != channelerTriangle->GetGUID()) - return Attack(channelerTriangle); - - return false; - } - - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && - !channelerSquare && !channelerStar && !channelerCircle && - !channelerDiamond && !channelerTriangle) - { - SetRtiTarget(botAI, "cross", magtheridon); - - if (bot->GetTarget() != magtheridon->GetGUID()) - return Attack(magtheridon); - } - - return false; -} - -// Assign Burning Abyssals to Warlocks to Banish -// Burning Abyssals in excess of Warlocks in party will be Feared -bool MagtheridonWarlockCCBurningAbyssalAction::Execute(Event /*event*/) -{ - Group* group = bot->GetGroup(); - if (!group) - return false; - - const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - - std::vector abyssals; - for (auto const& npc : npcs) - { - Unit* unit = botAI->GetUnit(npc); - if (unit && unit->GetEntry() == NPC_BURNING_ABYSSAL && unit->IsAlive()) - abyssals.push_back(unit); - } - - std::vector warlocks; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && member->getClass() == CLASS_WARLOCK && GET_PLAYERBOT_AI(member)) - warlocks.push_back(member); - } - - int warlockIndex = -1; - for (size_t i = 0; i < warlocks.size(); ++i) - { - if (warlocks[i] == bot) - { - warlockIndex = static_cast(i); - break; - } - } - - if (warlockIndex >= 0 && warlockIndex < abyssals.size()) - { - Unit* assignedAbyssal = abyssals[warlockIndex]; - if (!botAI->HasAura("banish", assignedAbyssal) && botAI->CanCastSpell("banish", assignedAbyssal)) - return botAI->CastSpell("banish", assignedAbyssal); - } - - for (size_t i = warlocks.size(); i < abyssals.size(); ++i) - { - Unit* excessAbyssal = abyssals[i]; - if (!botAI->HasAura("banish", excessAbyssal) && !botAI->HasAura("fear", excessAbyssal) && - botAI->CanCastSpell("fear", excessAbyssal)) - return botAI->CastSpell("fear", excessAbyssal); - } - - return false; -} - -// Main tank will back up to the Northern point of the room -bool MagtheridonMainTankPositionBossAction::Execute(Event /*event*/) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return false; - - MarkTargetWithCross(bot, magtheridon); - SetRtiTarget(botAI, "cross", magtheridon); - - if (bot->GetVictim() != magtheridon) - return Attack(magtheridon); - - if (magtheridon->GetVictim() == bot) - { - const Position& position = MAGTHERIDON_TANK_POSITION; - const float maxDistance = 2.0f; - float distanceToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); - - if (distanceToPosition > maxDistance) - { - float dX = position.GetPositionX() - bot->GetPositionX(); - float dY = position.GetPositionY() - bot->GetPositionY(); - float moveX = bot->GetPositionX() + (dX / distanceToPosition) * maxDistance; - float moveY = bot->GetPositionY() + (dY / distanceToPosition) * maxDistance; - - return MoveTo(MAGTHERIDON_MAP_ID, moveX, moveY, position.GetPositionZ(), false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, true); - } - } - - return false; -} - -// Ranged DPS will remain within 25 yards of the center of the room -// Healers will remain within 15 yards of a position that is between ranged DPS and the boss -std::unordered_map MagtheridonSpreadRangedAction::initialPositions; -std::unordered_map MagtheridonSpreadRangedAction::hasReachedInitialPosition; - -bool MagtheridonSpreadRangedAction::Execute(Event /*event*/) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - const uint32 instanceId = magtheridon->GetMap()->GetInstanceId(); - - // Wait for 6 seconds after Magtheridon activates to spread - const uint8 spreadWaitSeconds = 6; - auto it = spreadWaitTimer.find(instanceId); - if (it == spreadWaitTimer.end() || - (time(nullptr) - it->second) < spreadWaitSeconds) - return false; - - auto cubeIt = botToCubeAssignment.find(bot->GetGUID()); - if (cubeIt != botToCubeAssignment.end()) - { - time_t now = time(nullptr); - auto timerIt = blastNovaTimer.find(instanceId); - if (timerIt != blastNovaTimer.end()) - { - time_t lastBlastNova = timerIt->second; - if (now - lastBlastNova >= 49) - return false; - } - } - - std::vector members; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive()) - members.push_back(member); - } - - bool isHealer = botAI->IsHeal(bot); - const Position& center = isHealer - ? HEALER_SPREAD_POSITION - : RANGED_SPREAD_POSITION; - float maxSpreadRadius = isHealer ? 15.0f : 20.0f; - float centerX = center.GetPositionX(); - float centerY = center.GetPositionY(); - float centerZ = center.GetPositionZ(); - const float radiusBuffer = 3.0f; - - if (!initialPositions.count(bot->GetGUID())) - { - auto it = std::find(members.begin(), members.end(), bot); - uint8 botIndex = (it != members.end()) ? std::distance(members.begin(), it) : 0; - uint8 count = members.size(); - - float angle = 2 * M_PI * botIndex / count; - float radius = static_cast(rand()) / RAND_MAX * maxSpreadRadius; - float targetX = centerX + radius * cos(angle); - float targetY = centerY + radius * sin(angle); - - initialPositions[bot->GetGUID()] = Position(targetX, targetY, centerZ); - hasReachedInitialPosition[bot->GetGUID()] = false; - } - - Position targetPosition = initialPositions[bot->GetGUID()]; - if (!hasReachedInitialPosition[bot->GetGUID()]) - { - if (!bot->IsWithinDist2d(targetPosition.GetPositionX(), targetPosition.GetPositionY(), 2.0f)) - { - float destX = targetPosition.GetPositionX(); - float destY = targetPosition.GetPositionY(); - float destZ = targetPosition.GetPositionZ(); - - if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), - bot->GetPositionY(), bot->GetPositionZ(), destX, destY, destZ)) - return false; - - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(MAGTHERIDON_MAP_ID, destX, destY, destZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - hasReachedInitialPosition[bot->GetGUID()] = true; - } - - float distToCenter = bot->GetExactDist2d(centerX, centerY); - - if (distToCenter > maxSpreadRadius + radiusBuffer) - { - float angle = static_cast(rand()) / RAND_MAX * 2.0f * M_PI; - float radius = static_cast(rand()) / RAND_MAX * maxSpreadRadius; - float targetX = centerX + radius * cos(angle); - float targetY = centerY + radius * sin(angle); - - if (bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), - bot->GetPositionZ(), targetX, targetY, centerZ)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(false); - return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, centerZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - return false; -} - -// For bots that are assigned to click cubes -// Magtheridon casts Blast Nova every 54.35 to 55.40s, with a 2s cast time -bool MagtheridonUseManticronCubeAction::Execute(Event /*event*/) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return false; - - auto it = botToCubeAssignment.find(bot->GetGUID()); - const CubeInfo& cubeInfo = it->second; - GameObject* cube = botAI->GetGameObject(cubeInfo.guid); - if (!cube) - return false; - - // Release cubes after Blast Nova is interrupted - if (HandleCubeRelease(magtheridon, cube)) - return true; - - // Check if cube logic should be active (49+ second rule) - if (!ShouldActivateCubeLogic(magtheridon)) - return false; - - // Handle active cube logic based on Blast Nova casting state - bool blastNovaActive = magtheridon->HasUnitState(UNIT_STATE_CASTING) && - magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); - - if (!blastNovaActive) - // After 49 seconds, wait at safe distance from cube - return HandleWaitingPhase(cubeInfo); - else - // Blast Nova is casting - move to and click cube - return HandleCubeInteraction(cubeInfo, cube); - - return false; -} - -bool MagtheridonUseManticronCubeAction::HandleCubeRelease(Unit* magtheridon, GameObject* cube) -{ - if (bot->HasAura(SPELL_SHADOW_GRASP) && - !(magtheridon->HasUnitState(UNIT_STATE_CASTING) && - magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA))) - { - uint32 delay = urand(200, 3000); - botAI->AddTimedEvent( - [this] - { - botAI->Reset(); - }, - delay); - botAI->SetNextCheckDelay(delay + 50); - return true; - } - - return false; -} - -bool MagtheridonUseManticronCubeAction::ShouldActivateCubeLogic(Unit* magtheridon) -{ - auto timerIt = blastNovaTimer.find(magtheridon->GetMap()->GetInstanceId()); - if (timerIt == blastNovaTimer.end()) - return false; - - time_t now = time(nullptr); - time_t lastBlastNova = timerIt->second; - - return (now - lastBlastNova >= 49); -} - -bool MagtheridonUseManticronCubeAction::HandleWaitingPhase(const CubeInfo& cubeInfo) -{ - const float safeWaitDistance = 8.0f; - float cubeDist = bot->GetExactDist2d(cubeInfo.x, cubeInfo.y); - - if (fabs(cubeDist - safeWaitDistance) > 1.0f) - { - for (int i = 0; i < 12; ++i) - { - float angle = i * M_PI / 6.0f; - float targetX = cubeInfo.x + cos(angle) * safeWaitDistance; - float targetY = cubeInfo.y + sin(angle) * safeWaitDistance; - float targetZ = bot->GetPositionZ(); - - if (IsSafeFromMagtheridonHazards(botAI, bot, targetX, targetY, targetZ)) - { - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); - return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - } - - float angle = static_cast(rand()) / RAND_MAX * 2.0f * M_PI; - float fallbackX = cubeInfo.x + cos(angle) * safeWaitDistance; - float fallbackY = cubeInfo.y + sin(angle) * safeWaitDistance; - float fallbackZ = bot->GetPositionZ(); - - return MoveTo(MAGTHERIDON_MAP_ID, fallbackX, fallbackY, fallbackZ, false, false, false, false, - MovementPriority::MOVEMENT_COMBAT, true, false); - } - - return true; -} - -bool MagtheridonUseManticronCubeAction::HandleCubeInteraction(const CubeInfo& cubeInfo, GameObject* cube) -{ - const float interactDistance = 1.0f; - float cubeDist = bot->GetExactDist2d(cubeInfo.x, cubeInfo.y); - - if (cubeDist > interactDistance) - { - if (cubeDist <= interactDistance + 1.0f) - { - uint32 delay = urand(200, 1500); - botAI->AddTimedEvent( - [this, cube] - { - bot->StopMoving(); - cube->Use(bot); - }, - delay); - botAI->SetNextCheckDelay(delay + 50); - return true; - } - - float angle = atan2(cubeInfo.y - bot->GetPositionY(), cubeInfo.x - bot->GetPositionX()); - float targetX = cubeInfo.x - cos(angle) * interactDistance; - float targetY = cubeInfo.y - sin(angle) * interactDistance; - float targetZ = bot->GetPositionZ(); - - bot->AttackStop(); - bot->InterruptNonMeleeSpells(true); - return MoveTo(MAGTHERIDON_MAP_ID, targetX, targetY, targetZ, false, false, false, false, - MovementPriority::MOVEMENT_FORCED, true, false); - } - - return false; -} - -// The Blast Nova timer resets when Magtheridon stops casting it, which is needed to ensure that bots use cubes. -// However, Magtheridon's Blast Nova cooldown actually runs from when he starts casting it. This means that if a Blast Nova -// is not interrupted or takes too long to interrupt, the timer will be thrown off for the rest of the encounter. -// Correcting this issue is complicated and probably would need some rewriting--I have not done so and -// and view the current solution as sufficient since in TBC a missed Blast Nova would be a guaranteed wipe anyway. -bool MagtheridonManageTimersAndAssignmentsAction::Execute(Event /*event*/) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return false; - - const uint32 instanceId = magtheridon->GetMap()->GetInstanceId(); - const time_t now = time(nullptr); - - bool blastNovaActive = magtheridon->HasUnitState(UNIT_STATE_CASTING) && - magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA); - bool lastBlastNova = lastBlastNovaState[instanceId]; - - if (lastBlastNova && !blastNovaActive) - blastNovaTimer[instanceId] = now; - - lastBlastNovaState[instanceId] = blastNovaActive; - - if (!magtheridon->HasAura(SPELL_SHADOW_CAGE)) - { - if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) - { - spreadWaitTimer.try_emplace(instanceId, now); - blastNovaTimer.try_emplace(instanceId, now); - dpsWaitTimer.try_emplace(instanceId, now); - } - } - else - { - ObjectGuid guid = bot->GetGUID(); - MagtheridonSpreadRangedAction::initialPositions.erase(guid); - MagtheridonSpreadRangedAction::hasReachedInitialPosition.erase(guid); - botToCubeAssignment.erase(guid); - - if (IsMechanicTrackerBot(botAI, bot, MAGTHERIDON_MAP_ID, nullptr)) - { - spreadWaitTimer.erase(instanceId); - blastNovaTimer.erase(instanceId); - dpsWaitTimer.erase(instanceId); - } - } - - return false; -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h deleted file mode 100644 index d47d06459..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Action/RaidMagtheridonActions.h +++ /dev/null @@ -1,98 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONACTIONS_H -#define _PLAYERBOT_RAIDMAGTHERIDONACTIONS_H - -#include "RaidMagtheridonHelpers.h" -#include "Action.h" -#include "AttackAction.h" -#include "MovementActions.h" - -class MagtheridonMainTankAttackFirstThreeChannelersAction : public AttackAction -{ -public: - MagtheridonMainTankAttackFirstThreeChannelersAction(PlayerbotAI* botAI, std::string const name = "magtheridon main tank attack first three channelers") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonFirstAssistTankAttackNWChannelerAction : public AttackAction -{ -public: - MagtheridonFirstAssistTankAttackNWChannelerAction(PlayerbotAI* botAI, std::string const name = "magtheridon first assist tank attack nw channeler") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonSecondAssistTankAttackNEChannelerAction : public AttackAction -{ -public: - MagtheridonSecondAssistTankAttackNEChannelerAction(PlayerbotAI* botAI, std::string const name = "magtheridon second assist tank attack ne channeler") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonMisdirectHellfireChannelers : public AttackAction -{ -public: - MagtheridonMisdirectHellfireChannelers(PlayerbotAI* botAI, std::string const name = "magtheridon misdirect hellfire channelers") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonAssignDPSPriorityAction : public AttackAction -{ -public: - MagtheridonAssignDPSPriorityAction(PlayerbotAI* botAI, std::string const name = "magtheridon assign dps priority") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonWarlockCCBurningAbyssalAction : public AttackAction -{ -public: - MagtheridonWarlockCCBurningAbyssalAction(PlayerbotAI* botAI, std::string const name = "magtheridon warlock cc burning abyssal") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonMainTankPositionBossAction : public AttackAction -{ -public: - MagtheridonMainTankPositionBossAction(PlayerbotAI* botAI, std::string const name = "magtheridon main tank position boss") : AttackAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonSpreadRangedAction : public MovementAction -{ -public: - static std::unordered_map initialPositions; - static std::unordered_map hasReachedInitialPosition; - - MagtheridonSpreadRangedAction(PlayerbotAI* botAI, std::string const name = "magtheridon spread ranged") : MovementAction(botAI, name) {}; - - bool Execute(Event event) override; -}; - -class MagtheridonUseManticronCubeAction : public MovementAction -{ -public: - MagtheridonUseManticronCubeAction(PlayerbotAI* botAI, std::string const name = "magtheridon use manticron cube") : MovementAction(botAI, name) {}; - - bool Execute(Event event) override; - -private: - bool HandleCubeRelease(Unit* magtheridon, GameObject* cube); - bool ShouldActivateCubeLogic(Unit* magtheridon); - bool HandleWaitingPhase(const MagtheridonHelpers::CubeInfo& cubeInfo); - bool HandleCubeInteraction(const MagtheridonHelpers::CubeInfo& cubeInfo, GameObject* cube); -}; - -class MagtheridonManageTimersAndAssignmentsAction : public Action -{ -public: - MagtheridonManageTimersAndAssignmentsAction(PlayerbotAI* botAI, std::string const name = "magtheridon manage timers and assignments") : Action(botAI, name) {}; - - bool Execute(Event event) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp deleted file mode 100644 index 55aaf90e9..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include -#include - -#include "RaidMagtheridonMultipliers.h" -#include "RaidMagtheridonActions.h" -#include "RaidMagtheridonHelpers.h" -#include "ChooseTargetActions.h" -#include "GenericSpellActions.h" -#include "Playerbots.h" -#include "WarlockActions.h" -#include "WipeAction.h" - -using namespace MagtheridonHelpers; - -// Don't do anything other than clicking cubes when Magtheridon is casting Blast Nova -float MagtheridonUseManticronCubeMultiplier::GetValue(Action* action) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return 1.0f; - - if (magtheridon->HasUnitState(UNIT_STATE_CASTING) && - magtheridon->FindCurrentSpellBySpellId(SPELL_BLAST_NOVA)) - { - auto it = botToCubeAssignment.find(bot->GetGUID()); - if (it != botToCubeAssignment.end()) - { - if (dynamic_cast(action)) - return 1.0f; - else if (!dynamic_cast(action)) - return 0.0f; - } - } - - return 1.0f; -} - -// Bots will wait for 6 seconds after Magtheridon becomes attackable before engaging -float MagtheridonWaitToAttackMultiplier::GetValue(Action* action) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) - return 1.0f; - - if (botAI->IsMainTank(bot)) - return 1.0f; - - const uint8 dpsWaitSeconds = 6; - auto it = dpsWaitTimer.find(magtheridon->GetMap()->GetInstanceId()); - if (it == dpsWaitTimer.end() || - (time(nullptr) - it->second) < dpsWaitSeconds) - { - if (dynamic_cast(action) || - (!botAI->IsHeal(bot) && dynamic_cast(action))) - return 0.0f; - } - - return 1.0f; -} - -float MagtheridonDisableOffTankAssistMultiplier::GetValue(Action* action) -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon) - return 1.0f; - - if (bot->GetVictim() == nullptr) - return 1.0f; - - if ((botAI->IsAssistTankOfIndex(bot, 0) || botAI->IsAssistTankOfIndex(bot, 1)) && - dynamic_cast(action)) - return 0.0f; - - return 1.0f; -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.h deleted file mode 100644 index 2cce516b6..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Multiplier/RaidMagtheridonMultipliers.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONMULTIPLIERS_H -#define _PLAYERBOT_RAIDMAGTHERIDONMULTIPLIERS_H - -#include "Multiplier.h" - -class MagtheridonUseManticronCubeMultiplier : public Multiplier -{ -public: - MagtheridonUseManticronCubeMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon use manticron cube multiplier") {} - float GetValue(Action* action) override; -}; - -class MagtheridonWaitToAttackMultiplier : public Multiplier -{ -public: - MagtheridonWaitToAttackMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon wait to attack multiplier") {} - float GetValue(Action* action) override; -}; - -class MagtheridonDisableOffTankAssistMultiplier : public Multiplier -{ -public: - MagtheridonDisableOffTankAssistMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "magtheridon disable off tank assist multiplier") {} - float GetValue(Action* action) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h deleted file mode 100644 index 48e8cfd35..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONACTIONCONTEXT_H -#define _PLAYERBOT_RAIDMAGTHERIDONACTIONCONTEXT_H - -#include "RaidMagtheridonActions.h" -#include "NamedObjectContext.h" - -class RaidMagtheridonActionContext : public NamedObjectContext -{ -public: - RaidMagtheridonActionContext() - { - creators["magtheridon main tank attack first three channelers"] = &RaidMagtheridonActionContext::magtheridon_main_tank_attack_first_three_channelers; - creators["magtheridon first assist tank attack nw channeler"] = &RaidMagtheridonActionContext::magtheridon_first_assist_tank_attack_nw_channeler; - creators["magtheridon second assist tank attack ne channeler"] = &RaidMagtheridonActionContext::magtheridon_second_assist_tank_attack_ne_channeler; - creators["magtheridon misdirect hellfire channelers"] = &RaidMagtheridonActionContext::magtheridon_misdirect_hellfire_channelers; - creators["magtheridon assign dps priority"] = &RaidMagtheridonActionContext::magtheridon_assign_dps_priority; - creators["magtheridon warlock cc burning abyssal"] = &RaidMagtheridonActionContext::magtheridon_warlock_cc_burning_abyssal; - creators["magtheridon main tank position boss"] = &RaidMagtheridonActionContext::magtheridon_main_tank_position_boss; - creators["magtheridon spread ranged"] = &RaidMagtheridonActionContext::magtheridon_spread_ranged; - creators["magtheridon use manticron cube"] = &RaidMagtheridonActionContext::magtheridon_use_manticron_cube; - creators["magtheridon manage timers and assignments"] = &RaidMagtheridonActionContext::magtheridon_manage_timers_and_assignments; - } - -private: - static Action* magtheridon_main_tank_attack_first_three_channelers(PlayerbotAI* botAI) { return new MagtheridonMainTankAttackFirstThreeChannelersAction(botAI); } - static Action* magtheridon_first_assist_tank_attack_nw_channeler(PlayerbotAI* botAI) { return new MagtheridonFirstAssistTankAttackNWChannelerAction(botAI); } - static Action* magtheridon_second_assist_tank_attack_ne_channeler(PlayerbotAI* botAI) { return new MagtheridonSecondAssistTankAttackNEChannelerAction(botAI); } - static Action* magtheridon_misdirect_hellfire_channelers(PlayerbotAI* botAI) { return new MagtheridonMisdirectHellfireChannelers(botAI); } - static Action* magtheridon_assign_dps_priority(PlayerbotAI* botAI) { return new MagtheridonAssignDPSPriorityAction(botAI); } - static Action* magtheridon_warlock_cc_burning_abyssal(PlayerbotAI* botAI) { return new MagtheridonWarlockCCBurningAbyssalAction(botAI); } - static Action* magtheridon_main_tank_position_boss(PlayerbotAI* botAI) { return new MagtheridonMainTankPositionBossAction(botAI); } - static Action* magtheridon_spread_ranged(PlayerbotAI* botAI) { return new MagtheridonSpreadRangedAction(botAI); } - static Action* magtheridon_use_manticron_cube(PlayerbotAI* botAI) { return new MagtheridonUseManticronCubeAction(botAI); } - static Action* magtheridon_manage_timers_and_assignments(PlayerbotAI* botAI) { return new MagtheridonManageTimersAndAssignmentsAction(botAI); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h deleted file mode 100644 index 525fe496e..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H - -#include "RaidMagtheridonTriggers.h" -#include "AiObjectContext.h" - -class RaidMagtheridonTriggerContext : public NamedObjectContext -{ -public: - RaidMagtheridonTriggerContext() : NamedObjectContext() - { - creators["magtheridon first three channelers engaged by main tank"] = &RaidMagtheridonTriggerContext::magtheridon_first_three_channelers_engaged_by_main_tank; - creators["magtheridon nw channeler engaged by first assist tank"] = &RaidMagtheridonTriggerContext::magtheridon_nw_channeler_engaged_by_first_assist_tank; - creators["magtheridon ne channeler engaged by second assist tank"] = &RaidMagtheridonTriggerContext::magtheridon_ne_channeler_engaged_by_second_assist_tank; - creators["magtheridon pulling west and east channelers"] = &RaidMagtheridonTriggerContext::magtheridon_pull_west_and_east_channelers; - creators["magtheridon determining kill order"] = &RaidMagtheridonTriggerContext::magtheridon_determining_kill_order; - creators["magtheridon burning abyssal spawned"] = &RaidMagtheridonTriggerContext::magtheridon_burning_abyssal_spawned; - creators["magtheridon boss engaged by main tank"] = &RaidMagtheridonTriggerContext::magtheridon_boss_engaged_by_main_tank; - creators["magtheridon boss engaged by ranged"] = &RaidMagtheridonTriggerContext::magtheridon_boss_engaged_by_ranged; - creators["magtheridon incoming blast nova"] = &RaidMagtheridonTriggerContext::magtheridon_incoming_blast_nova; - creators["magtheridon need to manage timers and assignments"] = &RaidMagtheridonTriggerContext::magtheridon_need_to_manage_timers_and_assignments; - } - -private: - static Trigger* magtheridon_first_three_channelers_engaged_by_main_tank(PlayerbotAI* botAI) { return new MagtheridonFirstThreeChannelersEngagedByMainTankTrigger(botAI); } - static Trigger* magtheridon_nw_channeler_engaged_by_first_assist_tank(PlayerbotAI* botAI) { return new MagtheridonNWChannelerEngagedByFirstAssistTankTrigger(botAI); } - static Trigger* magtheridon_ne_channeler_engaged_by_second_assist_tank(PlayerbotAI* botAI) { return new MagtheridonNEChannelerEngagedBySecondAssistTankTrigger(botAI); } - static Trigger* magtheridon_pull_west_and_east_channelers(PlayerbotAI* botAI) { return new MagtheridonPullingWestAndEastChannelersTrigger(botAI); } - static Trigger* magtheridon_determining_kill_order(PlayerbotAI* botAI) { return new MagtheridonDeterminingKillOrderTrigger(botAI); } - static Trigger* magtheridon_burning_abyssal_spawned(PlayerbotAI* botAI) { return new MagtheridonBurningAbyssalSpawnedTrigger(botAI); } - static Trigger* magtheridon_boss_engaged_by_main_tank(PlayerbotAI* botAI) { return new MagtheridonBossEngagedByMainTankTrigger(botAI); } - static Trigger* magtheridon_boss_engaged_by_ranged(PlayerbotAI* botAI) { return new MagtheridonBossEngagedByRangedTrigger(botAI); } - static Trigger* magtheridon_incoming_blast_nova(PlayerbotAI* botAI) { return new MagtheridonIncomingBlastNovaTrigger(botAI); } - static Trigger* magtheridon_need_to_manage_timers_and_assignments(PlayerbotAI* botAI) { return new MagtheridonNeedToManageTimersAndAssignmentsTrigger(botAI); } -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp deleted file mode 100644 index 43aa3361f..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "RaidMagtheridonTriggers.h" -#include "RaidMagtheridonHelpers.h" -#include "Playerbots.h" - -using namespace MagtheridonHelpers; - -bool MagtheridonFirstThreeChannelersEngagedByMainTankTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - return magtheridon && botAI->IsMainTank(bot) && - magtheridon->HasAura(SPELL_SHADOW_CAGE); -} - -bool MagtheridonNWChannelerEngagedByFirstAssistTankTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - - return magtheridon && botAI->IsAssistTankOfIndex(bot, 0) && - channelerDiamond; -} - -bool MagtheridonNEChannelerEngagedBySecondAssistTankTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - - return magtheridon && botAI->IsAssistTankOfIndex(bot, 1) && - channelerTriangle; -} - -bool MagtheridonPullingWestAndEastChannelersTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - Creature* channelerStar = GetChanneler(bot, WEST_CHANNELER); - Creature* channelerCircle = GetChanneler(bot, EAST_CHANNELER); - - return magtheridon && bot->getClass() == CLASS_HUNTER && - (channelerStar || channelerCircle); -} - -bool MagtheridonDeterminingKillOrderTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Unit* channeler = AI_VALUE2(Unit*, "find target", "hellfire channeler"); - - Creature* channelerDiamond = GetChanneler(bot, NORTHWEST_CHANNELER); - Creature* channelerTriangle = GetChanneler(bot, NORTHEAST_CHANNELER); - - if (!magtheridon || botAI->IsHeal(bot) || botAI->IsMainTank(bot) || - (botAI->IsAssistTankOfIndex(bot, 0) && channelerDiamond) || - (botAI->IsAssistTankOfIndex(bot, 1) && channelerTriangle)) - return false; - - return channeler || (magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE)); -} - -bool MagtheridonBurningAbyssalSpawnedTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - if (!magtheridon || bot->getClass() != CLASS_WARLOCK) - return false; - - const GuidVector& npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); - return std::any_of(npcs.begin(), npcs.end(), [this](const ObjectGuid& npc) - { - Unit* unit = botAI->GetUnit(npc); - return unit && unit->GetEntry() == NPC_BURNING_ABYSSAL; - }); -} - -bool MagtheridonBossEngagedByMainTankTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - return magtheridon && botAI->IsMainTank(bot) && - !magtheridon->HasAura(SPELL_SHADOW_CAGE); -} - -bool MagtheridonBossEngagedByRangedTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - - return magtheridon && !magtheridon->HasAura(SPELL_SHADOW_CAGE) && botAI->IsRanged(bot); -} - -bool MagtheridonIncomingBlastNovaTrigger::IsActive() -{ - Unit* magtheridon = AI_VALUE2(Unit*, "find target", "magtheridon"); - Group* group = bot->GetGroup(); - if (!group || !magtheridon || magtheridon->HasAura(SPELL_SHADOW_CAGE)) - return false; - - bool needsReassign = botToCubeAssignment.empty(); - if (!needsReassign) - { - for (auto const& pair : botToCubeAssignment) - { - Player* assigned = ObjectAccessor::FindPlayer(pair.first); - if (!assigned || !assigned->IsAlive()) - { - needsReassign = true; - break; - } - } - } - - if (needsReassign) - { - std::vector cubes = GetAllCubeInfosByDbGuids(bot->GetMap(), MANTICRON_CUBE_DB_GUIDS); - AssignBotsToCubesByGuidAndCoords(group, cubes, botAI); - } - - return botToCubeAssignment.find(bot->GetGUID()) != botToCubeAssignment.end(); -} - -bool MagtheridonNeedToManageTimersAndAssignmentsTrigger::IsActive() -{ - return AI_VALUE2(Unit*, "find target", "magtheridon"); -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.h deleted file mode 100644 index 0039c4e27..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Trigger/RaidMagtheridonTriggers.h +++ /dev/null @@ -1,77 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONTRIGGERS_H -#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERS_H - -#include "Trigger.h" -#include "PlayerbotAI.h" - -class MagtheridonFirstThreeChannelersEngagedByMainTankTrigger : public Trigger -{ -public: - MagtheridonFirstThreeChannelersEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon first three channelers engaged by main tank") {}; - bool IsActive() override; -}; - -class MagtheridonNWChannelerEngagedByFirstAssistTankTrigger : public Trigger -{ -public: - MagtheridonNWChannelerEngagedByFirstAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon nw channeler engaged by first assist tank") {}; - bool IsActive() override; -}; - -class MagtheridonNEChannelerEngagedBySecondAssistTankTrigger : public Trigger -{ -public: - MagtheridonNEChannelerEngagedBySecondAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon ne channeler engaged by second assist tank") {}; - bool IsActive() override; -}; - -class MagtheridonPullingWestAndEastChannelersTrigger : public Trigger -{ -public: - MagtheridonPullingWestAndEastChannelersTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon pulling west and east channelers") {}; - bool IsActive() override; -}; - -class MagtheridonDeterminingKillOrderTrigger : public Trigger -{ -public: - MagtheridonDeterminingKillOrderTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon determining kill order") {}; - bool IsActive() override; -}; - -class MagtheridonBurningAbyssalSpawnedTrigger : public Trigger -{ -public: - MagtheridonBurningAbyssalSpawnedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon burning abyssal spawned") {}; - bool IsActive() override; -}; - -class MagtheridonBossEngagedByMainTankTrigger : public Trigger -{ -public: - MagtheridonBossEngagedByMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon boss engaged by main tank") {}; - bool IsActive() override; -}; - -class MagtheridonBossEngagedByRangedTrigger : public Trigger -{ -public: - MagtheridonBossEngagedByRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon boss engaged by ranged") {}; - bool IsActive() override; -}; - -class MagtheridonIncomingBlastNovaTrigger : public Trigger -{ -public: - MagtheridonIncomingBlastNovaTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon incoming blast nova") {}; - bool IsActive() override; -}; - -class MagtheridonNeedToManageTimersAndAssignmentsTrigger : public Trigger -{ -public: - MagtheridonNeedToManageTimersAndAssignmentsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "magtheridon need to manage timers and assignments") {}; - bool IsActive() override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp deleted file mode 100644 index a56d4b85b..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "RaidMagtheridonHelpers.h" -#include "Creature.h" -#include "GameObject.h" -#include "Map.h" -#include "ObjectGuid.h" -#include "Playerbots.h" - -namespace MagtheridonHelpers -{ - const Position WAITING_FOR_MAGTHERIDON_POSITION = { 1.359f, 2.048f, -0.406f, 3.135f }; - const Position MAGTHERIDON_TANK_POSITION = { 22.827f, 2.105f, -0.406f, 3.135f }; - const Position NW_CHANNELER_TANK_POSITION = { -11.764f, 30.818f, -0.411f, 0.0f }; - const Position NE_CHANNELER_TANK_POSITION = { -12.490f, -26.211f, -0.411f, 0.0f }; - const Position RANGED_SPREAD_POSITION = { -14.890f, 1.995f, -0.406f, 0.0f }; - const Position HEALER_SPREAD_POSITION = { -2.265f, 1.874f, -0.404f, 0.0f }; - - // Identify channelers by their database GUIDs - Creature* GetChanneler(Player* bot, uint32 dbGuid) - { - Map* map = bot->GetMap(); - if (!map) - return nullptr; - - auto it = map->GetCreatureBySpawnIdStore().find(dbGuid); - if (it == map->GetCreatureBySpawnIdStore().end()) - return nullptr; - - Creature* channeler = it->second; - if (!channeler->IsAlive()) - return nullptr; - - return channeler; - } - - const std::vector MANTICRON_CUBE_DB_GUIDS = { 43157, 43158, 43159, 43160, 43161 }; - - // Get the positions of all Manticron Cubes by their database GUIDs - std::vector GetAllCubeInfosByDbGuids(Map* map, const std::vector& cubeDbGuids) - { - std::vector cubes; - if (!map) - return cubes; - - for (uint32 dbGuid : cubeDbGuids) - { - auto bounds = map->GetGameObjectBySpawnIdStore().equal_range(dbGuid); - if (bounds.first == bounds.second) - continue; - - GameObject* go = bounds.first->second; - if (!go) - continue; - - CubeInfo info; - info.guid = go->GetGUID(); - info.x = go->GetPositionX(); - info.y = go->GetPositionY(); - info.z = go->GetPositionZ(); - cubes.push_back(info); - } - - return cubes; - } - - std::unordered_map botToCubeAssignment; - - void AssignBotsToCubesByGuidAndCoords(Group* group, const std::vector& cubes, PlayerbotAI* botAI) - { - botToCubeAssignment.clear(); - if (!group) - return; - - size_t cubeIndex = 0; - std::vector candidates; - - // Assign ranged DPS (excluding Warlocks) to cubes first - for (GroupReference* ref = group->GetFirstMember(); ref && cubeIndex < cubes.size(); ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !botAI->IsRangedDps(member, true) || - member->getClass() == CLASS_WARLOCK || !GET_PLAYERBOT_AI(member)) - continue; - - candidates.push_back(member); - if (candidates.size() >= cubes.size()) - break; - } - - // If there are still cubes left, assign any other non-tank bots - if (candidates.size() < cubes.size()) - { - for (GroupReference* ref = group->GetFirstMember(); - ref && candidates.size() < cubes.size(); ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || botAI->IsTank(member)) - continue; - - if (std::find(candidates.begin(), candidates.end(), member) == candidates.end()) - candidates.push_back(member); - } - } - - for (Player* member : candidates) - { - if (cubeIndex >= cubes.size()) - break; - - if (!member || !member->IsAlive()) - continue; - - botToCubeAssignment[member->GetGUID()] = cubes[cubeIndex++]; - } - } - - std::unordered_map lastBlastNovaState; - std::unordered_map blastNovaTimer; - std::unordered_map spreadWaitTimer; - std::unordered_map dpsWaitTimer; - - bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z) - { - // Debris - std::vector debrisHazards; - const GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) - { - Unit* unit = botAI->GetUnit(npcGuid); - if (!unit || unit->GetEntry() != NPC_TARGET_TRIGGER) - continue; - debrisHazards.push_back(unit); - } - for (Unit* hazard : debrisHazards) - { - float dist = std::sqrt(std::pow(x - hazard->GetPositionX(), 2) + std::pow(y - hazard->GetPositionY(), 2)); - if (dist < 9.0f) - return false; - } - - // Conflagration - GuidVector gos = *botAI->GetAiObjectContext()->GetValue("nearest game objects"); - for (auto const& goGuid : gos) - { - GameObject* go = botAI->GetGameObject(goGuid); - if (!go || go->GetEntry() != GO_BLAZE) - continue; - - float dist = std::sqrt(std::pow(x - go->GetPositionX(), 2) + std::pow(y - go->GetPositionY(), 2)); - if (dist < 5.0f) - return false; - } - - return true; - } -} diff --git a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h b/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h deleted file mode 100644 index 1335aff46..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Magtheridon/Util/RaidMagtheridonHelpers.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMAGTHERIDONHELPERS_H -#define _PLAYERBOT_RAIDMAGTHERIDONHELPERS_H - -#include -#include -#include - -#include "Group.h" -#include "ObjectGuid.h" -#include "PlayerbotAI.h" - -namespace MagtheridonHelpers -{ - enum MagtheridonSpells - { - // Magtheridon - SPELL_SHADOW_CAGE = 30205, - SPELL_BLAST_NOVA = 30616, - SPELL_SHADOW_GRASP = 30410, - - // Hunter - SPELL_MISDIRECTION = 35079, - }; - - enum MagtheridonNPCs - { - NPC_BURNING_ABYSSAL = 17454, - NPC_TARGET_TRIGGER = 17474, - }; - - enum MagtheridonObjects - { - GO_BLAZE = 181832, - }; - - constexpr uint32 MAGTHERIDON_MAP_ID = 544; - constexpr uint32 SOUTH_CHANNELER = 90978; - constexpr uint32 WEST_CHANNELER = 90979; - constexpr uint32 NORTHWEST_CHANNELER = 90980; - constexpr uint32 EAST_CHANNELER = 90982; - constexpr uint32 NORTHEAST_CHANNELER = 90981; - - Creature* GetChanneler(Player* bot, uint32 dbGuid); - bool IsSafeFromMagtheridonHazards(PlayerbotAI* botAI, Player* bot, float x, float y, float z); - - extern const Position WAITING_FOR_MAGTHERIDON_POSITION; - extern const Position MAGTHERIDON_TANK_POSITION; - extern const Position NW_CHANNELER_TANK_POSITION; - extern const Position NE_CHANNELER_TANK_POSITION; - extern const Position RANGED_SPREAD_POSITION; - extern const Position HEALER_SPREAD_POSITION; - - struct CubeInfo - { - ObjectGuid guid; - float x, y, z; - }; - - extern const std::vector MANTICRON_CUBE_DB_GUIDS; - extern std::unordered_map botToCubeAssignment; - std::vector GetAllCubeInfosByDbGuids(Map* map, const std::vector& cubeDbGuids); - void AssignBotsToCubesByGuidAndCoords(Group* group, const std::vector& cubes, PlayerbotAI* botAI); - extern std::unordered_map lastBlastNovaState; - extern std::unordered_map blastNovaTimer; - extern std::unordered_map spreadWaitTimer; - extern std::unordered_map dpsWaitTimer; -} - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h b/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h deleted file mode 100644 index 5dc821246..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/MoltenCore/Util/RaidMcHelpers.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef _PLAYERBOT_RAIDMCHELPERS_H -#define _PLAYERBOT_RAIDMCHELPERS_H - -namespace MoltenCoreHelpers -{ -enum MoltenCoreNPCs -{ - // Golemagg - NPC_CORE_RAGER = 11672, -}; -enum MoltenCoreSpells -{ - // Baron Geddon - SPELL_INFERNO = 19695, - SPELL_LIVING_BOMB = 20475, - - // Golemagg - SPELL_GOLEMAGGS_TRUST = 20553, -}; -} - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions.h new file mode 100644 index 000000000..4262faef0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions.h @@ -0,0 +1,332 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXACTIONS_H +#define PLAYERBOTS_NAXXACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "GenericActions.h" +#include "MovementActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "NaxxBossHelper.h" + +class GrobbulusGoBehindAction : public MovementAction +{ +public: + GrobbulusGoBehindAction(PlayerbotAI* ai, float distance = 24.0f, float delta_angle = M_PI / 8) + : MovementAction(ai, "grobbulus go behind") + { + this->distance = distance; + this->delta_angle = delta_angle; + } + virtual bool Execute(Event event); + +protected: + float distance, delta_angle; +}; + +class GrobbulusRotateAction : public RotateAroundTheCenterPointAction +{ +public: + GrobbulusRotateAction(PlayerbotAI* botAI) + : RotateAroundTheCenterPointAction(botAI, "rotate grobbulus", 3281.23f, -3310.38f, 35.0f, 8, true, M_PI) {} + virtual bool isUseful() override + { + return RotateAroundTheCenterPointAction::isUseful() && botAI->IsMainTank(bot) && + AI_VALUE2(bool, "has aggro", "boss target"); + } + uint32 GetCurrWaypoint() override; +}; + +class GrobbulusMoveCenterAction : public MoveInsideAction +{ +public: + GrobbulusMoveCenterAction(PlayerbotAI* ai) : MoveInsideAction(ai, 3281.23f, -3310.38f, 5.0f) {} +}; + +class GrobbulusMoveAwayAction : public MovementAction +{ +public: + GrobbulusMoveAwayAction(PlayerbotAI* ai, float distance = 18.0f) + : MovementAction(ai, "grobbulus move away"), distance(distance) + { + } + bool Execute(Event event) override; + +private: + float distance; +}; + +//class HeiganDanceAction : public MovementAction +//{ +//public: +// HeiganDanceAction(PlayerbotAI* ai) : MovementAction(ai, "heigan dance") +// { +// this->last_eruption_ms = 0; +// this->platform_phase = false; +// ResetSafe(); +// waypoints.push_back(std::make_pair(2794.88f, -3668.12f)); +// waypoints.push_back(std::make_pair(2775.49f, -3674.43f)); +// waypoints.push_back(std::make_pair(2762.30f, -3684.59f)); +// waypoints.push_back(std::make_pair(2755.99f, -3703.96f)); +// platform = std::make_pair(2794.26f, -3706.67f); +// } +// +//protected: +// bool CalculateSafe(); +// void ResetSafe() +// { +// curr_safe = 0; +// curr_dir = 1; +// } +// void NextSafe() +// { +// curr_safe += curr_dir; +// if (curr_safe == 3 || curr_safe == 0) +// { +// curr_dir = -curr_dir; +// } +// } +// uint32 last_eruption_ms; +// bool platform_phase; +// uint32 curr_safe, curr_dir; +// std::vector> waypoints; +// std::pair platform; +//}; +// +//class HeiganDanceMeleeAction : public HeiganDanceAction +//{ +//public: +// HeiganDanceMeleeAction(PlayerbotAI* ai) : HeiganDanceAction(ai) {} +// virtual bool Execute(Event event); +//}; +// +//class HeiganDanceRangedAction : public HeiganDanceAction +//{ +//public: +// HeiganDanceRangedAction(PlayerbotAI* ai) : HeiganDanceAction(ai) {} +// virtual bool Execute(Event event); +//}; + +class ThaddiusAttackNearestPetAction : public AttackAction +{ +public: + ThaddiusAttackNearestPetAction(PlayerbotAI* ai) : AttackAction(ai, "thaddius attack nearest pet"), helper(ai) {} + virtual bool Execute(Event event); + virtual bool isUseful(); + +private: + ThaddiusBossHelper helper; +}; + +// class ThaddiusMeleeToPlaceAction : public MovementAction +// { +// public: +// ThaddiusMeleeToPlaceAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius melee to place") {} +// virtual bool Execute(Event event); +// virtual bool isUseful(); +// }; + +// class ThaddiusRangedToPlaceAction : public MovementAction +// { +// public: +// ThaddiusRangedToPlaceAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius ranged to place") {} +// virtual bool Execute(Event event); +// virtual bool isUseful(); +// }; + +class ThaddiusMoveToPlatformAction : public MovementAction +{ +public: + ThaddiusMoveToPlatformAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius move to platform") {} + virtual bool Execute(Event event); + virtual bool isUseful(); +}; + +class ThaddiusMovePolarityAction : public MovementAction +{ +public: + ThaddiusMovePolarityAction(PlayerbotAI* ai) : MovementAction(ai, "thaddius move polarity") {} + virtual bool Execute(Event event); + virtual bool isUseful(); +}; + +class RazuviousUseObedienceCrystalAction : public MovementAction +{ +public: + RazuviousUseObedienceCrystalAction(PlayerbotAI* ai) + : MovementAction(ai, "razuvious use obedience crystal"), helper(ai) + { + } + bool Execute(Event event) override; + +private: + RazuviousBossHelper helper; +}; + +class RazuviousTargetAction : public AttackAction +{ +public: + RazuviousTargetAction(PlayerbotAI* ai) : AttackAction(ai, "razuvious target"), helper(ai) {} + bool Execute(Event event) override; + +private: + RazuviousBossHelper helper; +}; + +class FourHorsemenAttractAlternativelyAction : public AttackAction +{ +public: + FourHorsemenAttractAlternativelyAction(PlayerbotAI* ai) : AttackAction(ai, "four horsemen attract alternatively"), helper(ai) + { + } + bool Execute(Event event) override; + +protected: + FourHorsemenBossHelper helper; +}; + +class FourHorsemenAttackInOrderAction : public AttackAction +{ +public: + FourHorsemenAttackInOrderAction(PlayerbotAI* ai) : AttackAction(ai, "four horsemen attack in order"), helper(ai) {} + bool Execute(Event event) override; + +protected: + FourHorsemenBossHelper helper; +}; + +// class SapphironGroundMainTankPositionAction : public MovementAction +// { +// public: +// SapphironGroundMainTankPositionAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron ground main tank +// position") {} virtual bool Execute(Event event); +// }; + +class SapphironGroundPositionAction : public MovementAction +{ +public: + SapphironGroundPositionAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron ground position"), helper(ai) {} + bool Execute(Event event) override; + +protected: + SapphironBossHelper helper; +}; + +class SapphironFlightPositionAction : public MovementAction +{ +public: + SapphironFlightPositionAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron flight position"), helper(ai) {} + bool Execute(Event event) override; + +protected: + SapphironBossHelper helper; + bool MoveToNearestIcebolt(); +}; + +// class SapphironAvoidChillAction : public MovementAction +// { +// public: +// SapphironAvoidChillAction(PlayerbotAI* ai) : MovementAction(ai, "sapphiron avoid chill") {} +// virtual bool Execute(Event event); +// }; + +class KelthuzadChooseTargetAction : public AttackAction +{ +public: + KelthuzadChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "kel'thuzad choose target"), helper(ai) {} + virtual bool Execute(Event event); + +private: + KelthuzadBossHelper helper; +}; + +class KelthuzadPositionAction : public MovementAction +{ +public: + KelthuzadPositionAction(PlayerbotAI* ai) : MovementAction(ai, "kel'thuzad position"), helper(ai) {} + virtual bool Execute(Event event); + +private: + KelthuzadBossHelper helper; +}; + +class AnubrekhanChooseTargetAction : public AttackAction +{ +public: + AnubrekhanChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "anub'rekhan choose target") {} + bool Execute(Event event) override; +}; + +class AnubrekhanPositionAction : public RotateAroundTheCenterPointAction +{ +public: + AnubrekhanPositionAction(PlayerbotAI* ai) + : RotateAroundTheCenterPointAction(ai, "anub'rekhan position", 3272.49f, -3476.27f, 45.0f, 16) {} + bool Execute(Event event) override; +}; + +class GluthChooseTargetAction : public AttackAction +{ +public: + GluthChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "gluth choose target"), helper(ai) {} + bool Execute(Event event) override; + +private: + GluthBossHelper helper; +}; + +class GluthPositionAction : public RotateAroundTheCenterPointAction +{ +public: + GluthPositionAction(PlayerbotAI* ai) + : RotateAroundTheCenterPointAction(ai, "gluth position", 3293.61f, -3149.01f, 12.0f, 12), helper(ai) {} + bool Execute(Event event) override; + +private: + GluthBossHelper helper; +}; + +class GluthSlowdownAction : public Action +{ +public: + GluthSlowdownAction(PlayerbotAI* ai) : Action(ai, "gluth slowdown"), helper(ai) {} + bool Execute(Event event) override; + +private: + GluthBossHelper helper; +}; + +class LoathebPositionAction : public MovementAction +{ +public: + LoathebPositionAction(PlayerbotAI* ai) : MovementAction(ai, "loatheb position"), helper(ai) {} + virtual bool Execute(Event event); + +private: + LoathebBossHelper helper; +}; + +class LoathebChooseTargetAction : public AttackAction +{ +public: + LoathebChooseTargetAction(PlayerbotAI* ai) : AttackAction(ai, "loatheb choose target"), helper(ai) {} + virtual bool Execute(Event event); + +private: + LoathebBossHelper helper; +}; + +//class PatchwerkRangedPositionAction : public MovementAction +//{ +//public: +// PatchwerkRangedPositionAction(PlayerbotAI* ai) : MovementAction(ai, "patchwerk ranged position") {} +// bool Execute(Event event) override; +//}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Anubrekhan.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Anubrekhan.cpp new file mode 100644 index 000000000..68b8e68e3 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Anubrekhan.cpp @@ -0,0 +1,83 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ObjectGuid.h" +#include "Playerbots.h" +#include "NaxxActions.h" + +bool AnubrekhanChooseTargetAction::Execute(Event /*event*/) +{ + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + Unit* target_boss = nullptr; + std::vector target_guards; + for (ObjectGuid const guid : attackers) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit) + continue; + if (botAI->EqualLowercaseName(unit->GetName(), "crypt guard")) + target_guards.push_back(unit); + + if (botAI->EqualLowercaseName(unit->GetName(), "anub'rekhan")) + target_boss = unit; + } + if (botAI->IsMainTank(bot)) + target = target_boss; + else + { + if (target_guards.size() == 0) + target = target_boss; + else + { + if (botAI->IsAssistTank(bot)) + { + for (Unit* t : target_guards) + { + if (target == nullptr || (target->GetVictim() && target->GetVictim()->ToPlayer() && + botAI->IsTank(target->GetVictim()->ToPlayer()))) + target = t; + } + } + else + { + for (Unit* t : target_guards) + { + if (target == nullptr || target->GetHealthPct() > t->GetHealthPct()) + target = t; + } + } + } + } + if (context->GetValue("current target")->Get() == target) + return false; + + return Attack(target); +} + +bool AnubrekhanPositionAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'rekhan"); + if (!boss) + return false; + + bool inPhase = botAI->HasAura("locust swarm", boss) || boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (inPhase) + { + if (botAI->IsMainTank(bot)) + { + uint32 nearest = FindNearestWaypoint(); + uint32 next_point; + next_point = (nearest + 1) % intervals; + + return MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, + bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + else + return MoveInside(533, 3272.49f, -3476.27f, bot->GetPositionZ(), 3.0f, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Faerlina.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Faerlina.cpp new file mode 100644 index 000000000..4df098461 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Faerlina.cpp @@ -0,0 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +// Reserved for Faerlina-specific actions. diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_FourHorsemen.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_FourHorsemen.cpp new file mode 100644 index 000000000..d9a3d721d --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_FourHorsemen.cpp @@ -0,0 +1,65 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "Playerbots.h" + +bool FourHorsemenAttractAlternativelyAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + helper.CalculatePosToGo(bot); + auto [posX, posY] = helper.CurrentAttractPos(); + if (MoveTo(bot->GetMapId(), posX, posY, helper.posZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + Unit* attackTarget = helper.CurrentAttackTarget(); + if (attackTarget && context->GetValue("current target")->Get() != attackTarget) + return Attack(attackTarget); + + return false; +} + +bool FourHorsemenAttackInOrderAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + Unit* target = nullptr; + Unit* thane = AI_VALUE2(Unit*, "find target", "thane korth'azz"); + Unit* lady = AI_VALUE2(Unit*, "find target", "lady blaumeux"); + Unit* sir = AI_VALUE2(Unit*, "find target", "sir zeliek"); + Unit* fourth = AI_VALUE2(Unit*, "find target", "baron rivendare"); + if (!fourth) + fourth = AI_VALUE2(Unit*, "find target", "highlord mograine"); + + std::vector attack_order; + if (botAI->IsAssistTank(bot)) + attack_order = {fourth, thane, lady, sir}; + else + attack_order = {thane, fourth, lady, sir}; + for (Unit* t : attack_order) + { + if (t && t->IsAlive()) + { + target = t; + break; + } + } + if (target) + { + if (context->GetValue("current target")->Get() == target && botAI->GetState() == BOT_STATE_COMBAT) + return false; + + if (!bot->IsWithinLOSInMap(target)) + return MoveNear(target, 22.0f, MovementPriority::MOVEMENT_COMBAT); + + return Attack(target); + } + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gluth.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gluth.cpp new file mode 100644 index 000000000..2bdac0e04 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gluth.cpp @@ -0,0 +1,184 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +bool GluthChooseTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + GuidVector attackers = context->GetValue("possible targets")->Get(); + Unit* target = nullptr; + Unit* target_boss = nullptr; + std::vector target_zombies; + for (GuidVector::iterator i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + + if (!unit->IsAlive()) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "zombie chow")) + target_zombies.push_back(unit); + + if (botAI->EqualLowercaseName(unit->GetName(), "gluth")) + target_boss = unit; + } + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0)) + target = target_boss; + else if (botAI->IsAssistTankOfIndex(bot, 1)) + { + for (Unit* t : target_zombies) + { + if (t->GetHealthPct() > helper.decimatedZombiePct && t->GetVictim() != bot && t->GetDistance2d(bot) <= 10.0f) + { + if (!target || t->GetDistance2d(bot) < target->GetDistance2d(bot)) + target = t; + } + } + } + else if (botAI->GetClassIndex(bot, CLASS_HUNTER) == 0 || botAI->GetClassIndex(bot, CLASS_HUNTER) == 1) + { + // prevent zombie go straight to gluth + for (Unit* t : target_zombies) + { + if (t->GetHealthPct() > helper.decimatedZombiePct && t->GetVictim() == target_boss && + t->GetDistance2d(bot) <= sPlayerbotAIConfig.spellDistance) + { + if (!target || t->GetDistance2d(bot) < target->GetDistance2d(bot)) + target = t; + } + } + if (!target) + target = target_boss; + } + else + { + for (Unit* t : target_zombies) + { + if (t->GetHealthPct() <= helper.decimatedZombiePct) + { + if (target == nullptr || + target->GetDistance2d(helper.mainTankPos25.first, helper.mainTankPos25.second) > + t->GetDistance2d(helper.mainTankPos25.first, helper.mainTankPos25.second)) + target = t; + } + } + if (target == nullptr) + target = target_boss; + } + if (!target || context->GetValue("current target")->Get() == target) + return false; + + if (target_boss && target == target_boss) + return Attack(target, true); + + return Attack(target, false); + // return Attack(target); +} + +bool GluthPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0)) + { + if (AI_VALUE2(bool, "has aggro", "boss target")) + { + if (raid25) + { + if (MoveTo(NAXX_MAP_ID, helper.mainTankPos25.first, helper.mainTankPos25.second, bot->GetPositionZ(), false, false, false, + false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(NAXX_MAP_ID, helper.mainTankPos25.first, helper.mainTankPos25.second, bot->GetPositionZ(), 2.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else + { + if (MoveTo(NAXX_MAP_ID, helper.mainTankPos10.first, helper.mainTankPos10.second, bot->GetPositionZ(), false, false, false, + false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(NAXX_MAP_ID, helper.mainTankPos10.first, helper.mainTankPos10.second, bot->GetPositionZ(), 2.0f, + MovementPriority::MOVEMENT_COMBAT); + } + } + } + else if (botAI->IsAssistTankOfIndex(bot, 1)) + { + if (helper.BeforeDecimate()) + { + if (MoveTo(bot->GetMapId(), helper.beforeDecimatePos.first, helper.beforeDecimatePos.second, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(bot->GetMapId(), helper.beforeDecimatePos.first, helper.beforeDecimatePos.second, bot->GetPositionZ(), 2.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else + { + if (AI_VALUE2(bool, "has aggro", "current target")) + { + uint32 nearest = FindNearestWaypoint(); + uint32 next_point = (nearest + 1) % intervals; + return MoveTo(bot->GetMapId(), waypoints[next_point].first, waypoints[next_point].second, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + } + } + else if (botAI->IsRangedDps(bot)) + { + if (raid25) + { + if (botAI->GetClassIndex(bot, CLASS_HUNTER) == 0) + return MoveInside(NAXX_MAP_ID, helper.leftSlowDownPos.first, helper.leftSlowDownPos.second, bot->GetPositionZ(), 0.0f, + MovementPriority::MOVEMENT_COMBAT); + + if (botAI->GetClassIndex(bot, CLASS_HUNTER) == 1) + return MoveInside(NAXX_MAP_ID, helper.rightSlowDownPos.first, helper.rightSlowDownPos.second, bot->GetPositionZ(), 0.0f, + MovementPriority::MOVEMENT_COMBAT); + } + return MoveInside(NAXX_MAP_ID, helper.rangedPos.first, helper.rangedPos.second, bot->GetPositionZ(), 3.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsHeal(bot)) + return MoveInside(NAXX_MAP_ID, helper.healPos.first, helper.healPos.second, bot->GetPositionZ(), 0.0f, + MovementPriority::MOVEMENT_COMBAT); + return false; +} + +bool GluthSlowdownAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + if (!raid25) + return false; + + if (helper.JustStartCombat()) + return false; + + switch (bot->getClass()) + { + case CLASS_HUNTER: + return botAI->CastSpell("frost trap", bot); + break; + default: + break; + } + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gothik.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gothik.cpp new file mode 100644 index 000000000..d893abf91 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Gothik.cpp @@ -0,0 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +// Reserved for Gothik-specific actions. diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Grobbulus.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Grobbulus.cpp new file mode 100644 index 000000000..8a35db96e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Grobbulus.cpp @@ -0,0 +1,52 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "Playerbots.h" + +bool GrobbulusGoBehindAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE(Unit*, "boss target"); + if (!boss) + return false; + + // Position* pos = boss->GetPosition(); + float orientation = boss->GetOrientation() + M_PI + delta_angle; + float x = boss->GetPositionX(); + float y = boss->GetPositionY(); + float z = boss->GetPositionZ(); + float rx = x + cos(orientation) * distance; + float ry = y + sin(orientation) * distance; + return MoveTo(bot->GetMapId(), rx, ry, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +bool GrobbulusMoveAwayAction::Execute(Event /*event*/) +{ + Unit* boss = AI_VALUE(Unit*, "boss target"); + if (!boss) + return false; + + const float currentDistance = bot->GetExactDist2d(boss); + if (currentDistance >= distance) + return false; + + const float angle = boss->GetAngle(bot); + const float x = boss->GetPositionX() + cos(angle) * distance; + const float y = boss->GetPositionY() + sin(angle) * distance; + const float z = bot->GetPositionZ(); + + return MoveTo(bot->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} + +uint32 GrobbulusRotateAction::GetCurrWaypoint() +{ + uint32 current = FindNearestWaypoint(); + if (clockwise) + return (current + 1) % intervals; + + return (current + intervals - 1) % intervals; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Heigan.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Heigan.cpp new file mode 100644 index 000000000..e87f1e061 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Heigan.cpp @@ -0,0 +1,83 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "Playerbots.h" +#include "NaxxActions.h" +#include "NaxxSpellIds.h" +#include "Spell.h" +#include "Timer.h" + +//bool HeiganDanceAction::CalculateSafe() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!boss) +// { +// return false; +// } +// uint32 now = getMSTime(); +// platform_phase = boss->IsWithinDist2d(platform.first, platform.second, 10.0f); +// if (last_eruption_ms != 0 && now - last_eruption_ms > 15000) +// { +// ResetSafe(); +// } +// if (boss->HasUnitState(UNIT_STATE_CASTING)) +// { +// Spell* spell = boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); +// if (!spell) +// { +// spell = boss->GetCurrentSpell(CURRENT_CHANNELED_SPELL); +// } +// if (spell) +// { +// SpellInfo const* info = spell->GetSpellInfo(); +// bool isEruption = NaxxSpellIds::MatchesAnySpellId(info, {NaxxSpellIds::Eruption10}); +// if (!isEruption && info && info->SpellName[LOCALE_enUS]) +// { +// // Fallback to name for custom spell data. +// isEruption = botAI->EqualLowercaseName(info->SpellName[LOCALE_enUS], "eruption"); +// } +// if (isEruption) +// { +// if (last_eruption_ms == 0 || now - last_eruption_ms > 500) +// { +// NextSafe(); +// } +// last_eruption_ms = now; +// } +// } +// } +// return true; +//} +// +//bool HeiganDanceMeleeAction::Execute(Event event) +//{ +// CalculateSafe(); +// if (!platform_phase && botAI->IsMainTank(bot) && !AI_VALUE2(bool, "has aggro", "boss target")) +// { +// return false; +// } +// assert(curr_safe >= 0 && curr_safe <= 3); +// return MoveInside(bot->GetMapId(), waypoints[curr_safe].first, waypoints[curr_safe].second, bot->GetPositionZ(), +// botAI->IsMainTank(bot) ? 0 : 0, MovementPriority::MOVEMENT_COMBAT); +//} +// +//bool HeiganDanceRangedAction::Execute(Event event) +//{ +// CalculateSafe(); +// if (!platform_phase) +// { +// if (MoveTo(bot->GetMapId(), platform.first, platform.second, 276.54f, false, false, false, false, +// MovementPriority::MOVEMENT_COMBAT)) +// { +// return true; +// } +// return MoveInside(bot->GetMapId(), platform.first, platform.second, 276.54f, 2.0f, +// MovementPriority::MOVEMENT_COMBAT); +// } +// botAI->InterruptSpell(); +// return MoveInside(bot->GetMapId(), waypoints[curr_safe].first, waypoints[curr_safe].second, bot->GetPositionZ(), 0, +// MovementPriority::MOVEMENT_COMBAT); +//} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Kelthuzad.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Kelthuzad.cpp new file mode 100644 index 000000000..241e76d4d --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Kelthuzad.cpp @@ -0,0 +1,183 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" + +bool KelthuzadChooseTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + Unit *target_soldier = nullptr, *target_weaver = nullptr, *target_abomination = nullptr, *target_kelthuzad = nullptr, + *target_guardian = nullptr; + for (auto i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "guardian of icecrown")) + { + if (!target_guardian) + target_guardian = unit; + else if (unit->GetVictim() && target_guardian->GetVictim() && unit->GetVictim()->ToPlayer() && + target_guardian->GetVictim()->ToPlayer() && !botAI->IsAssistTank(unit->GetVictim()->ToPlayer()) && + botAI->IsAssistTank(target_guardian->GetVictim()->ToPlayer())) + { + target_guardian = unit; + } + else if (unit->GetVictim() && target_guardian->GetVictim() && unit->GetVictim()->ToPlayer() && + target_guardian->GetVictim()->ToPlayer() && !botAI->IsAssistTank(unit->GetVictim()->ToPlayer()) && + !botAI->IsAssistTank(target_guardian->GetVictim()->ToPlayer()) && + target_guardian->GetDistance2d(helper.center.first, helper.center.second) > + bot->GetDistance2d(unit)) + { + target_guardian = unit; + } + } + + if (unit->GetDistance2d(helper.center.first, helper.center.second) > 30.0f) + continue; + + if (bot->GetDistance2d(unit) > sPlayerbotAIConfig.spellDistance) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "unstoppable abomination")) + { + if (target_abomination == nullptr || + target_abomination->GetDistance2d(helper.center.first, helper.center.second) > + unit->GetDistance2d(helper.center.first, helper.center.second)) + { + target_abomination = unit; + } + } + if (botAI->EqualLowercaseName(unit->GetName(), "soldier of the frozen wastes")) + { + if (target_soldier == nullptr || + target_soldier->GetDistance2d(helper.center.first, helper.center.second) > + unit->GetDistance2d(helper.center.first, helper.center.second)) + { + target_soldier = unit; + } + } + if (botAI->EqualLowercaseName(unit->GetName(), "soul weaver")) + { + if (target_weaver == nullptr || target_weaver->GetDistance2d(helper.center.first, helper.center.second) > + unit->GetDistance2d(helper.center.first, helper.center.second)) + target_weaver = unit; + } + + if (botAI->EqualLowercaseName(unit->GetName(), "kel'thuzad")) + target_kelthuzad = unit; + } + std::vector targets; + if (botAI->IsRanged(bot)) + { + if (botAI->GetRangedDpsIndex(bot) <= 1) + targets = {target_soldier, target_weaver, target_abomination, target_kelthuzad}; + else + targets = {target_weaver, target_soldier, target_abomination, target_kelthuzad}; + } + else if (botAI->IsAssistTank(bot)) + targets = {target_abomination, target_guardian, target_kelthuzad}; + else + targets = {target_abomination, target_kelthuzad}; + + for (Unit* t : targets) + { + if (t) + { + target = t; + break; + } + } + if (context->GetValue("current target")->Get() == target) + return false; + + if (target_kelthuzad && target == target_kelthuzad) + return Attack(target, true); + + return Attack(target, false); +} + +bool KelthuzadPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (helper.IsPhaseOne()) + { + if (AI_VALUE(Unit*, "current target") == nullptr) + return MoveInside(NAXX_MAP_ID, helper.center.first, helper.center.second, bot->GetPositionZ(), 3.0f, + MovementPriority::MOVEMENT_COMBAT); + } + else if (helper.IsPhaseTwo()) + { + Unit* shadow_fissure = helper.GetAnyShadowFissure(); + if (!shadow_fissure || !bot->IsWithinDistInMap(shadow_fissure, 10.0f)) + { + float distance, angle; + if (botAI->IsMainTank(bot)) + { + if (AI_VALUE2(bool, "has aggro", "current target")) + return MoveTo(NAXX_MAP_ID, helper.tank_pos.first, helper.tank_pos.second, bot->GetPositionZ(), false, false, false, + false, MovementPriority::MOVEMENT_COMBAT); + else + return false; + } + else if (botAI->IsRanged(bot)) + { + uint32 index = botAI->GetRangedIndex(bot); + if (index < 8) + { + distance = 20.0f; + angle = index * M_PI / 4; + } + else + { + distance = 32.0f; + angle = (index - 8) * M_PI / 4; + } + float dx, dy; + dx = helper.center.first + cos(angle) * distance; + dy = helper.center.second + sin(angle) * distance; + return MoveTo(NAXX_MAP_ID, dx, dy, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsTank(bot)) + { + Unit* cur_tar = AI_VALUE(Unit*, "current target"); + if (cur_tar && cur_tar->GetVictim() && cur_tar->GetVictim()->ToPlayer() && + botAI->EqualLowercaseName(cur_tar->GetName(), "guardian of icecrown") && + botAI->IsAssistTank(cur_tar->GetVictim()->ToPlayer())) + { + return MoveTo(NAXX_MAP_ID, helper.assist_tank_pos.first, helper.assist_tank_pos.second, bot->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + else + return false; + } + } + else + { + float dx, dy; + float angle; + if (!botAI->IsRanged(bot)) + angle = shadow_fissure->GetAngle(helper.center.first, helper.center.second); + else + angle = bot->GetAngle(shadow_fissure) + M_PI; + + dx = shadow_fissure->GetPositionX() + cos(angle) * 10.0f; + dy = shadow_fissure->GetPositionY() + sin(angle) * 10.0f; + return MoveTo(NAXX_MAP_ID, dx, dy, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + } + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Loatheb.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Loatheb.cpp new file mode 100644 index 000000000..b48e4f1cb --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Loatheb.cpp @@ -0,0 +1,61 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "Playerbots.h" + +bool LoathebPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (botAI->IsTank(bot)) + { + if (AI_VALUE2(bool, "has aggro", "boss target")) + return MoveTo(533, helper.mainTankPos.first, helper.mainTankPos.second, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsRanged(bot)) + return MoveInside(533, helper.rangePos.first, helper.rangePos.second, bot->GetPositionZ(), 1.0f, + MovementPriority::MOVEMENT_COMBAT); + return false; +} + +bool LoathebChooseTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + Unit* target_boss = nullptr; + Unit* target_spore = nullptr; + for (auto i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + + if (!unit->IsAlive()) + continue; + + if (botAI->EqualLowercaseName(unit->GetName(), "spore")) + target_spore = unit; + + if (botAI->EqualLowercaseName(unit->GetName(), "loatheb")) + target_boss = unit; + } + if (target_spore && bot->GetDistance2d(target_spore) <= 1.0f) + target = target_spore; + else + target = target_boss; + + if (!target || context->GetValue("current target")->Get() == target) + return false; + + return Attack(target); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Maexxna.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Maexxna.cpp new file mode 100644 index 000000000..794b7e907 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Maexxna.cpp @@ -0,0 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +// Reserved for Maexxna-specific actions. diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Noth.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Noth.cpp new file mode 100644 index 000000000..209d279fe --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Noth.cpp @@ -0,0 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +// Reserved for Noth-specific actions. diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Patchwerk.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Patchwerk.cpp new file mode 100644 index 000000000..ee5eb0cc8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Patchwerk.cpp @@ -0,0 +1,37 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include +#include + +//bool PatchwerkRangedPositionAction::Execute(Event event) +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// return false; +// +// constexpr float minDistance = 12.0f; +// constexpr float maxDistance = 15.0f; +// const float distance = bot->GetExactDist2d(boss); +// +// if (distance >= minDistance && distance <= maxDistance) +// return false; +// +// const float desiredDistance = std::clamp(distance, minDistance, maxDistance); +// float angle = boss->GetAngle(bot); +// +// if (distance < 0.1f) +// angle = boss->GetOrientation(); +// +// const float x = boss->GetPositionX() + std::cos(angle) * desiredDistance; +// const float y = boss->GetPositionY() + std::sin(angle) * desiredDistance; +// const float z = bot->GetPositionZ(); +// +// return MoveTo(boss->GetMapId(), x, y, z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, +// false); +//} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Razuvious.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Razuvious.cpp new file mode 100644 index 000000000..70bdcc664 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Razuvious.cpp @@ -0,0 +1,161 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "ObjectGuid.h" +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "SharedDefines.h" + +namespace +{ +constexpr uint32 SPELL_UNDERSTUDY_TAUNT = 29060; +constexpr uint32 SPELL_BONE_BARRIER = 29061; +constexpr uint32 SPELL_BLOOD_STRIKE = 61696; +} + +bool RazuviousUseObedienceCrystalAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + // bot->GetCharm + if (Unit* charm = bot->GetCharm()) + { + Unit* target = AI_VALUE2(Unit*, "find target", "instructor razuvious"); + if (!target) + return false; + + if (charm->GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_ACTIVE) == NULL_MOTION_TYPE) + { + charm->GetMotionMaster()->Clear(); + charm->GetMotionMaster()->MoveChase(target); + charm->GetAI()->AttackStart(target); + } + Aura* forceObedience = botAI->GetAura("force obedience", charm); + uint32 duration_time; + if (!forceObedience) + { + forceObedience = botAI->GetAura("mind control", charm); + duration_time = 60000; + } + else + duration_time = 90000; + + if (!forceObedience) + return false; + + if (charm->GetDistance(target) <= 0.51f) + { + // taunt + bool tauntUseful = true; + if (forceObedience->GetDuration() <= int32(duration_time - 5000)) + { + Unit* victim = target->GetVictim(); + if (victim && victim->HasAura(SPELL_BONE_BARRIER)) + tauntUseful = false; + + if (forceObedience->GetDuration() <= 3000) + tauntUseful = false; + + } + if (forceObedience->GetDuration() >= int32(duration_time - 500)) + tauntUseful = false; + + if (tauntUseful && !charm->HasSpellCooldown(29060)) + { + // shield + if (!charm->HasSpellCooldown(SPELL_BONE_BARRIER)) + { + charm->CastSpell(charm, SPELL_BONE_BARRIER, true); + charm->AddSpellCooldown(SPELL_BONE_BARRIER, 0, 30 * 1000); + } + charm->CastSpell(target, SPELL_UNDERSTUDY_TAUNT, true); + charm->AddSpellCooldown(SPELL_UNDERSTUDY_TAUNT, 0, 20 * 1000); + } + // strike + if (!charm->HasSpellCooldown(SPELL_BLOOD_STRIKE)) + { + charm->CastSpell(target, SPELL_BLOOD_STRIKE, true); + charm->AddSpellCooldown(SPELL_BLOOD_STRIKE, 0, 4 * 1000); + } + } + } + else + { + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_10MAN_NORMAL) + { + GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); + for (auto i = npcs.begin(); i != npcs.end(); i++) + { + Creature* unit = botAI->GetCreature(*i); + if (!unit) + continue; + + if (botAI->IsMainTank(bot) && unit->GetSpawnId() != 128352) + continue; + + if (!botAI->IsMainTank(bot) && unit->GetSpawnId() != 128353) + continue; + + if (MoveTo(unit, 0.0f, MovementPriority::MOVEMENT_COMBAT)) + return true; + + Creature* creature = bot->GetNPCIfCanInteractWith(*i, UNIT_NPC_FLAG_SPELLCLICK); + if (!creature) + continue; + + creature->HandleSpellClick(bot); + return true; + } + } + else + { + GuidVector attackers = context->GetValue("attackers")->Get(); + Unit* target = nullptr; + for (auto i = attackers.begin(); i != attackers.end(); ++i) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + if (botAI->EqualLowercaseName(unit->GetName(), "death knight understudy")) + { + target = unit; + break; + } + } + if (target) + { + if (bot->GetDistance2d(target) > sPlayerbotAIConfig.spellDistance) + return MoveNear(target, sPlayerbotAIConfig.spellDistance, MovementPriority::MOVEMENT_COMBAT); + else + return botAI->CastSpell("mind control", target); + } + } + } + return false; +} + +bool RazuviousTargetAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + Unit* razuvious = AI_VALUE2(Unit*, "find target", "instructor razuvious"); + Unit* understudy = AI_VALUE2(Unit*, "find target", "death knight understudy"); + Unit* target = nullptr; + if (botAI->IsTank(bot)) + target = understudy; + else + target = razuvious; + + if (AI_VALUE(Unit*, "current target") == target) + return false; + + return Attack(target); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Sapphiron.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Sapphiron.cpp new file mode 100644 index 000000000..625e545d9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Sapphiron.cpp @@ -0,0 +1,109 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "NaxxBossHelper.h" +#include "NaxxSpellIds.h" + +bool SapphironGroundPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (botAI->IsMainTank(bot)) + { + if (AI_VALUE2(bool, "has aggro", "current target")) + return MoveTo(NAXX_MAP_ID, helper.mainTankPos.first, helper.mainTankPos.second, helper.GENERIC_HEIGHT, false, false, false, + false, MovementPriority::MOVEMENT_COMBAT); + + return false; + } + if (helper.JustLanded()) + { + uint32 index = botAI->GetGroupSlotIndex(bot); + float start_angle = 0.85 * M_PI; + float offset_angle = M_PI * 0.02 * index; + float angle = start_angle + offset_angle; + float distance; + if (botAI->IsRanged(bot)) + distance = 35.0f; + else if (botAI->IsHeal(bot)) + distance = 30.0f; + else + distance = 5.0f; + + float posX = helper.center.first + cos(angle) * distance; + float posY = helper.center.second + sin(angle) * distance; + if (MoveTo(NAXX_MAP_ID, posX, posY, helper.GENERIC_HEIGHT, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveInside(NAXX_MAP_ID, posX, posY, helper.GENERIC_HEIGHT, 2.0f, MovementPriority::MOVEMENT_COMBAT); + } + else + { + std::vector dest; + if (helper.FindPosToAvoidChill(dest)) + return MoveTo(NAXX_MAP_ID, dest[0], dest[1], dest[2], false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} + +bool SapphironFlightPositionAction::Execute(Event /*event*/) +{ + if (!helper.UpdateBossAI()) + return false; + + if (helper.WaitForExplosion()) + return MoveToNearestIcebolt(); + else + { + std::vector dest; + if (helper.FindPosToAvoidChill(dest)) + return MoveTo(NAXX_MAP_ID, dest[0], dest[1], dest[2], false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} + +bool SapphironFlightPositionAction::MoveToNearestIcebolt() +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* playerWithIcebolt = nullptr; + float minDistance; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (NaxxSpellIds::HasAnyAura(member, {NaxxSpellIds::Icebolt10, NaxxSpellIds::Icebolt25}) || + botAI->HasAura("icebolt", member, false, false, -1, true)) + { + if (!playerWithIcebolt || minDistance > bot->GetDistance(member)) + { + playerWithIcebolt = member; + minDistance = bot->GetDistance(member); + } + } + } + if (playerWithIcebolt) + { + Unit* boss = AI_VALUE2(Unit*, "find target", "sapphiron"); + if (boss) + { + float angle = boss->GetAngle(playerWithIcebolt); + float posX = playerWithIcebolt->GetPositionX() + cos(angle) * 3.0f; + float posY = playerWithIcebolt->GetPositionY() + sin(angle) * 3.0f; + if (MoveTo(NAXX_MAP_ID, posX, posY, helper.GENERIC_HEIGHT, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + return true; + + return MoveNear(playerWithIcebolt, 3.0f, MovementPriority::MOVEMENT_COMBAT); + } + } + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Shared.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Shared.cpp new file mode 100644 index 000000000..787b2a1ce --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Shared.cpp @@ -0,0 +1,24 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +uint32 RotateAroundTheCenterPointAction::FindNearestWaypoint() +{ + float minDistance = 0; + int ret = -1; + for (uint32 i = 0; i < intervals; i++) + { + float w_x = waypoints[i].first, w_y = waypoints[i].second; + float dis = bot->GetDistance2d(w_x, w_y); + if (ret == -1 || dis < minDistance) + { + ret = i; + minDistance = dis; + } + } + return ret; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Thaddius.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Thaddius.cpp new file mode 100644 index 000000000..09f91fe4d --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/Action/NaxxActions_Thaddius.cpp @@ -0,0 +1,138 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxActions.h" + +#include "PlayerbotAIConfig.h" +#include "Playerbots.h" +#include "NaxxSpellIds.h" + +bool ThaddiusAttackNearestPetAction::isUseful() +{ + if (!helper.UpdateBossAI()) + return false; + + if (!helper.IsPhasePet()) + return false; + + Unit* target = helper.GetNearestPet(); + if (!target || !bot->IsWithinDistInMap(target, 50.0f)) + return false; + + return true; +} + +bool ThaddiusAttackNearestPetAction::Execute(Event /*event*/) +{ + Unit* target = helper.GetNearestPet(); + if (!target || !bot->IsWithinLOSInMap(target)) + return MoveTo(target, 0, MovementPriority::MOVEMENT_COMBAT); + + if (AI_VALUE(Unit*, "current target") != target) + return Attack(target); + + if (botAI->IsTank(bot) && AI_VALUE2(bool, "has aggro", "current target")) + { + std::pair posForTank = helper.PetPhaseGetPosForTank(); + return MoveTo(533, posForTank.first, posForTank.second, helper.tankPosZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + if (botAI->IsRanged(bot)) + { + std::pair posForRanged = helper.PetPhaseGetPosForRanged(); + return MoveTo(533, posForRanged.first, posForRanged.second, helper.tankPosZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + } + return false; +} + +bool ThaddiusMoveToPlatformAction::isUseful() { return true; } + +bool ThaddiusMoveToPlatformAction::Execute(Event /*event*/) +{ + std::vector> position = { + // high left + {3462.99f, -2918.90f}, + // high right + {3520.65f, -2976.51f}, + // low left + {3471.36f, -2910.65f}, + // low right + {3528.80f, -2967.04f}, + // center + {3512.19f, -2928.58f}, + }; + float high_z = 312.00f, low_z = 304.02f; + bool is_left = bot->GetDistance2d(position[0].first, position[0].second) < + bot->GetDistance2d(position[1].first, position[1].second); + if (bot->GetPositionZ() >= (high_z - 3.0f)) + { + if (is_left) + { + if (!MoveTo(bot->GetMapId(), position[0].first, position[0].second, high_z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + { + float distance = bot->GetExactDist2d(position[0].first, position[0].second); + if (distance < sPlayerbotAIConfig.contactDistance) + JumpTo(bot->GetMapId(), position[2].first, position[2].second, low_z, MovementPriority::MOVEMENT_COMBAT); + // bot->TeleportTo(bot->GetMapId(), position[2].first, position[2].second, low_z, bot->GetOrientation()); + } + } + else + { + if (!MoveTo(bot->GetMapId(), position[1].first, position[1].second, high_z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT)) + { + float distance = bot->GetExactDist2d(position[1].first, position[1].second); + if (distance < sPlayerbotAIConfig.contactDistance) + JumpTo(bot->GetMapId(), position[3].first, position[3].second, low_z, MovementPriority::MOVEMENT_COMBAT); + // bot->TeleportTo(bot->GetMapId(), position[3].first, position[3].second, low_z, bot->GetOrientation()); + } + } + } + else + return MoveTo(bot->GetMapId(), position[4].first, position[4].second, low_z, false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + + return true; +} + +bool ThaddiusMovePolarityAction::isUseful() +{ + return !botAI->IsMainTank(bot) || AI_VALUE2(bool, "has aggro", "current target"); +} + +bool ThaddiusMovePolarityAction::Execute(Event /*event*/) +{ + std::vector> position = { + // left melee + {3508.29f, -2920.12f}, + // left ranged + {3501.72f, -2913.36f}, + // right melee + {3519.74f, -2931.69f}, + // right ranged + {3524.32f, -2936.26f}, + // center melee + {3512.19f, -2928.58f}, + // center ranged + {3504.68f, -2936.68f}, + }; + uint32 idx; + if (NaxxSpellIds::HasAnyAura(bot, + {NaxxSpellIds::NegativeCharge10, NaxxSpellIds::NegativeCharge25, NaxxSpellIds::NegativeChargeStack}) || + botAI->HasAura("negative charge", bot, false, false, -1, true)) + { + idx = 0; + } + else if (NaxxSpellIds::HasAnyAura(bot, + {NaxxSpellIds::PositiveCharge10, NaxxSpellIds::PositiveCharge25, NaxxSpellIds::PositiveChargeStack}) || + botAI->HasAura("positive charge", bot, false, false, -1, true)) + { + idx = 1; + } + else + { + idx = 2; + } + idx = idx * 2 + botAI->IsRanged(bot); + return MoveTo(bot->GetMapId(), position[idx].first, position[idx].second, bot->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxActionContext.h new file mode 100644 index 000000000..27d18dd47 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxActionContext.h @@ -0,0 +1,93 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXACTIONCONTEXT_H +#define PLAYERBOTS_NAXXACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "NaxxActions.h" + +class RaidNaxxActionContext : public NamedObjectContext +{ +public: + RaidNaxxActionContext() + { + creators["grobbulus go behind the boss"] = &RaidNaxxActionContext::go_behind_the_boss; + creators["rotate grobbulus"] = &RaidNaxxActionContext::rotate_grobbulus; + creators["grobbulus move center"] = &RaidNaxxActionContext::grobbulus_move_center; + creators["grobbulus move away"] = &RaidNaxxActionContext::grobbulus_move_away; + + //creators["heigan dance melee"] = &RaidNaxxActionContext::heigan_dance_melee; + //creators["heigan dance ranged"] = &RaidNaxxActionContext::heigan_dance_ranged; + creators["thaddius attack nearest pet"] = &RaidNaxxActionContext::thaddius_attack_nearest_pet; + // creators["thaddius melee to place"] = &RaidNaxxActionContext::thaddius_tank_to_place; + // creators["thaddius ranged to place"] = &RaidNaxxActionContext::thaddius_ranged_to_place; + creators["thaddius move to platform"] = &RaidNaxxActionContext::thaddius_move_to_platform; + creators["thaddius move polarity"] = &RaidNaxxActionContext::thaddius_move_polarity; + + creators["razuvious use obedience crystal"] = &RaidNaxxActionContext::razuvious_use_obedience_crystal; + creators["razuvious target"] = &RaidNaxxActionContext::razuvious_target; + + creators["four horsemen attract alternatively"] = &RaidNaxxActionContext::four_horsemen_attract_alternatively; + creators["four horsemen attack in order"] = &RaidNaxxActionContext::four_horsemen_attack_in_order; + + creators["sapphiron ground position"] = &RaidNaxxActionContext::sapphiron_ground_position; + creators["sapphiron flight position"] = &RaidNaxxActionContext::sapphiron_flight_position; + + creators["kel'thuzad choose target"] = &RaidNaxxActionContext::kelthuzad_choose_target; + creators["kel'thuzad position"] = &RaidNaxxActionContext::kelthuzad_position; + + creators["anub'rekhan choose target"] = &RaidNaxxActionContext::anubrekhan_choose_target; + creators["anub'rekhan position"] = &RaidNaxxActionContext::anubrekhan_position; + + creators["gluth choose target"] = &RaidNaxxActionContext::gluth_choose_target; + creators["gluth position"] = &RaidNaxxActionContext::gluth_position; + creators["gluth slowdown"] = &RaidNaxxActionContext::gluth_slowdown; + + //creators["patchwerk ranged position"] = &RaidNaxxActionContext::patchwerk_ranged_position; + + creators["loatheb position"] = &RaidNaxxActionContext::loatheb_position; + creators["loatheb choose target"] = &RaidNaxxActionContext::loatheb_choose_target; + } + +private: + static Action* go_behind_the_boss(PlayerbotAI* ai) { return new GrobbulusGoBehindAction(ai); } + static Action* rotate_grobbulus(PlayerbotAI* ai) { return new GrobbulusRotateAction(ai); } + static Action* grobbulus_move_center(PlayerbotAI* ai) { return new GrobbulusMoveCenterAction(ai); } + static Action* grobbulus_move_away(PlayerbotAI* ai) { return new GrobbulusMoveAwayAction(ai); } + //static Action* heigan_dance_melee(PlayerbotAI* ai) { return new HeiganDanceMeleeAction(ai); } + //static Action* heigan_dance_ranged(PlayerbotAI* ai) { return new HeiganDanceRangedAction(ai); } + static Action* thaddius_attack_nearest_pet(PlayerbotAI* ai) { return new ThaddiusAttackNearestPetAction(ai); } + // static Action* thaddius_tank_to_place(PlayerbotAI* ai) { return new ThaddiusMeleeToPlaceAction(ai); } + // static Action* thaddius_ranged_to_place(PlayerbotAI* ai) { return new ThaddiusRangedToPlaceAction(ai); } + static Action* thaddius_move_to_platform(PlayerbotAI* ai) { return new ThaddiusMoveToPlatformAction(ai); } + static Action* thaddius_move_polarity(PlayerbotAI* ai) { return new ThaddiusMovePolarityAction(ai); } + static Action* razuvious_target(PlayerbotAI* ai) { return new RazuviousTargetAction(ai); } + static Action* razuvious_use_obedience_crystal(PlayerbotAI* ai) + { + return new RazuviousUseObedienceCrystalAction(ai); + } + static Action* four_horsemen_attract_alternatively(PlayerbotAI* ai) { return new FourHorsemenAttractAlternativelyAction(ai); } + static Action* four_horsemen_attack_in_order(PlayerbotAI* ai) { return new FourHorsemenAttackInOrderAction(ai); } + // static Action* sapphiron_ground_main_tank_position(PlayerbotAI* ai) { return new + // SapphironGroundMainTankPositionAction(ai); } + static Action* sapphiron_ground_position(PlayerbotAI* ai) { return new SapphironGroundPositionAction(ai); } + static Action* sapphiron_flight_position(PlayerbotAI* ai) { return new SapphironFlightPositionAction(ai); } + // static Action* sapphiron_avoid_chill(PlayerbotAI* ai) { return new SapphironAvoidChillAction(ai); } + static Action* kelthuzad_choose_target(PlayerbotAI* ai) { return new KelthuzadChooseTargetAction(ai); } + static Action* kelthuzad_position(PlayerbotAI* ai) { return new KelthuzadPositionAction(ai); } + static Action* anubrekhan_choose_target(PlayerbotAI* ai) { return new AnubrekhanChooseTargetAction(ai); } + static Action* anubrekhan_position(PlayerbotAI* ai) { return new AnubrekhanPositionAction(ai); } + static Action* gluth_choose_target(PlayerbotAI* ai) { return new GluthChooseTargetAction(ai); } + static Action* gluth_position(PlayerbotAI* ai) { return new GluthPositionAction(ai); } + static Action* gluth_slowdown(PlayerbotAI* ai) { return new GluthSlowdownAction(ai); } + //static Action* patchwerk_ranged_position(PlayerbotAI* ai) { return new PatchwerkRangedPositionAction(ai); } + static Action* loatheb_position(PlayerbotAI* ai) { return new LoathebPositionAction(ai); } + static Action* loatheb_choose_target(PlayerbotAI* ai) { return new LoathebChooseTargetAction(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxBossHelper.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxBossHelper.h new file mode 100644 index 000000000..e3eb6ec7f --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxBossHelper.h @@ -0,0 +1,540 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXBOSSHELPER_H +#define PLAYERBOTS_NAXXBOSSHELPER_H + +#include + +#include "AiObject.h" +#include "AiObjectContext.h" +#include "EventMap.h" +#include "Log.h" +#include "NamedObjectContext.h" +#include "ObjectGuid.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "ScriptedCreature.h" +#include "SharedDefines.h" +#include "Spell.h" +#include "Timer.h" +#include "NaxxSpellIds.h" + +const uint32 NAXX_MAP_ID = 533; + +template +class GenericBossHelper : public AiObject +{ +public: + GenericBossHelper(PlayerbotAI* botAI, std::string name) : AiObject(botAI), _name(name) {} + virtual bool UpdateBossAI() + { + if (!bot->IsInCombat()) + _unit = nullptr; + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + _unit = nullptr; + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", _name); + if (!_unit) + return false; + + _target = _unit->ToCreature(); + if (!_target) + return false; + + _ai = dynamic_cast(_target->GetAI()); + if (!_ai) + return false; + + _event_map = &_ai->events; + if (!_event_map) + return false; + } + if (!_event_map) + return false; + + _timer = getMSTime(); + return true; + } + virtual void Reset() + { + _unit = nullptr; + _target = nullptr; + _ai = nullptr; + _event_map = nullptr; + _timer = 0; + } + +protected: + std::string _name; + Unit* _unit = nullptr; + Creature* _target = nullptr; + BossAiType* _ai = nullptr; + EventMap* _event_map = nullptr; + uint32 _timer = 0; +}; + +class KelthuzadBossHelper : public AiObject +{ +public: + KelthuzadBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + const std::pair center = {3716.19f, -5106.58f}; + const std::pair tank_pos = {3709.19f, -5104.86f}; + const std::pair assist_tank_pos = {3746.05f, -5112.74f}; + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + _unit = AI_VALUE2(Unit*, "find target", "kel'thuzad"); + + return _unit != nullptr; + } + bool IsPhaseOne() { return _unit && _unit->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE); } + bool IsPhaseTwo() { return _unit && !_unit->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE); } + Unit* GetAnyShadowFissure() + { + Unit* shadow_fissure = nullptr; + GuidVector units = *context->GetValue("nearest triggers"); + for (auto i = units.begin(); i != units.end(); i++) + { + Unit* unit = botAI->GetUnit(*i); + if (!unit) + continue; + if (botAI->EqualLowercaseName(unit->GetName(), "shadow fissure")) + shadow_fissure = unit; + } + return shadow_fissure; + } + +private: + void Reset() { _unit = nullptr; } + + Unit* _unit = nullptr; +}; + +class RazuviousBossHelper : public AiObject +{ +public: + RazuviousBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + _unit = AI_VALUE2(Unit*, "find target", "instructor razuvious"); + + return _unit != nullptr; + } + +private: + void Reset() { _unit = nullptr; } + + Unit* _unit = nullptr; +}; + +class SapphironBossHelper : public AiObject +{ +public: + const std::pair mainTankPos = {3512.07f, -5274.06f}; + const std::pair center = {3517.31f, -5253.74f}; + const float GENERIC_HEIGHT = 137.29f; + SapphironBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", "sapphiron"); + if (!_unit) + return false; + } + bool now_flying = _unit->IsFlying(); + if (_was_flying && !now_flying) + _last_land_ms = getMSTime(); + + _was_flying = now_flying; + return true; + } + bool IsPhaseGround() { return _unit && !_unit->IsFlying(); } + bool IsPhaseFlight() { return _unit && _unit->IsFlying(); } + bool JustLanded() + { + if (!_last_land_ms) + return false; + + return getMSTime() - _last_land_ms <= POSITION_TIME_AFTER_LANDED; + } + bool WaitForExplosion() + { + if (!IsPhaseFlight()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && + (NaxxSpellIds::HasAnyAura(member, {NaxxSpellIds::Icebolt10, NaxxSpellIds::Icebolt25}) || + botAI->HasAura("icebolt", member, false, false, -1, true))) + { + return true; + } + } + return false; + } + bool FindPosToAvoidChill(std::vector& dest) + { + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::Chill10, NaxxSpellIds::Chill25}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("chill", bot); + } + if (!aura) + return false; + + DynamicObject* dyn_obj = aura->GetDynobjOwner(); + if (!dyn_obj) + return false; + + Unit* currentTarget = AI_VALUE(Unit*, "current target"); + float angle = 0; + uint32 index = botAI->GetGroupSlotIndex(bot); + if (currentTarget) + { + if (botAI->IsRanged(bot)) + { + if (bot->GetExactDist2d(currentTarget) <= 45.0f) + angle = bot->GetAngle(dyn_obj) - M_PI + (rand_norm() - 0.5) * M_PI / 2; + else + { + if (index % 2 == 0) + angle = bot->GetAngle(currentTarget) + M_PI / 2; + else + angle = bot->GetAngle(currentTarget) - M_PI / 2; + } + } + else + { + if (index % 3 == 0) + angle = bot->GetAngle(currentTarget); + else if (index % 3 == 1) + angle = bot->GetAngle(currentTarget) + M_PI / 2; + else + angle = bot->GetAngle(currentTarget) - M_PI / 2; + } + } + else + angle = bot->GetAngle(dyn_obj) - M_PI + (rand_norm() - 0.5) * M_PI / 2; + + dest = {bot->GetPositionX() + cos(angle) * 5.0f, bot->GetPositionY() + sin(angle) * 5.0f, bot->GetPositionZ()}; + return true; + } + +private: + void Reset() + { + _unit = nullptr; + _was_flying = false; + _last_land_ms = 0; + } + + const uint32 POSITION_TIME_AFTER_LANDED = 5000; + Unit* _unit = nullptr; + bool _was_flying = false; + uint32 _last_land_ms = 0; +}; + +class GluthBossHelper : public AiObject +{ +public: + const std::pair mainTankPos25 = {3331.48f, -3109.06f}; + const std::pair mainTankPos10 = {3278.29f, -3162.06f}; + const std::pair beforeDecimatePos = {3267.34f, -3175.68f}; + const std::pair leftSlowDownPos = {3290.68f, -3141.65f}; + const std::pair rightSlowDownPos = {3300.78f, -3151.98f}; + const std::pair rangedPos = {3301.45f, -3139.29f}; + const std::pair healPos = {3303.09f, -3135.24f}; + + const float decimatedZombiePct = 10.0f; + GluthBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", "gluth"); + if (!_unit) + return false; + } + if (_unit->IsInCombat()) + { + if (_combat_start_ms == 0) + _combat_start_ms = getMSTime(); + } + else + _combat_start_ms = 0; + + return true; + } + bool BeforeDecimate() + { + if (!_unit || !_unit->HasUnitState(UNIT_STATE_CASTING)) + return false; + + Spell* spell = _unit->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell) + spell = _unit->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + + if (!spell) + return false; + + SpellInfo const* info = spell->GetSpellInfo(); + if (!info) + return false; + + if (NaxxSpellIds::MatchesAnySpellId( + info, {NaxxSpellIds::Decimate10, NaxxSpellIds::Decimate25, NaxxSpellIds::Decimate25Alt})) + return true; + + // Fallback to name for custom spell data. + return info->SpellName[LOCALE_enUS] && botAI->EqualLowercaseName(info->SpellName[LOCALE_enUS], "decimate"); + } + bool JustStartCombat() const { return _combat_start_ms != 0 && getMSTime() - _combat_start_ms < 10000; } + bool IsZombieChow(Unit* unit) const { return unit && botAI->EqualLowercaseName(unit->GetName(), "zombie chow"); } + +private: + void Reset() + { + _unit = nullptr; + _combat_start_ms = 0; + } + + Unit* _unit = nullptr; + uint32 _combat_start_ms = 0; +}; + +class LoathebBossHelper : public AiObject +{ +public: + const std::pair mainTankPos = {2877.57f, -3967.00f}; + const std::pair rangePos = {2896.96f, -3980.61f}; + LoathebBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + _unit = AI_VALUE2(Unit*, "find target", "loatheb"); + + return _unit != nullptr; + } + +private: + void Reset() { _unit = nullptr; } + + Unit* _unit = nullptr; +}; + +class FourHorsemenBossHelper : public AiObject +{ +public: + const float posZ = 241.27f; + const std::pair attractPos[2] = {{2502.03f, -2910.90f}, + {2484.61f, -2947.07f}}; // left (sir zeliek), right (lady blaumeux) + FourHorsemenBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + else if (_combat_start_ms == 0) + _combat_start_ms = getMSTime(); + + if (_sir && (!_sir->IsInWorld() || !_sir->IsAlive())) + Reset(); + + if (!_sir) + { + _sir = AI_VALUE2(Unit*, "find target", "sir zeliek"); + if (!_sir) + return false; + } + _lady = AI_VALUE2(Unit*, "find target", "lady blaumeux"); + return true; + } + void Reset() + { + _sir = nullptr; + _lady = nullptr; + _combat_start_ms = 0; + posToGo = 0; + } + bool IsAttracter(Player* bot) + { + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_25MAN_NORMAL) + { + return botAI->IsAssistRangedDpsOfIndex(bot, 0) || botAI->IsAssistHealOfIndex(bot, 0) || + botAI->IsAssistHealOfIndex(bot, 1) || botAI->IsAssistHealOfIndex(bot, 2); + } + return botAI->IsAssistRangedDpsOfIndex(bot, 0) || botAI->IsAssistHealOfIndex(bot, 0); + } + void CalculatePosToGo(Player* bot) + { + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + Unit* lady = _lady; + if (!lady) + posToGo = 0; + else + { + uint32 elapsed_ms = _combat_start_ms ? getMSTime() - _combat_start_ms : 0; + // Interval: 24s - 15s - 15s - ... + posToGo = !(elapsed_ms <= 9000 || ((elapsed_ms - 9000) / 67500) % 2 == 0); + if (botAI->IsAssistRangedDpsOfIndex(bot, 0) || (raid25 && botAI->IsAssistHealOfIndex(bot, 1))) + posToGo = 1 - posToGo; + } + } + std::pair CurrentAttractPos() + { + bool raid25 = bot->GetRaidDifficulty() == RAID_DIFFICULTY_25MAN_NORMAL; + float posX = attractPos[posToGo].first, posY = attractPos[posToGo].second; + if (posToGo == 1) + { + float offset_x = 0.0f; + float offset_y = 0.0f; + float bias = 4.5f; + if (raid25) + { + offset_x = -bias; + offset_y = bias; + } + posX += offset_x; + posY += offset_y; + } + return {posX, posY}; + } + Unit* CurrentAttackTarget() + { + if (posToGo == 0) + return _sir; + + return _lady; + } + +protected: + Unit* _sir = nullptr; + Unit* _lady = nullptr; + uint32 _combat_start_ms = 0; + int posToGo = 0; +}; +class ThaddiusBossHelper : public AiObject +{ +public: + const std::pair tankPosFeugen = {3522.94f, -3002.60f}; + const std::pair tankPosStalagg = {3436.14f, -2919.98f}; + const std::pair rangedPosFeugen = {3500.45f, -2997.92f}; + const std::pair rangedPosStalagg = {3441.01f, -2942.04f}; + const float tankPosZ = 312.61f; + ThaddiusBossHelper(PlayerbotAI* botAI) : AiObject(botAI) {} + bool UpdateBossAI() + { + if (!bot->IsInCombat()) + Reset(); + + if (_unit && (!_unit->IsInWorld() || !_unit->IsAlive())) + Reset(); + + if (!_unit) + { + _unit = AI_VALUE2(Unit*, "find target", "thaddius"); + if (!_unit) + return false; + } + feugen = AI_VALUE2(Unit*, "find target", "feugen"); + stalagg = AI_VALUE2(Unit*, "find target", "stalagg"); + return true; + } + bool IsPhasePet() { return (feugen && feugen->IsAlive()) || (stalagg && stalagg->IsAlive()); } + bool IsPhaseTransition() + { + if (IsPhasePet()) + return false; + + return _unit && _unit->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE); + } + bool IsPhaseThaddius() { return !IsPhasePet() && !IsPhaseTransition(); } + Unit* GetNearestPet() + { + Unit* unit = nullptr; + if (feugen && feugen->IsAlive()) + unit = feugen; + + if (stalagg && stalagg->IsAlive() && + (!feugen || !feugen->IsAlive() || bot->GetDistance(stalagg) < bot->GetDistance(feugen))) + unit = stalagg; + + return unit; + } + std::pair PetPhaseGetPosForTank() + { + if (GetNearestPet() == feugen) + return tankPosFeugen; + + return tankPosStalagg; + } + std::pair PetPhaseGetPosForRanged() + { + if (GetNearestPet() == feugen) + return rangedPosFeugen; + + return rangedPosStalagg; + } + +protected: + void Reset() + { + _unit = nullptr; + feugen = nullptr; + stalagg = nullptr; + } + + Unit* _unit = nullptr; + Unit* feugen = nullptr; + Unit* stalagg = nullptr; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.cpp new file mode 100644 index 000000000..f83357d86 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.cpp @@ -0,0 +1,328 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxMultipliers.h" + +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "FollowActions.h" +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "MovementActions.h" +#include "PaladinActions.h" +#include "PriestActions.h" +#include "NaxxActions.h" +#include "NaxxSpellIds.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ScriptedCreature.h" +#include "ShamanActions.h" +#include "Spell.h" +#include "UseMeetingStoneAction.h" +#include "WarriorActions.h" + +float GrobbulusMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return 1.0f; + + if (dynamic_cast(action)) + return botAI->IsMainTank(bot) ? 0.0f : 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +//float HeiganDanceMultiplier::GetValue(Action* action) +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!boss) +// { +// return 1.0f; +// } +// bool platform_phase = boss->IsWithinDist2d(2794.26f, -3706.67f, 10.0f); +// bool eruption_casting = false; +// if (boss->HasUnitState(UNIT_STATE_CASTING)) +// { +// Spell* spell = boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); +// if (!spell) +// { +// spell = boss->GetCurrentSpell(CURRENT_CHANNELED_SPELL); +// } +// if (spell) +// { +// SpellInfo const* info = spell->GetSpellInfo(); +// bool isEruption = NaxxSpellIds::MatchesAnySpellId(info, {NaxxSpellIds::Eruption10}); +// if (!isEruption && info && info->SpellName[LOCALE_enUS]) +// { +// // Fallback to name for custom spell data. +// isEruption = botAI->EqualLowercaseName(info->SpellName[LOCALE_enUS], "eruption"); +// } +// if (isEruption) +// { +// eruption_casting = true; +// } +// } +// } +// if (dynamic_cast(action) || +// dynamic_cast(action) || +// dynamic_cast(action) ) +// { +// return 0.0f; +// } +// if (!platform_phase && !eruption_casting) +// { +// return 1.0f; +// } +// if (dynamic_cast(action) || dynamic_cast(action)) +// { +// return 1.0f; +// } +// if (dynamic_cast(action) && !dynamic_cast(action)) +// { +// CastSpellAction* spellAction = dynamic_cast(action); +// uint32 spellId = AI_VALUE2(uint32, "spell id", spellAction->getSpell()); +// SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); +// if (!spellInfo) +// { +// return 0.0f; +// } +// uint32 castTime = spellInfo->CalcCastTime(); +// if (castTime == 0 && !spellInfo->IsChanneled()) +// { +// return 1.0f; +// } +// } +// return 0.0f; +//} + +float LoathebGenericMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "loatheb"); + if (!boss) + return 1.0f; + + context->GetValue("neglect threat")->Set(true); + if (botAI->GetState() == BOT_STATE_COMBAT && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + if (!dynamic_cast(action)) + return 1.0f; + + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::NecroticAura10}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("necrotic aura", bot); + } + if (!aura || aura->GetDuration() <= 1500) + return 1.0f; + + return 0.0f; +} + +float ThaddiusGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + // pet phase + if (helper.IsPhasePet() && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + { + return 0.0f; + } + // die at the same time + Unit* target = AI_VALUE(Unit*, "current target"); + Unit* feugen = AI_VALUE2(Unit*, "find target", "feugen"); + Unit* stalagg = AI_VALUE2(Unit*, "find target", "stalagg"); + if (helper.IsPhasePet() && target && feugen && stalagg && target->GetHealthPct() <= 40 && + (feugen->GetHealthPct() >= target->GetHealthPct() + 3 || stalagg->GetHealthPct() >= target->GetHealthPct() + 3)) + { + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + } + // magnetic pull + // uint32 curr_timer = eventMap->GetTimer(); + // // if (curr_phase == 2 && bot->GetPositionZ() > 312.5f && dynamic_cast(action)) + // { + // if (curr_phase == 2 && (curr_timer % 20000 >= 18000 || curr_timer % 20000 <= 2000) && + // dynamic_cast(action)) + // { + // // MotionMaster *mm = bot->GetMotionMaster(); + // // mm->Clear(); + // return 0.0f; + // } + // thaddius phase + // if (curr_phase == 8 && dynamic_cast(action)) + // { + // return 0.0f; + // } + return 1.0f; +} + +float SapphironGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float InstructorRazuviousGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + context->GetValue("neglect threat")->Set(true); + if (botAI->GetState() == BOT_STATE_COMBAT && + (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + { + return 0.0f; + } + return 1.0f; +} + +float KelthuzadGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if ((dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action))) + { + return 0.0f; + } + if (helper.IsPhaseOne()) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + { + return 0.0f; + } + } + if (helper.IsPhaseTwo()) + { + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + } + return 1.0f; +} + +float AnubrekhanGenericMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'rekhan"); + if (!boss) + return 1.0f; + + if (NaxxSpellIds::HasAnyAura( + boss, {NaxxSpellIds::LocustSwarm10, NaxxSpellIds::LocustSwarm10Alt, NaxxSpellIds::LocustSwarm25}) || + botAI->HasAura("locust swarm", boss)) + { + if (dynamic_cast(action)) + return 0.0f; + } + return 1.0f; +} + +float FourHorsemenGenericMultiplier::GetValue(Action* action) +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "sir zeliek"); + if (!boss) + return 1.0f; + + context->GetValue("neglect threat")->Set(true); + if ((dynamic_cast(action) || dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +// float GothikGenericMultiplier::GetValue(Action* action) +// { +// Unit* boss = AI_VALUE2(Unit*, "find target", "gothik the harvester"); +// if (!boss) +// { +// return 1.0f; +// } +// BossAI* boss_ai = dynamic_cast(boss->GetAI()); +// EventMap* eventMap = boss_botAI->GetEvents(); +// uint32 curr_phase = eventMap->GetPhaseMask(); +// if (curr_phase == 1 && (dynamic_cast(action))) +// { +// return 0.0f; +// } +// if (curr_phase == 1 && (dynamic_cast(action))) +// { +// Unit* target = action->GetTarget(); +// if (target == boss) +// { +// return 0.0f; +// } +// } +// return 1.0f; +// } + +float GluthGenericMultiplier::GetValue(Action* action) +{ + if (!helper.UpdateBossAI()) + return 1.0f; + + if ((dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action))) + { + return 0.0f; + } + + if (botAI->IsMainTank(bot)) + { + Aura* aura = NaxxSpellIds::GetAnyAura(bot, {NaxxSpellIds::MortalWound10, NaxxSpellIds::MortalWound25}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("mortal wound", bot, false, true); + } + if (aura && aura->GetStackAmount() >= 5) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + { + return 0.0f; + } + } + } + if (dynamic_cast(action)) + { + Unit* target = AI_VALUE(Unit*, "current target"); + if (helper.IsZombieChow(target)) + return 0.0f; + } + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.h new file mode 100644 index 000000000..616db7f87 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxMultipliers.h @@ -0,0 +1,120 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXMULTIPLIERS_H +#define PLAYERBOTS_NAXXMULTIPLIERS_H + +#include "Multiplier.h" +#include "NaxxBossHelper.h" + +class GrobbulusMultiplier : public Multiplier +{ +public: + GrobbulusMultiplier(PlayerbotAI* ai) : Multiplier(ai, "grobbulus") {} + +public: + virtual float GetValue(Action* action); +}; + +//class HeiganDanceMultiplier : public Multiplier +//{ +//public: +// HeiganDanceMultiplier(PlayerbotAI* ai) : Multiplier(ai, "helgan dance") {} +// +//public: +// virtual float GetValue(Action* action); +//}; + +class LoathebGenericMultiplier : public Multiplier +{ +public: + LoathebGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "loatheb generic") {} + +public: + virtual float GetValue(Action* action); +}; + +class ThaddiusGenericMultiplier : public Multiplier +{ +public: + ThaddiusGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "thaddius generic"), helper(ai) {} + +public: + virtual float GetValue(Action* action); + +private: + ThaddiusBossHelper helper; +}; + +class SapphironGenericMultiplier : public Multiplier +{ +public: + SapphironGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "sapphiron generic"), helper(ai) {} + + virtual float GetValue(Action* action); + +private: + SapphironBossHelper helper; +}; + +class InstructorRazuviousGenericMultiplier : public Multiplier +{ +public: + InstructorRazuviousGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "instructor razuvious generic"), helper(ai) {} + virtual float GetValue(Action* action); + +private: + RazuviousBossHelper helper; +}; + +class KelthuzadGenericMultiplier : public Multiplier +{ +public: + KelthuzadGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "kelthuzad generic"), helper(ai) {} + virtual float GetValue(Action* action); + +private: + KelthuzadBossHelper helper; +}; + +class AnubrekhanGenericMultiplier : public Multiplier +{ +public: + AnubrekhanGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "anubrekhan generic") {} + +public: + virtual float GetValue(Action* action); +}; + +class FourHorsemenGenericMultiplier : public Multiplier +{ +public: + FourHorsemenGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "four horsemen generic") {} + +public: + virtual float GetValue(Action* action); +}; + +// class GothikGenericMultiplier : public Multiplier +// { +// public: +// GothikGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "gothik generic") {} + +// public: +// virtual float GetValue(Action* action); +// }; + +class GluthGenericMultiplier : public Multiplier +{ +public: + GluthGenericMultiplier(PlayerbotAI* ai) : Multiplier(ai, "gluth generic"), helper(ai) {} + float GetValue(Action* action) override; + +private: + GluthBossHelper helper; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxSpellIds.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxSpellIds.h new file mode 100644 index 000000000..8665737e8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxSpellIds.h @@ -0,0 +1,172 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXSPELLIDS_H +#define PLAYERBOTS_NAXXSPELLIDS_H + +#include + +#include "PlayerbotAI.h" + +// use src/server/scripts/Northrend/Naxxramas/naxxramas.h for CreatureId, NaxxramasSay, NaxxramasEvent, NaxxramasMisc +namespace NaxxSpellIds +{ + // Heigan + static constexpr uint32 Eruption10 = 29371; +/* + SPELL_SPELL_DISRUPTION = 29310, + SPELL_DECREPIT_FEVER = 29998, + SPELL_PLAGUE_CLOUD = 29350, + SPELL_TELEPORT_SELF = 30211 +*/ + + // Grobbulus + static constexpr uint32 PoisonCloud = 28240; + + // Thaddius polarity + static constexpr uint32 PositiveCharge10 = 28059; + static constexpr uint32 PositiveCharge25 = 28062; + static constexpr uint32 PositiveChargeStack = 29659; + static constexpr uint32 NegativeCharge10 = 28084; + static constexpr uint32 NegativeCharge25 = 28085; + static constexpr uint32 NegativeChargeStack = 29660; +/* + SPELL_MAGNETIC_PULL = 28337, + SPELL_TESLA_SHOCK = 28099, + SPELL_SHOCK_VISUAL = 28159, + + // Stalagg + SPELL_POWER_SURGE = 54529, + SPELL_STALAGG_CHAIN = 28096, + + // Feugen + SPELL_STATIC_FIELD = 28135, + SPELL_FEUGEN_CHAIN = 28111, + + // Thaddius + SPELL_POLARITY_SHIFT = 28089, + SPELL_BALL_LIGHTNING = 28299, + SPELL_CHAIN_LIGHTNING = 28167, + SPELL_BERSERK = 27680, + SPELL_THADDIUS_VISUAL_LIGHTNING = 28136, + SPELL_THADDIUS_SPAWN_STUN = 28160, + + SPELL_POSITIVE_CHARGE = 28062, + SPELL_POSITIVE_CHARGE_STACK = 29659, + SPELL_NEGATIVE_CHARGE = 28085, + SPELL_NEGATIVE_CHARGE_STACK = 29660, + SPELL_POSITIVE_POLARITY = 28059, + SPELL_NEGATIVE_POLARITY = 28084 +*/ + // Sapphiron + static constexpr uint32 Icebolt10 = 28522; + static constexpr uint32 Icebolt25 = 28526; + static constexpr uint32 Chill10 = 28547; + static constexpr uint32 Chill25 = 55699; +/* + // Fight + SPELL_FROST_AURA = 28531, + SPELL_CLEAVE = 19983, + SPELL_TAIL_SWEEP = 55697, + SPELL_SUMMON_BLIZZARD = 28560, + SPELL_LIFE_DRAIN = 28542, + SPELL_BERSERK = 26662, + + // Ice block + SPELL_ICEBOLT_CAST = 28526, + SPELL_ICEBOLT_TRIGGER = 28522, + SPELL_FROST_MISSILE = 30101, + SPELL_FROST_EXPLOSION = 28524, + + // Visuals + SPELL_SAPPHIRON_DIES = 29357 +*/ + // Gluth + static constexpr uint32 Decimate10 = 28374; + static constexpr uint32 Decimate25 = 54426; + static constexpr uint32 Decimate25Alt = 28375; + static constexpr uint32 MortalWound10 = 25646; + static constexpr uint32 MortalWound25 = 54378; +/* + SPELL_MORTAL_WOUND = 25646, + SPELL_ENRAGE = 28371, + SPELL_DECIMATE = 28374, + SPELL_DECIMATE_DAMAGE = 28375, + SPELL_BERSERK = 26662, + SPELL_INFECTED_WOUND = 29306, + SPELL_CHOW_SEARCHER = 28404 +*/ + // Anub'Rekhan + static constexpr uint32 LocustSwarm10 = 28785; + static constexpr uint32 LocustSwarm10Alt = 28786; + static constexpr uint32 LocustSwarm25 = 54021; // 25-man Locust Swarm +/* + SPELL_IMPALE = 28783, + SPELL_LOCUST_SWARM = 28785, + SPELL_SUMMON_CORPSE_SCARABS_5 = 29105, + SPELL_SUMMON_CORPSE_SCARABS_10 = 28864, + SPELL_BERSERK = 26662 + ACHIEV_TIMED_START_EVENT = 9891, + EVENT_SPAWN_CRYPT_GUARDS_1 = 0, + EVENT_BERSERK = 1, + //// + Position const cryptguardPositions[] = { + { 3299.732f, -3502.489f, 287.077f, 2.378f }, + { 3299.086f, -3450.929f, 287.077f, 3.999f }, + { 3331.217f, -3476.607f, 287.074f, 3.269f } +}; + +*/ + // Loatheb + static constexpr uint32 NecroticAura10 = 55593; +/* + SPELL_NECROTIC_AURA = 55593, + SPELL_SUMMON_SPORE = 29234, + SPELL_DEATHBLOOM = 29865, + SPELL_INEVITABLE_DOOM = 29204, + SPELL_BERSERK = 26662 +*/ + inline bool HasAnyAura(Unit* unit, std::initializer_list spellIds) + { + if (!unit) + return false; + + for (uint32 spellId : spellIds) + { + if (unit->HasAura(spellId)) + return true; + } + return false; + } + + inline Aura* GetAnyAura(Unit* unit, std::initializer_list spellIds) + { + if (!unit) + return nullptr; + + for (uint32 spellId : spellIds) + { + if (Aura* aura = unit->GetAura(spellId)) + return aura; + } + return nullptr; + } + + inline bool MatchesAnySpellId(SpellInfo const* info, std::initializer_list spellIds) + { + if (!info) + return false; + + for (uint32 spellId : spellIds) + { + if (info->Id == spellId) + return true; + } + return false; + } +} // namespace NaxxSpellIds + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.cpp new file mode 100644 index 000000000..0ab4ade66 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.cpp @@ -0,0 +1,162 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxStrategy.h" + +#include "NaxxMultipliers.h" + +void RaidNaxxStrategy::InitTriggers(std::vector& triggers) +{ + // Grobbulus + triggers.push_back(new TriggerNode("mutating injection melee", + { NextAction("grobbulus move away", ACTION_RAID + 2) } + )); + + triggers.push_back(new TriggerNode("mutating injection ranged", + { NextAction("grobbulus go behind the boss", ACTION_RAID + 2) } + )); + + triggers.push_back(new TriggerNode("mutating injection removed", + { NextAction("grobbulus move center", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("grobbulus cloud", + { NextAction("rotate grobbulus", ACTION_RAID + 1) } + )); + + // Heigan the Unclean + //triggers.push_back(new TriggerNode("heigan melee", + // { NextAction("heigan dance melee", ACTION_RAID + 1) } + //)); + + //triggers.push_back(new TriggerNode("heigan ranged", + // { NextAction("heigan dance ranged", ACTION_RAID + 1) } + //)); + + // Kel'Thuzad + triggers.push_back( + new TriggerNode("kel'thuzad", + { + NextAction("kel'thuzad position", ACTION_RAID + 2), + NextAction("kel'thuzad choose target", ACTION_RAID + 1) + }) + ); + + // Anub'Rekhan + triggers.push_back(new TriggerNode("anub'rekhan", + { NextAction("anub'rekhan position", ACTION_RAID + 1) } + )); + + // Grand Widow Faerlina + triggers.push_back(new TriggerNode("faerlina", + { NextAction("avoid aoe", ACTION_RAID + 1) } + )); + + // Maexxna + triggers.push_back( + new TriggerNode("maexxna", + { + NextAction("rear flank", ACTION_RAID + 1), + NextAction("avoid aoe", ACTION_RAID + 1) + }) + ); + + // Patchwerk + //triggers.push_back(new TriggerNode("patchwerk tank", + // { NextAction("tank face", ACTION_RAID + 2) } + //)); + + //triggers.push_back(new TriggerNode("patchwerk ranged", + // { NextAction("patchwerk ranged position", ACTION_RAID + 2) } + //)); + + //triggers.push_back(new TriggerNode("patchwerk non-tank", + // { NextAction("rear flank", ACTION_RAID + 1) } + //)); + + // Thaddius + triggers.push_back(new TriggerNode("thaddius phase pet", + { NextAction("thaddius attack nearest pet", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("thaddius phase pet lose aggro", + { NextAction("taunt spell", ACTION_RAID + 2) } + )); + + triggers.push_back(new TriggerNode("thaddius phase transition", + { NextAction("thaddius move to platform", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("thaddius phase thaddius", + { NextAction("thaddius move polarity", ACTION_RAID + 1) } + )); + + // Instructor Razuvious + triggers.push_back(new TriggerNode("razuvious tank", + { NextAction("razuvious use obedience crystal", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("razuvious nontank", + { NextAction("razuvious target", ACTION_RAID + 1) } + )); + + // four horsemen + triggers.push_back(new TriggerNode("four horsemen attractors", + { NextAction("four horsemen attract alternatively", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("four horsemen except attractors", + { NextAction("four horsemen attack in order", ACTION_RAID + 1) } + )); + + // sapphiron + triggers.push_back(new TriggerNode("sapphiron ground", + { NextAction("sapphiron ground position", ACTION_RAID + 1) } + )); + + triggers.push_back(new TriggerNode("sapphiron flight", + { NextAction("sapphiron flight position", ACTION_RAID + 1) } + )); + + // Gluth + triggers.push_back( + new TriggerNode("gluth", + { + NextAction("gluth choose target", ACTION_RAID + 1), + NextAction("gluth position", ACTION_RAID + 1), + NextAction("gluth slowdown", ACTION_RAID) + }) + ); + + triggers.push_back(new TriggerNode("gluth main tank mortal wound", + { NextAction("taunt spell", ACTION_RAID + 1) } + )); + + // Loatheb + triggers.push_back( + new TriggerNode("loatheb", + { + NextAction("loatheb position", ACTION_RAID + 1), + NextAction("loatheb choose target", ACTION_RAID + 1) + }) + ); + +} + +void RaidNaxxStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new GrobbulusMultiplier(botAI)); + //multipliers.push_back(new HeiganDanceMultiplier(botAI)); + multipliers.push_back(new LoathebGenericMultiplier(botAI)); + multipliers.push_back(new ThaddiusGenericMultiplier(botAI)); + multipliers.push_back(new SapphironGenericMultiplier(botAI)); + multipliers.push_back(new InstructorRazuviousGenericMultiplier(botAI)); + multipliers.push_back(new KelthuzadGenericMultiplier(botAI)); + multipliers.push_back(new AnubrekhanGenericMultiplier(botAI)); + multipliers.push_back(new FourHorsemenGenericMultiplier(botAI)); + // multipliers.push_back(new GothikGenericMultiplier(botAI)); + multipliers.push_back(new GluthGenericMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.h new file mode 100644 index 000000000..9465bc793 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxStrategy.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXSTRATEGY_H +#define PLAYERBOTS_NAXXSTRATEGY_H + +#include "Strategy.h" + +class RaidNaxxStrategy : public Strategy +{ +public: + RaidNaxxStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "naxx"; } + virtual void InitTriggers(std::vector& triggers) override; + virtual void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggerContext.h new file mode 100644 index 000000000..ccb825771 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggerContext.h @@ -0,0 +1,86 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXTRIGGERCONTEXT_H +#define PLAYERBOTS_NAXXTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "NaxxTriggers.h" + +class RaidNaxxTriggerContext : public NamedObjectContext +{ +public: + RaidNaxxTriggerContext() + { + creators["mutating injection melee"] = &RaidNaxxTriggerContext::mutating_injection_melee; + creators["mutating injection ranged"] = &RaidNaxxTriggerContext::mutating_injection_ranged; + creators["mutating injection removed"] = &RaidNaxxTriggerContext::mutating_injection_removed; + creators["grobbulus cloud"] = &RaidNaxxTriggerContext::grobbulus_cloud; + //creators["heigan melee"] = &RaidNaxxTriggerContext::heigan_melee; + //creators["heigan ranged"] = &RaidNaxxTriggerContext::heigan_ranged; + + creators["thaddius phase pet"] = &RaidNaxxTriggerContext::thaddius_phase_pet; + creators["thaddius phase pet lose aggro"] = &RaidNaxxTriggerContext::thaddius_phase_pet_lose_aggro; + creators["thaddius phase transition"] = &RaidNaxxTriggerContext::thaddius_phase_transition; + creators["thaddius phase thaddius"] = &RaidNaxxTriggerContext::thaddius_phase_thaddius; + + creators["razuvious tank"] = &RaidNaxxTriggerContext::razuvious_tank; + creators["razuvious nontank"] = &RaidNaxxTriggerContext::razuvious_nontank; + + creators["four horsemen attractors"] = &RaidNaxxTriggerContext::four_horsemen_attractors; + creators["four horsemen except attractors"] = &RaidNaxxTriggerContext::four_horsemen_except_attractors; + + creators["sapphiron ground"] = &RaidNaxxTriggerContext::sapphiron_ground; + creators["sapphiron flight"] = &RaidNaxxTriggerContext::sapphiron_flight; + + creators["kel'thuzad"] = &RaidNaxxTriggerContext::kelthuzad; + + creators["anub'rekhan"] = &RaidNaxxTriggerContext::anubrekhan; + creators["faerlina"] = &RaidNaxxTriggerContext::faerlina; + creators["maexxna"] = &RaidNaxxTriggerContext::maexxna; + //creators["patchwerk tank"] = &RaidNaxxTriggerContext::patchwerk_tank; + //creators["patchwerk non-tank"] = &RaidNaxxTriggerContext::patchwerk_non_tank; + //creators["patchwerk ranged"] = &RaidNaxxTriggerContext::patchwerk_ranged; + + creators["gluth"] = &RaidNaxxTriggerContext::gluth; + creators["gluth main tank mortal wound"] = &RaidNaxxTriggerContext::gluth_main_tank_mortal_wound; + + creators["loatheb"] = &RaidNaxxTriggerContext::loatheb; + } + +private: + static Trigger* mutating_injection_melee(PlayerbotAI* ai) { return new MutatingInjectionMeleeTrigger(ai); } + static Trigger* mutating_injection_ranged(PlayerbotAI* ai) { return new MutatingInjectionRangedTrigger(ai); } + static Trigger* mutating_injection_removed(PlayerbotAI* ai) { return new MutatingInjectionRemovedTrigger(ai); } + static Trigger* grobbulus_cloud(PlayerbotAI* ai) { return new GrobbulusCloudTrigger(ai); } + //static Trigger* heigan_melee(PlayerbotAI* ai) { return new HeiganMeleeTrigger(ai); } + //static Trigger* heigan_ranged(PlayerbotAI* ai) { return new HeiganRangedTrigger(ai); } + + static Trigger* thaddius_phase_pet(PlayerbotAI* ai) { return new ThaddiusPhasePetTrigger(ai); } + static Trigger* thaddius_phase_pet_lose_aggro(PlayerbotAI* ai) { return new ThaddiusPhasePetLoseAggroTrigger(ai); } + static Trigger* thaddius_phase_transition(PlayerbotAI* ai) { return new ThaddiusPhaseTransitionTrigger(ai); } + static Trigger* thaddius_phase_thaddius(PlayerbotAI* ai) { return new ThaddiusPhaseThaddiusTrigger(ai); } + static Trigger* razuvious_tank(PlayerbotAI* ai) { return new RazuviousTankTrigger(ai); } + static Trigger* razuvious_nontank(PlayerbotAI* ai) { return new RazuviousNontankTrigger(ai); } + + static Trigger* four_horsemen_attractors(PlayerbotAI* ai) { return new FourHorsemenAttractorsTrigger(ai); } + static Trigger* four_horsemen_except_attractors(PlayerbotAI* ai) { return new FourHorsemenExceptAttractorsTrigger(ai); } + + static Trigger* sapphiron_ground(PlayerbotAI* ai) { return new SapphironGroundTrigger(ai); } + static Trigger* sapphiron_flight(PlayerbotAI* ai) { return new SapphironFlightTrigger(ai); } + static Trigger* kelthuzad(PlayerbotAI* ai) { return new KelthuzadTrigger(ai); } + static Trigger* anubrekhan(PlayerbotAI* ai) { return new AnubrekhanTrigger(ai); } + static Trigger* faerlina(PlayerbotAI* ai) { return new FaerlinaTrigger(ai); } + static Trigger* maexxna(PlayerbotAI* ai) { return new MaexxnaTrigger(ai); } + //static Trigger* patchwerk_tank(PlayerbotAI* ai) { return new PatchwerkTankTrigger(ai); } + //static Trigger* patchwerk_non_tank(PlayerbotAI* ai) { return new PatchwerkNonTankTrigger(ai); } + //static Trigger* patchwerk_ranged(PlayerbotAI* ai) { return new PatchwerkRangedTrigger(ai); } + static Trigger* gluth(PlayerbotAI* ai) { return new GluthTrigger(ai); } + static Trigger* gluth_main_tank_mortal_wound(PlayerbotAI* ai) { return new GluthMainTankMortalWoundTrigger(ai); } + static Trigger* loatheb(PlayerbotAI* ai) { return new LoathebTrigger(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.cpp new file mode 100644 index 000000000..31b578341 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.cpp @@ -0,0 +1,263 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NaxxTriggers.h" + +#include "Playerbots.h" +#include "NaxxSpellIds.h" +#include "Timer.h" +#include "Trigger.h" + +bool MutatingInjectionMeleeTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + return MutatingInjectionTrigger::IsActive() && !botAI->IsRanged(bot); +} + +bool MutatingInjectionRangedTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + return MutatingInjectionTrigger::IsActive() && botAI->IsRanged(bot); +} + +bool AuraRemovedTrigger::IsActive() +{ + bool check = botAI->HasAura(name, bot, false, false, -1, true); + bool ret = false; + if (prev_check && !check) + ret = true; + + prev_check = check; + return ret; +} + +bool MutatingInjectionRemovedTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + return HasNoAuraTrigger::IsActive() && botAI->GetState() == BOT_STATE_COMBAT && botAI->IsRanged(bot); +} + +bool GrobbulusCloudTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grobbulus"); + if (!boss) + return false; + + if (!botAI->IsMainTank(bot)) + return false; + + // bot->Yell("has aggro on " + boss->GetName() + " : " + to_string(AI_VALUE2(bool, "has aggro", "boss target")), + // LANG_UNIVERSAL); + if (!AI_VALUE2(bool, "has aggro", "boss target")) + return false; + + uint32 now = getMSTime(); + bool poison_cloud_casting = false; + if (boss->HasUnitState(UNIT_STATE_CASTING)) + { + Spell* spell = boss->GetCurrentSpell(CURRENT_GENERIC_SPELL); + if (!spell) + spell = boss->GetCurrentSpell(CURRENT_CHANNELED_SPELL); + + if (spell) + poison_cloud_casting = NaxxSpellIds::MatchesAnySpellId(spell->GetSpellInfo(), {NaxxSpellIds::PoisonCloud}); + + } + if (!poison_cloud_casting && last_cloud_ms != 0 && now - last_cloud_ms < CloudRotationDelayMs) + return false; + + last_cloud_ms = now; + return true; +} + +//bool HeiganMeleeTrigger::IsActive() +//{ +// Unit* heigan = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!heigan) +// { +// return false; +// } +// return !botAI->IsRanged(bot); +//} +// +//bool HeiganRangedTrigger::IsActive() +//{ +// Unit* heigan = AI_VALUE2(Unit*, "find target", "heigan the unclean"); +// if (!heigan) +// { +// return false; +// } +// return botAI->IsRanged(bot); +//} + +bool RazuviousTankTrigger::IsActive() +{ + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_10MAN_NORMAL) + return helper.UpdateBossAI() && botAI->IsTank(bot); + + return helper.UpdateBossAI() && bot->getClass() == CLASS_PRIEST; +} + +bool RazuviousNontankTrigger::IsActive() +{ + Difficulty diff = bot->GetRaidDifficulty(); + if (diff == RAID_DIFFICULTY_10MAN_NORMAL) + return helper.UpdateBossAI() && !(botAI->IsTank(bot)); + + return helper.UpdateBossAI() && !(bot->getClass() == CLASS_PRIEST); +} + +bool FourHorsemenAttractorsTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsAttracter(bot); +} + +bool FourHorsemenExceptAttractorsTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return !helper.IsAttracter(bot); +} + +bool SapphironGroundTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseGround(); +} + +bool SapphironFlightTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseFlight(); +} + +bool GluthTrigger::IsActive() { return helper.UpdateBossAI(); } + +bool GluthMainTankMortalWoundTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + if (!botAI->IsAssistTankOfIndex(bot, 0)) + return false; + + Unit* mt = AI_VALUE(Unit*, "main tank"); + if (!mt) + return false; + + Aura* aura = NaxxSpellIds::GetAnyAura(mt, {NaxxSpellIds::MortalWound10, NaxxSpellIds::MortalWound25}); + if (!aura) + { + // Fallback to name for custom spell data. + aura = botAI->GetAura("mortal wound", mt, false, true); + } + if (!aura || aura->GetStackAmount() < 5) + return false; + + return true; +} + +bool KelthuzadTrigger::IsActive() { return helper.UpdateBossAI(); } + +bool AnubrekhanTrigger::IsActive() { + Unit* boss = AI_VALUE2(Unit*, "find target", "anub'rekhan"); + if (!boss) + return false; + + return true; +} + +bool FaerlinaTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "grand widow faerlina"); + if (!boss) + return false; + + return true; +} + +bool MaexxnaTrigger::IsActive() +{ + Unit* boss = AI_VALUE2(Unit*, "find target", "maexxna"); + if (!boss) + return false; + + return !botAI->IsTank(bot); +} + +//bool PatchwerkTankTrigger::IsActive() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// { +// return false; +// } +// return !botAI->IsTank(bot) && !botAI->IsRanged(bot); +//} +// +//bool PatchwerkRangedTrigger::IsActive() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// { +// return false; +// } +// return !botAI->IsTank(bot) && botAI->IsRanged(bot); +//} +// +//bool PatchwerkNonTankTrigger::IsActive() +//{ +// Unit* boss = AI_VALUE2(Unit*, "find target", "patchwerk"); +// if (!boss) +// { +// return false; +// } +// return !botAI->IsTank(bot); +//} + +bool LoathebTrigger::IsActive() { return helper.UpdateBossAI(); } + +bool ThaddiusPhasePetTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhasePet(); +} + +bool ThaddiusPhaseTransitionTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseTransition(); +} + +bool ThaddiusPhaseThaddiusTrigger::IsActive() +{ + if (!helper.UpdateBossAI()) + return false; + + return helper.IsPhaseThaddius(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.h new file mode 100644 index 000000000..01f93e5b5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Naxx/NaxxTriggers.h @@ -0,0 +1,264 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NAXXTRIGGERS_H +#define PLAYERBOTS_NAXXTRIGGERS_H + +#include "EventMap.h" +#include "GenericTriggers.h" +#include "PlayerbotAIConfig.h" +#include "NaxxBossHelper.h" +#include "Trigger.h" + +class MutatingInjectionTrigger : public HasAuraTrigger +{ +public: + MutatingInjectionTrigger(PlayerbotAI* ai) : HasAuraTrigger(ai, "mutating injection", 1) {} +}; + +class MutatingInjectionMeleeTrigger : public MutatingInjectionTrigger +{ +public: + MutatingInjectionMeleeTrigger(PlayerbotAI* ai) : MutatingInjectionTrigger(ai) {} + bool IsActive() override; +}; + +class MutatingInjectionRangedTrigger : public MutatingInjectionTrigger +{ +public: + MutatingInjectionRangedTrigger(PlayerbotAI* ai) : MutatingInjectionTrigger(ai) {} + bool IsActive() override; +}; + +class AuraRemovedTrigger : public Trigger +{ +public: + AuraRemovedTrigger(PlayerbotAI* botAI, std::string name) : Trigger(botAI, name, 1) + { + this->prev_check = false; + } + virtual bool IsActive() override; + +protected: + bool prev_check; +}; + +class MutatingInjectionRemovedTrigger : public HasNoAuraTrigger +{ +public: + MutatingInjectionRemovedTrigger(PlayerbotAI* ai) : HasNoAuraTrigger(ai, "mutating injection") {} + virtual bool IsActive(); +}; + +class GrobbulusCloudTrigger : public Trigger +{ +public: + GrobbulusCloudTrigger(PlayerbotAI* ai) : Trigger(ai, "grobbulus cloud event"), last_cloud_ms(0) {} + bool IsActive() override; + +private: + uint32 last_cloud_ms; + static constexpr uint32 CloudRotationDelayMs = 15000; +}; + +//class HeiganMeleeTrigger : public Trigger +//{ +//public: +// HeiganMeleeTrigger(PlayerbotAI* ai) : Trigger(ai, "heigan melee") {} +// virtual bool IsActive(); +//}; +// +//class HeiganRangedTrigger : public Trigger +//{ +//public: +// HeiganRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "heigan ranged") {} +// bool IsActive() override; +//}; + +class RazuviousTankTrigger : public Trigger +{ +public: + RazuviousTankTrigger(PlayerbotAI* ai) : Trigger(ai, "instructor razuvious tank"), helper(ai) {} + bool IsActive() override; + +private: + RazuviousBossHelper helper; +}; + +class RazuviousNontankTrigger : public Trigger +{ +public: + RazuviousNontankTrigger(PlayerbotAI* ai) : Trigger(ai, "instructor razuvious non-tank"), helper(ai) {} + bool IsActive() override; + +private: + RazuviousBossHelper helper; +}; + +class KelthuzadTrigger : public Trigger +{ +public: + KelthuzadTrigger(PlayerbotAI* ai) : Trigger(ai, "kel'thuzad trigger"), helper(ai) {} + bool IsActive() override; + +private: + KelthuzadBossHelper helper; +}; + +class AnubrekhanTrigger : public Trigger +{ +public: + AnubrekhanTrigger(PlayerbotAI* ai) : Trigger(ai, "anub'rekhan") {} + bool IsActive() override; +}; + + class FaerlinaTrigger : public Trigger + { + public: + FaerlinaTrigger(PlayerbotAI* ai) : Trigger(ai, "faerlina") {} + bool IsActive() override; + }; + +class MaexxnaTrigger : public Trigger +{ +public: + MaexxnaTrigger(PlayerbotAI* ai) : Trigger(ai, "maexxna") {} + bool IsActive() override; +}; + +//class PatchwerkTankTrigger : public Trigger +//{ +//public: +// PatchwerkTankTrigger(PlayerbotAI* ai) : Trigger(ai, "patchwerk tank") {} +// bool IsActive() override; +//}; +// +//class PatchwerkNonTankTrigger : public Trigger +//{ +//public: +// PatchwerkNonTankTrigger(PlayerbotAI* ai) : Trigger(ai, "patchwerk non-tank") {} +// bool IsActive() override; +//}; +// +//class PatchwerkRangedTrigger : public Trigger +//{ +//public: +// PatchwerkRangedTrigger(PlayerbotAI* ai) : Trigger(ai, "patchwerk ranged") {} +// bool IsActive() override; +//}; + +class ThaddiusPhasePetTrigger : public Trigger +{ +public: + ThaddiusPhasePetTrigger(PlayerbotAI* ai) : Trigger(ai, "thaddius phase pet"), helper(ai) {} + bool IsActive() override; + +private: + ThaddiusBossHelper helper; +}; + +class ThaddiusPhasePetLoseAggroTrigger : public ThaddiusPhasePetTrigger +{ +public: + ThaddiusPhasePetLoseAggroTrigger(PlayerbotAI* ai) : ThaddiusPhasePetTrigger(ai) {} + virtual bool IsActive() + { + Unit* target = AI_VALUE(Unit*, "current target"); + return ThaddiusPhasePetTrigger::IsActive() && botAI->IsTank(bot) && target && target->GetVictim() != bot; + } +}; + +class ThaddiusPhaseTransitionTrigger : public Trigger +{ +public: + ThaddiusPhaseTransitionTrigger(PlayerbotAI* ai) : Trigger(ai, "thaddius phase transition"), helper(ai) {} + bool IsActive() override; + +private: + ThaddiusBossHelper helper; +}; + +class ThaddiusPhaseThaddiusTrigger : public Trigger +{ +public: + ThaddiusPhaseThaddiusTrigger(PlayerbotAI* ai) : Trigger(ai, "thaddius phase thaddius"), helper(ai) {} + bool IsActive() override; + +private: + ThaddiusBossHelper helper; +}; + +class FourHorsemenAttractorsTrigger : public Trigger +{ +public: + FourHorsemenAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "four horsemen attractors"), helper(ai) {} + bool IsActive() override; + +private: + FourHorsemenBossHelper helper; +}; + +class FourHorsemenExceptAttractorsTrigger : public Trigger +{ +public: + FourHorsemenExceptAttractorsTrigger(PlayerbotAI* ai) : Trigger(ai, "four horsemen except attractors"), helper(ai) {} + bool IsActive() override; + +private: + FourHorsemenBossHelper helper; +}; + +class SapphironGroundTrigger : public Trigger +{ +public: + SapphironGroundTrigger(PlayerbotAI* ai) : Trigger(ai, "sapphiron ground"), helper(ai) {} + bool IsActive() override; + +private: + SapphironBossHelper helper; +}; + +class SapphironFlightTrigger : public Trigger +{ +public: + SapphironFlightTrigger(PlayerbotAI* ai) : Trigger(ai, "sapphiron flight"), helper(ai) {} + bool IsActive() override; + +private: + SapphironBossHelper helper; +}; + +class GluthTrigger : public Trigger +{ +public: + GluthTrigger(PlayerbotAI* ai) : Trigger(ai, "gluth trigger"), helper(ai) {} + bool IsActive() override; + +private: + GluthBossHelper helper; +}; + +class GluthMainTankMortalWoundTrigger : public Trigger +{ +public: + GluthMainTankMortalWoundTrigger(PlayerbotAI* ai) : Trigger(ai, "gluth main tank mortal wound trigger"), helper(ai) {} + bool IsActive() override; + +private: + GluthBossHelper helper; +}; + +class LoathebTrigger : public Trigger +{ +public: + LoathebTrigger(PlayerbotAI* ai) : Trigger(ai, "loatheb"), helper(ai) {} + bool IsActive() override; + +private: + LoathebBossHelper helper; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/RaidOsActionContext.h b/modules/mod-playerbots/src/Ai/Raid/OS/OSActionContext.h similarity index 79% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/RaidOsActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/OS/OSActionContext.h index 55afe5260..faac07cc6 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/RaidOsActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_RAIDOSACTIONCONTEXT_H -#define _PLAYERBOT_RAIDOSACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OSACTIONCONTEXT_H +#define PLAYERBOTS_OSACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "RaidOsActions.h" +#include "OSActions.h" class RaidOsActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Action/RaidOsActions.cpp b/modules/mod-playerbots/src/Ai/Raid/OS/OSActions.cpp similarity index 92% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Action/RaidOsActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/OS/OSActions.cpp index ee9dfb0f7..5d9b8d3d0 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Action/RaidOsActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSActions.cpp @@ -1,5 +1,11 @@ -#include "RaidOsActions.h" -#include "RaidOsTriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OSActions.h" +#include "OSTriggers.h" #include "Playerbots.h" @@ -8,9 +14,6 @@ bool SartharionTankPositionAction::Execute(Event /*event*/) Unit* boss = AI_VALUE2(Unit*, "find target", "sartharion"); if (!boss) { return false; } - // Unit* shadron = AI_VALUE2(Unit*, "find target", "shadron"); - // Unit* tenebron = AI_VALUE2(Unit*, "find target", "tenebron"); - // Unit* vesperon = AI_VALUE2(Unit*, "find target", "vesperon"); Unit* shadron = nullptr; Unit* tenebron = nullptr; Unit* vesperon = nullptr; @@ -96,9 +99,7 @@ bool AvoidTwilightFissureAction::Execute(Event /*event*/) { float currentDistance = bot->GetDistance2d(unit); if (currentDistance < radius) - { return MoveAway(unit, radius - currentDistance); - } } } return false; @@ -127,39 +128,29 @@ bool AvoidFlameTsunamiAction::Execute(Event /*event*/) { bool wavePassed = currentPos.GetPositionX() > unit->GetPositionX(); if (wavePassed) - { return false; - } if (bot->GetExactDist2d(currentPos.GetPositionX(), TSUNAMI_RIGHT_SAFE_ALL) > looseDistance) - { return MoveTo(OS_MAP_ID, currentPos.GetPositionX(), TSUNAMI_RIGHT_SAFE_ALL, currentPos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } } else // LEFT WAVE { bool wavePassed = currentPos.GetPositionX() < unit->GetPositionX(); if (wavePassed) - { return false; - } if (botAI->IsMelee(bot)) { if (bot->GetExactDist2d(currentPos.GetPositionX(), TSUNAMI_LEFT_SAFE_MELEE) > looseDistance) - { return MoveTo(OS_MAP_ID, currentPos.GetPositionX(), TSUNAMI_LEFT_SAFE_MELEE, currentPos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } } else // Ranged/healers { if (bot->GetExactDist2d(currentPos.GetPositionX(), TSUNAMI_LEFT_SAFE_RANGED) > looseDistance) - { return MoveTo(OS_MAP_ID, currentPos.GetPositionX(), TSUNAMI_LEFT_SAFE_RANGED, currentPos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); - } } } } @@ -178,30 +169,18 @@ bool SartharionAttackPriorityAction::Execute(Event /*event*/) Unit* target = nullptr; if (acolyte) - { target = acolyte; - } else if (vesperon) - { target = vesperon; - } else if (tenebron) - { target = tenebron; - } else if (shadron) - { target = shadron; - } else if (sartharion) - { target = sartharion; - } if (target && AI_VALUE(Unit*, "current target") != target) - { return Attack(target); - } return false; } @@ -215,9 +194,7 @@ bool EnterTwilightPortalAction::Execute(Event /*event*/) if (!portal) { return false; } if (!portal->IsAtInteractDistance(bot)) - { return MoveTo(portal, fmaxf(portal->GetInteractionDistance() - 1.0f, 0.0f)); - } // Go through portal WorldPacket data1(CMSG_GAMEOBJ_USE); @@ -230,12 +207,11 @@ bool EnterTwilightPortalAction::Execute(Event /*event*/) bool ExitTwilightPortalAction::Execute(Event /*event*/) { GameObject* portal = bot->FindNearestGameObject(GO_NORMAL_PORTAL, 100.0f); - if (!portal) { return false; } + if (!portal) + return false; if (!portal->IsAtInteractDistance(bot)) - { return MoveTo(portal, fmaxf(portal->GetInteractionDistance() - 1.0f, 0.0f)); - } // Go through portal WorldPacket data1(CMSG_GAMEOBJ_USE); diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Action/RaidOsActions.h b/modules/mod-playerbots/src/Ai/Raid/OS/OSActions.h similarity index 86% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Action/RaidOsActions.h rename to modules/mod-playerbots/src/Ai/Raid/OS/OSActions.h index eba7c11d3..6bd006060 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Action/RaidOsActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDOSACTIONS_H -#define _PLAYERBOT_RAIDOSACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OSACTIONS_H +#define PLAYERBOTS_OSACTIONS_H #include "MovementActions.h" #include "AttackAction.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.cpp similarity index 81% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.cpp index bc63f967d..36c0b4142 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.cpp @@ -1,4 +1,10 @@ -#include "RaidOsMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OSMultipliers.h" #include "ChooseTargetActions.h" #include "DKActions.h" @@ -9,8 +15,8 @@ #include "GenericSpellActions.h" #include "MovementActions.h" #include "PaladinActions.h" -#include "RaidOsActions.h" -#include "RaidOsTriggers.h" +#include "OSActions.h" +#include "OSTriggers.h" #include "ReachTargetActions.h" #include "ScriptedCreature.h" #include "WarriorActions.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.h new file mode 100644 index 000000000..e3a0ad998 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSMultipliers.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OSMULTIPLIERS_H +#define PLAYERBOTS_OSMULTIPLIERS_H + +#include "Multiplier.h" + +class SartharionMultiplier : public Multiplier +{ +public: + SartharionMultiplier(PlayerbotAI* ai) : Multiplier(ai, "sartharion") {} + +public: + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/OS/OSStrategy.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/OS/OSStrategy.cpp index 4468de991..83947e99d 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSStrategy.cpp @@ -1,5 +1,11 @@ -#include "RaidOsStrategy.h" -#include "RaidOsMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OSStrategy.h" +#include "OSMultipliers.h" #include "Strategy.h" void RaidOsStrategy::InitTriggers(std::vector& triggers) diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h b/modules/mod-playerbots/src/Ai/Raid/OS/OSStrategy.h similarity index 53% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/OS/OSStrategy.h index 44983f1fa..792b310ad 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Strategy/RaidOsStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSStrategy.h @@ -1,8 +1,12 @@ -#ifndef _PLAYERBOT_RAIDOSSTRATEGY_H -#define _PLAYERBOT_RAIDOSSTRATEGY_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OSSTRATEGY_H +#define PLAYERBOTS_OSSTRATEGY_H -#include "AiObjectContext.h" -#include "Multiplier.h" #include "Strategy.h" class RaidOsStrategy : public Strategy diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/OS/OSTriggerContext.h similarity index 80% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/OS/OSTriggerContext.h index b8a1f4b31..a1f66fddf 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSTriggerContext.h @@ -1,9 +1,14 @@ -#ifndef _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OSTRIGGERCONTEXT_H +#define PLAYERBOTS_OSTRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "NamedObjectContext.h" -#include "RaidOsTriggers.h" +#include "OSTriggers.h" class RaidOsTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Trigger/RaidOsTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/OS/OSTriggers.cpp similarity index 92% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Trigger/RaidOsTriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/OS/OSTriggers.cpp index 710e3cac1..0a4f45e4b 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Trigger/RaidOsTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSTriggers.cpp @@ -1,4 +1,10 @@ -#include "RaidOsTriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OSTriggers.h" #include "SharedDefines.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Trigger/RaidOsTriggers.h b/modules/mod-playerbots/src/Ai/Raid/OS/OSTriggers.h similarity index 92% rename from modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Trigger/RaidOsTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/OS/OSTriggers.h index b7a457036..a58800b91 100644 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Trigger/RaidOsTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/OS/OSTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDOSTRIGGERS_H -#define _PLAYERBOT_RAIDOSTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_OSTRIGGERS_H +#define PLAYERBOTS_OSTRIGGERS_H #include "PlayerbotAI.h" #include "Playerbots.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.h deleted file mode 100644 index 802335e13..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/ObsidianSanctum/Multiplier/RaidOsMultipliers.h +++ /dev/null @@ -1,16 +0,0 @@ - -#ifndef _PLAYERRBOT_RAIDOSMULTIPLIERS_H -#define _PLAYERRBOT_RAIDOSMULTIPLIERS_H - -#include "Multiplier.h" - -class SartharionMultiplier : public Multiplier -{ -public: - SartharionMultiplier(PlayerbotAI* ai) : Multiplier(ai, "sartharion") {} - -public: - virtual float GetValue(Action* action); -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/RaidOnyxiaActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyActionContext.h similarity index 75% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/RaidOnyxiaActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyActionContext.h index b2923f969..f0cc7fe11 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/RaidOnyxiaActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyActionContext.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_RAIDONYXIAACTIONS_CONTEXT_H -#define _PLAYERBOT_RAIDONYXIAACTIONS_CONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ONYACTIONCONTEXT_H +#define PLAYERBOTS_ONYACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "RaidOnyxiaActions.h" +#include "OnyActions.h" class RaidOnyxiaActionContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyActions.cpp similarity index 91% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyActions.cpp index e2fe3cbe6..889f1a2e5 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyActions.cpp @@ -1,4 +1,10 @@ -#include "RaidOnyxiaActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OnyActions.h" #include "GenericSpellActions.h" #include "LastMovementValue.h" @@ -99,8 +105,12 @@ bool RaidOnyxiaMoveToSafeZoneAction::Execute(Event /*event*/) if (bot->IsWithinDist2d(bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->radius)) return false; // Already safe + // Stop current spell first + bot->AttackStop(); + bot->InterruptNonMeleeSpells(false); + // bot->Yell("Moving to Safe Zone!", LANG_UNIVERSAL); - return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bot->GetPositionZ(), + return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->pos.GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_COMBAT); } diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyActions.h similarity index 61% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyActions.h index 3943aaf60..6c89f9a04 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Action/RaidOnyxiaActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyActions.h @@ -1,8 +1,13 @@ -// RaidOnyxiaActions.h -#ifndef _PLAYERBOT_RAIDONYXIAACTIONS_H_ -#define _PLAYERBOT_RAIDONYXIAACTIONS_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +// RaidOnyxiaActions.h +#ifndef PLAYERBOTS_ONYACTIONS_H +#define PLAYERBOTS_ONYACTIONS_H -#include "Action.h" #include "AttackAction.h" #include "GenericSpellActions.h" #include "MovementActions.h" @@ -45,42 +50,45 @@ public: bool Execute(Event event) override; private: - std::vector GetSafeZonesForBreath(uint32 spellId) + static std::vector GetSafeZonesForBreath(uint32 spellId) { - // Define your safe zone coordinates based on the map - // Example assumes Onyxia's lair map coordinates - float z = bot->GetPositionZ(); // Stay at current height + // Safe zone coordinates based on the map + // Assumes Onyxia's lair map coordinates switch (spellId) { case 17086: // N to S case 18351: // S to N - return {SafeZone{Position(-10.0f, -180.0f, z), 5.0f}, - SafeZone{Position(-20.0f, -250.0f, z), 5.0f}}; // Bottom Safe Zone + return { + SafeZone{Position(-10.0f, -180.0f, -87.0f), 5.0f}, + SafeZone{Position(-20.0f, -250.0f, -88.0f), 5.0f} + }; // Bottom Safe Zone case 18576: // E to W case 18609: // W to E return { - SafeZone{Position(20.0f, -210.0f, z), 5.0f}, - SafeZone{Position(-75.0f, -210.0f, z), 5.0f}, + SafeZone{Position(20.0f, -210.0f, -85.5f), 5.0f}, + SafeZone{Position(-75.0f, -210.0f, -83.4f), 5.0f}, }; // Left Safe Zone case 18564: // SE to NW case 18584: // NW to SE return { - SafeZone{Position(-60.0f, -195.0f, z), 5.0f}, - SafeZone{Position(10.0f, -240.0f, z), 5.0f}, + SafeZone{Position(-60.0f, -195.0f, -85.0f), 5.0f}, + SafeZone{Position(10.0f, -240.0f, -85.9f), 5.0f}, }; // NW Safe Zone case 18596: // SW to NE case 18617: // NE to SW return { - SafeZone{Position(7.0f, -185.0f, z), 5.0f}, - SafeZone{Position(-60.0f, -240.0f, z), 5.0f}, + SafeZone{Position(7.0f, -185.0f, -86.2f), 5.0f}, + SafeZone{Position(-60.0f, -240.0f, -85.2f), 5.0f}, }; // NE Safe Zone default: - return {SafeZone{Position(0.0f, 0.0f, z), 5.0f}}; // Fallback center - shouldn't ever happen + return { + SafeZone{Position(-40.0f, -214.0f, -86.6f), 5.0f} + }; // Fallback center - shouldn't ever happen } } }; diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyStrategy.cpp similarity index 76% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyStrategy.cpp index b1217f59e..788b3c3e9 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyStrategy.cpp @@ -1,4 +1,10 @@ -#include "RaidOnyxiaStrategy.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OnyStrategy.h" void RaidOnyxiaStrategy::InitTriggers(std::vector& triggers) { @@ -24,7 +30,7 @@ void RaidOnyxiaStrategy::InitTriggers(std::vector& triggers) "ony whelps spawn", { NextAction("ony kill whelps", ACTION_RAID + 1) })); } -void RaidOnyxiaStrategy::InitMultipliers(std::vector& multipliers) +void RaidOnyxiaStrategy::InitMultipliers(std::vector& /*multipliers*/) { // Empty for now } diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyStrategy.h similarity index 54% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyStrategy.h index 189b2491f..201c35695 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Strategy/RaidOnyxiaStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyStrategy.h @@ -1,7 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ // RaidOnyxiaStrategy.h -#ifndef _PLAYERBOT_RAIDONYXIASTRATEGY_H_ -#define _PLAYERBOT_RAIDONYXIASTRATEGY_H_ +#ifndef PLAYERBOTS_ONYSTRATEGY_H +#define PLAYERBOTS_ONYSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggerContext.h similarity index 75% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggerContext.h index dba18f564..c3145d9ed 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggerContext.h @@ -1,9 +1,14 @@ -#ifndef _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ONYTRIGGERCONTEXT_H +#define PLAYERBOTS_ONYTRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "NamedObjectContext.h" -#include "RaidOnyxiaTriggers.h" +#include "OnyTriggers.h" class RaidOnyxiaTriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Trigger/RaidOnyxiaTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggers.cpp similarity index 92% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/Trigger/RaidOnyxiaTriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggers.cpp index 56486ebf9..7f1b50c87 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Trigger/RaidOnyxiaTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggers.cpp @@ -1,4 +1,10 @@ -#include "RaidOnyxiaTriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "OnyTriggers.h" #include "GenericTriggers.h" #include "ObjectAccessor.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Trigger/RaidOnyxiaTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggers.h similarity index 71% rename from modules/mod-playerbots/src/Ai/Raid/Onyxia/Trigger/RaidOnyxiaTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggers.h index d8205357b..d38b890bc 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Onyxia/Trigger/RaidOnyxiaTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/Ony/OnyTriggers.h @@ -1,6 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + // OnyxiaTriggers.h -#ifndef _PLAYERBOT_ONYXIATRIGGERS_H_ -#define _PLAYERBOT_ONYXIATRIGGERS_H_ +#ifndef PLAYERBOTS_ONYTRIGGERS_H +#define PLAYERBOTS_ONYTRIGGERS_H #include "PlayerbotAI.h" #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions.h b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions.h new file mode 100644 index 000000000..8ccac47b0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions.h @@ -0,0 +1,1355 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSA_H +#define PLAYERBOTS_RSA_H + +#include +#include +#include +#include +#include +#include +#include + +#include "AttackAction.h" +#include "MovementActions.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "RSScripts.h" +#include "RSShared.h" +#include "RSStrategy.h" +#include "RSTriggers.h" +#include "RtiTargetValue.h" +#include "ThreatManager.h" +#include "Timer.h" +#include "Vehicle.h" + +inline const Position RS_SAVIANA_TANK_POSITION = Position(3152.36f, 619.4369f, 78.645874f); +inline const Position RS_ZARITHRIAN_TANK_POSITION = Position(3049.7f, 528.125f, 89.5219f); +inline const Position RS_BALTHARUS_TANK_POSITION = Position(3138.6873f, 381.2857f, 87.57249f); +inline const Position RS_BALTHARUS_HEALER_POSITION = Position(3133.5005f, 414.67023f, 85.509735f); +inline const Position RS_HALION_TANK_POSITION = Position(3141.0532f, 532.5485f, 72.88935f); +inline const Position RS_HALION_CENTER_POSITION = Position(3156.0f, 533.811f, 72.9882f); +inline const Position RS_HALION_TANK_METEOR_SPOT = Position(3170.5989f, 530.64606f, 72.887024f); +inline const Position RS_HALION_COMBUSTION_SPOT_A = Position(3186.9033f, 497.85657f, 72.289635f); +inline const Position RS_HALION_COMBUSTION_SPOT_B = Position(3164.8992f, 580.4305f, 72.873024f); +inline const Position RS_HALION_METEOR_SPOT_A = Position(3167.8013f, 510.99084f, 72.85342f); +inline const Position RS_HALION_METEOR_SPOT_B = Position(3160.1658f, 553.1889f, 72.88719f); +inline const Position RS_HALION_METEOR_MID = Position(3158.5593f, 534.84015f, 72.88689f); + +inline float RsAngleDiff(float a, float b) +{ + float diff = a - b; + while (diff > static_cast(M_PI)) + diff -= 2.0f * static_cast(M_PI); + while (diff < -static_cast(M_PI)) + diff += 2.0f * static_cast(M_PI); + return diff; +} + +inline bool RsSpotInFrontArc(Unit* caster, float x, float y, float range) +{ + if (!caster || !caster->IsAlive()) + return false; + + float const dx = x - caster->GetPositionX(); + float const dy = y - caster->GetPositionY(); + if (std::sqrt(dx * dx + dy * dy) > range) + return false; + + float const diff = RsAngleDiff(std::atan2(dy, dx), caster->GetOrientation()); + return std::fabs(diff) <= static_cast(M_PI) / 2.0f; +} + +inline bool RsCastClassTaunt(PlayerbotAI* botAI, Player* bot, Unit* target) +{ + if (!target || !target->IsAlive()) + return false; + + bool const threatReset = botAI->HasCheat(BotCheatMask::raid); + + if (threatReset) + RsForceThreat(target, bot); + + switch (bot->getClass()) + { + case CLASS_PALADIN: + if (threatReset) + bot->RemoveSpellCooldown(RS_SPELL_TAUNT_PALADIN, true); + if (botAI->CastSpell("hand of reckoning", target)) + return true; + break; + case CLASS_DEATH_KNIGHT: + if (threatReset) + bot->RemoveSpellCooldown(RS_SPELL_TAUNT_DK, true); + if (botAI->CastSpell("dark command", target)) + return true; + break; + case CLASS_DRUID: + if (threatReset) + bot->RemoveSpellCooldown(RS_SPELL_TAUNT_DRUID, true); + if (botAI->CastSpell("growl", target)) + return true; + break; + case CLASS_WARRIOR: + if (threatReset) + bot->RemoveSpellCooldown(RS_SPELL_TAUNT_WARRIOR, true); + if (botAI->CastSpell("taunt", target)) + return true; + break; + default: + break; + } + + if (botAI->CastSpell("shoot", target) || botAI->CastSpell("throw", target)) + return true; + + return false; +} + +inline float RsHalionBeamSignedDist(float px, float py, Unit* a, Unit* b) +{ + float const ax = a->GetPositionX(); + float const ay = a->GetPositionY(); + float const ex = b->GetPositionX() - ax; + float const ey = b->GetPositionY() - ay; + float const len = std::sqrt(ex * ex + ey * ey); + if (len < 0.01f) + return std::numeric_limits::max(); + + return ((px - ax) * ey - (py - ay) * ex) / len; +} + +inline bool RsHalionEngaged(PlayerbotAI* botAI) +{ + Unit* boss = botAI->GetAiObjectContext()->GetValue("find target", "halion")->Get(); + return boss && boss->IsAlive() && boss->IsInCombat() && + (boss->GetEntry() == NPC_HALION || boss->GetEntry() == NPC_TWILIGHT_HALION); +} + +inline Unit* RsHalionPhase1Boss(PlayerbotAI* botAI) +{ + Unit* boss = botAI->GetAiObjectContext()->GetValue("find target", "halion")->Get(); + if (!boss || !boss->IsAlive() || boss->GetEntry() != NPC_HALION) + return nullptr; + + if (!boss->HealthAbovePct(75)) + return nullptr; + + if (boss->GetDisplayId() != boss->GetNativeDisplayId()) + return nullptr; + + return boss; +} + +inline bool RsHalionInTwilight(Player* bot) +{ + return bot && bot->HasAura(SPELL_TWILIGHT_REALM); +} + +inline Unit* RsHalionFindBoss(PlayerbotAI* botAI, std::string const& name, uint32 entry) +{ + Unit* boss = botAI->GetAiObjectContext()->GetValue("find target", name)->Get(); + if (boss && boss->IsAlive() && boss->GetEntry() == entry) + return boss; + + return RsFindTarget(botAI, [entry](Unit* unit) { return unit->GetEntry() == entry; }); +} + +inline Unit* RsHalionTwilightBoss(PlayerbotAI* botAI) +{ + ObjectGuid const guid = RubySanctumHelpers::RsState(botAI->GetBot()->GetInstanceId()).cutterTiming.bossGuid; + + if (!guid.IsEmpty()) + if (Unit* boss = botAI->GetUnit(guid); boss && boss->IsAlive() && boss->GetEntry() == NPC_TWILIGHT_HALION) + return boss; + + return RsHalionFindBoss(botAI, "twilight halion", NPC_TWILIGHT_HALION); +} + +inline Unit* RsHalionPhase2Boss(PlayerbotAI* botAI) +{ + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss || !boss->HealthAbovePct(50)) + return nullptr; + + return boss; +} + +inline Unit* RsHalionAnyPhysicalBoss(PlayerbotAI* botAI) +{ + ObjectGuid const guid = + RubySanctumHelpers::RsState(botAI->GetBot()->GetInstanceId()).halionCorporeality.physicalGuid; + + if (!guid.IsEmpty()) + if (Unit* boss = botAI->GetUnit(guid); boss && boss->IsAlive() && boss->GetEntry() == NPC_HALION) + return boss; + + return RsHalionFindBoss(botAI, "halion", NPC_HALION); +} + +inline bool RsHalionIsPhase3(PlayerbotAI* botAI) +{ + Unit* physical = RsHalionAnyPhysicalBoss(botAI); + if (!physical || physical->HealthAbovePct(50)) + return false; + return RsHalionTwilightBoss(botAI) != nullptr; +} + +inline constexpr uint8 RS_HALION_CORP_BALANCED = 5; +inline constexpr uint8 RS_HALION_CORP_STOP_AT = 4; +inline constexpr uint8 RS_HALION_CORP_STOP_ALL_AT = 2; +inline constexpr uint32 RS_HALION_CORP_FRESH_MS = 3000; + +inline bool RsHalionOwnCorpIndex(PlayerbotAI* botAI, Player* bot, uint8& outIndex) +{ + if (!RsHalionIsPhase3(botAI)) + return false; + + RubySanctumHelpers::HalionCorporeality const& corp = + RubySanctumHelpers::RsState(bot->GetInstanceId()).halionCorporeality; + bool const inTwilight = RsHalionInTwilight(bot); + uint32 const ownStamp = inTwilight ? corp.twilightStamp : corp.physicalStamp; + if (ownStamp == 0 || getMSTimeDiff(ownStamp, getMSTime()) > RS_HALION_CORP_FRESH_MS) + return false; + + outIndex = inTwilight ? corp.twilightIndex : corp.physicalIndex; + return true; +} + +inline bool RsHalionRealmThrottled(PlayerbotAI* botAI, Player* bot) +{ + uint8 ownIndex = RS_HALION_CORP_BALANCED; + if (!RsHalionOwnCorpIndex(botAI, bot, ownIndex)) + return false; + + bool& held = RubySanctumHelpers::RsState(bot->GetInstanceId()).realmThrottled[bot->GetGUID()]; + + if (ownIndex <= RS_HALION_CORP_STOP_AT) + held = true; + else if (ownIndex >= RS_HALION_CORP_BALANCED) + held = false; + + return held; +} + +inline bool RsHalionLeadingTooMuch(PlayerbotAI* botAI, Player* bot) +{ + uint8 ownIndex = RS_HALION_CORP_BALANCED; + if (!RsHalionOwnCorpIndex(botAI, bot, ownIndex)) + return false; + + return ownIndex <= RS_HALION_CORP_STOP_ALL_AT; +} + +inline bool RsHalionInThrottledHalf(Player* bot) +{ + Group* group = bot->GetGroup(); + if (!group) + return true; + + uint32 const instanceId = bot->GetInstanceId(); + bool const inTwilight = RsHalionInTwilight(bot); + ObjectGuid const selfGuid = bot->GetGUID(); + + uint32 total = 0; + uint32 rank = 0; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member->GetInstanceId() != instanceId) + continue; + if (!GET_PLAYERBOT_AI(member)) + continue; + if (PlayerbotAI::IsTank(member) || PlayerbotAI::IsHeal(member)) + continue; + if (RsHalionInTwilight(member) != inTwilight) + continue; + + ++total; + if (member->GetGUID() < selfGuid) + ++rank; + } + + return rank < total / 2; +} + +inline bool RsHalionP3TwilightAssigned(Player* bot) +{ + if (PlayerbotAI::IsMainTank(bot)) + return false; + if (PlayerbotAI::IsTank(bot)) + return true; + + std::map& assignment = RubySanctumHelpers::RsState(bot->GetInstanceId()).p3TwilightAssignment; + ObjectGuid const selfGuid = bot->GetGUID(); + + auto it = assignment.find(selfGuid); + if (it != assignment.end()) + return it->second; + + Group* group = bot->GetGroup(); + if (!group) + { + assignment[selfGuid] = false; + return false; + } + + std::vector melee, ranged, heals; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || PlayerbotAI::IsTank(member)) + continue; + ObjectGuid const guid = member->GetGUID(); + if (PlayerbotAI::IsHeal(member)) + heals.push_back(guid); + else if (PlayerbotAI::IsRanged(member)) + ranged.push_back(guid); + else + melee.push_back(guid); + } + + auto assignHalf = [&](std::vector& guids) + { + std::sort(guids.begin(), guids.end()); + int const half = static_cast(guids.size()) / 2; + for (int i = 0; i < static_cast(guids.size()); ++i) + assignment[guids[i]] = (i >= half); + }; + + for (ObjectGuid const& guid : ranged) + assignment[guid] = false; + for (ObjectGuid const& guid : melee) + assignment[guid] = true; + + assignHalf(heals); + + it = assignment.find(selfGuid); + return it != assignment.end() && it->second; +} + +inline GameObject* RsHalionFindPortal(PlayerbotAI* botAI) +{ + Player* bot = botAI->GetBot(); + constexpr float SEARCH_RANGE = 200.0f; + + if (RsHalionInTwilight(bot)) + return bot->FindNearestGameObject(GO_HALION_PORTAL_4, SEARCH_RANGE, true); + + if (GameObject* go = bot->FindNearestGameObject(GO_HALION_PORTAL_1, SEARCH_RANGE, true)) + return go; + if (GameObject* go = bot->FindNearestGameObject(GO_HALION_PORTAL_2, SEARCH_RANGE, true)) + return go; + if (GameObject* go = bot->FindNearestGameObject(GO_HALION_PORTAL_3, SEARCH_RANGE, true)) + return go; + return bot->FindNearestGameObject(GO_HALION_PORTAL_4, SEARCH_RANGE, true); +} + +inline constexpr uint32 RS_ZARITHRIAN_CLEAVE_SWAP_STACKS = 5; +inline constexpr float RS_BALTHARUS_TANK_FACING = 0.0f; +inline constexpr float RS_BALTHARUS_HEALER_REACH = 5.0f; +inline constexpr float RS_BALTHARUS_BRAND_RANGE = 14.0f; + +class RsBaltharusBrandAction : public MoveAwayFromPlayerWithDebuffAction +{ +public: + RsBaltharusBrandAction(PlayerbotAI* botAI) + : MoveAwayFromPlayerWithDebuffAction(botAI, "rs baltharus brand", SPELL_ENERVATING_BRAND, + RS_BALTHARUS_BRAND_RANGE) {} + bool Execute(Event event) override; +}; + +class RsBaltharusHealerPositionAction : public AttackAction +{ +public: + RsBaltharusHealerPositionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs baltharus healer position") {} + bool Execute(Event event) override; +}; + +class RsBaltharusAvoidFrontAction : public AttackAction +{ +public: + RsBaltharusAvoidFrontAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs baltharus avoid front") {} + bool Execute(Event event) override; +}; + +class RsBaltharusTankPositionAction : public AttackAction +{ +public: + RsBaltharusTankPositionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs baltharus tank position") {} + bool Execute(Event event) override; + +private: + bool HoldAt(Position const& spot, Unit* faceAwayFrom); +}; + +class RsSavianaConflagrationAction : public MoveAwayFromPlayerWithDebuffAction +{ +public: + RsSavianaConflagrationAction(PlayerbotAI* botAI) + : MoveAwayFromPlayerWithDebuffAction(botAI, "rs saviana conflagration", SPELL_FLAME_BEACON, 10.0f) {} + bool Execute(Event event) override; +}; + +class RsSavianaTankPositionAction : public AttackAction +{ +public: + RsSavianaTankPositionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs saviana tank position") {} + bool Execute(Event event) override; +}; + +class RsSavianaAvoidFrontAction : public AttackAction +{ +public: + RsSavianaAvoidFrontAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs saviana avoid front") {} + bool Execute(Event event) override; +}; + +class RsSavianaMeleeSpreadAction : public AttackAction +{ +public: + RsSavianaMeleeSpreadAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs saviana melee spread") {} + bool Execute(Event event) override; +}; + +class RsZarithrianAddsAction : public AttackAction +{ +public: + RsZarithrianAddsAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs zarithrian adds") {} + bool Execute(Event event) override; + +private: + Unit* FindPriorityAdd(); +}; + +class RsZarithrianTankAction : public AttackAction +{ +public: + RsZarithrianTankAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs zarithrian tank") {} + bool Execute(Event event) override; + +private: + uint32 GetCleaveStacks(Unit* unit); + Unit* FindBoss(); + void CollectAdds(std::vector& adds); + bool HoldBoss(Unit* boss); + bool RunAddsToBoss(Unit* boss, std::vector const& adds); +}; + +inline constexpr float RS_TRASH_TANK_SPREAD = 20.0f; +inline constexpr float RS_TRASH_RANGED_DIST = 25.0f; +inline constexpr float RS_TRASH_RANGED_TOL = 10.0f; +inline constexpr float RS_TRASH_RANGED_STEP = 7.0f; +inline constexpr float RS_TRASH_TANK_SPACE_TOL = 5.0f; +inline constexpr float RS_TRASH_DETECT_RANGE = 40.0f; +inline constexpr float RS_TRASH_TANK_GRAB_RANGE = 100.0f; + +inline bool RsTrashIsCommander(uint32 entry) +{ + return entry == NPC_CHARSCALE_COMMANDER || entry == NPC_CHARSCALE_COMMANDER_H; +} + +inline bool RsTrashIsInvoker(uint32 entry) +{ + return entry == NPC_CHARSCALE_INVOKER || entry == NPC_CHARSCALE_INVOKER_H; +} + +inline bool RsTrashIsCharscale(uint32 entry) +{ + switch (entry) + { + case NPC_CHARSCALE_INVOKER: + case NPC_CHARSCALE_INVOKER_H: + case NPC_CHARSCALE_ASSAULTER: + case NPC_CHARSCALE_ASSAULTER_H: + case NPC_CHARSCALE_ELITE: + case NPC_CHARSCALE_ELITE_H: + case NPC_CHARSCALE_COMMANDER: + case NPC_CHARSCALE_COMMANDER_H: + return true; + default: + return false; + } +} + +inline bool RsTrashIsTrashEntry(uint32 entry) +{ + return RsTrashIsCharscale(entry) || entry == NPC_ONYX_FLAMECALLER; +} + +inline int RsTrashPriorityRank(uint32 entry) +{ + switch (entry) + { + case NPC_CHARSCALE_COMMANDER: + case NPC_CHARSCALE_COMMANDER_H: + return 0; + case NPC_CHARSCALE_INVOKER: + case NPC_CHARSCALE_INVOKER_H: + return 1; + case NPC_ONYX_FLAMECALLER: + return 2; + case NPC_CHARSCALE_ASSAULTER: + case NPC_CHARSCALE_ASSAULTER_H: + case NPC_CHARSCALE_ELITE: + case NPC_CHARSCALE_ELITE_H: + return 3; + default: + return std::numeric_limits::max(); + } +} + +inline void RsTrashCollect(PlayerbotAI* botAI, std::vector& out) +{ + RsCollectTargets(botAI, out, [](Unit* unit) { return RsTrashIsTrashEntry(unit->GetEntry()); }); +} + +inline bool RsTrashActive(PlayerbotAI* botAI, Player* bot) +{ + if (!bot || !bot->IsInCombat()) + return false; + + return RsFindTarget(botAI, [bot](Unit* unit) + { + return RsTrashIsCharscale(unit->GetEntry()) && bot->GetExactDist2d(unit) <= RS_TRASH_DETECT_RANGE; + }) != nullptr; +} + +class RsTrashAddsAction : public AttackAction +{ +public: + RsTrashAddsAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs trash adds") {} + bool Execute(Event event) override; + +private: + Unit* FindPriorityAdd(); +}; + +class RsTrashTankAction : public AttackAction +{ +public: + RsTrashTankAction(PlayerbotAI* botAI, std::string const name) : AttackAction(botAI, name) {} + +protected: + bool HoldAt(std::vector const& assigned, float spotX, float spotY, float spotZ, bool moveToSpot, + bool hasAway, float awayOri); +}; + +class RsTrashMainTankAction : public RsTrashTankAction +{ +public: + RsTrashMainTankAction(PlayerbotAI* botAI) : RsTrashTankAction(botAI, "rs trash main tank") {} + bool Execute(Event event) override; +}; + +class RsTrashAssistTankAction : public RsTrashTankAction +{ +public: + RsTrashAssistTankAction(PlayerbotAI* botAI) : RsTrashTankAction(botAI, "rs trash assist tank") {} + bool Execute(Event event) override; +}; + +class RsTrashRangedAction : public AttackAction +{ +public: + RsTrashRangedAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs trash ranged") {} + bool Execute(Event event) override; +}; + +inline float RsHalionTargetOrientation() +{ + float angle = std::atan2(RS_HALION_TANK_POSITION.GetPositionY() - RS_HALION_CENTER_POSITION.GetPositionY(), + RS_HALION_TANK_POSITION.GetPositionX() - RS_HALION_CENTER_POSITION.GetPositionX()); + if (angle < 0.0f) + angle += 2.0f * static_cast(M_PI); + return angle; +} + +inline constexpr float RS_HALION_COMBUSTION_REACH = 4.0f; + +inline constexpr float RS_HALION_COMBUSTION_RANGE = 10.0f; + +inline bool RsHalionHasCombustion(Unit* u) +{ + return u && (u->HasAura(SPELL_FIERY_COMBUSTION) || u->HasAura(SPELL_MARK_OF_COMBUSTION)); +} + +inline Position const& RsHalionCombustionSpot(uint32 instanceId) +{ + uint32 const count = RubySanctumHelpers::RsState(instanceId).meteorPingPong.count; + return (count % 2u) == 0u ? RS_HALION_COMBUSTION_SPOT_A : RS_HALION_COMBUSTION_SPOT_B; +} + +inline Position const& RsHalionCombustionSpot(Player* bot) +{ + return RsHalionCombustionSpot(bot->GetInstanceId()); +} + +inline bool RsHalionCombustionAtSpot(Unit* u) +{ + if (!u) + return false; + + Player* p = u->ToPlayer(); + if (!p) + return false; + + Position const& spot = RsHalionCombustionSpot(p); + return p->GetExactDist2d(spot.GetPositionX(), spot.GetPositionY()) <= RS_HALION_COMBUSTION_REACH; +} + +inline constexpr float RS_HALION_COMBUSTION_PLAYER_SAFE_DIST = 35.0f; + +inline bool RsHalionCombustionPlayerSafe(Player* p) +{ + if (!p) + return false; + + return p->GetExactDist2d(RS_HALION_CENTER_POSITION.GetPositionX(), + RS_HALION_CENTER_POSITION.GetPositionY()) >= + RS_HALION_COMBUSTION_PLAYER_SAFE_DIST; +} + +inline bool RsHalionCombustionNotClear(Player* bot) +{ + Group* group = bot ? bot->GetGroup() : nullptr; + if (!group) + return false; + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && RsHalionHasCombustion(member) && !RsHalionCombustionAtSpot(member)) + return true; + } + return false; +} + +inline bool RsHalionIsCombustionDispeller(PlayerbotAI* botAI) +{ + return botAI->IsHeal(botAI->GetBot()) && RsHalionCombustionNotClear(botAI->GetBot()); +} + +inline constexpr float RS_HALION_CONSUMPTION_OUT_DIST = 46.0f; +inline constexpr float RS_HALION_CONSUMPTION_OUT_REACH = 4.0f; +inline constexpr float RS_HALION_CONSUMPTION_STEP = 10.0f; +inline constexpr float RS_HALION_CONSUMPTION_POOL_CLEAR = 9.0f; + +inline bool RsHalionHasConsumption(Unit* u) +{ + return u && (u->HasAura(SPELL_MARK_OF_CONSUMPTION) || u->HasAura(SPELL_SOUL_CONSUMPTION)); +} + +inline void RsHalionCollectHazardPools(Unit* from, std::vector& pools) +{ + for (uint32 const entry : {NPC_CONSUMPTION, NPC_COMBUSTION}) + { + std::list found; + from->GetCreatureListWithEntryInGrid(found, entry, 100.0f); + for (Creature* c : found) + if (c && c->IsAlive()) + pools.push_back(c); + } +} + +inline bool RsHalionSpotClearOfPools(std::vector const& pools, float px, float py) +{ + for (Unit* pool : pools) + if (pool->GetExactDist2d(px, py) <= RS_HALION_CONSUMPTION_POOL_CLEAR) + return false; + return true; +} + +inline bool RsHalionPathClearOfPools(std::vector const& pools, float fromX, float fromY, + float toX, float toY) +{ + float const dx = toX - fromX; + float const dy = toY - fromY; + float const lenSq = dx * dx + dy * dy; + + for (Unit* pool : pools) + { + float const px = pool->GetPositionX(); + float const py = pool->GetPositionY(); + + float t = 0.0f; + if (lenSq > 0.0001f) + t = std::clamp(((px - fromX) * dx + (py - fromY) * dy) / lenSq, 0.0f, 1.0f); + + float const nearX = px - (fromX + dx * t); + float const nearY = py - (fromY + dy * t); + if (std::sqrt(nearX * nearX + nearY * nearY) <= RS_HALION_CONSUMPTION_POOL_CLEAR) + return false; + } + return true; +} + +inline bool RsHalionConsumptionAtSpot(PlayerbotAI* botAI, Player* member) +{ + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss || !member) + return false; + + return member->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()) >= + RS_HALION_CONSUMPTION_OUT_DIST - RS_HALION_CONSUMPTION_OUT_REACH; +} + +inline bool RsHalionConsumptionNotClear(PlayerbotAI* botAI) +{ + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + return false; + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && RsHalionHasConsumption(member) && + !RsHalionConsumptionAtSpot(botAI, member)) + return true; + } + return false; +} + +inline constexpr uint32 RS_HALION_CONSUMPTION_HOLD_PCT = 75; +inline constexpr float RS_HALION_CONSUMPTION_HEAL_PCT = 90.0f; + +inline Player* RsHalionConsumptionHealTarget(PlayerbotAI* botAI) +{ + Player* bot = botAI->GetBot(); + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* worst = nullptr; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !RsHalionHasConsumption(member)) + continue; + if (RsHalionInTwilight(member) != RsHalionInTwilight(bot)) + continue; + if (member->GetHealthPct() >= RS_HALION_CONSUMPTION_HEAL_PCT) + continue; + if (!worst || member->GetHealthPct() < worst->GetHealthPct()) + worst = member; + } + return worst; +} + +inline bool RsHalionIsAdd(Unit* unit) +{ + return unit->GetEntry() == NPC_LIVING_INFERNO || unit->GetEntry() == NPC_LIVING_EMBER; +} + +inline void RsHalionCollectAdds(PlayerbotAI* botAI, std::vector& adds) +{ + RsCollectTargets(botAI, adds, RsHalionIsAdd); +} + +inline bool RsHalionAnyAddAlive(PlayerbotAI* botAI) +{ + return RsFindTarget(botAI, RsHalionIsAdd) != nullptr; +} + +inline constexpr uint32 RS_HALION_PORTAL_ADD_CLEAR_MS = 5000; + +inline bool RsHalionPortalHeldForAdds(PlayerbotAI* botAI) +{ + if (!RsHalionFindPortal(botAI)) + return false; + + RubySanctumHelpers::RsInstanceState& state = RubySanctumHelpers::RsState(botAI->GetBot()->GetInstanceId()); + + bool const bossHigh = state.bossHealth.stamp != 0 && + GetMSTimeDiffToNow(state.bossHealth.stamp) <= RS_HALION_CORP_FRESH_MS && + state.bossHealth.pct >= 65; + if (!bossHigh) + return false; + + if (RsHalionAnyAddAlive(botAI)) + return true; + + RubySanctumHelpers::PortalAddGate& gate = state.portalAddGate; + uint32 const now = getMSTime(); + + if (gate.armTime == 0) + gate.armTime = now; + + uint32 const since = std::max(gate.armTime, gate.lastAddAliveTime); + return getMSTimeDiff(since, now) < RS_HALION_PORTAL_ADD_CLEAR_MS; +} + +inline uint32 RsHalionPortalAddHoldRemainingMs(PlayerbotAI* botAI) +{ + if (RsHalionAnyAddAlive(botAI)) + return RS_HALION_PORTAL_ADD_CLEAR_MS; + + RubySanctumHelpers::PortalAddGate const& gate = + RubySanctumHelpers::RsState(botAI->GetBot()->GetInstanceId()).portalAddGate; + if (gate.armTime == 0 && gate.lastAddAliveTime == 0) + return RS_HALION_PORTAL_ADD_CLEAR_MS; + + uint32 const since = std::max(gate.armTime, gate.lastAddAliveTime); + uint32 const elapsed = getMSTimeDiff(since, getMSTime()); + return elapsed >= RS_HALION_PORTAL_ADD_CLEAR_MS ? 0 : RS_HALION_PORTAL_ADD_CLEAR_MS - elapsed; +} + +inline bool RsHalionCutterShouldMove(uint32 instanceId); + +inline bool RsHalionEnteringTwilight(PlayerbotAI* botAI, Player* bot) +{ + if (PlayerbotAI::IsMainTank(bot) || RsHalionInTwilight(bot)) + return false; + + Unit* physBoss = RsHalionAnyPhysicalBoss(botAI); + bool const phase3 = physBoss != nullptr && !physBoss->HealthAbovePct(50); + if (phase3 && !RsHalionP3TwilightAssigned(bot)) + return false; + + if (RsHalionPortalHeldForAdds(botAI)) + return false; + + if (RsHalionCutterShouldMove(bot->GetInstanceId())) + return false; + + return RsHalionFindPortal(botAI) != nullptr; +} + +template +inline Player* RsHalionPickTank(Group* group, Filter const& filter) +{ + Player* assist = nullptr; + Player* anyTank = nullptr; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !PlayerbotAI::IsTank(member) || !filter(member)) + continue; + + if (!anyTank || member->GetGUID() < anyTank->GetGUID()) + anyTank = member; + + if (PlayerbotAI::IsAssistTank(member) && (!assist || member->GetGUID() < assist->GetGUID())) + assist = member; + } + + return assist ? assist : anyTank; +} + +inline Player* RsHalionAddTank(PlayerbotAI* botAI) +{ + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + return nullptr; + + return RsHalionPickTank(group, [](Player* member) { return !RsHalionInTwilight(member); }); +} + +inline Player* RsHalionTwilightTankUncached(PlayerbotAI* botAI) +{ + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + { + Player* bot = botAI->GetBot(); + return (PlayerbotAI::IsTank(bot) && bot->IsAlive() && RsHalionInTwilight(bot)) ? bot : nullptr; + } + + return RsHalionPickTank(group, [](Player* member) { return RsHalionInTwilight(member); }); +} + +inline Player* RsHalionTwilightTank(PlayerbotAI* botAI) +{ + Player* selfBot = botAI->GetBot(); + if (!selfBot->GetGroup()) + return RsHalionTwilightTankUncached(botAI); + + RubySanctumHelpers::TankCache& slot = RubySanctumHelpers::RsState(selfBot->GetInstanceId()).twilightTankCache; + uint32 const now = getMSTime(); + if (slot.stamp == now) + { + if (slot.guid.IsEmpty()) + return nullptr; + if (Player* cached = botAI->GetPlayer(slot.guid); cached && cached->IsAlive()) + return cached; + } + + Player* result = RsHalionTwilightTankUncached(botAI); + slot.stamp = now; + slot.guid = result ? result->GetGUID() : ObjectGuid::Empty; + return result; +} + +inline Player* RsHalionFirstCrosser(PlayerbotAI* botAI) +{ + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + { + Player* bot = botAI->GetBot(); + return (PlayerbotAI::IsTank(bot) && bot->IsAlive() && !PlayerbotAI::IsMainTank(bot)) ? bot : nullptr; + } + + return RsHalionPickTank(group, [](Player* member) { return !PlayerbotAI::IsMainTank(member); }); +} + +inline Player* RsHalionLiveMainTank(PlayerbotAI* botAI) +{ + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + { + Player* bot = botAI->GetBot(); + return (PlayerbotAI::IsMainTank(bot) && bot->IsAlive()) ? bot : nullptr; + } + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member->IsAlive() && PlayerbotAI::IsMainTank(member)) + return member; + } + + return nullptr; +} + +inline Player* RsHalionBossTankUncached(PlayerbotAI* botAI) +{ + if (Player* mt = RsHalionLiveMainTank(botAI)) + return mt; + + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + return nullptr; + + Player* anyTank = nullptr; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || !PlayerbotAI::IsTank(member)) + continue; + + if (!anyTank || member->GetGUID() < anyTank->GetGUID()) + anyTank = member; + } + + return anyTank; +} + +inline Player* RsHalionBossTank(PlayerbotAI* botAI) +{ + Player* selfBot = botAI->GetBot(); + if (!selfBot->GetGroup()) + return RsHalionBossTankUncached(botAI); + + RubySanctumHelpers::TankCache& slot = RubySanctumHelpers::RsState(selfBot->GetInstanceId()).bossTankCache; + uint32 const now = getMSTime(); + if (slot.stamp == now) + { + if (slot.guid.IsEmpty()) + return nullptr; + if (Player* cached = botAI->GetPlayer(slot.guid); cached && cached->IsAlive()) + return cached; + } + + Player* result = RsHalionBossTankUncached(botAI); + slot.stamp = now; + slot.guid = result ? result->GetGUID() : ObjectGuid::Empty; + return result; +} + +inline bool RsHalionTanksAdds(PlayerbotAI* botAI, Player* bot) +{ + return RsHalionAnyAddAlive(botAI) && RsHalionAddTank(botAI) == bot; +} + +inline bool RsHalionAssistTankAsMelee(PlayerbotAI* botAI) +{ + Player* bot = botAI->GetBot(); + if (!PlayerbotAI::IsTank(bot)) + return false; + + if (!RsHalionAnyPhysicalBoss(botAI)) + return false; + + if (RsHalionBossTank(botAI) == bot) + return false; + + return !RsHalionTanksAdds(botAI, bot); +} + +inline constexpr float RS_HALION_TANK_LEASH = 10.0f; +inline constexpr float RS_HALION_METEOR_LEASH = 25.0f; +inline constexpr float RS_HALION_METEOR_REACH = 4.0f; + +inline Position const& RsHalionMeteorSpot(uint32 instanceId) +{ + uint32 const count = RubySanctumHelpers::RsState(instanceId).meteorPingPong.count; + return (count % 2u) == 0u ? RS_HALION_METEOR_SPOT_A : RS_HALION_METEOR_SPOT_B; +} + +inline constexpr uint32 RS_HALION_METEOR_RECENT_MS = 5000; + +inline bool RsHalionMeteorCastRecently(uint32 instanceId) +{ + uint32 const lastCast = RubySanctumHelpers::RsState(instanceId).meteorPingPong.lastCastTime; + return lastCast != 0 && GetMSTimeDiffToNow(lastCast) <= RS_HALION_METEOR_RECENT_MS; +} + +inline bool RsHalionMeteorTargetedBossTank(PlayerbotAI* botAI) +{ + Player* bot = botAI->GetBot(); + if (RsHalionBossTank(botAI) != bot) + return false; + + uint32 const t = RubySanctumHelpers::RsState(bot->GetInstanceId()).meteorPingPong.tankMeteorTime; + return t != 0 && GetMSTimeDiffToNow(t) <= RS_HALION_METEOR_RECENT_MS; +} + +inline bool RsHalionTankMeteorCommitted(PlayerbotAI* botAI) +{ + Player* bot = botAI->GetBot(); + RubySanctumHelpers::RsInstanceState& state = RubySanctumHelpers::RsState(bot->GetInstanceId()); + auto& committed = state.meteorCommitted; + ObjectGuid const guid = bot->GetGUID(); + + if (RsHalionBossTank(botAI) != bot) + { + committed.erase(guid); + return false; + } + + uint32 const homeTime = state.meteorPingPong.tankMeteorTime; + uint32 const returnTime = state.meteorPingPong.tankReturnTime; + + bool const homeRecent = homeTime != 0 && GetMSTimeDiffToNow(homeTime) <= RS_HALION_METEOR_RECENT_MS; + bool const returnRecent = returnTime != 0 && GetMSTimeDiffToNow(returnTime) <= RS_HALION_METEOR_RECENT_MS; + + if (returnRecent && returnTime >= homeTime) + { + committed.erase(guid); + return false; + } + + if (homeRecent) + committed.insert(guid); + + return committed.count(guid) != 0; +} + +inline bool RsHalionMeteorShouldRally(Player* bot) +{ + auto& committed = RubySanctumHelpers::RsState(bot->GetInstanceId()).rallyCommitted; + ObjectGuid const guid = bot->GetGUID(); + + if (RsHalionInTwilight(bot)) + { + committed.erase(guid); + return false; + } + + float const dist = bot->GetExactDist2d(RsHalionMeteorSpot(bot->GetInstanceId())); + + if (dist <= RS_HALION_METEOR_REACH) + { + committed.erase(guid); + return false; + } + + if (dist > RS_HALION_METEOR_LEASH) + committed.insert(guid); + + return committed.count(guid) != 0; +} + +inline bool RsHalionCombustionReturning(Player* bot) +{ + auto& returning = RubySanctumHelpers::RsState(bot->GetInstanceId()).combustionReturning; + ObjectGuid const guid = bot->GetGUID(); + + if (RsHalionHasCombustion(bot)) + { + returning.insert(guid); + return false; + } + + if (!returning.count(guid)) + return false; + + Position const& spot = RsHalionMeteorSpot(bot->GetInstanceId()); + if (bot->GetExactDist2d(spot.GetPositionX(), spot.GetPositionY()) <= RS_HALION_METEOR_REACH) + { + returning.erase(guid); + return false; + } + + return true; +} + +inline constexpr float RS_HALION_LINE_MELEE_MIN = 5.0f; +inline constexpr float RS_HALION_LINE_MELEE_MAX = 18.0f; +inline constexpr float RS_HALION_LINE_RANGED_MIN = 20.0f; +inline constexpr float RS_HALION_LINE_RANGED_MAX = 30.0f; +inline constexpr float RS_HALION_LINE_OFFSET_OK = 4.0f; +inline constexpr float RS_HALION_LINE_LEASH = 10.0f; + +inline constexpr float RS_HALION_CUTTER_HALF_WIDTH = 2.5f; +inline constexpr float RS_HALION_CUTTER_MARGIN = 4.0f; +inline constexpr float RS_HALION_CUTTER_BOT_CLEAR = 5.0f; +inline constexpr float RS_HALION_CUTTER_FLANK_DIST = 15.0f; +inline constexpr float RS_HALION_CUTTER_SAFE_DIST = 15.0f; +inline constexpr float RS_HALION_CUTTER_TANK_SAFE_DIST = 15.0f; +inline constexpr float RS_HALION_CUTTER_TANK_BEAM_GAP = 12.0f; +inline constexpr float RS_HALION_CUTTER_TANK_TRAIL = 3.0f; +inline constexpr float RS_HALION_CUTTER_TANK_TRAIL_TOL = 1.0f; +inline constexpr float RS_HALION_CUTTER_TANK_TRAIL_NORMAL = 8.0f; +inline constexpr float RS_HALION_CUTTER_LEADER_NEAR = 5.0f; +inline constexpr float RS_HALION_CUTTER_LEADER_FAR = 8.0f; + +inline constexpr uint32 RS_HALION_CUTTER_CYCLE_MS = 29000; +inline constexpr uint32 RS_HALION_CUTTER_LEAD_MS = 5000; +inline constexpr uint32 RS_HALION_FIRST_SHOOT_MS = 21000; + +inline bool RsHalionCutterShouldMove(uint32 instanceId) +{ + RubySanctumHelpers::CutterTiming const& state = RubySanctumHelpers::RsState(instanceId).cutterTiming; + + if (state.lastShootTime == 0) + { + if (state.active) + return true; + if (state.encounterStart == 0) + return false; + + uint32 const sinceStart = GetMSTimeDiffToNow(state.encounterStart); + if (sinceStart < RS_HALION_FIRST_SHOOT_MS) + return sinceStart >= RS_HALION_FIRST_SHOOT_MS - RS_HALION_CUTTER_LEAD_MS; + + uint32 const intoCycle = (sinceStart - RS_HALION_FIRST_SHOOT_MS) % RS_HALION_CUTTER_CYCLE_MS; + return intoCycle >= RS_HALION_CUTTER_CYCLE_MS - RS_HALION_CUTTER_LEAD_MS; + } + + uint32 const since = GetMSTimeDiffToNow(state.lastShootTime); + + if (state.active) + return true; + + uint32 const intoCycle = since % RS_HALION_CUTTER_CYCLE_MS; + return intoCycle >= RS_HALION_CUTTER_CYCLE_MS - RS_HALION_CUTTER_LEAD_MS; +} + +inline bool RsHalionCutterActive(uint32 instanceId) +{ + return RubySanctumHelpers::RsState(instanceId).cutterTiming.active; +} + +inline uint32 RsHalionCutterMsUntilShoot(uint32 instanceId) +{ + RubySanctumHelpers::CutterTiming const& state = RubySanctumHelpers::RsState(instanceId).cutterTiming; + if (state.active) + return 0; + + if (state.lastShootTime == 0) + { + if (state.encounterStart == 0) + return RS_HALION_CUTTER_CYCLE_MS; + uint32 const elapsed = GetMSTimeDiffToNow(state.encounterStart); + return elapsed >= RS_HALION_FIRST_SHOOT_MS ? 0 : RS_HALION_FIRST_SHOOT_MS - elapsed; + } + + uint32 const since = GetMSTimeDiffToNow(state.lastShootTime); + uint32 const intoCycle = since % RS_HALION_CUTTER_CYCLE_MS; + return RS_HALION_CUTTER_CYCLE_MS - intoCycle; +} + +inline bool RsHalionCollectOrbPairs(Unit* boss, std::vector>& pairs) +{ + Creature* carrier = boss->FindNearestCreature(NPC_ORB_CARRIER, 200.0f); + if (!carrier) + return false; + + Vehicle* vehicle = carrier->GetVehicleKit(); + if (!vehicle) + return false; + + Unit* north = vehicle->GetPassenger(0); + Unit* south = vehicle->GetPassenger(1); + if (north && south) + pairs.emplace_back(north, south); + + Unit* east = vehicle->GetPassenger(2); + Unit* west = vehicle->GetPassenger(3); + if (east && west) + pairs.emplace_back(east, west); + + return !pairs.empty(); +} + +inline constexpr float RS_HALION_P2_MELEE_DIST = 18.0f; +inline constexpr float RS_HALION_P2_RANGED_DIST = 25.0f; +inline constexpr float RS_HALION_P2_STEP = 7.0f; +inline constexpr float RS_HALION_CUTTER_DANGER = 7.0f; + +inline bool RsHalionCutterBeamDanger(PlayerbotAI* botAI, Player* bot) +{ + if (!RsHalionInTwilight(bot) || !RsHalionCutterShouldMove(bot->GetInstanceId())) + return false; + + auto& cache = RubySanctumHelpers::RsState(bot->GetInstanceId()).cutterDangerCache; + uint32 const now = getMSTime(); + auto it = cache.find(bot->GetGUID()); + if (it != cache.end() && getMSTimeDiff(it->second.first, now) < 250) + return it->second.second; + + bool danger = false; + if (Unit* boss = RsHalionTwilightBoss(botAI)) + { + std::vector> pairs; + if (RsHalionCollectOrbPairs(boss, pairs)) + { + for (auto const& pair : pairs) + { + if (std::fabs(RsHalionBeamSignedDist(bot->GetPositionX(), bot->GetPositionY(), + pair.first, pair.second)) <= RS_HALION_CUTTER_DANGER) + { + danger = true; + break; + } + } + } + } + + cache[bot->GetGUID()] = {now, danger}; + return danger; +} + +inline constexpr float RS_HALION_PORTAL_FINISH_DIST = 15.0f; +inline constexpr uint32 RS_HALION_PORTAL_COMMIT_MARGIN_MS = 1200; + +inline bool RsHalionPortalCommit(PlayerbotAI* botAI, Player* bot) +{ + if (!RsHalionInTwilight(bot)) + return false; + + Unit* twilightBoss = RsHalionTwilightBoss(botAI); + if (!twilightBoss || twilightBoss->HealthAbovePct(50)) + return false; + if (RsHalionP3TwilightAssigned(bot)) + return false; + if (RsHalionHasConsumption(bot)) + return false; + + GameObject* portal = RsHalionFindPortal(botAI); + if (!portal) + return false; + + float const dist = bot->GetExactDist2d(portal->GetPositionX(), portal->GetPositionY()); + if (dist <= RS_HALION_PORTAL_FINISH_DIST) + return true; + + if (RsHalionCutterActive(bot->GetInstanceId())) + return false; + + float speed = bot->GetSpeed(MOVE_RUN); + if (speed < 1.0f) + speed = 7.0f; + uint32 const reachMs = static_cast((dist / speed) * 1000.0f); + return reachMs + RS_HALION_PORTAL_COMMIT_MARGIN_MS <= RsHalionCutterMsUntilShoot(bot->GetInstanceId()); +} + +class RsHalionTankPositionAction : public AttackAction +{ +public: + RsHalionTankPositionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion tank position") {} + bool Execute(Event event) override; +}; + +class RsHalionAvoidConesAction : public AttackAction +{ +public: + RsHalionAvoidConesAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion avoid cones") {} + bool Execute(Event event) override; +}; + +class RsHalionCombustionAction : public MoveAwayFromPlayerWithDebuffAction +{ +public: + RsHalionCombustionAction(PlayerbotAI* botAI) + : MoveAwayFromPlayerWithDebuffAction(botAI, "rs halion combustion", SPELL_MARK_OF_COMBUSTION, + RS_HALION_COMBUSTION_RANGE) {} + bool Execute(Event event) override; +}; + +class RsHalionMeteorAction : public AttackAction +{ +public: + RsHalionMeteorAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion meteor") {} + bool Execute(Event event) override; +}; + +class RsHalionAddsAction : public AttackAction +{ +public: + RsHalionAddsAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion adds") {} + bool Execute(Event event) override; +}; + +class RsHalionAddTankAction : public AttackAction +{ +public: + RsHalionAddTankAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion add tank") {} + bool Execute(Event event) override; +}; + +class RsHalionStartPositionAction : public AttackAction +{ +public: + RsHalionStartPositionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion start position") {} + bool Execute(Event event) override; +}; + +class RsHalionEnterPortalAction : public AttackAction +{ +public: + RsHalionEnterPortalAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion enter portal") {} + bool Execute(Event event) override; +}; + +class RsHalionP2TankPositionAction : public AttackAction +{ +public: + RsHalionP2TankPositionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion p2 tank position") {} + bool Execute(Event event) override; +}; + +class RsHalionP2AvoidConesAction : public AttackAction +{ +public: + RsHalionP2AvoidConesAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion p2 avoid cones") {} + bool Execute(Event event) override; +}; + +class RsHalionConsumptionAction : public AttackAction +{ +public: + RsHalionConsumptionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion consumption") {} + bool Execute(Event event) override; +}; + +class RsHalionCutterAction : public AttackAction +{ +public: + RsHalionCutterAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion cutter") {} + bool Execute(Event event) override; +}; + +class RsHalionHealConsumptionAction : public AttackAction +{ +public: + RsHalionHealConsumptionAction(PlayerbotAI* botAI) : AttackAction(botAI, "rs halion heal consumption") {} + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ADD.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ADD.cpp new file mode 100644 index 000000000..072fe7be7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ADD.cpp @@ -0,0 +1,415 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include +#include +#include +#include + +#include "NearestNpcsValue.h" +#include "Playerbots.h" +#include "RSActions.h" + +namespace +{ + void ResolveTanks(PlayerbotAI* botAI, Player*& mainTank, Player*& assistTank) + { + mainTank = nullptr; + assistTank = nullptr; + + Group* group = botAI->GetBot()->GetGroup(); + if (!group) + return; + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (!mainTank && botAI->IsMainTank(member)) + mainTank = member; + + if (!assistTank && botAI->IsAssistTank(member)) + assistTank = member; + + if (mainTank && assistTank) + break; + } + } + + void CollectTrashForTank(Player* bot, std::vector& out) + { + for (uint32 const entry : {NPC_CHARSCALE_INVOKER, NPC_CHARSCALE_INVOKER_H, + NPC_CHARSCALE_ASSAULTER, NPC_CHARSCALE_ASSAULTER_H, + NPC_CHARSCALE_ELITE, NPC_CHARSCALE_ELITE_H, + NPC_CHARSCALE_COMMANDER, NPC_CHARSCALE_COMMANDER_H, + NPC_ONYX_FLAMECALLER}) + { + std::list found; + bot->GetCreatureListWithEntryInGrid(found, entry, RS_TRASH_TANK_GRAB_RANGE); + for (Creature* c : found) + if (c && c->IsAlive() && c->IsInCombat()) + out.push_back(c); + } + } + + void PartitionMobs(std::vector const& mobs, std::vector& assistSet, std::vector& mainSet) + { + Unit* assistInvoker = nullptr; + for (Unit* mob : mobs) + { + if (mob && mob->IsAlive() && RsTrashIsInvoker(mob->GetEntry())) + if (!assistInvoker || mob->GetGUID() < assistInvoker->GetGUID()) + assistInvoker = mob; + } + + for (Unit* mob : mobs) + { + if (!mob || !mob->IsAlive()) + continue; + + if (RsTrashIsCommander(mob->GetEntry()) || mob == assistInvoker) + assistSet.push_back(mob); + else + mainSet.push_back(mob); + } + } + + bool FrontReference(PlayerbotAI* botAI, Player* bot, float& rx, float& ry) + { + Group* group = bot->GetGroup(); + if (group) + { + float sx = 0.0f; + float sy = 0.0f; + int count = 0; + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->GetMapId() != bot->GetMapId() || member->GetInstanceId() != bot->GetInstanceId()) + continue; + + if (botAI->IsTank(member) || !botAI->IsRanged(member)) + continue; + + sx += member->GetPositionX(); + sy += member->GetPositionY(); + ++count; + } + + if (count > 0) + { + rx = sx / count; + ry = sy / count; + return true; + } + } + + Player* master = botAI->GetMaster(); + if (master && master->IsInWorld() && master->GetMapId() == bot->GetMapId() && + master->GetInstanceId() == bot->GetInstanceId() && !botAI->IsTank(master)) + { + rx = master->GetPositionX(); + ry = master->GetPositionY(); + return true; + } + + return false; + } + + bool AwayFromRanged(PlayerbotAI* botAI, Player* bot, float& awayOri) + { + float rx = 0.0f; + float ry = 0.0f; + if (!FrontReference(botAI, bot, rx, ry)) + return false; + + float const dx = bot->GetPositionX() - rx; + float const dy = bot->GetPositionY() - ry; + if (dx * dx + dy * dy < 0.01f) + return false; + + awayOri = std::atan2(dy, dx); + return true; + } + + bool MarkableAdd(PlayerbotAI* botAI, Unit* mob) + { + if (!mob || !mob->IsAlive()) + return false; + + if (mob->IsInCombat()) + return true; + + Unit* victim = mob->GetVictim(); + return victim && victim->IsPlayer() && botAI->IsTank(victim->ToPlayer()); + } +} + +bool RsTrashAddsAction::Execute(Event ) +{ + context->GetValue("rti")->Set("skull"); + + if (!RsIsDesignatedMarker(bot)) + return false; + + RsUpdateSkullMarker(bot, FindPriorityAdd()); + return false; +} + +Unit* RsTrashAddsAction::FindPriorityAdd() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + std::vector mobs; + RsTrashCollect(botAI, mobs); + + std::vector markable; + for (Unit* mob : mobs) + if (MarkableAdd(botAI, mob)) + markable.push_back(mob); + + if (markable.empty()) + return nullptr; + + int bestRank = std::numeric_limits::max(); + for (Unit* mob : markable) + bestRank = std::min(bestRank, RsTrashPriorityRank(mob->GetEntry())); + + Unit* current = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + if (current && MarkableAdd(botAI, current) && RsTrashIsTrashEntry(current->GetEntry()) && + RsTrashPriorityRank(current->GetEntry()) == bestRank) + return current; + + Unit* best = nullptr; + for (Unit* mob : markable) + { + if (RsTrashPriorityRank(mob->GetEntry()) != bestRank) + continue; + + if (!best || mob->GetGUID() < best->GetGUID()) + best = mob; + } + + return best; +} + +bool RsTrashTankAction::HoldAt(std::vector const& assigned, float spotX, float spotY, float spotZ, + bool moveToSpot, bool hasAway, float awayOri) +{ + Unit* nearest = nullptr; + float best = std::numeric_limits::max(); + for (Unit* mob : assigned) + { + if (!mob || !mob->IsAlive()) + continue; + + if (botAI->HasCheat(BotCheatMask::raid) && mob->IsInCombat()) + RsForceThreat(mob, bot); + + if (hasAway) + mob->SetFacingTo(awayOri); + + float const d = bot->GetExactDist2d(mob); + if (d < best) + { + best = d; + nearest = mob; + } + } + + if (nearest) + bot->SetTarget(nearest->GetGUID()); + + if (moveToSpot && bot->GetExactDist2d(spotX, spotY) > RS_TRASH_TANK_SPACE_TOL) + return MoveTo(bot->GetMapId(), spotX, spotY, spotZ, false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + + if (nearest) + bot->SetFacingToObject(nearest); + + return false; +} + +bool RsTrashMainTankAction::Execute(Event ) +{ + if (!botAI->IsTank(bot)) + return false; + + if (RsReleaseIfFollowing(bot)) + return false; + + std::vector mobs; + CollectTrashForTank(bot, mobs); + if (mobs.empty()) + return false; + + float awayOri = 0.0f; + bool const hasAway = AwayFromRanged(botAI, bot, awayOri); + + Player* mainTank = nullptr; + Player* assistTank = nullptr; + ResolveTanks(botAI, mainTank, assistTank); + + if (!assistTank || assistTank == bot) + return HoldAt(mobs, 0.0f, 0.0f, 0.0f, false, hasAway, awayOri); + + std::vector assistSet; + std::vector mainSet; + PartitionMobs(mobs, assistSet, mainSet); + + bool hasCommander = false; + for (Unit* mob : mobs) + if (mob && mob->IsAlive() && RsTrashIsCommander(mob->GetEntry())) + { + hasCommander = true; + break; + } + + if (!hasCommander) + return HoldAt(mainSet, 0.0f, 0.0f, 0.0f, false, hasAway, awayOri); + + float dirX = bot->GetPositionX() - assistTank->GetPositionX(); + float dirY = bot->GetPositionY() - assistTank->GetPositionY(); + + float rx = 0.0f; + float ry = 0.0f; + if (FrontReference(botAI, bot, rx, ry)) + { + float ux = rx - assistTank->GetPositionX(); + float uy = ry - assistTank->GetPositionY(); + float const ulen = std::sqrt(ux * ux + uy * uy); + if (ulen > 0.01f) + { + ux /= ulen; + uy /= ulen; + float px = -uy; + float py = ux; + if (dirX * px + dirY * py < 0.0f) + { + px = -px; + py = -py; + } + dirX = px; + dirY = py; + } + } + + float len = std::sqrt(dirX * dirX + dirY * dirY); + if (len < 0.01f) + { + dirX = std::cos(bot->GetOrientation()); + dirY = std::sin(bot->GetOrientation()); + len = 1.0f; + } + + float const spotX = assistTank->GetPositionX() + (dirX / len) * RS_TRASH_TANK_SPREAD; + float const spotY = assistTank->GetPositionY() + (dirY / len) * RS_TRASH_TANK_SPREAD; + + return HoldAt(mainSet, spotX, spotY, bot->GetPositionZ(), true, hasAway, awayOri); +} + +bool RsTrashAssistTankAction::Execute(Event ) +{ + if (!botAI->IsTank(bot)) + return false; + + if (RsReleaseIfFollowing(bot)) + return false; + + std::vector mobs; + CollectTrashForTank(bot, mobs); + if (mobs.empty()) + return false; + + float awayOri = 0.0f; + bool const hasAway = AwayFromRanged(botAI, bot, awayOri); + + Player* mainTank = nullptr; + Player* assistTank = nullptr; + ResolveTanks(botAI, mainTank, assistTank); + + if (!mainTank || mainTank == bot) + return HoldAt(mobs, 0.0f, 0.0f, 0.0f, false, hasAway, awayOri); + + std::vector assistSet; + std::vector mainSet; + PartitionMobs(mobs, assistSet, mainSet); + + return HoldAt(assistSet, 0.0f, 0.0f, 0.0f, false, hasAway, awayOri); +} + +bool RsTrashRangedAction::Execute(Event ) +{ + if (botAI->IsTank(bot)) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* nearestTank = nullptr; + float nd = std::numeric_limits::max(); + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; + + if (!botAI->IsTank(member)) + continue; + + if (member->GetMapId() != bot->GetMapId() || member->GetInstanceId() != bot->GetInstanceId()) + continue; + + float const d = bot->GetExactDist2d(member); + if (d < nd) + { + nd = d; + nearestTank = member; + } + } + + if (!nearestTank) + return false; + + float ax = bot->GetPositionX() - nearestTank->GetPositionX(); + float ay = bot->GetPositionY() - nearestTank->GetPositionY(); + float alen = std::sqrt(ax * ax + ay * ay); + if (alen < 0.01f) + { + ax = std::cos(bot->GetOrientation()); + ay = std::sin(bot->GetOrientation()); + alen = 1.0f; + } + + float const desiredX = nearestTank->GetPositionX() + (ax / alen) * RS_TRASH_RANGED_DIST; + float const desiredY = nearestTank->GetPositionY() + (ay / alen) * RS_TRASH_RANGED_DIST; + + float const dx = desiredX - bot->GetPositionX(); + float const dy = desiredY - bot->GetPositionY(); + float const dist = std::sqrt(dx * dx + dy * dy); + if (dist <= RS_TRASH_RANGED_TOL) + return false; + + float const stepLen = std::min(RS_TRASH_RANGED_STEP, dist); + float const moveX = bot->GetPositionX() + (dx / dist) * stepLen; + float const moveY = bot->GetPositionY() + (dy / dist) * stepLen; + float const moveZ = bot->GetPositionZ(); + + if (!bot->IsWithinLOS(moveX, moveY, moveZ)) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_BAL.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_BAL.cpp new file mode 100644 index 000000000..8176a9c73 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_BAL.cpp @@ -0,0 +1,641 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include +#include +#include +#include +#include + +#include "NearestNpcsValue.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RSActions.h" + +namespace +{ + constexpr float RS_BALTHARUS_FRONT_RANGE = 16.0f; + + std::vector RsBaltharusCasters(PlayerbotAI* botAI) + { + std::vector casters; + + if (Unit* boss = botAI->GetAiObjectContext()->GetValue("find target", "baltharus the warborn")->Get()) + casters.push_back(boss); + + RsCollectTargets(botAI, casters, + [](Unit* unit) { return unit->GetEntry() == NPC_BALTHARUS_THE_WARBORN_CLONE; }); + + return casters; + } + + bool RsBaltharusSpotInFrontOfAny(std::vector const& casters, float x, float y) + { + for (Unit* caster : casters) + if (RsSpotInFrontArc(caster, x, y, RS_BALTHARUS_FRONT_RANGE)) + return true; + return false; + } + + struct MarkInfo + { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + bool hasDest = false; + bool reached = false; + }; + + std::map, MarkInfo> gMarkedAnchors; + + MarkInfo* GetMarkInfo(uint32 instanceId, ObjectGuid guid) + { + auto it = gMarkedAnchors.find(std::make_pair(instanceId, guid)); + return it != gMarkedAnchors.end() ? &it->second : nullptr; + } + + void EnsureMarkedAnchor(uint32 instanceId, ObjectGuid guid) + { + gMarkedAnchors.emplace(std::make_pair(instanceId, guid), MarkInfo{}); + } +} + +bool RsBaltharusBrandAction::Execute(Event ) +{ + if (botAI->IsTank(bot)) + return false; + + uint32 const instanceId = bot->GetInstanceId(); + + for (auto it = gMarkedAnchors.begin(); it != gMarkedAnchors.end();) + { + if (it->first.first == instanceId) + { + Player* owner = ObjectAccessor::FindPlayer(it->first.second); + if (!owner || !owner->HasAura(SPELL_ENERVATING_BRAND)) + { + it = gMarkedAnchors.erase(it); + continue; + } + } + ++it; + } + + if (bot->HasAura(SPELL_ENERVATING_BRAND)) + EnsureMarkedAnchor(instanceId, bot->GetGUID()); + + Group* const group = bot->GetGroup(); + if (!group) + return false; + + std::vector others; + for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) + { + Player* member = gref->GetSource(); + if (member && member != bot && member->IsAlive()) + others.push_back(member); + } + + if (others.empty()) + return false; + + MarkInfo* myMark = GetMarkInfo(instanceId, bot->GetGUID()); + + if (myMark && myMark->hasDest) + { + + constexpr float settleDist = 1.5f; + + float const distToDest = bot->GetExactDist2d(myMark->x, myMark->y); + if (distToDest <= settleDist) + { + myMark->reached = true; + + return false; + } + + return MoveTo(bot->GetMapId(), myMark->x, myMark->y, myMark->z, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); + } + + constexpr float MAX_NON_MARKED_RANGED_DISTANCE = 35.0f; + bool const isNonMarkedRanged = (myMark == nullptr) && (!botAI->IsMelee(bot)); + + auto minDistFrom = [&](float x, float y) -> float + { + float best = std::numeric_limits::max(); + for (Player* member : others) + { + MarkInfo* memberMark = GetMarkInfo(instanceId, member->GetGUID()); + + if (myMark) + { + if (memberMark) + continue; + } + else + { + + if (memberMark && !memberMark->reached) + continue; + } + + best = std::min(best, member->GetExactDist2d(x, y)); + } + return best; + }; + + bool const hereClear = minDistFrom(bot->GetPositionX(), bot->GetPositionY()) >= RS_BALTHARUS_BRAND_RANGE; + std::vector const casters = RsBaltharusCasters(botAI); + bool const hereInFront = RsBaltharusSpotInFrontOfAny(casters, bot->GetPositionX(), bot->GetPositionY()); + + float const tankX = RS_BALTHARUS_TANK_POSITION.GetPositionX(); + float const tankY = RS_BALTHARUS_TANK_POSITION.GetPositionY(); + float const distToTank = bot->GetExactDist2d(tankX, tankY); + bool effectiveHereClear = hereClear; + if (isNonMarkedRanged && distToTank > MAX_NON_MARKED_RANGED_DISTANCE) + effectiveHereClear = false; + + if (effectiveHereClear && !hereInFront) + return false; + + int const directions = 8; + float const increment = 3.0f; + float const searchRadius = 30.0f; + float bestX = bot->GetPositionX(); + float bestY = bot->GetPositionY(); + float bestZ = bot->GetPositionZ(); + float bestScore = -1.0f; + + for (int i = 0; i < directions; ++i) + { + float const angle = (i * 2.0f * static_cast(M_PI)) / directions; + for (float distance = increment; distance <= searchRadius; distance += increment) + { + float const moveX = bot->GetPositionX() + distance * std::cos(angle); + float const moveY = bot->GetPositionY() + distance * std::sin(angle); + float const moveZ = bot->GetPositionZ(); + + if (isNonMarkedRanged) + { + float const dxTank = moveX - tankX; + float const dyTank = moveY - tankY; + float const dTank = std::sqrt(dxTank * dxTank + dyTank * dyTank); + if (dTank > MAX_NON_MARKED_RANGED_DISTANCE) + continue; + } + + float const clearance = minDistFrom(moveX, moveY); + if (clearance < RS_BALTHARUS_BRAND_RANGE) + continue; + + if (RsBaltharusSpotInFrontOfAny(casters, moveX, moveY)) + continue; + + if (!bot->IsWithinLOS(moveX, moveY, moveZ)) + continue; + + if (myMark) + { + bool tooCloseToOtherMarked = false; + for (auto const& entry : gMarkedAnchors) + { + + if (entry.first.first != instanceId || entry.first.second == bot->GetGUID()) + continue; + MarkInfo const& other = entry.second; + if (!other.hasDest) + continue; + float const dx = moveX - other.x; + float const dy = moveY - other.y; + float const dd = std::sqrt(dx * dx + dy * dy); + if (dd < RS_BALTHARUS_BRAND_RANGE) + { + tooCloseToOtherMarked = true; + break; + } + } + if (tooCloseToOtherMarked) + continue; + } + + if (clearance > bestScore) + { + bestScore = clearance; + bestX = moveX; + bestY = moveY; + bestZ = moveZ; + } + } + } + + if (bestScore > 0.0f) + { + + if (myMark) + { + myMark->x = bestX; + myMark->y = bestY; + myMark->z = bestZ; + myMark->hasDest = true; + myMark->reached = false; + } + + return MoveTo(bot->GetMapId(), bestX, bestY, bestZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true); + } + + if (myMark) + { + Player* nearest = nullptr; + float nearestDist = std::numeric_limits::max(); + for (Player* member : others) + { + float const d = member->GetExactDist2d(bot->GetPositionX(), bot->GetPositionY()); + if (d < nearestDist) + { + nearestDist = d; + nearest = member; + } + } + + if (nearest) + { + float dx = bot->GetPositionX() - nearest->GetPositionX(); + float dy = bot->GetPositionY() - nearest->GetPositionY(); + float const len = std::sqrt(dx * dx + dy * dy); + if (len < 0.01f) + { + dx = 1.0f; + dy = 0.0f; + } + else + { + dx /= len; + dy /= len; + } + + float const stepOut = (RS_BALTHARUS_BRAND_RANGE - nearestDist) + 2.0f; + float fbX = bot->GetPositionX() + dx * stepOut; + float fbY = bot->GetPositionY() + dy * stepOut; + float fbZ = bot->GetPositionZ(); + + if (!RsBaltharusSpotInFrontOfAny(casters, fbX, fbY) && bot->IsWithinLOS(fbX, fbY, fbZ)) + { + myMark->x = fbX; + myMark->y = fbY; + myMark->z = fbZ; + myMark->hasDest = true; + myMark->reached = false; + + return MoveTo(bot->GetMapId(), fbX, fbY, fbZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); + } + } + } + + if (isNonMarkedRanged && distToTank > MAX_NON_MARKED_RANGED_DISTANCE) + { + float const dx = bot->GetPositionX() - tankX; + float const dy = bot->GetPositionY() - tankY; + float len = std::sqrt(dx * dx + dy * dy); + float tx, ty, tz = bot->GetPositionZ(); + if (len > 0.0001f) + { + tx = tankX + (dx / len) * MAX_NON_MARKED_RANGED_DISTANCE; + ty = tankY + (dy / len) * MAX_NON_MARKED_RANGED_DISTANCE; + } + else + { + + tx = tankX + MAX_NON_MARKED_RANGED_DISTANCE; + ty = tankY; + } + + if (!RsBaltharusSpotInFrontOfAny(casters, tx, ty) && bot->IsWithinLOS(tx, ty, tz)) + return MoveTo(bot->GetMapId(), tx, ty, tz, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true); + } + + return false; +} + +bool RsBaltharusHealerPositionAction::Execute(Event ) +{ + if (!botAI->IsHeal(bot) || botAI->IsTank(bot)) + return false; + + if (bot->HasAura(SPELL_ENERVATING_BRAND)) + return false; + + float const distToHealSpot = bot->GetExactDist2d(RS_BALTHARUS_HEALER_POSITION.GetPositionX(), + RS_BALTHARUS_HEALER_POSITION.GetPositionY()); + if (distToHealSpot <= RS_BALTHARUS_HEALER_REACH) + return false; + + return MoveTo(bot->GetMapId(), RS_BALTHARUS_HEALER_POSITION.GetPositionX(), + RS_BALTHARUS_HEALER_POSITION.GetPositionY(), + RS_BALTHARUS_HEALER_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); +} + +bool RsBaltharusAvoidFrontAction::Execute(Event ) +{ + if (botAI->IsTank(bot)) + return false; + + std::vector const casters = RsBaltharusCasters(botAI); + if (casters.empty()) + return false; + + Unit* anchor = nullptr; + if (Unit* victim = bot->GetVictim()) + for (Unit* caster : casters) + if (caster == victim) + anchor = caster; + if (!anchor) + { + float nearestDist = std::numeric_limits::max(); + for (Unit* caster : casters) + { + float const d = bot->GetExactDist2d(caster); + if (d < nearestDist) + { + nearestDist = d; + anchor = caster; + } + } + } + if (!anchor) + return false; + + float nearestCaster = std::numeric_limits::max(); + for (Unit* caster : casters) + nearestCaster = std::min(nearestCaster, bot->GetExactDist2d(caster)); + if (nearestCaster > 20.0f) + return false; + + if (botAI->IsMelee(bot)) + { + float const flankDist = 4.0f; + float const settleDist = 2.0f; + + static float const flankOffsets[] = { + static_cast(M_PI), + static_cast(M_PI) * 0.75f, + static_cast(M_PI) * 1.25f, + static_cast(M_PI) * 0.5f, + static_cast(M_PI) * 1.5f + }; + + for (float const offset : flankOffsets) + { + float const angle = anchor->GetOrientation() + offset; + float const targetX = anchor->GetPositionX() + flankDist * std::cos(angle); + float const targetY = anchor->GetPositionY() + flankDist * std::sin(angle); + float const targetZ = anchor->GetPositionZ(); + + if (RsBaltharusSpotInFrontOfAny(casters, targetX, targetY)) + continue; + + if (!bot->IsWithinLOS(targetX, targetY, targetZ)) + continue; + + if (bot->GetExactDist2d(targetX, targetY) < settleDist) + return false; + + return MoveTo(bot->GetMapId(), targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true); + } + } + + if (!RsBaltharusSpotInFrontOfAny(casters, bot->GetPositionX(), bot->GetPositionY())) + return false; + + float const step = 7.0f; + int const directions = 12; + + for (int i = 0; i < directions; ++i) + { + float const angle = (i * 2.0f * static_cast(M_PI)) / directions; + float const moveX = bot->GetPositionX() + step * std::cos(angle); + float const moveY = bot->GetPositionY() + step * std::sin(angle); + float const moveZ = bot->GetPositionZ(); + + if (RsBaltharusSpotInFrontOfAny(casters, moveX, moveY)) + continue; + + if (!bot->IsWithinLOS(moveX, moveY, moveZ)) + continue; + + return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true); + } + + return false; +} + +bool RsBaltharusTankPositionAction::HoldAt(Position const& spot, Unit* faceAwayFrom) +{ + bool const onSpot = bot->GetExactDist2d(spot.GetPositionX(), spot.GetPositionY()) <= 3.0f; + bool const inMelee = faceAwayFrom && bot->IsWithinMeleeRange(faceAwayFrom); + + if (faceAwayFrom && !inMelee && !onSpot) + return MoveTo(bot->GetMapId(), faceAwayFrom->GetPositionX(), faceAwayFrom->GetPositionY(), + faceAwayFrom->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + + if (!onSpot) + return MoveTo(bot->GetMapId(), spot.GetPositionX(), spot.GetPositionY(), spot.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + if (faceAwayFrom && std::fabs(bot->GetOrientation() - RS_BALTHARUS_TANK_FACING) > 0.1f) + bot->SetFacingTo(RS_BALTHARUS_TANK_FACING); + + return false; +} + +bool RsBaltharusTankPositionAction::Execute(Event ) +{ + if (!botAI->IsTank(bot)) + return false; + + if (!(bot->HasAura(RS_SPELL_PAIN_SUPPRESION))) + bot->AddAura(RS_SPELL_PAIN_SUPPRESION, bot); + + Unit* boss = RsFindTarget(botAI, [](Unit* unit) { return unit->GetEntry() == NPC_BALTHARUS_THE_WARBORN; }); + if (!boss) + return false; + + bool const isMainTank = botAI->IsMainTank(bot); + + Player* assistTankPlayer = nullptr; + Player* mainTankPlayer = nullptr; + bool assistTankAlive = false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (!assistTankPlayer && botAI->IsAssistTank(member)) + { + assistTankPlayer = member; + assistTankAlive = true; + } + + if (!mainTankPlayer && botAI->IsMainTank(member)) + { + mainTankPlayer = member; + } + + if (assistTankPlayer && mainTankPlayer) + break; + } + } + + auto IsPlayerMarked = [&](Player* player) -> bool + { + if (!player) + return false; + if (Group* grp = player->GetGroup()) + { + for (uint8 icon = 0; icon < 8; ++icon) + { + if (grp->GetTargetIcon(icon) == player->GetGUID()) + return true; + } + } + return false; + }; + + std::vector clones; + RsCollectTargets(botAI, clones, [](Unit* unit) { return unit->GetEntry() == NPC_BALTHARUS_THE_WARBORN_CLONE; }); + + if (isMainTank) + { + bot->SetTarget(boss->GetGUID()); + + if (Group* group = bot->GetGroup()) + { + if (group->GetTargetIcon(RtiTargetValue::skullIndex) != boss->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + } + + if (boss->GetVictim() != bot) + RsCastClassTaunt(botAI, bot, boss); + + if (!assistTankAlive) + { + for (Unit* clone : clones) + { + if (!clone) + continue; + + if (clone->GetVictim() != bot) + RsCastClassTaunt(botAI, bot, clone); + } + } + + boss->SetFacingTo(RS_BALTHARUS_TANK_FACING); + for (Unit* clone : clones) + { + if (clone && clone->IsAlive()) + clone->SetFacingTo(RS_BALTHARUS_TANK_FACING); + } + + if (IsPlayerMarked(bot)) + return false; + + return HoldAt(RS_BALTHARUS_TANK_POSITION, boss); + } + + if (botAI->IsAssistTank(bot)) + { + if (clones.empty()) + return false; + + std::vector myClones; + for (Unit* clone : clones) + { + if (!clone) + continue; + + if (mainTankPlayer) + { + float const dAssist = bot->GetExactDist2d(clone); + float const dMain = mainTankPlayer->GetExactDist2d(clone); + if (dAssist < dMain) + myClones.push_back(clone); + } + else + { + + myClones.push_back(clone); + } + } + + if (myClones.empty()) + { + Unit* nearest = nullptr; + float best = std::numeric_limits::max(); + for (Unit* clone : clones) + { + if (!clone) + continue; + float const d = bot->GetExactDist2d(clone); + if (d < best) + { + best = d; + nearest = clone; + } + } + if (nearest) + myClones.push_back(nearest); + } + + for (Unit* clone : myClones) + { + if (!clone) + continue; + + if (clone->GetVictim() != bot) + RsCastClassTaunt(botAI, bot, clone); + + if (clone->IsAlive()) + clone->SetFacingTo(RS_BALTHARUS_TANK_FACING); + } + + Unit* myClone = myClones.front(); + if (myClone) + bot->SetTarget(myClone->GetGUID()); + + boss->SetFacingTo(RS_BALTHARUS_TANK_FACING); + + if (IsPlayerMarked(bot)) + return false; + + float const assistX = RS_BALTHARUS_TANK_POSITION.GetPositionX() - 25.0f; + float const assistY = RS_BALTHARUS_TANK_POSITION.GetPositionY(); + float const assistZ = RS_BALTHARUS_TANK_POSITION.GetPositionZ(); + + bool const onAssistSpot = bot->GetExactDist2d(assistX, assistY) <= 3.0f; + bool const cloneInMelee = myClone && bot->IsWithinMeleeRange(myClone); + + if (myClone && !cloneInMelee && !onAssistSpot) + return MoveTo(bot->GetMapId(), myClone->GetPositionX(), myClone->GetPositionY(), + myClone->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + if (!onAssistSpot) + return MoveTo(bot->GetMapId(), assistX, assistY, assistZ, false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + if (std::fabs(bot->GetOrientation() - RS_BALTHARUS_TANK_FACING) > 0.1f) + bot->SetFacingTo(RS_BALTHARUS_TANK_FACING); + return false; + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_HAL.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_HAL.cpp new file mode 100644 index 000000000..991efc352 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_HAL.cpp @@ -0,0 +1,938 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include +#include +#include +#include +#include + +#include "ObjectAccessor.h" +#include "Pet.h" +#include "Playerbots.h" +#include "RSActions.h" +#include "RSScripts.h" +#include "ThreatManager.h" +#include "Timer.h" +#include "Vehicle.h" + +bool RsHalionTankPositionAction::Execute(Event ) +{ + if (RsHalionBossTank(botAI) != bot) + return false; + + Unit* boss = RsHalionAnyPhysicalBoss(botAI); + if (!boss || !boss->IsInCombat()) + return false; + + context->GetValue("rti")->Set("skull"); + + if (bot->GetTarget() != boss->GetGUID()) + bot->SetTarget(boss->GetGUID()); + + std::vector adds; + RsHalionCollectAdds(botAI, adds); + + if (botAI->HasCheat(BotCheatMask::raid)) + { + for (Unit* add : adds) + RsForceThreat(add, bot); + } + + if (Group* group = bot->GetGroup()) + { + Unit* skullTarget = nullptr; + + Unit* current = ObjectAccessor::GetUnit(*bot, group->GetTargetIcon(RtiTargetValue::skullIndex)); + bool const currentIsAliveAdd = current && current->IsAlive() && + std::find(adds.begin(), adds.end(), current) != adds.end(); + + if (currentIsAliveAdd) + { + skullTarget = current; + } + else if (!adds.empty()) + { + for (Unit* add : adds) + { + if (add->GetEntry() == NPC_LIVING_INFERNO) + { + skullTarget = add; + break; + } + } + if (!skullTarget) + { + float best = std::numeric_limits::max(); + for (Unit* add : adds) + { + float const d = bot->GetExactDist2d(add); + if (d < best) + { + best = d; + skullTarget = add; + } + } + } + } + else + { + skullTarget = boss; + } + + if (skullTarget && group->GetTargetIcon(RtiTargetValue::skullIndex) != skullTarget->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), skullTarget->GetGUID()); + + if (!adds.empty()) + { + if (group->GetTargetIcon(RtiTargetValue::starIndex) != boss->GetGUID()) + group->SetTargetIcon(RtiTargetValue::starIndex, bot->GetGUID(), boss->GetGUID()); + } + else if (!group->GetTargetIcon(RtiTargetValue::starIndex).IsEmpty()) + { + group->SetTargetIcon(RtiTargetValue::starIndex, bot->GetGUID(), ObjectGuid::Empty); + } + } + + if (botAI->HasCheat(BotCheatMask::raid) && boss->GetVictim() != bot) + { + ThreatManager& mgr = boss->GetThreatMgr(); + mgr.AddThreat(bot, 1000000.0f, nullptr, true, true); + } + + bool const committed = RsHalionTankMeteorCommitted(botAI); + Position const& home = committed ? RS_HALION_TANK_METEOR_SPOT : RS_HALION_TANK_POSITION; + + if (bot->GetExactDist2d(home) > 1.0f) + { + if (committed && bot->IsNonMeleeSpellCast(false)) + bot->InterruptNonMeleeSpells(false); + + if (committed && !bot->HasAura(RS_SPELL_NITRO_BOOSTS)) + bot->AddAura(RS_SPELL_NITRO_BOOSTS, bot); + + return MoveTo(bot->GetMapId(), home.GetPositionX(), home.GetPositionY(), home.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + bot->SetFacingToObject(boss); + + return false; +} + +bool RsHalionAvoidConesAction::Execute(Event ) +{ + if (PlayerbotAI::IsMainTank(bot)) + return false; + + if (botAI->IsTank(bot) && !RsHalionAssistTankAsMelee(botAI)) + return false; + + Unit* boss = RsHalionAnyPhysicalBoss(botAI); + if (!boss) + return false; + + bool const melee = botAI->IsMelee(bot); + bool const addsUp = RsHalionAnyAddAlive(botAI); + + if (!botAI->IsTank(bot) && !addsUp && RsHalionRealmThrottled(botAI, bot) && + (RsHalionLeadingTooMuch(botAI, bot) || RsHalionInThrottledHalf(bot))) + { + if (bot->GetVictim()) + bot->AttackStop(); + if (Pet* pet = bot->GetPet()) + if (pet->GetVictim()) + pet->AttackStop(); + } + + if (melee && addsUp) + context->GetValue("rti")->Set("star"); + else + context->GetValue("rti")->Set("skull"); + + if (melee && bot->GetTarget() != boss->GetGUID()) + bot->SetTarget(boss->GetGUID()); + + float const bossX = boss->GetPositionX(); + float const bossY = boss->GetPositionY(); + + float const dA = bot->GetExactDist2d(RS_HALION_METEOR_SPOT_A.GetPositionX(), RS_HALION_METEOR_SPOT_A.GetPositionY()); + float const dB = bot->GetExactDist2d(RS_HALION_METEOR_SPOT_B.GetPositionX(), RS_HALION_METEOR_SPOT_B.GetPositionY()); + + auto& botUsesSpotA = RubySanctumHelpers::RsState(bot->GetInstanceId()).meteorSpotUsesA; + ObjectGuid const botGuid = bot->GetGUID(); + auto memIt = botUsesSpotA.find(botGuid); + bool usesA = memIt != botUsesSpotA.end() ? memIt->second : dA <= dB; + if (usesA && dB + 8.0f < dA) + usesA = false; + else if (!usesA && dA + 8.0f < dB) + usesA = true; + botUsesSpotA[botGuid] = usesA; + + Position const& spot = usesA ? RS_HALION_METEOR_SPOT_A : RS_HALION_METEOR_SPOT_B; + + if (bot->GetExactDist2d(spot.GetPositionX(), spot.GetPositionY()) <= RS_HALION_LINE_LEASH) + return false; + + float const lineX = spot.GetPositionX() - bossX; + float const lineY = spot.GetPositionY() - bossY; + float const lineLen = std::sqrt(lineX * lineX + lineY * lineY); + if (lineLen < 0.01f) + return false; + + float const ux = lineX / lineLen; + float const uy = lineY / lineLen; + + float const minDist = melee ? RS_HALION_LINE_MELEE_MIN : RS_HALION_LINE_RANGED_MIN; + float const maxDist = melee ? RS_HALION_LINE_MELEE_MAX : RS_HALION_LINE_RANGED_MAX; + + float const distToBoss = bot->GetExactDist2d(bossX, bossY); + + float const botDX = bot->GetPositionX() - bossX; + float const botDY = bot->GetPositionY() - bossY; + float const along = botDX * ux + botDY * uy; + float const offset = std::fabs(botDX * uy - botDY * ux); + + bool const onLine = along > 0.0f && offset <= RS_HALION_LINE_OFFSET_OK; + bool const inBand = distToBoss >= minDist && distToBoss <= maxDist; + + if (onLine && inBand) + return false; + + float desired = distToBoss; + if (desired < minDist) + desired = minDist; + else if (desired > maxDist) + desired = maxDist; + + float const moveX = bossX + ux * desired; + float const moveY = bossY + uy * desired; + + if (!bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); +} + +bool RsHalionCombustionAction::Execute(Event ) +{ + if (RsHalionHasCombustion(bot) && RsHalionCombustionAtSpot(bot)) + { + bot->RemoveAurasDueToSpell(SPELL_FIERY_COMBUSTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + bot->RemoveAurasDueToSpell(SPELL_MARK_OF_COMBUSTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + return false; + } + + if (RsHalionBossTank(botAI) == bot) + return false; + + if (!RsHalionHasCombustion(bot)) + { + if (RsHalionCombustionReturning(bot)) + { + Position const& rally = RsHalionMeteorSpot(bot->GetInstanceId()); + return MoveTo(bot->GetMapId(), rally.GetPositionX(), rally.GetPositionY(), rally.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED); + } + return false; + } + + if (!bot->HasAura(RS_SPELL_NITRO_BOOSTS)) + bot->AddAura(RS_SPELL_NITRO_BOOSTS, bot); + + Position const& spot = RsHalionCombustionSpot(bot); + + if (bot->GetExactDist2d(spot.GetPositionX(), spot.GetPositionY()) <= RS_HALION_COMBUSTION_REACH) + return false; + + return MoveTo(bot->GetMapId(), spot.GetPositionX(), spot.GetPositionY(), spot.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED); +} + +bool RsHalionMeteorAction::Execute(Event ) +{ + if (botAI->IsTank(bot) && !RsHalionAssistTankAsMelee(botAI)) + return false; + + if (!RsHalionAnyPhysicalBoss(botAI)) + return false; + + if (!RsHalionMeteorShouldRally(bot)) + return false; + + if (bot->IsNonMeleeSpellCast(false)) + bot->InterruptNonMeleeSpells(false); + botAI->Reset(); + + Position const& target = RsHalionMeteorSpot(bot->GetInstanceId()); + Position const& mid = RS_HALION_METEOR_MID; + + float const distToTarget = bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + float const midToTarget = mid.GetExactDist2d(target.GetPositionX(), target.GetPositionY()); + float const distToMid = bot->GetExactDist2d(mid.GetPositionX(), mid.GetPositionY()); + + Position const& leg = (distToMid > RS_HALION_METEOR_REACH && distToTarget > midToTarget) ? mid : target; + + return MoveTo(bot->GetMapId(), leg.GetPositionX(), leg.GetPositionY(), leg.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_FORCED, true); +} + +bool RsHalionStartPositionAction::Execute(Event ) +{ + if (RsHalionBossTank(botAI) == bot) + return false; + + Unit* boss = RsHalionPhase1Boss(botAI); + if (!boss || !boss->HealthAbovePct(99)) + return false; + + if (bot->GetExactDist2d(RS_HALION_METEOR_SPOT_A.GetPositionX(), RS_HALION_METEOR_SPOT_A.GetPositionY()) <= + RS_HALION_METEOR_REACH) + return false; + + return MoveTo(bot->GetMapId(), RS_HALION_METEOR_SPOT_A.GetPositionX(), RS_HALION_METEOR_SPOT_A.GetPositionY(), + RS_HALION_METEOR_SPOT_A.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); +} + +bool RsHalionAddsAction::Execute(Event ) +{ + return false; +} + +bool RsHalionAddTankAction::Execute(Event ) +{ + return false; +} + +bool RsHalionEnterPortalAction::Execute(Event ) +{ + if (PlayerbotAI::IsMainTank(bot)) + return false; + + Unit* physBoss = RsHalionAnyPhysicalBoss(botAI); + bool const phase3 = physBoss != nullptr && !physBoss->HealthAbovePct(50); + + bool p3TwilightExit = false; + if (RsHalionInTwilight(bot)) + { + Unit* twilightBoss = RsHalionTwilightBoss(botAI); + bool const tBossLow = twilightBoss != nullptr && !twilightBoss->HealthAbovePct(50); + if (!tBossLow || RsHalionP3TwilightAssigned(bot)) + return false; + + if (RsHalionHasConsumption(bot)) + return false; + + if (!RsHalionPortalCommit(botAI, bot)) + return false; + + p3TwilightExit = true; + } + else if (RsHalionPortalHeldForAdds(botAI)) + { + if (RsHalionFirstCrosser(botAI) == bot) + { + uint32 const remaining = RsHalionPortalAddHoldRemainingMs(botAI); + uint32 const shown = (remaining + 999) / 1000 + 2; + + uint32& last = RubySanctumHelpers::RsState(bot->GetInstanceId()).portalCountdownLastShown; + if (last != shown) + { + last = shown; + botAI->Yell(std::to_string(shown)); + } + } + return false; + } + + auto& botPortalTarget = RubySanctumHelpers::RsState(bot->GetInstanceId()).botPortalTarget; + ObjectGuid const botGuid = bot->GetGUID(); + + GameObject* portal = nullptr; + auto cachedIt = botPortalTarget.find(botGuid); + if (cachedIt != botPortalTarget.end()) + { + portal = ObjectAccessor::GetGameObject(*bot, cachedIt->second); + if (!portal) + botPortalTarget.erase(cachedIt); + } + + if (!portal) + { + portal = RsHalionFindPortal(botAI); + if (!portal) + return false; + botPortalTarget[botGuid] = portal->GetGUID(); + } + + bool const isFirstCrosser = (p3TwilightExit || phase3) ? false : (RsHalionFirstCrosser(botAI) == bot); + if (!isFirstCrosser && !p3TwilightExit && !phase3) + { + Player* firstCrosser = RsHalionFirstCrosser(botAI); + if (!firstCrosser || !RsHalionInTwilight(firstCrosser)) + return false; + + uint32& seen = RubySanctumHelpers::RsState(bot->GetInstanceId()).portalSeen[bot->GetGUID()]; + if (seen == 0) + seen = getMSTime(); + + if (GetMSTimeDiffToNow(seen) < 1000) + return false; + } + + float const distToPortal = bot->GetExactDist2d(portal->GetPositionX(), portal->GetPositionY()); + + if (distToPortal > INTERACTION_DISTANCE - 0.5f) + { + float goalX = portal->GetPositionX(); + float goalY = portal->GetPositionY(); + + std::vector pools; + RsHalionCollectHazardPools(bot, pools); + if (!pools.empty()) + { + float const dx = goalX - bot->GetPositionX(); + float const dy = goalY - bot->GetPositionY(); + float const len = std::sqrt(dx * dx + dy * dy); + if (len > 0.01f) + { + float const ux = dx / len; + float const uy = dy / len; + float const probe = std::min(len, RS_HALION_CONSUMPTION_STEP); + float const aheadX = bot->GetPositionX() + ux * probe; + float const aheadY = bot->GetPositionY() + uy * probe; + + if (!RsHalionSpotClearOfPools(pools, aheadX, aheadY)) + { + bool routed = false; + for (float const deg : {35.0f, -35.0f, 60.0f, -60.0f, 90.0f, -90.0f}) + { + float const a = std::atan2(uy, ux) + deg * static_cast(M_PI) / 180.0f; + float const sideX = bot->GetPositionX() + std::cos(a) * probe; + float const sideY = bot->GetPositionY() + std::sin(a) * probe; + if (RsHalionSpotClearOfPools(pools, sideX, sideY) && + bot->IsWithinLOS(sideX, sideY, bot->GetPositionZ())) + { + goalX = sideX; + goalY = sideY; + routed = true; + break; + } + } + if (!routed) + return false; + } + } + } + + if (p3TwilightExit && RsHalionCutterShouldMove(bot->GetInstanceId()) && + !bot->HasAura(RS_SPELL_MAGIC_BARRIER)) + { + bot->AddAura(RS_SPELL_MAGIC_BARRIER, bot); + ObjectGuid const barrierGuid = bot->GetGUID(); + botAI->AddTimedEvent([barrierGuid]() + { + if (Player* p = ObjectAccessor::FindPlayer(barrierGuid)) + p->RemoveAura(RS_SPELL_MAGIC_BARRIER); + }, 5000); + } + + return MoveTo(bot->GetMapId(), goalX, goalY, portal->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED); + } + + botPortalTarget.erase(botGuid); + botAI->RemoveShapeshift(); + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + bot->SetFacingToObject(portal); + + portal->Use(bot); + + if (p3TwilightExit) + bot->RemoveAura(SPELL_TWILIGHT_REALM); + + if (!p3TwilightExit && !phase3) + { + bot->AddAura(RS_SPELL_MAGIC_BARRIER, bot); + + ObjectGuid const guid = bot->GetGUID(); + botAI->AddTimedEvent([guid]() + { + if (Player* p = ObjectAccessor::FindPlayer(guid)) + p->RemoveAura(RS_SPELL_MAGIC_BARRIER); + }, 5000); + } + + return true; +} + +bool RsHalionP2TankPositionAction::Execute(Event ) +{ + if (RsHalionTwilightTank(botAI) != bot || !RsHalionInTwilight(bot)) + return false; + + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss) + return false; + + if (bot->GetTarget() != boss->GetGUID()) + bot->SetTarget(boss->GetGUID()); + + if (Group* group = bot->GetGroup()) + { + if (group->GetTargetIcon(RtiTargetValue::crossIndex) != boss->GetGUID()) + group->SetTargetIcon(RtiTargetValue::crossIndex, bot->GetGUID(), boss->GetGUID()); + } + + if (botAI->HasCheat(BotCheatMask::raid)) + { + ThreatManager& mgr = boss->GetThreatMgr(); + if (boss->GetVictim() != bot) + mgr.AddThreat(bot, 1000000.0f, nullptr, true, true); + mgr.FixateTarget(bot); + } + + context->GetValue("rti")->Set("cross"); + + float const distToBoss = bot->GetExactDist2d(boss->GetPositionX(), boss->GetPositionY()); + if (std::fabs(distToBoss - RS_HALION_CUTTER_FLANK_DIST) > 2.0f) + { + float const tankAngle = std::atan2(bot->GetPositionY() - boss->GetPositionY(), + bot->GetPositionX() - boss->GetPositionX()); + float const rx = boss->GetPositionX() + std::cos(tankAngle) * RS_HALION_CUTTER_FLANK_DIST; + float const ry = boss->GetPositionY() + std::sin(tankAngle) * RS_HALION_CUTTER_FLANK_DIST; + if (bot->IsWithinLOS(rx, ry, bot->GetPositionZ())) + return MoveTo(bot->GetMapId(), rx, ry, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool RsHalionP2AvoidConesAction::Execute(Event ) +{ + if (RsHalionTwilightTank(botAI) == bot) + return false; + + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss) + return false; + + if (RsHalionPortalCommit(botAI, bot)) + return false; + + if (RsHalionCutterShouldMove(bot->GetInstanceId())) + { + auto& botPortalTarget = RubySanctumHelpers::RsState(bot->GetInstanceId()).botPortalTarget; + auto portalIt = botPortalTarget.find(bot->GetGUID()); + if (portalIt != botPortalTarget.end()) + { + botPortalTarget.erase(portalIt); + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == POINT_MOTION_TYPE) + { + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + } + } + } + + if (!PlayerbotAI::IsTank(bot) && RsHalionRealmThrottled(botAI, bot) && + (RsHalionLeadingTooMuch(botAI, bot) || RsHalionInThrottledHalf(bot))) + { + if (bot->GetVictim()) + bot->AttackStop(); + if (Pet* pet = bot->GetPet()) + if (pet->GetVictim()) + pet->AttackStop(); + } + + if (PlayerbotAI::IsTank(bot)) + { + Player* twilightTank = RsHalionTwilightTank(botAI); + if (!twilightTank) + return false; + if (bot->GetExactDist2d(twilightTank) <= 2.0f) + return false; + return MoveTo(bot->GetMapId(), twilightTank->GetPositionX(), twilightTank->GetPositionY(), + twilightTank->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT); + } + + context->GetValue("rti")->Set("cross"); + + Player* tank = RsHalionTwilightTank(botAI); + if (!tank) + return false; + + std::vector> pairs; + bool const singleCutter = RsHalionCollectOrbPairs(boss, pairs) && pairs.size() == 1; + bool const cutterFiring = RsHalionCutterActive(bot->GetInstanceId()); + + if (RsHalionCutterBeamDanger(botAI, bot) && singleCutter && cutterFiring) + { + if (bot->IsNonMeleeSpellCast(false)) + bot->InterruptNonMeleeSpells(false); + + Unit* na = pairs[0].first; + Unit* nb = pairs[0].second; + float const nearestSigned = RsHalionBeamSignedDist(bot->GetPositionX(), bot->GetPositionY(), na, nb); + float const nearest = std::fabs(nearestSigned); + float const ex = nb->GetPositionX() - na->GetPositionX(); + float const ey = nb->GetPositionY() - na->GetPositionY(); + float const elen = std::sqrt(ex * ex + ey * ey); + if (elen > 0.01f) + { + float const side = nearestSigned >= 0.0f ? 1.0f : -1.0f; + float const nx = side * ey / elen; + float const ny = side * -ex / elen; + float const clear = RS_HALION_CUTTER_DANGER + RS_HALION_CUTTER_MARGIN; + float const stepLen = std::max(clear - nearest, RS_HALION_P2_STEP); + float const moveX = bot->GetPositionX() + nx * stepLen; + float const moveY = bot->GetPositionY() + ny * stepLen; + if (bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, + true, MovementPriority::MOVEMENT_FORCED, true); + } + } + + bool const lockedClass = bot->getClass() == CLASS_ROGUE || bot->getClass() == CLASS_WARRIOR; + bool const loose = RsHalionCutterShouldMove(bot->GetInstanceId()) && + !lockedClass && + !RsHalionCutterBeamDanger(botAI, bot); + + if (loose && bot->IsNonMeleeSpellCast(false, false, true)) + return false; + + if (RsHalionCutterBeamDanger(botAI, bot) && cutterFiring && bot->IsNonMeleeSpellCast(false)) + bot->InterruptNonMeleeSpells(false); + + float const bossX = boss->GetPositionX(); + float const bossY = boss->GetPositionY(); + float const bossToTank = std::atan2(tank->GetPositionY() - bossY, tank->GetPositionX() - bossX); + float const radius = botAI->IsMelee(bot) ? RS_HALION_P2_MELEE_DIST : RS_HALION_P2_RANGED_DIST; + float slotAngle = bossToTank + static_cast(M_PI) * 95.0f / 180.0f; + + if (singleCutter && cutterFiring) + { + float const clear = RS_HALION_CUTTER_DANGER + RS_HALION_CUTTER_MARGIN; + auto slotClear = [&](float ang) -> bool + { + float const sx = bossX + std::cos(ang) * radius; + float const sy = bossY + std::sin(ang) * radius; + return std::fabs(RsHalionBeamSignedDist(sx, sy, pairs[0].first, pairs[0].second)) > clear; + }; + + if (!slotClear(slotAngle)) + { + float const stepArc = static_cast(M_PI) / 18.0f; + for (int i = 1; i <= 18; ++i) + { + if (slotClear(slotAngle + stepArc * static_cast(i))) + { + slotAngle += stepArc * static_cast(i); + break; + } + if (slotClear(slotAngle - stepArc * static_cast(i))) + { + slotAngle -= stepArc * static_cast(i); + break; + } + } + } + } + + float const targetX = bossX + std::cos(slotAngle) * radius; + float const targetY = bossY + std::sin(slotAngle) * radius; + + float const dx = targetX - bot->GetPositionX(); + float const dy = targetY - bot->GetPositionY(); + float const dist = std::sqrt(dx * dx + dy * dy); + if (dist <= (loose ? RS_HALION_P2_STEP : 1.0f)) + return false; + + float const botZ = bot->GetPositionZ(); + auto losOk = [&](float x, float y) -> bool + { + return bot->IsWithinLOS(x, y, botZ) || bot->IsWithinLOS(x, y, botZ + 2.0f) || + bot->IsWithinLOS(x, y, botZ - 2.0f); + }; + + float const stepLen = std::min(dist, RS_HALION_P2_STEP); + float const moveX = bot->GetPositionX() + (dx / dist) * stepLen; + float const moveY = bot->GetPositionY() + (dy / dist) * stepLen; + + if (losOk(moveX, moveY)) + return MoveTo(bot->GetMapId(), moveX, moveY, botZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true); + + float const toBossX = bossX - bot->GetPositionX(); + float const toBossY = bossY - bot->GetPositionY(); + float const toBossLen = std::sqrt(toBossX * toBossX + toBossY * toBossY); + if (toBossLen < 0.01f) + return false; + + float const radialStep = std::min(toBossLen, RS_HALION_P2_STEP); + float const radialX = bot->GetPositionX() + (toBossX / toBossLen) * radialStep; + float const radialY = bot->GetPositionY() + (toBossY / toBossLen) * radialStep; + if (!losOk(radialX, radialY)) + return false; + + return MoveTo(bot->GetMapId(), radialX, radialY, botZ, false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true); +} + +bool RsHalionConsumptionAction::Execute(Event ) +{ + auto& clearedForConsumption = RubySanctumHelpers::RsState(bot->GetInstanceId()).clearedForConsumption; + ObjectGuid const botGuid = bot->GetGUID(); + + if (!bot->HasAura(SPELL_MARK_OF_CONSUMPTION) && !bot->HasAura(SPELL_SOUL_CONSUMPTION)) + { + clearedForConsumption.erase(botGuid); + return false; + } + + if (RsHalionIsPhase3(botAI) && bot->HealthBelowPct(RS_HALION_CONSUMPTION_HOLD_PCT)) + { + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == POINT_MOTION_TYPE) + { + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + } + return false; + } + + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss) + return false; + + if (!clearedForConsumption.count(botGuid)) + { + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() == POINT_MOTION_TYPE) + { + bot->GetMotionMaster()->Clear(); + bot->StopMoving(); + } + clearedForConsumption.insert(botGuid); + } + + if (!bot->HasAura(RS_SPELL_NITRO_BOOSTS)) + bot->AddAura(RS_SPELL_NITRO_BOOSTS, bot); + + float const bossX = boss->GetPositionX(); + float const bossY = boss->GetPositionY(); + float const distToBoss = bot->GetExactDist2d(bossX, bossY); + + if (distToBoss >= RS_HALION_CONSUMPTION_OUT_DIST - RS_HALION_CONSUMPTION_OUT_REACH) + { + bot->RemoveAurasDueToSpell(SPELL_SOUL_CONSUMPTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + bot->RemoveAurasDueToSpell(SPELL_MARK_OF_CONSUMPTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + return false; + } + + std::vector> pairs; + RsHalionCollectOrbPairs(boss, pairs); + + std::vector pools; + RsHalionCollectHazardPools(bot, pools); + + auto beamClear = [&](float px, float py) -> bool + { + for (auto const& pair : pairs) + if (std::fabs(RsHalionBeamSignedDist(px, py, pair.first, pair.second)) <= RS_HALION_CUTTER_BOT_CLEAR) + return false; + return true; + }; + + bool const startInPool = !RsHalionSpotClearOfPools(pools, bot->GetPositionX(), bot->GetPositionY()); + + float const radAngle = distToBoss > 0.01f + ? std::atan2(bot->GetPositionY() - bossY, bot->GetPositionX() - bossX) + : 0.0f; + float const stepArc = static_cast(M_PI) / 36.0f; + + bool moveResult = false; + auto tryReachOut = [&](bool needBeamClear, bool needPoolClear, bool needPathClear) -> bool + { + for (int i = 0; i <= 24; ++i) + { + for (float const sign : {1.0f, -1.0f}) + { + float const a = radAngle + sign * stepArc * static_cast(i); + float const goalX = bossX + std::cos(a) * RS_HALION_CONSUMPTION_OUT_DIST; + float const goalY = bossY + std::sin(a) * RS_HALION_CONSUMPTION_OUT_DIST; + + if (needBeamClear && !beamClear(goalX, goalY)) + continue; + if (needPoolClear && !RsHalionSpotClearOfPools(pools, goalX, goalY)) + continue; + + float const dx = goalX - bot->GetPositionX(); + float const dy = goalY - bot->GetPositionY(); + float const dist = std::sqrt(dx * dx + dy * dy); + float const stepLen = std::min(RS_HALION_CONSUMPTION_STEP, dist); + float const moveX = dist < 0.01f ? goalX : bot->GetPositionX() + (dx / dist) * stepLen; + float const moveY = dist < 0.01f ? goalY : bot->GetPositionY() + (dy / dist) * stepLen; + + if (needBeamClear && !beamClear(moveX, moveY)) + continue; + if (needPoolClear && !RsHalionSpotClearOfPools(pools, moveX, moveY)) + continue; + if (needPathClear && !startInPool && + !RsHalionPathClearOfPools(pools, bot->GetPositionX(), bot->GetPositionY(), moveX, moveY)) + continue; + if (!bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + continue; + + moveResult = MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, + true, MovementPriority::MOVEMENT_FORCED); + return true; + } + } + return false; + }; + + if (tryReachOut(true, true, true) || tryReachOut(true, true, false) || + tryReachOut(true, false, false) || tryReachOut(false, false, false)) + return moveResult; + + return false; +} + +bool RsHalionHealConsumptionAction::Execute(Event ) +{ + Player* target = RsHalionConsumptionHealTarget(botAI); + if (!target) + return false; + + if (bot->GetExactDist2d(target) > 36.0f) + return MoveTo(bot->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + + switch (bot->getClass()) + { + case CLASS_PRIEST: + return botAI->CastSpell("flash heal", target) || botAI->CastSpell("greater heal", target) || + botAI->CastSpell("renew", target); + case CLASS_DRUID: + return botAI->CastSpell("regrowth", target) || botAI->CastSpell("nourish", target) || + botAI->CastSpell("rejuvenation", target); + case CLASS_SHAMAN: + return botAI->CastSpell("lesser healing wave", target) || botAI->CastSpell("healing wave", target) || + botAI->CastSpell("riptide", target); + case CLASS_PALADIN: + return botAI->CastSpell("flash of light", target) || botAI->CastSpell("holy light", target); + default: + break; + } + + return false; +} + +bool RsHalionCutterAction::Execute(Event ) +{ + if (bot->HasAura(SPELL_MARK_OF_CONSUMPTION) || bot->HasAura(SPELL_SOUL_CONSUMPTION)) + return false; + + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss) + return false; + + std::vector> pairs; + if (!RsHalionCollectOrbPairs(boss, pairs)) + return false; + + if (!RsHalionCutterShouldMove(bot->GetInstanceId())) + return false; + + if (RsHalionPortalCommit(botAI, bot)) + return false; + + float const bossX = boss->GetPositionX(); + float const bossY = boss->GetPositionY(); + + auto nearestBeamDist = [&](float px, float py) -> float + { + float best = std::numeric_limits::max(); + for (auto const& pair : pairs) + best = std::min(best, std::fabs(RsHalionBeamSignedDist(px, py, pair.first, pair.second))); + return best; + }; + + Player* twilightTank = RsHalionTwilightTank(botAI); + + RubySanctumHelpers::CutterTiming& timing = RubySanctumHelpers::RsState(bot->GetInstanceId()).cutterTiming; + float const spinSign = timing.spinSign != 0.0f ? timing.spinSign : 1.0f; + + if (twilightTank == bot) + { + if (boss->GetVictim() != bot) + return false; + + float const radius = RS_HALION_CUTTER_FLANK_DIST; + float const distToBoss = bot->GetExactDist2d(bossX, bossY); + float const tankAngle = std::atan2(bot->GetPositionY() - bossY, bot->GetPositionX() - bossX); + + if (std::fabs(distToBoss - radius) > 2.0f) + { + float const rx = bossX + std::cos(tankAngle) * radius; + float const ry = bossY + std::sin(tankAngle) * radius; + if (bot->IsWithinLOS(rx, ry, bot->GetPositionZ())) + return MoveTo(bot->GetMapId(), rx, ry, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + float const trailAngle = RS_HALION_CUTTER_TANK_TRAIL / radius; + float const tolAngle = RS_HALION_CUTTER_TANK_TRAIL_TOL / radius; + + float bestDist = std::numeric_limits::max(); + float follow = 0.0f; + for (auto const& pair : pairs) + { + float const phi = std::atan2(pair.second->GetPositionY() - pair.first->GetPositionY(), + pair.second->GetPositionX() - pair.first->GetPositionX()); + for (float const c : {phi, phi + static_cast(M_PI)}) + { + float const targetAngle = c - spinSign * trailAngle; + float f = spinSign * (targetAngle - tankAngle); + while (f > static_cast(M_PI)) + f -= 2.0f * static_cast(M_PI); + while (f < -static_cast(M_PI)) + f += 2.0f * static_cast(M_PI); + if (std::fabs(f) < bestDist) + { + bestDist = std::fabs(f); + follow = f; + } + } + } + + if (std::fabs(follow) <= tolAngle) + return false; + + float const stepLen = 3.0f; + float const stepAngle = std::min(std::fabs(follow), stepLen / radius); + float const moveSign = follow >= 0.0f ? spinSign : -spinSign; + float const moveAngle = tankAngle + moveSign * stepAngle; + + float const moveX = bossX + std::cos(moveAngle) * radius; + float const moveY = bossY + std::sin(moveAngle) * radius; + + if (nearestBeamDist(moveX, moveY) <= RS_HALION_CUTTER_HALF_WIDTH) + return false; + if (!bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true); + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_SAV.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_SAV.cpp new file mode 100644 index 000000000..34ed0a63c --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_SAV.cpp @@ -0,0 +1,354 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "Playerbots.h" +#include "RSActions.h" + +namespace +{ + constexpr int RS_SAVIANA_BEACON_SLOTS = 8; + constexpr float RS_SAVIANA_BEACON_RING = 18.0f; + constexpr float RS_SAVIANA_BEACON_REACH = 3.0f; + + std::map, int> g_savianaBeaconSlot; + + Position RsSavianaBeaconSlotPos(int slot) + { + float const angle = slot * (2.0f * static_cast(M_PI) / RS_SAVIANA_BEACON_SLOTS); + return Position(RS_SAVIANA_TANK_POSITION.GetPositionX() + std::cos(angle) * RS_SAVIANA_BEACON_RING, + RS_SAVIANA_TANK_POSITION.GetPositionY() + std::sin(angle) * RS_SAVIANA_BEACON_RING, + RS_SAVIANA_TANK_POSITION.GetPositionZ()); + } + + constexpr int RS_SAVIANA_MELEE_INNER = 8; + constexpr int RS_SAVIANA_MELEE_OUTER = 12; + constexpr int RS_SAVIANA_MELEE_SLOTS = RS_SAVIANA_MELEE_INNER + RS_SAVIANA_MELEE_OUTER; + constexpr float RS_SAVIANA_MELEE_INNER_RING = 12.0f; + constexpr float RS_SAVIANA_MELEE_OUTER_RING = 20.0f; + constexpr float RS_SAVIANA_MELEE_REACH = 3.0f; + + constexpr float RS_SAVIANA_BEACON_DANGER = 12.0f; + + std::map, int> g_savianaMeleeSlot; + + constexpr float RS_SAVIANA_FRONT_RANGE = 30.0f; + + Position RsSavianaMeleeSlotPos(int slot) + { + bool const inner = slot < RS_SAVIANA_MELEE_INNER; + int const count = inner ? RS_SAVIANA_MELEE_INNER : RS_SAVIANA_MELEE_OUTER; + int const index = inner ? slot : slot - RS_SAVIANA_MELEE_INNER; + float const ring = inner ? RS_SAVIANA_MELEE_INNER_RING : RS_SAVIANA_MELEE_OUTER_RING; + + float const offset = inner ? 0.0f : (static_cast(M_PI) / count); + float const angle = index * (2.0f * static_cast(M_PI) / count) + offset; + + return Position(RS_SAVIANA_TANK_POSITION.GetPositionX() + std::cos(angle) * ring, + RS_SAVIANA_TANK_POSITION.GetPositionY() + std::sin(angle) * ring, + RS_SAVIANA_TANK_POSITION.GetPositionZ()); + } +} + +bool RsSavianaConflagrationAction::Execute(Event ) +{ + uint32 const instanceId = bot->GetInstanceId(); + auto const myKey = std::make_pair(instanceId, bot->GetGUID()); + + if (!bot->HasAura(SPELL_FLAME_BEACON)) + { + g_savianaBeaconSlot.erase(myKey); + return false; + } + + std::set reserved; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + if (!member->HasAura(SPELL_FLAME_BEACON)) + continue; + + auto it = g_savianaBeaconSlot.find(std::make_pair(instanceId, member->GetGUID())); + if (it != g_savianaBeaconSlot.end()) + reserved.insert(it->second); + } + } + + int mySlot = -1; + + auto mem = g_savianaBeaconSlot.find(myKey); + if (mem != g_savianaBeaconSlot.end() && !reserved.count(mem->second)) + mySlot = mem->second; + + if (mySlot < 0) + { + float bestDist = std::numeric_limits::max(); + for (int i = 0; i < RS_SAVIANA_BEACON_SLOTS; ++i) + { + if (reserved.count(i)) + continue; + Position const slotPos = RsSavianaBeaconSlotPos(i); + float const d = bot->GetExactDist2d(slotPos.GetPositionX(), slotPos.GetPositionY()); + if (d < bestDist) + { + bestDist = d; + mySlot = i; + } + } + } + + if (mySlot < 0) + return false; + + g_savianaBeaconSlot[myKey] = mySlot; + + Position const target = RsSavianaBeaconSlotPos(mySlot); + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) <= RS_SAVIANA_BEACON_REACH) + return false; + + float destX = target.GetPositionX(); + float destY = target.GetPositionY(); + float destZ = target.GetPositionZ(); + + bool exact = true; + if (!bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), destX, destY, destZ)) + exact = false; + + return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, true, exact, + MovementPriority::MOVEMENT_FORCED); +} + +bool RsSavianaTankPositionAction::Execute(Event ) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + if (!boss) + return false; + + float const safeThreshold = 3.0f; + float const unsafeLeash = 20.0f; + float const leash = boss->IsLevitating() ? unsafeLeash : safeThreshold; + + float const distance = bot->GetExactDist2d(RS_SAVIANA_TANK_POSITION.GetPositionX(), + RS_SAVIANA_TANK_POSITION.GetPositionY()); + + if (!boss->IsLevitating() && distance <= leash && !bot->IsWithinMeleeRange(boss)) + return MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), boss->GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + + if (distance > leash) + return MoveTo(bot->GetMapId(), RS_SAVIANA_TANK_POSITION.GetPositionX(), + RS_SAVIANA_TANK_POSITION.GetPositionY(), RS_SAVIANA_TANK_POSITION.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_COMBAT); + + return false; +} + +bool RsSavianaAvoidFrontAction::Execute(Event ) +{ + if (botAI->IsTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + if (!boss || boss->IsLevitating()) + return false; + + if (!RsSpotInFrontArc(boss, bot->GetPositionX(), bot->GetPositionY(), RS_SAVIANA_FRONT_RANGE)) + return false; + + float const step = 5.0f; + + if (!botAI->IsMelee(bot)) + { + float const bossX = boss->GetPositionX(); + float const bossY = boss->GetPositionY(); + float const radius = bot->GetExactDist2d(bossX, bossY); + + float const toBot = std::atan2(bot->GetPositionY() - bossY, bot->GetPositionX() - bossX); + float const leftFlank = boss->GetOrientation() + static_cast(M_PI) / 2.0f; + float const rightFlank = boss->GetOrientation() - static_cast(M_PI) / 2.0f; + + float const flank = std::fabs(RsAngleDiff(toBot, leftFlank)) <= std::fabs(RsAngleDiff(toBot, rightFlank)) + ? leftFlank : rightFlank; + float const flankX = bossX + std::cos(flank) * radius; + float const flankY = bossY + std::sin(flank) * radius; + + float const dx = flankX - bot->GetPositionX(); + float const dy = flankY - bot->GetPositionY(); + float const dist = std::sqrt(dx * dx + dy * dy); + if (dist < 0.01f) + return false; + + float const hop = std::min(step, dist); + float const moveX = bot->GetPositionX() + (dx / dist) * hop; + float const moveY = bot->GetPositionY() + (dy / dist) * hop; + + if (!bot->IsWithinLOS(moveX, moveY, bot->GetPositionZ())) + return false; + + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); + } + + int const directions = 12; + + for (int i = 0; i < directions; ++i) + { + float const angle = (i * 2.0f * static_cast(M_PI)) / directions; + float const moveX = bot->GetPositionX() + step * std::cos(angle); + float const moveY = bot->GetPositionY() + step * std::sin(angle); + float const moveZ = bot->GetPositionZ(); + + if (RsSpotInFrontArc(boss, moveX, moveY, RS_SAVIANA_FRONT_RANGE)) + continue; + + if (!bot->IsWithinLOS(moveX, moveY, moveZ)) + continue; + + return MoveTo(bot->GetMapId(), moveX, moveY, moveZ, false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true); + } + + return false; +} + +bool RsSavianaMeleeSpreadAction::Execute(Event ) +{ + uint32 const instanceId = bot->GetInstanceId(); + auto const myKey = std::make_pair(instanceId, bot->GetGUID()); + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + if (!boss || !boss->IsLevitating()) + { + g_savianaMeleeSlot.erase(myKey); + return false; + } + + auto moveChecked = [&](float destX, float destY, float destZ) + { + bool const exact = bot->GetMap()->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), + bot->GetPositionY(), bot->GetPositionZ(), + destX, destY, destZ); + return MoveTo(bot->GetMapId(), destX, destY, destZ, false, false, true, exact, + MovementPriority::MOVEMENT_FORCED); + }; + + std::set reserved; + std::vector beacons; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_FLAME_BEACON)) + { + beacons.push_back(member->GetPosition()); + continue; + } + + auto it = g_savianaMeleeSlot.find(std::make_pair(instanceId, member->GetGUID())); + if (it != g_savianaMeleeSlot.end()) + reserved.insert(it->second); + } + } + + auto slotSafe = [&](int slot) -> bool + { + if (reserved.count(slot)) + return false; + Position const slotPos = RsSavianaMeleeSlotPos(slot); + for (Position const& beacon : beacons) + if (beacon.GetExactDist2d(slotPos.GetPositionX(), slotPos.GetPositionY()) < RS_SAVIANA_BEACON_DANGER) + return false; + return true; + }; + + int mySlot = -1; + + auto mem = g_savianaMeleeSlot.find(myKey); + if (mem != g_savianaMeleeSlot.end() && slotSafe(mem->second)) + mySlot = mem->second; + + if (mySlot < 0) + { + float bestDist = std::numeric_limits::max(); + for (int i = 0; i < RS_SAVIANA_MELEE_SLOTS; ++i) + { + if (!slotSafe(i)) + continue; + Position const slotPos = RsSavianaMeleeSlotPos(i); + float const d = bot->GetExactDist2d(slotPos.GetPositionX(), slotPos.GetPositionY()); + if (d < bestDist) + { + bestDist = d; + mySlot = i; + } + } + } + + if (mySlot < 0) + { + g_savianaMeleeSlot.erase(myKey); + + Position const* nearest = nullptr; + float nearestDist = std::numeric_limits::max(); + for (Position const& beacon : beacons) + { + float const d = bot->GetExactDist2d(beacon.GetPositionX(), beacon.GetPositionY()); + if (d < nearestDist) + { + nearestDist = d; + nearest = &beacon; + } + } + + if (!nearest || nearestDist >= RS_SAVIANA_BEACON_DANGER) + return false; + + float dx = bot->GetPositionX() - nearest->GetPositionX(); + float dy = bot->GetPositionY() - nearest->GetPositionY(); + float const len = std::sqrt(dx * dx + dy * dy); + if (len < 0.01f) + { + dx = 1.0f; + dy = 0.0f; + } + else + { + dx /= len; + dy /= len; + } + + float const stepOut = RS_SAVIANA_BEACON_DANGER - nearestDist + 2.0f; + return moveChecked(bot->GetPositionX() + dx * stepOut, bot->GetPositionY() + dy * stepOut, + bot->GetPositionZ()); + } + + g_savianaMeleeSlot[myKey] = mySlot; + + Position const target = RsSavianaMeleeSlotPos(mySlot); + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) <= RS_SAVIANA_MELEE_REACH) + return false; + + return moveChecked(target.GetPositionX(), target.GetPositionY(), target.GetPositionZ()); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ZAR.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ZAR.cpp new file mode 100644 index 000000000..29422362d --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/Action/RSActions_ZAR.cpp @@ -0,0 +1,218 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include +#include + +#include "NearestNpcsValue.h" +#include "Playerbots.h" +#include "RSActions.h" + +bool RsZarithrianAddsAction::Execute(Event ) +{ + context->GetValue("rti")->Set("skull"); + + if (!RsIsDesignatedMarker(bot)) + return false; + + RsUpdateSkullMarker(bot, FindPriorityAdd()); + return false; +} + +Unit* RsZarithrianAddsAction::FindPriorityAdd() +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Unit* boss = AI_VALUE2(Unit*, "find target", "general zarithrian"); + + Unit* current = botAI->GetUnit(group->GetTargetIcon(RtiTargetValue::skullIndex)); + if (current && current->IsAlive() && current->GetEntry() == NPC_ONYX_FLAMECALLER) + return current; + + constexpr float markRange = 20.0f; + Unit* best = nullptr; + GuidVector const targets = AI_VALUE(GuidVector, "possible targets no los"); + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit || !unit->IsAlive() || unit->GetEntry() != NPC_ONYX_FLAMECALLER) + continue; + + if (boss && unit->GetExactDist2d(boss) > markRange) + continue; + + if (!best || unit->GetGUID() < best->GetGUID()) + best = unit; + } + + return best ? best : boss; +} + +uint32 RsZarithrianTankAction::GetCleaveStacks(Unit* unit) +{ + if (!unit) + return 0; + + Aura* aura = botAI->GetAura("cleave armor", unit, false, true); + return aura ? aura->GetStackAmount() : 0; +} + +Unit* RsZarithrianTankAction::FindBoss() +{ + return RsFindTarget(botAI, [](Unit* unit) { return unit->GetEntry() == NPC_GENERAL_ZARITHRIAN; }); +} + +void RsZarithrianTankAction::CollectAdds(std::vector& adds) +{ + RsCollectTargets(botAI, adds, [](Unit* unit) { return unit->GetEntry() == NPC_ONYX_FLAMECALLER; }); +} + +bool RsZarithrianTankAction::HoldBoss(Unit* boss) +{ + bot->SetTarget(boss->GetGUID()); + + if (boss->GetVictim() != bot) + RsCastClassTaunt(botAI, bot, boss); + + bool const onSpot = bot->GetExactDist2d(RS_ZARITHRIAN_TANK_POSITION.GetPositionX(), + RS_ZARITHRIAN_TANK_POSITION.GetPositionY()) <= 3.0f; + + if (!bot->IsWithinMeleeRange(boss) && onSpot) + return MoveTo(bot->GetMapId(), boss->GetPositionX(), boss->GetPositionY(), boss->GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + if (!onSpot) + return MoveTo(bot->GetMapId(), RS_ZARITHRIAN_TANK_POSITION.GetPositionX(), + RS_ZARITHRIAN_TANK_POSITION.GetPositionY(), RS_ZARITHRIAN_TANK_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + return false; +} + +bool RsZarithrianTankAction::RunAddsToBoss(Unit* boss, std::vector const& adds) +{ + for (Unit* add : adds) + { + if (!add || !add->IsAlive()) + continue; + + if (add->GetVictim() != bot) + RsCastClassTaunt(botAI, bot, add); + } + + Unit* nearest = nullptr; + float best = std::numeric_limits::max(); + for (Unit* add : adds) + { + if (!add || !add->IsAlive()) + continue; + float const d = bot->GetExactDist2d(add); + if (d < best) + { + best = d; + nearest = add; + } + } + Unit* meleeTarget = nearest ? nearest : boss; + bot->SetTarget(meleeTarget->GetGUID()); + + bool const onSpot = bot->GetExactDist2d(RS_ZARITHRIAN_TANK_POSITION.GetPositionX(), + RS_ZARITHRIAN_TANK_POSITION.GetPositionY()) <= 3.0f; + + if (!bot->IsWithinMeleeRange(meleeTarget) && onSpot) + return MoveTo(bot->GetMapId(), meleeTarget->GetPositionX(), meleeTarget->GetPositionY(), + meleeTarget->GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT); + + if (!onSpot) + return MoveTo(bot->GetMapId(), RS_ZARITHRIAN_TANK_POSITION.GetPositionX(), + RS_ZARITHRIAN_TANK_POSITION.GetPositionY(), RS_ZARITHRIAN_TANK_POSITION.GetPositionZ(), + false, false, false, true, MovementPriority::MOVEMENT_COMBAT); + + return false; +} + +bool RsZarithrianTankAction::Execute(Event ) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* boss = FindBoss(); + if (!boss) + return false; + + if (RsReleaseIfFollowing(bot)) + return false; + + Player* mainTankPlayer = nullptr; + Player* assistTankPlayer = nullptr; + if (Group* group = bot->GetGroup()) + { + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (!mainTankPlayer && botAI->IsMainTank(member)) + mainTankPlayer = member; + + if (!assistTankPlayer && botAI->IsAssistTank(member)) + assistTankPlayer = member; + + if (mainTankPlayer && assistTankPlayer) + break; + } + } + + std::vector adds; + CollectAdds(adds); + + if (!mainTankPlayer || !assistTankPlayer || mainTankPlayer == assistTankPlayer) + { + for (Unit* add : adds) + { + if (add && add->IsAlive() && add->GetVictim() != bot) + RsCastClassTaunt(botAI, bot, add); + } + return HoldBoss(boss); + } + + if (GetCleaveStacks(bot) >= RS_ZARITHRIAN_CLEAVE_SWAP_STACKS) + { + + if (boss->GetVictim() == bot) + bot->AttackStop(); + + return RunAddsToBoss(boss, adds); + } + + Player* bossVictimTank = nullptr; + if (Unit* victim = boss->GetVictim()) + { + if (victim == mainTankPlayer || victim == assistTankPlayer) + bossVictimTank = victim->ToPlayer(); + } + + if (bossVictimTank == bot) + return HoldBoss(boss); + + bool const holderShredded = + bossVictimTank && GetCleaveStacks(bossVictimTank) >= RS_ZARITHRIAN_CLEAVE_SWAP_STACKS; + if (!bossVictimTank || holderShredded) + { + Player* takeOverTank = mainTankPlayer; + if (holderShredded) + takeOverTank = (bossVictimTank == mainTankPlayer) ? assistTankPlayer : mainTankPlayer; + + if (bot == takeOverTank) + return HoldBoss(boss); + } + + return RunAddsToBoss(boss, adds); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSActionContext.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSActionContext.h new file mode 100644 index 000000000..1f27f534a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSActionContext.h @@ -0,0 +1,78 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSACTIONCONTEXT_H +#define PLAYERBOTS_RSACTIONCONTEXT_H + +#include "Action.h" +#include "NamedObjectContext.h" +#include "RSActions.h" + +class RaidRsActionContext : public NamedObjectContext +{ +public: + RaidRsActionContext() + { + creators["rs baltharus brand"] = &RaidRsActionContext::rs_baltharus_brand; + creators["rs baltharus tank position"] = &RaidRsActionContext::rs_baltharus_tank_position; + creators["rs baltharus avoid front"] = &RaidRsActionContext::rs_baltharus_avoid_front; + creators["rs baltharus healer position"] = &RaidRsActionContext::rs_baltharus_healer_position; + creators["rs saviana conflagration"] = &RaidRsActionContext::rs_saviana_conflagration; + creators["rs saviana avoid front"] = &RaidRsActionContext::rs_saviana_avoid_front; + creators["rs saviana tank position"] = &RaidRsActionContext::rs_saviana_tank_position; + creators["rs saviana melee spread"] = &RaidRsActionContext::rs_saviana_melee_spread; + creators["rs zarithrian adds"] = &RaidRsActionContext::rs_zarithrian_adds; + creators["rs zarithrian tank"] = &RaidRsActionContext::rs_zarithrian_tank; + creators["rs halion tank position"] = &RaidRsActionContext::rs_halion_tank_position; + creators["rs halion avoid cones"] = &RaidRsActionContext::rs_halion_avoid_cones; + creators["rs halion combustion"] = &RaidRsActionContext::rs_halion_combustion; + creators["rs halion meteor"] = &RaidRsActionContext::rs_halion_meteor; + creators["rs halion adds"] = &RaidRsActionContext::rs_halion_adds; + creators["rs halion add tank"] = &RaidRsActionContext::rs_halion_add_tank; + creators["rs halion start position"] = &RaidRsActionContext::rs_halion_start_position; + creators["rs halion enter portal"] = &RaidRsActionContext::rs_halion_enter_portal; + creators["rs halion p2 tank position"] = &RaidRsActionContext::rs_halion_p2_tank_position; + creators["rs halion p2 avoid cones"] = &RaidRsActionContext::rs_halion_p2_avoid_cones; + creators["rs halion consumption"] = &RaidRsActionContext::rs_halion_consumption; + creators["rs halion cutter"] = &RaidRsActionContext::rs_halion_cutter; + creators["rs halion heal consumption"] = &RaidRsActionContext::rs_halion_heal_consumption; + creators["rs trash adds"] = &RaidRsActionContext::rs_trash_adds; + creators["rs trash main tank"] = &RaidRsActionContext::rs_trash_main_tank; + creators["rs trash assist tank"] = &RaidRsActionContext::rs_trash_assist_tank; + creators["rs trash ranged"] = &RaidRsActionContext::rs_trash_ranged; + } + +private: + static Action* rs_baltharus_brand(PlayerbotAI* ai) { return new RsBaltharusBrandAction(ai); } + static Action* rs_baltharus_tank_position(PlayerbotAI* ai) { return new RsBaltharusTankPositionAction(ai); } + static Action* rs_baltharus_avoid_front(PlayerbotAI* ai) { return new RsBaltharusAvoidFrontAction(ai); } + static Action* rs_baltharus_healer_position(PlayerbotAI* ai) { return new RsBaltharusHealerPositionAction(ai); } + static Action* rs_saviana_conflagration(PlayerbotAI* ai) { return new RsSavianaConflagrationAction(ai); } + static Action* rs_saviana_avoid_front(PlayerbotAI* ai) { return new RsSavianaAvoidFrontAction(ai); } + static Action* rs_saviana_tank_position(PlayerbotAI* ai) { return new RsSavianaTankPositionAction(ai); } + static Action* rs_saviana_melee_spread(PlayerbotAI* ai) { return new RsSavianaMeleeSpreadAction(ai); } + static Action* rs_zarithrian_adds(PlayerbotAI* ai) { return new RsZarithrianAddsAction(ai); } + static Action* rs_zarithrian_tank(PlayerbotAI* ai) { return new RsZarithrianTankAction(ai); } + static Action* rs_halion_tank_position(PlayerbotAI* ai) { return new RsHalionTankPositionAction(ai); } + static Action* rs_halion_avoid_cones(PlayerbotAI* ai) { return new RsHalionAvoidConesAction(ai); } + static Action* rs_halion_combustion(PlayerbotAI* ai) { return new RsHalionCombustionAction(ai); } + static Action* rs_halion_meteor(PlayerbotAI* ai) { return new RsHalionMeteorAction(ai); } + static Action* rs_halion_adds(PlayerbotAI* ai) { return new RsHalionAddsAction(ai); } + static Action* rs_halion_add_tank(PlayerbotAI* ai) { return new RsHalionAddTankAction(ai); } + static Action* rs_halion_start_position(PlayerbotAI* ai) { return new RsHalionStartPositionAction(ai); } + static Action* rs_halion_enter_portal(PlayerbotAI* ai) { return new RsHalionEnterPortalAction(ai); } + static Action* rs_halion_p2_tank_position(PlayerbotAI* ai) { return new RsHalionP2TankPositionAction(ai); } + static Action* rs_halion_p2_avoid_cones(PlayerbotAI* ai) { return new RsHalionP2AvoidConesAction(ai); } + static Action* rs_halion_consumption(PlayerbotAI* ai) { return new RsHalionConsumptionAction(ai); } + static Action* rs_halion_cutter(PlayerbotAI* ai) { return new RsHalionCutterAction(ai); } + static Action* rs_halion_heal_consumption(PlayerbotAI* ai) { return new RsHalionHealConsumptionAction(ai); } + static Action* rs_trash_adds(PlayerbotAI* ai) { return new RsTrashAddsAction(ai); } + static Action* rs_trash_main_tank(PlayerbotAI* ai) { return new RsTrashMainTankAction(ai); } + static Action* rs_trash_assist_tank(PlayerbotAI* ai) { return new RsTrashAssistTankAction(ai); } + static Action* rs_trash_ranged(PlayerbotAI* ai) { return new RsTrashRangedAction(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.cpp new file mode 100644 index 000000000..d40269b5e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.cpp @@ -0,0 +1,378 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "RSMultipliers.h" + +#include "FollowActions.h" +#include "ChooseTargetActions.h" +#include "MovementActions.h" +#include "NearestNpcsValue.h" +#include "Playerbots.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "DruidCatActions.h" +#include "GenericActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "WarlockActions.h" +#include "PaladinActions.h" +#include "PriestActions.h" +#include "ICCActions.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "UseMeetingStoneAction.h" +#include "WarriorActions.h" +#include "RSActions.h" +#include "RSTriggers.h" +#include "RSScripts.h" +#include "Timer.h" + +namespace +{ + bool RsIsAoeDamageAction(Action* action) + { + return dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action); + } +} + +float RsSavianaBeaconMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_FLAME_BEACON)) + return 1.0f; + + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsBaltharusBrandSafeMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) || !bot->HasAura(SPELL_ENERVATING_BRAND)) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + if (RsIsAoeDamageAction(action)) + return 0.0f; + + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (member && member != bot && member->IsAlive() && + member->GetExactDist2d(bot) < RS_BALTHARUS_BRAND_RANGE) + return 1.0f; + } + + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsSavianaMeleeSpreadMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) || (!botAI->IsMelee(bot) && !botAI->IsHeal(bot))) + return 1.0f; + + if (bot->HasAura(SPELL_FLAME_BEACON)) + return 1.0f; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + if (!boss || !boss->IsLevitating()) + return 1.0f; + + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsZarithrianAddsMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot)) + return 1.0f; + + Unit* boss = AI_VALUE2(Unit*, "find target", "general zarithrian"); + if (!boss) + return 1.0f; + + if (!RsFindTarget(botAI, [](Unit* unit) { return unit->GetEntry() == NPC_ONYX_FLAMECALLER; })) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsZarithrianTankSwapMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* boss = AI_VALUE2(Unit*, "find target", "general zarithrian"); + if (!boss) + return 1.0f; + + Aura* aura = botAI->GetAura("cleave armor", bot, false, true); + if (!aura || aura->GetStackAmount() < RS_ZARITHRIAN_CLEAVE_SWAP_STACKS) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 1.0f; + + return 0.0f; +} + +float RsHalionCombustionMultiplier::GetValue(Action* action) +{ + + if (RsHalionEngaged(botAI) && RsHalionCombustionNotClear(bot)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + if (botAI->IsTank(bot) && !RsHalionAssistTankAsMelee(botAI)) + return 1.0f; + + if (!RsHalionHasCombustion(bot)) + return 1.0f; + + if (RsIsAoeDamageAction(action)) + return 0.0f; + + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsHalionMeteorMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) && !RsHalionAssistTankAsMelee(botAI)) + return 1.0f; + + if (RsHalionInTwilight(bot)) + return 1.0f; + + Unit* physBoss = RsHalionAnyPhysicalBoss(botAI); + if (!physBoss || !physBoss->IsInCombat()) + return 1.0f; + + if (RsHalionHasCombustion(bot) || RsHalionIsCombustionDispeller(botAI) || RsHalionCombustionReturning(bot)) + return 1.0f; + + if (!RsHalionMeteorShouldRally(bot)) + return 1.0f; + + if (dynamic_cast(action) && !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsHalionMeleeFlankMultiplier::GetValue(Action* action) +{ + if (!RsHalionEngaged(botAI)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) && + (RsHalionEnteringTwilight(botAI, bot) || RsHalionPortalHeldForAdds(botAI))) + return 1.0f; + + if (botAI->IsTank(bot) && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + if (RsHalionHasCombustion(bot) || RsHalionIsCombustionDispeller(botAI) || RsHalionCombustionReturning(bot)) + return 1.0f; + + Player* const bossTank = RsHalionBossTank(botAI); + + if (Unit* boss = RsHalionPhase1Boss(botAI)) + if (boss->HealthAbovePct(99) && bossTank != bot && dynamic_cast(action)) + return 0.0f; + + Unit* const physBoss = RsHalionAnyPhysicalBoss(botAI); + + if (physBoss && bossTank == bot && dynamic_cast(action)) + return 0.0f; + + if (physBoss) + if ((!botAI->IsTank(bot) || RsHalionAssistTankAsMelee(botAI)) && botAI->IsMelee(bot) && + bot->GetExactDist2d(physBoss->GetPositionX(), physBoss->GetPositionY()) <= RS_HALION_LINE_MELEE_MAX && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsHalionHpBalanceMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot)) + return 1.0f; + + if (!RsHalionRealmThrottled(botAI, bot)) + return 1.0f; + + if (!RsHalionInTwilight(bot) && RsHalionAnyAddAlive(botAI)) + return 1.0f; + + if (!RsHalionLeadingTooMuch(botAI, bot) && !RsHalionInThrottledHalf(bot)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action)) + return 0.0f; + + CastSpellAction* cast = dynamic_cast(action); + if (cast && cast->GetTargetName() == "current target") + return 0.0f; + + return 1.0f; +} + +float RsHalionRealmIsolationMultiplier::GetValue(Action* action) +{ + if (!RsHalionEngaged(botAI)) + return 1.0f; + + Unit* target = action->GetTarget(); + if (!target || target == bot) + return 1.0f; + + Player* member = target->ToPlayer(); + if (!member || !member->IsAlive()) + return 1.0f; + + if (RsHalionInTwilight(member) != RsHalionInTwilight(bot)) + return 0.0f; + + return 1.0f; +} + +float RsTrashAddsMultiplier::GetValue(Action* action) +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot)) + return 1.0f; + + if (!RsTrashActive(botAI, bot)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float RsHalionP2Multiplier::GetValue(Action* action) +{ + if (!RsHalionInTwilight(bot)) + return 1.0f; + + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action) + || dynamic_cast(action) || dynamic_cast(action) + || dynamic_cast(action)) + return 0.0f; + + if (!RsHalionTwilightBoss(botAI)) + { + if (bot->HasAura(SPELL_MARK_OF_CONSUMPTION) || bot->HasAura(SPELL_SOUL_CONSUMPTION)) + return 1.0f; + + if (!RsHalionEnteringTwilight(botAI, bot) && + !dynamic_cast(action) && + !dynamic_cast(action) && + (dynamic_cast(action) || dynamic_cast(action))) + return 0.0f; + + return 1.0f; + } + + if (RsHalionConsumptionNotClear(botAI)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + } + + if (bot->HasAura(SPELL_MARK_OF_CONSUMPTION) || bot->HasAura(SPELL_SOUL_CONSUMPTION)) + { + if (dynamic_cast(action) && !dynamic_cast(action)) + return 0.0f; + return 1.0f; + } + + Player* const twilightTank = RsHalionTwilightTank(botAI); + + if (twilightTank != bot && RsHalionCutterShouldMove(bot->GetInstanceId())) + { + if ((bot->getClass() == CLASS_ROGUE || bot->getClass() == CLASS_WARRIOR) && + !dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (RsHalionCutterBeamDanger(botAI, bot) && + !dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + } + + if (twilightTank != bot && dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.h new file mode 100644 index 000000000..4001c9807 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSMultipliers.h @@ -0,0 +1,96 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSM_H +#define PLAYERBOTS_RSM_H + +#include "Multiplier.h" + +class RsSavianaBeaconMultiplier : public Multiplier +{ +public: + RsSavianaBeaconMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs saviana beacon") {} + virtual float GetValue(Action* action); +}; + +class RsBaltharusBrandSafeMultiplier : public Multiplier +{ +public: + RsBaltharusBrandSafeMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs baltharus brand safe") {} + virtual float GetValue(Action* action); +}; + +class RsSavianaMeleeSpreadMultiplier : public Multiplier +{ +public: + RsSavianaMeleeSpreadMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs saviana melee spread") {} + virtual float GetValue(Action* action); +}; + +class RsZarithrianAddsMultiplier : public Multiplier +{ +public: + RsZarithrianAddsMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs zarithrian adds") {} + virtual float GetValue(Action* action); +}; + +class RsZarithrianTankSwapMultiplier : public Multiplier +{ +public: + RsZarithrianTankSwapMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs zarithrian tank swap") {} + virtual float GetValue(Action* action); +}; + +class RsHalionCombustionMultiplier : public Multiplier +{ +public: + RsHalionCombustionMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs halion combustion safe") {} + virtual float GetValue(Action* action); +}; + +class RsHalionMeteorMultiplier : public Multiplier +{ +public: + RsHalionMeteorMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs halion meteor") {} + virtual float GetValue(Action* action); +}; + +class RsHalionMeleeFlankMultiplier : public Multiplier +{ +public: + RsHalionMeleeFlankMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs halion melee flank") {} + virtual float GetValue(Action* action); +}; + +class RsHalionP2Multiplier : public Multiplier +{ +public: + RsHalionP2Multiplier(PlayerbotAI* ai) : Multiplier(ai, "rs halion p2") {} + virtual float GetValue(Action* action); +}; + +class RsHalionHpBalanceMultiplier : public Multiplier +{ +public: + RsHalionHpBalanceMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs halion hp balance") {} + virtual float GetValue(Action* action); +}; + +class RsHalionRealmIsolationMultiplier : public Multiplier +{ +public: + RsHalionRealmIsolationMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs halion realm isolation") {} + virtual float GetValue(Action* action); +}; + +class RsTrashAddsMultiplier : public Multiplier +{ +public: + RsTrashAddsMultiplier(PlayerbotAI* ai) : Multiplier(ai, "rs trash adds") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.cpp new file mode 100644 index 000000000..88aaeb380 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.cpp @@ -0,0 +1,541 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "RSScripts.h" +#include "Player.h" +#include "RSActions.h" +#include "RSTriggers.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "SpellInfo.h" +#include "Timer.h" +#include "Vehicle.h" +#include +#include +#include +#include + +namespace RubySanctumHelpers +{ + static std::mutex stateMutex; + static std::unordered_map instanceStates; + + RsInstanceState& RsState(uint32 instanceId) + { + std::lock_guard lock(stateMutex); + return instanceStates[instanceId]; + } + + void ResetInstance(uint32 instanceId) + { + std::lock_guard lock(stateMutex); + instanceStates.erase(instanceId); + } +} + +static uint8 RsHalionReadCorporealityIndex(Creature* creature) +{ + using namespace RubySanctumHelpers; + for (uint8 i = 0; i <= 10; ++i) + { + if (creature->HasAura(HALION_CORPOREALITY_AURAS[i])) + return i; + } + return 5; +} + +class RsHalionRootScript : public AllCreatureScript +{ +public: + RsHalionRootScript() : AllCreatureScript("RsHalionRootScript") { } + + void OnAllCreatureUpdate(Creature* creature, uint32 ) override + { + if (!creature || creature->GetMapId() != RS_MAP_RUBY_SANCTUM) + return; + + if (creature->GetEntry() == NPC_METEOR_STRIKE_MARK) + { + HandleMeteorMark(creature); + return; + } + + if (creature->GetEntry() == NPC_HALION) + HandleHalionRoot(creature); + + if (creature->GetEntry() == NPC_TWILIGHT_HALION) + HandleTwilightHalionRoot(creature); + + if (creature->GetEntry() == NPC_ORB_CARRIER) + HandleCutterTiming(creature); + + if ((creature->GetEntry() == NPC_LIVING_INFERNO || creature->GetEntry() == NPC_LIVING_EMBER) && + creature->IsAlive()) + RubySanctumHelpers::RsState(creature->GetMap()->GetInstanceId()) + .portalAddGate.lastAddAliveTime = getMSTime(); + } + +private: + void HandleCutterTiming(Creature* carrier) + { + uint32 const instanceId = carrier->GetMap()->GetInstanceId(); + RubySanctumHelpers::CutterTiming& state = RubySanctumHelpers::RsState(instanceId).cutterTiming; + + bool firing = false; + Unit* tracked = nullptr; + if (Vehicle* vehicle = carrier->GetVehicleKit()) + { + tracked = vehicle->GetPassenger(0); + for (int8 seat = 0; seat < 4; ++seat) + { + if (Unit* orb = vehicle->GetPassenger(seat); orb && orb->HasAura(SPELL_TWILIGHT_PULSE_PERIODIC)) + { + firing = true; + break; + } + } + } + + if (firing && !state.active) + state.lastShootTime = getMSTime(); + + state.active = firing; + + if (tracked) + { + float const angle = std::atan2(tracked->GetPositionY() - carrier->GetPositionY(), + tracked->GetPositionX() - carrier->GetPositionX()); + if (state.hasOrbAngle) + { + float const delta = RsAngleDiff(angle, state.lastOrbAngle); + if (std::fabs(delta) > 0.0005f) + state.spinSign = delta > 0.0f ? 1.0f : -1.0f; + } + state.lastOrbAngle = angle; + state.hasOrbAngle = true; + } + else + { + state.hasOrbAngle = false; + } + } + + void HandleTankAuras(Creature* creature) + { + uint32 const instanceId = creature->GetMap()->GetInstanceId(); + uint32 const now = getMSTime(); + + uint32& last = RubySanctumHelpers::RsState(instanceId).tankAuraLastApply; + if (last != 0 && getMSTimeDiff(last, now) < 2000) + return; + last = now; + + Map::PlayerList const& players = creature->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || !GET_PLAYERBOT_AI(player) || !PlayerbotAI::IsTank(player)) + continue; + if (!player->HasAura(RS_SPELL_PAIN_SUPPRESION)) + player->AddAura(RS_SPELL_PAIN_SUPPRESION, player); + } + } + + void HandleAddBuff(Creature* creature) + { + uint32 const instanceId = creature->GetMap()->GetInstanceId(); + uint32 const now = getMSTime(); + + uint32& last = RubySanctumHelpers::RsState(instanceId).addBuffLastApply; + if (last != 0 && getMSTimeDiff(last, now) < 2000) + return; + last = now; + + bool addsAlive = false; + Map::PlayerList const& players = creature->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player) + continue; + if (Creature* add = player->FindNearestCreature(NPC_LIVING_INFERNO, 200.0f); add && add->IsAlive()) + { + addsAlive = true; + break; + } + if (Creature* add = player->FindNearestCreature(NPC_LIVING_EMBER, 200.0f); add && add->IsAlive()) + { + addsAlive = true; + break; + } + } + + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || !GET_PLAYERBOT_AI(player)) + continue; + bool const wantEmpowered = addsAlive && !player->HasAura(SPELL_TWILIGHT_REALM); + if (wantEmpowered && !player->HasAura(RS_SPELL_EMPOWERED_BLOOD)) + player->AddAura(RS_SPELL_EMPOWERED_BLOOD, player); + else if (!wantEmpowered && player->HasAura(RS_SPELL_EMPOWERED_BLOOD)) + player->RemoveAura(RS_SPELL_EMPOWERED_BLOOD); + } + } + + void HandleP3TankRescue(Creature* creature) + { + uint32 const now = getMSTime(); + uint32 const rescueMs = 5000; + float const bossHp = creature->GetHealthPct(); + + auto& grantStart = RubySanctumHelpers::RsState(creature->GetMap()->GetInstanceId()).p3RescueGrant; + + Map::PlayerList const& players = creature->GetMap()->GetPlayers(); + + bool physicalHealerAlive = false; + bool twilightHealerAlive = false; + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || !PlayerbotAI::IsHeal(player)) + continue; + if (RsHalionInTwilight(player)) + twilightHealerAlive = true; + else + physicalHealerAlive = true; + } + + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || !GET_PLAYERBOT_AI(player) || !PlayerbotAI::IsTank(player)) + continue; + + bool const tankTwilight = RsHalionInTwilight(player); + bool const healerInRealm = tankTwilight ? twilightHealerAlive : physicalHealerAlive; + + ObjectGuid const guid = player->GetGUID(); + + if (!tankTwilight && bossHp >= 46.0f) + { + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + grantStart.erase(guid); + continue; + } + + if (bossHp > 50.0f) + continue; + + auto granted = grantStart.find(guid); + + if (granted != grantStart.end()) + { + if (getMSTimeDiff(granted->second, now) >= rescueMs) + { + player->RemoveAura(RS_SPELL_MAGIC_BARRIER); + grantStart.erase(granted); + } + else + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + continue; + } + + if (!player->HealthAbovePct(50) && healerInRealm) + { + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + grantStart[guid] = now; + } + } + } + + void HandleBreathGodMode(Creature* boss, uint32 const* breathIds, size_t breathCount, bool twilight) + { + uint32 const now = getMSTime(); + uint32 const holdMs = 1000; + + bool casting = false; + for (size_t i = 0; i < breathCount; ++i) + { + if (boss->FindCurrentSpellBySpellId(breathIds[i])) + { + casting = true; + break; + } + } + + RubySanctumHelpers::RsInstanceState& state = RubySanctumHelpers::RsState(boss->GetMap()->GetInstanceId()); + std::unordered_map& grantStart = + twilight ? state.breathTwilightGrant : state.breathPhysicalGrant; + + Map::PlayerList const& players = boss->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || !GET_PLAYERBOT_AI(player) || PlayerbotAI::IsTank(player) || RsHalionInTwilight(player) != twilight) + continue; + + ObjectGuid const guid = player->GetGUID(); + if (casting) + { + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + grantStart[guid] = now; + continue; + } + + auto granted = grantStart.find(guid); + if (granted == grantStart.end()) + continue; + + if (getMSTimeDiff(granted->second, now) >= holdMs) + { + player->RemoveAura(RS_SPELL_MAGIC_BARRIER); + grantStart.erase(granted); + } + else + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + } + } + + void HandleCombustionDispel(Creature* physical) + { + Map::PlayerList const& players = physical->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || GET_PLAYERBOT_AI(player)) + continue; + if (!RsHalionHasCombustion(player) || !RsHalionCombustionPlayerSafe(player)) + continue; + + player->RemoveAurasDueToSpell(SPELL_FIERY_COMBUSTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + player->RemoveAurasDueToSpell(SPELL_MARK_OF_COMBUSTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + } + } + + void HandleConsumptionGodMode(Creature* twilight) + { + if (!twilight->GetMap()->IsHeroic()) + return; + + uint32 const now = getMSTime(); + uint32 const holdMs = 2000; + + auto& grantStart = RubySanctumHelpers::RsState(twilight->GetMap()->GetInstanceId()).consumptionGrant; + + Map::PlayerList const& players = twilight->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || !GET_PLAYERBOT_AI(player)) + continue; + + ObjectGuid const guid = player->GetGUID(); + if (player->HasAura(SPELL_SOUL_CONSUMPTION)) + { + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + grantStart[guid] = now; + continue; + } + + auto granted = grantStart.find(guid); + if (granted == grantStart.end()) + continue; + + if (getMSTimeDiff(granted->second, now) >= holdMs) + { + player->RemoveAura(RS_SPELL_MAGIC_BARRIER); + grantStart.erase(granted); + } + else + player->AddAura(RS_SPELL_MAGIC_BARRIER, player); + } + } + + void HandleConsumptionDispel(Creature* twilight) + { + float const bossX = twilight->GetPositionX(); + float const bossY = twilight->GetPositionY(); + + Map::PlayerList const& players = twilight->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (!player || !player->IsAlive() || GET_PLAYERBOT_AI(player)) + continue; + if (!RsHalionHasConsumption(player)) + continue; + + float const distToBoss = player->GetExactDist2d(bossX, bossY); + if (distToBoss < RS_HALION_CONSUMPTION_OUT_DIST - RS_HALION_CONSUMPTION_OUT_REACH) + continue; + + player->RemoveAurasDueToSpell(SPELL_SOUL_CONSUMPTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + player->RemoveAurasDueToSpell(SPELL_MARK_OF_CONSUMPTION, ObjectGuid::Empty, 0, AURA_REMOVE_BY_EXPIRE); + } + } + + void HandleTwilightHalionRoot(Creature* creature) + { + RubySanctumHelpers::RsInstanceState& instState = + RubySanctumHelpers::RsState(creature->GetMap()->GetInstanceId()); + + if (creature->IsAlive() && creature->IsInCombat()) + { + RubySanctumHelpers::HalionCorporeality& corp = instState.halionCorporeality; + corp.twilightIndex = RsHalionReadCorporealityIndex(creature); + corp.twilightStamp = getMSTime(); + instState.bossHealth = { uint8(creature->GetHealthPct()), getMSTime() }; + } + + RubySanctumHelpers::CutterTiming& state = instState.cutterTiming; + if (state.bossGuid != creature->GetGUID()) + { + state = RubySanctumHelpers::CutterTiming{}; + state.bossGuid = creature->GetGUID(); + } + if (state.encounterStart == 0 && creature->IsInCombat()) + state.encounterStart = getMSTime(); + + bool twilightTankAlive = false; + Map::PlayerList const& players = creature->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (player && player->IsAlive() && PlayerbotAI::IsTank(player) && RsHalionInTwilight(player)) + { + twilightTankAlive = true; + break; + } + } + + bool const shouldRoot = creature->IsAlive() && creature->IsInCombat() && twilightTankAlive; + bool const rooted = creature->HasUnitState(UNIT_STATE_ROOT); + + if (shouldRoot && !rooted) + creature->SetControlled(true, UNIT_STATE_ROOT); + else if (!shouldRoot && rooted) + creature->SetControlled(false, UNIT_STATE_ROOT); + + if (shouldRoot) + { + Position const& home = creature->GetHomePosition(); + if (creature->GetExactDist2d(home.GetPositionX(), home.GetPositionY()) > 3.0f) + creature->NearTeleportTo(home.GetPositionX(), home.GetPositionY(), + home.GetPositionZ(), home.GetOrientation()); + } + + if (creature->IsAlive() && creature->IsInCombat()) + { + HandleTankAuras(creature); + HandleAddBuff(creature); + static uint32 const darkBreathIds[] = { + SPELL_DARK_BREATH, SPELL_DARK_BREATH_25N, SPELL_DARK_BREATH_10H, SPELL_DARK_BREATH_25H + }; + HandleBreathGodMode(creature, darkBreathIds, sizeof(darkBreathIds) / sizeof(darkBreathIds[0]), true); + HandleConsumptionDispel(creature); + HandleConsumptionGodMode(creature); + } + } + void HandleMeteorMark(Creature* mark) + { + if (!mark->IsAlive()) + return; + + RubySanctumHelpers::RsInstanceState& instState = RubySanctumHelpers::RsState(mark->GetMap()->GetInstanceId()); + + if (!instState.seenMeteorMarks.insert(mark->GetGUID()).second) + return; + + RubySanctumHelpers::MeteorPingPong& state = instState.meteorPingPong; + state.lastCastTime = getMSTime(); + + bool const tankMeteor = mark->GetExactDist2d(RS_HALION_TANK_POSITION.GetPositionX(), RS_HALION_TANK_POSITION.GetPositionY()) <= 10.0f; + bool const tankEscapeMeteor = mark->GetExactDist2d(RS_HALION_TANK_METEOR_SPOT.GetPositionX(), RS_HALION_TANK_METEOR_SPOT.GetPositionY()) <= 10.0f; + if (tankMeteor) + state.tankMeteorTime = getMSTime(); + else if (tankEscapeMeteor) + state.tankReturnTime = getMSTime(); + else + ++state.count; + } + + void HandleHalionRoot(Creature* creature) + { + uint32 const instanceId = creature->GetMap()->GetInstanceId(); + + bool const fightActive = creature->IsAlive() && creature->IsInCombat(); + + if (!fightActive) + { + if (creature->HasUnitState(UNIT_STATE_ROOT)) + creature->SetControlled(false, UNIT_STATE_ROOT); + RubySanctumHelpers::ResetInstance(instanceId); + return; + } + + RubySanctumHelpers::RsInstanceState& instState = RubySanctumHelpers::RsState(instanceId); + + RubySanctumHelpers::HalionCorporeality& corp = instState.halionCorporeality; + corp.physicalIndex = RsHalionReadCorporealityIndex(creature); + corp.physicalStamp = getMSTime(); + corp.physicalGuid = creature->GetGUID(); + instState.bossHealth = { uint8(creature->GetHealthPct()), getMSTime() }; + + HandleTankAuras(creature); + HandleAddBuff(creature); + + static uint32 const flameBreathIds[] = { + SPELL_FLAME_BREATH, SPELL_FLAME_BREATH_ALT1, SPELL_FLAME_BREATH_ALT2, SPELL_FLAME_BREATH_ALT3, + SPELL_FLAME_BREATH_ALT4, SPELL_FLAME_BREATH_ALT5, SPELL_FLAME_BREATH_ALT6 + }; + HandleBreathGodMode(creature, flameBreathIds, sizeof(flameBreathIds) / sizeof(flameBreathIds[0]), false); + HandleCombustionDispel(creature); + + bool const physicalActive = creature->GetDisplayId() == creature->GetNativeDisplayId(); + + if (!physicalActive) + { + if (creature->HasUnitState(UNIT_STATE_ROOT)) + creature->SetControlled(false, UNIT_STATE_ROOT); + return; + } + + if (creature->GetMap()->IsHeroic() && !creature->HealthAbovePct(51)) + HandleP3TankRescue(creature); + + uint32 const now = getMSTime(); + uint32& last = instState.halionRootLastScan; + if (last != 0 && getMSTimeDiff(last, now) < 1000) + return; + last = now; + + bool mainTankAlive = false; + Map::PlayerList const& players = creature->GetMap()->GetPlayers(); + for (Map::PlayerList::const_iterator it = players.begin(); it != players.end(); ++it) + { + Player* player = it->GetSource(); + if (player && player->IsAlive() && PlayerbotAI::IsMainTank(player)) + { + mainTankAlive = true; + break; + } + } + + bool const shouldRoot = mainTankAlive; + bool const rooted = creature->HasUnitState(UNIT_STATE_ROOT); + + if (shouldRoot && !rooted) + creature->SetControlled(true, UNIT_STATE_ROOT); + else if (!shouldRoot && rooted) + creature->SetControlled(false, UNIT_STATE_ROOT); + } +}; + +void AddSC_RubySanctumBotScripts() +{ + new RsHalionRootScript(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.h new file mode 100644 index 000000000..772e4dda7 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSScripts.h @@ -0,0 +1,113 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSSCRIPTS_H +#define PLAYERBOTS_RSSCRIPTS_H + +#include +#include +#include +#include +#include "ObjectGuid.h" +#include "Position.h" + +namespace RubySanctumHelpers +{ + + struct MeteorPingPong + { + uint32 count = 0; + uint32 lastCastTime = 0; + uint32 tankMeteorTime = 0; + uint32 tankReturnTime = 0; + }; + + struct CutterTiming + { + uint32 lastShootTime = 0; + bool active = false; + uint32 encounterStart = 0; + ObjectGuid bossGuid; + float spinSign = 0.0f; + float lastOrbAngle = 0.0f; + bool hasOrbAngle = false; + }; + + struct PortalAddGate + { + uint32 armTime = 0; + uint32 lastAddAliveTime = 0; + }; + + struct HalionCorporeality + { + uint8 physicalIndex = 0; + uint32 physicalStamp = 0; + uint8 twilightIndex = 0; + uint32 twilightStamp = 0; + ObjectGuid physicalGuid; + }; + + // boss health % stamped server-side (visible in every phase), read by bots that cannot see the boss through a portal + struct BossHealth + { + uint8 pct = 0; + uint32 stamp = 0; + }; + + struct TankCache + { + uint32 stamp = 0; + ObjectGuid guid; + }; + + struct RsInstanceState + { + MeteorPingPong meteorPingPong; + CutterTiming cutterTiming; + PortalAddGate portalAddGate; + HalionCorporeality halionCorporeality; + BossHealth bossHealth; + TankCache bossTankCache; + TankCache twilightTankCache; + uint32 tankAuraLastApply = 0; + uint32 addBuffLastApply = 0; + uint32 halionRootLastScan = 0; + uint32 portalCountdownLastShown = 0; + std::map p3TwilightAssignment; + std::map portalSeen; + std::unordered_map breathTwilightGrant; + std::unordered_map breathPhysicalGrant; + std::unordered_map p3RescueGrant; + std::unordered_map consumptionGrant; + std::set meteorCommitted; + std::set rallyCommitted; + std::set combustionReturning; + std::map> cutterDangerCache; + std::map realmThrottled; + std::map meteorSpotUsesA; + std::map botPortalTarget; + std::set clearedForConsumption; + std::set seenMeteorMarks; + }; + + // maps update on a worker-thread pool, so bots in different RS instances tick on + // different threads; only the outer instanceId lookup is locked - the returned state + // is only ever touched by that instance's own map thread + RsInstanceState& RsState(uint32 instanceId); + + // drop the whole state for an instance (encounter reset / kill / idle), freeing all of it + void ResetInstance(uint32 instanceId); + + inline constexpr uint32 HALION_CORPOREALITY_AURAS[11] = + { + 74836, 74835, 74834, 74833, 74832, 74826, 74827, 74828, 74829, 74830, 74831 + }; +} + +void AddSC_RubySanctumBotScripts(); + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSShared.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSShared.h new file mode 100644 index 000000000..80f8334fe --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSShared.h @@ -0,0 +1,109 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSSHARED_H +#define PLAYERBOTS_RSSHARED_H + +#include + +#include "Group.h" +#include "MotionMaster.h" +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "RtiTargetValue.h" +#include "ThreatManager.h" + +template +inline void RsCollectTargets(PlayerbotAI* botAI, std::vector& out, Pred pred) +{ + GuidVector const targets = botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && pred(unit)) + out.push_back(unit); + } +} + +template +inline Unit* RsFindTarget(PlayerbotAI* botAI, Pred pred) +{ + GuidVector const targets = botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + for (ObjectGuid const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && pred(unit)) + return unit; + } + return nullptr; +} + +inline void RsForceThreat(Unit* target, Player* bot) +{ + if (!target || target->GetVictim() == bot) + return; + + ThreatManager& mgr = target->GetThreatMgr(); + mgr.AddThreat(bot, 1000000.0f, nullptr, true, true); + mgr.FixateTarget(bot); +} + +inline bool RsReleaseIfFollowing(Player* bot) +{ + if (bot->GetMotionMaster()->GetCurrentMovementGeneratorType() != FOLLOW_MOTION_TYPE) + return false; + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + if (bot->GetTarget()) + bot->SetTarget(ObjectGuid::Empty); + return true; +} + +inline bool RsIsDesignatedMarker(Player* bot) +{ + Group* group = bot->GetGroup(); + if (!group) + return true; + + ObjectGuid lowest = bot->GetGUID(); + for (GroupReference* itr = group->GetFirstMember(); itr; itr = itr->next()) + { + Player* member = itr->GetSource(); + if (!member || !member->IsAlive()) + continue; + + PlayerbotAI* memberAI = sPlayerbotsMgr.GetPlayerbotAI(member); + if (!memberAI) + continue; + + if (memberAI->IsTank(member) || memberAI->IsHeal(member)) + continue; + + if (member->GetMapId() != bot->GetMapId() || member->GetInstanceId() != bot->GetInstanceId()) + continue; + + if (member->GetGUID() < lowest) + lowest = member->GetGUID(); + } + + return bot->GetGUID() == lowest; +} + +inline void RsUpdateSkullMarker(Player* bot, Unit* priorityAdd) +{ + if (!priorityAdd) + return; + + Group* group = bot->GetGroup(); + if (!group) + return; + + if (group->GetTargetIcon(RtiTargetValue::skullIndex) != priorityAdd->GetGUID()) + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), priorityAdd->GetGUID()); +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.cpp new file mode 100644 index 000000000..88516e3d0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.cpp @@ -0,0 +1,99 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "RSStrategy.h" + +#include "RSMultipliers.h" + +void RaidRsStrategy::InitTriggers(std::vector& triggers) +{ + + triggers.push_back(new TriggerNode("rs baltharus brand", + { NextAction("rs baltharus brand", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs baltharus tank position", + { NextAction("rs baltharus tank position", ACTION_RAID + 6) })); + triggers.push_back(new TriggerNode("rs baltharus avoid front", + { NextAction("rs baltharus avoid front", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("rs baltharus healer position", + { NextAction("rs baltharus healer position", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("rs saviana conflagration", + { NextAction("rs saviana conflagration", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs saviana avoid front", + { NextAction("rs saviana avoid front", ACTION_RAID + 6) })); + triggers.push_back(new TriggerNode("rs saviana tank position", + { NextAction("rs saviana tank position", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs saviana melee spread", + { NextAction("rs saviana melee spread", ACTION_RAID + 7) })); + + triggers.push_back(new TriggerNode("rs zarithrian adds", + { NextAction("rs zarithrian adds", ACTION_RAID + 5), + NextAction("attack rti target", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("rs zarithrian tank", + { NextAction("rs zarithrian tank", ACTION_RAID + 6) })); + + triggers.push_back(new TriggerNode("rs halion start position", + { NextAction("rs halion start position", ACTION_RAID + 6) })); + triggers.push_back(new TriggerNode("rs halion combustion", + { NextAction("rs halion combustion", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs halion meteor", + { NextAction("rs halion meteor", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs halion tank position", + { NextAction("rs halion tank position", ACTION_RAID + 6), + NextAction("attack rti target", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("rs halion avoid cones", + { NextAction("rs halion avoid cones", ACTION_RAID + 5) })); + + triggers.push_back(new TriggerNode("rs halion adds", + { NextAction("rs halion adds", ACTION_RAID + 6), + NextAction("attack rti target", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("rs halion add tank", + { NextAction("rs halion add tank", ACTION_RAID + 6) })); + + triggers.push_back(new TriggerNode("rs halion enter portal", + { NextAction("rs halion enter portal", ACTION_RAID + 8) })); + triggers.push_back(new TriggerNode("rs halion cutter", + { NextAction("rs halion cutter", ACTION_RAID + 8) })); + triggers.push_back(new TriggerNode("rs halion consumption", + { NextAction("rs halion consumption", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs halion heal consumption", + { NextAction("rs halion heal consumption", ACTION_RAID + 7) })); + triggers.push_back(new TriggerNode("rs halion p2 tank position", + { NextAction("rs halion p2 tank position", ACTION_RAID + 6), + NextAction("attack rti target", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("rs halion p2 avoid cones", + { NextAction("rs halion p2 avoid cones", ACTION_RAID + 5), + NextAction("attack rti target", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("rs trash main tank", + { NextAction("rs trash main tank", ACTION_RAID + 6) })); + triggers.push_back(new TriggerNode("rs trash assist tank", + { NextAction("rs trash assist tank", ACTION_RAID + 6) })); + triggers.push_back(new TriggerNode("rs trash ranged", + { NextAction("rs trash ranged", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("rs trash melee flank", + { NextAction("rear flank", ACTION_RAID + 5) })); + triggers.push_back(new TriggerNode("rs trash adds", + { NextAction("rs trash adds", ACTION_RAID + 5), + NextAction("attack rti target", ACTION_RAID + 4) })); +} + +void RaidRsStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new RsBaltharusBrandSafeMultiplier(botAI)); + multipliers.push_back(new RsSavianaBeaconMultiplier(botAI)); + multipliers.push_back(new RsSavianaMeleeSpreadMultiplier(botAI)); + multipliers.push_back(new RsZarithrianAddsMultiplier(botAI)); + multipliers.push_back(new RsZarithrianTankSwapMultiplier(botAI)); + multipliers.push_back(new RsHalionCombustionMultiplier(botAI)); + multipliers.push_back(new RsHalionMeteorMultiplier(botAI)); + multipliers.push_back(new RsHalionMeleeFlankMultiplier(botAI)); + multipliers.push_back(new RsHalionP2Multiplier(botAI)); + multipliers.push_back(new RsHalionHpBalanceMultiplier(botAI)); + multipliers.push_back(new RsHalionRealmIsolationMultiplier(botAI)); + multipliers.push_back(new RsTrashAddsMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.h new file mode 100644 index 000000000..6cbafd697 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSStrategy.h @@ -0,0 +1,21 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSS_H +#define PLAYERBOTS_RSS_H + +#include "Strategy.h" + +class RaidRsStrategy : public Strategy +{ +public: + RaidRsStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "rs"; } + virtual void InitTriggers(std::vector& triggers) override; + virtual void InitMultipliers(std::vector &multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggerContext.h new file mode 100644 index 000000000..416243893 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggerContext.h @@ -0,0 +1,79 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RSTRIGGERCONTEXT_H +#define PLAYERBOTS_RSTRIGGERCONTEXT_H + +#include "NamedObjectContext.h" +#include "RSTriggers.h" + +class RaidRsTriggerContext : public NamedObjectContext +{ +public: + RaidRsTriggerContext() + { + creators["rs baltharus brand"] = &RaidRsTriggerContext::rs_baltharus_brand; + creators["rs baltharus tank position"] = &RaidRsTriggerContext::rs_baltharus_tank_position; + creators["rs baltharus avoid front"] = &RaidRsTriggerContext::rs_baltharus_avoid_front; + creators["rs baltharus healer position"] = &RaidRsTriggerContext::rs_baltharus_healer_position; + creators["rs saviana conflagration"] = &RaidRsTriggerContext::rs_saviana_conflagration; + creators["rs saviana avoid front"] = &RaidRsTriggerContext::rs_saviana_avoid_front; + creators["rs saviana tank position"] = &RaidRsTriggerContext::rs_saviana_tank_position; + creators["rs saviana melee spread"] = &RaidRsTriggerContext::rs_saviana_melee_spread; + creators["rs zarithrian adds"] = &RaidRsTriggerContext::rs_zarithrian_adds; + creators["rs zarithrian tank"] = &RaidRsTriggerContext::rs_zarithrian_tank; + creators["rs halion tank position"] = &RaidRsTriggerContext::rs_halion_tank_position; + creators["rs halion avoid cones"] = &RaidRsTriggerContext::rs_halion_avoid_cones; + creators["rs halion combustion"] = &RaidRsTriggerContext::rs_halion_combustion; + creators["rs halion meteor"] = &RaidRsTriggerContext::rs_halion_meteor; + creators["rs halion adds"] = &RaidRsTriggerContext::rs_halion_adds; + creators["rs halion add tank"] = &RaidRsTriggerContext::rs_halion_add_tank; + creators["rs halion start position"] = &RaidRsTriggerContext::rs_halion_start_position; + creators["rs halion enter portal"] = &RaidRsTriggerContext::rs_halion_enter_portal; + creators["rs halion p2 tank position"] = &RaidRsTriggerContext::rs_halion_p2_tank_position; + creators["rs halion p2 avoid cones"] = &RaidRsTriggerContext::rs_halion_p2_avoid_cones; + creators["rs halion consumption"] = &RaidRsTriggerContext::rs_halion_consumption; + creators["rs halion cutter"] = &RaidRsTriggerContext::rs_halion_cutter; + creators["rs halion heal consumption"] = &RaidRsTriggerContext::rs_halion_heal_consumption; + creators["rs trash adds"] = &RaidRsTriggerContext::rs_trash_adds; + creators["rs trash main tank"] = &RaidRsTriggerContext::rs_trash_main_tank; + creators["rs trash assist tank"] = &RaidRsTriggerContext::rs_trash_assist_tank; + creators["rs trash ranged"] = &RaidRsTriggerContext::rs_trash_ranged; + creators["rs trash melee flank"] = &RaidRsTriggerContext::rs_trash_melee_flank; + } + +private: + static Trigger* rs_baltharus_brand(PlayerbotAI* ai) { return new RsBaltharusBrandTrigger(ai); } + static Trigger* rs_baltharus_tank_position(PlayerbotAI* ai) { return new RsBaltharusTankPositionTrigger(ai); } + static Trigger* rs_baltharus_avoid_front(PlayerbotAI* ai) { return new RsBaltharusAvoidFrontTrigger(ai); } + static Trigger* rs_baltharus_healer_position(PlayerbotAI* ai) { return new RsBaltharusHealerPositionTrigger(ai); } + static Trigger* rs_saviana_conflagration(PlayerbotAI* ai) { return new RsSavianaConflagrationTrigger(ai); } + static Trigger* rs_saviana_avoid_front(PlayerbotAI* ai) { return new RsSavianaAvoidFrontTrigger(ai); } + static Trigger* rs_saviana_tank_position(PlayerbotAI* ai) { return new RsSavianaTankPositionTrigger(ai); } + static Trigger* rs_saviana_melee_spread(PlayerbotAI* ai) { return new RsSavianaMeleeSpreadTrigger(ai); } + static Trigger* rs_zarithrian_adds(PlayerbotAI* ai) { return new RsZarithrianAddsTrigger(ai); } + static Trigger* rs_zarithrian_tank(PlayerbotAI* ai) { return new RsZarithrianTankTrigger(ai); } + static Trigger* rs_halion_tank_position(PlayerbotAI* ai) { return new RsHalionTankPositionTrigger(ai); } + static Trigger* rs_halion_avoid_cones(PlayerbotAI* ai) { return new RsHalionAvoidConesTrigger(ai); } + static Trigger* rs_halion_combustion(PlayerbotAI* ai) { return new RsHalionCombustionTrigger(ai); } + static Trigger* rs_halion_meteor(PlayerbotAI* ai) { return new RsHalionMeteorTrigger(ai); } + static Trigger* rs_halion_adds(PlayerbotAI* ai) { return new RsHalionAddsTrigger(ai); } + static Trigger* rs_halion_add_tank(PlayerbotAI* ai) { return new RsHalionAddTankTrigger(ai); } + static Trigger* rs_halion_start_position(PlayerbotAI* ai) { return new RsHalionStartPositionTrigger(ai); } + static Trigger* rs_halion_enter_portal(PlayerbotAI* ai) { return new RsHalionEnterPortalTrigger(ai); } + static Trigger* rs_halion_p2_tank_position(PlayerbotAI* ai) { return new RsHalionP2TankPositionTrigger(ai); } + static Trigger* rs_halion_p2_avoid_cones(PlayerbotAI* ai) { return new RsHalionP2AvoidConesTrigger(ai); } + static Trigger* rs_halion_consumption(PlayerbotAI* ai) { return new RsHalionConsumptionTrigger(ai); } + static Trigger* rs_halion_cutter(PlayerbotAI* ai) { return new RsHalionCutterTrigger(ai); } + static Trigger* rs_halion_heal_consumption(PlayerbotAI* ai) { return new RsHalionHealConsumptionTrigger(ai); } + static Trigger* rs_trash_adds(PlayerbotAI* ai) { return new RsTrashAddsTrigger(ai); } + static Trigger* rs_trash_main_tank(PlayerbotAI* ai) { return new RsTrashMainTankTrigger(ai); } + static Trigger* rs_trash_assist_tank(PlayerbotAI* ai) { return new RsTrashAssistTankTrigger(ai); } + static Trigger* rs_trash_ranged(PlayerbotAI* ai) { return new RsTrashRangedTrigger(ai); } + static Trigger* rs_trash_melee_flank(PlayerbotAI* ai) { return new RsTrashMeleeFlankTrigger(ai); } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.cpp new file mode 100644 index 000000000..3b3a37650 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.cpp @@ -0,0 +1,316 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "RSTriggers.h" +#include "RSActions.h" +#include "RSScripts.h" +#include "Playerbots.h" +#include "Trigger.h" +#include "Timer.h" +#include + +bool RsBaltharusBrandTrigger::IsActive() +{ + if (!bot->HasAura(SPELL_ENERVATING_BRAND)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "baltharus the warborn"); + return boss != nullptr; +} + +bool RsBaltharusTankPositionTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "baltharus the warborn"); + return boss != nullptr; +} + +bool RsBaltharusHealerPositionTrigger::IsActive() +{ + if (!botAI->IsHeal(bot) || botAI->IsTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "baltharus the warborn"); + return boss != nullptr; +} + +bool RsBaltharusAvoidFrontTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "baltharus the warborn"); + return boss != nullptr; +} + +bool RsSavianaConflagrationTrigger::IsActive() +{ + if (!bot->HasAura(SPELL_FLAME_BEACON)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + return boss != nullptr; +} + +bool RsSavianaTankPositionTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + return boss != nullptr; +} + +bool RsSavianaAvoidFrontTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + if (!boss) + return false; + + return !boss->IsLevitating(); +} + +bool RsSavianaMeleeSpreadTrigger::IsActive() +{ + if (botAI->IsTank(bot) || (!botAI->IsMelee(bot) && !botAI->IsHeal(bot))) + return false; + + if (bot->HasAura(SPELL_FLAME_BEACON)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "saviana ragefire"); + if (!boss) + return false; + + return boss->IsLevitating(); +} + +bool RsZarithrianAddsTrigger::IsActive() +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "general zarithrian"); + if (!boss || !boss->IsAlive()) + return false; + + return true; +} + +bool RsZarithrianTankTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* boss = AI_VALUE2(Unit*, "find target", "general zarithrian"); + return boss != nullptr; +} + +bool RsHalionTankPositionTrigger::IsActive() +{ + if (RsHalionInTwilight(bot)) + return false; + + Unit* boss = RsHalionAnyPhysicalBoss(botAI); + if (!boss || !boss->IsInCombat()) + return false; + + return RsHalionBossTank(botAI) == bot; +} + +bool RsHalionAvoidConesTrigger::IsActive() +{ + if (RsHalionInTwilight(bot)) + return false; + + if (botAI->IsTank(bot) && !RsHalionAssistTankAsMelee(botAI)) + return false; + + Unit* boss = RsHalionAnyPhysicalBoss(botAI); + return boss != nullptr && boss->IsInCombat(); +} + +bool RsHalionCombustionTrigger::IsActive() +{ + if (RsHalionInTwilight(bot)) + return false; + + Unit* boss = RsHalionAnyPhysicalBoss(botAI); + if (!boss || !boss->IsInCombat()) + return false; + + if (RsHalionBossTank(botAI) == bot) + return false; + + bool const returning = RsHalionCombustionReturning(bot); + return RsHalionHasCombustion(bot) || returning; +} + +bool RsHalionMeteorTrigger::IsActive() +{ + if (RsHalionInTwilight(bot)) + return false; + + if (botAI->IsTank(bot) && !RsHalionAssistTankAsMelee(botAI)) + return false; + + Unit* boss = RsHalionAnyPhysicalBoss(botAI); + if (!boss || !boss->IsInCombat()) + return false; + + if (RsHalionHasCombustion(bot)) + return false; + + return RsHalionMeteorShouldRally(bot); +} + +bool RsHalionAddsTrigger::IsActive() +{ + if (RsHalionInTwilight(bot)) + return false; + + if (botAI->IsTank(bot) || botAI->IsHeal(bot) || botAI->IsMelee(bot)) + return false; + + if (RsHalionHasCombustion(bot)) + return false; + + if (!RsHalionEngaged(botAI)) + return false; + + return RsHalionAnyAddAlive(botAI); +} + +bool RsHalionAddTankTrigger::IsActive() +{ + if (!RsHalionEngaged(botAI)) + return false; + + return RsHalionTanksAdds(botAI, bot); +} + +bool RsHalionStartPositionTrigger::IsActive() +{ + Unit* boss = RsHalionPhase1Boss(botAI); + if (!boss || !boss->HealthAbovePct(98) || !boss->IsInCombat()) + return false; + + return RsHalionBossTank(botAI) != bot; +} + +bool RsHalionEnterPortalTrigger::IsActive() +{ + bool const inTwilight = RsHalionInTwilight(bot); + + if (inTwilight) + return RsHalionPortalCommit(botAI, bot); + + if (RsHalionEnteringTwilight(botAI, bot)) + return true; + + return !PlayerbotAI::IsMainTank(bot) && RsHalionPortalHeldForAdds(botAI); +} + +bool RsHalionP2TankPositionTrigger::IsActive() +{ + if (RsHalionTwilightTank(botAI) != bot || !RsHalionInTwilight(bot)) + return false; + + return RsHalionTwilightBoss(botAI) != nullptr; +} + +bool RsHalionP2AvoidConesTrigger::IsActive() +{ + if (!RsHalionInTwilight(bot)) + return false; + + if (RsHalionTwilightTank(botAI) == bot) + return false; + + return RsHalionTwilightBoss(botAI) != nullptr; +} + +bool RsHalionConsumptionTrigger::IsActive() +{ + if (!RsHalionInTwilight(bot)) + return false; + + if (!bot->HasAura(SPELL_MARK_OF_CONSUMPTION) && !bot->HasAura(SPELL_SOUL_CONSUMPTION)) + return false; + + return RsHalionTwilightBoss(botAI) != nullptr; +} + +bool RsHalionCutterTrigger::IsActive() +{ + if (!RsHalionInTwilight(bot)) + return false; + + Unit* boss = RsHalionTwilightBoss(botAI); + if (!boss) + return false; + + return boss->FindNearestCreature(NPC_ORB_CARRIER, 200.0f) != nullptr; +} + +bool RsHalionHealConsumptionTrigger::IsActive() +{ + if (!botAI->IsHeal(bot) || !RsHalionInTwilight(bot)) + return false; + + if (!RsHalionIsPhase3(botAI)) + return false; + + return RsHalionConsumptionHealTarget(botAI) != nullptr; +} + +bool RsTrashAddsTrigger::IsActive() +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot)) + return false; + + return RsTrashActive(botAI, bot); +} + +bool RsTrashMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + return RsTrashActive(botAI, bot); +} + +bool RsTrashAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTank(bot)) + return false; + + return RsTrashActive(botAI, bot); +} + +bool RsTrashRangedTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + if (!botAI->IsRanged(bot) && !botAI->IsHeal(bot)) + return false; + + return RsTrashActive(botAI, bot); +} + +bool RsTrashMeleeFlankTrigger::IsActive() +{ + if (botAI->IsTank(bot) || botAI->IsHeal(bot) || !botAI->IsMelee(bot)) + return false; + + return RsTrashActive(botAI, bot); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.h b/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.h new file mode 100644 index 000000000..80a6263f8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/RS/RSTriggers.h @@ -0,0 +1,297 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RST_H +#define PLAYERBOTS_RST_H + +#include "PlayerbotAI.h" +#include "Playerbots.h" +#include "Trigger.h" + +inline constexpr uint32 RS_MAP_RUBY_SANCTUM = 724; + +enum CreatureIdsRS +{ + + NPC_BALTHARUS_THE_WARBORN = 39751, + NPC_BALTHARUS_THE_WARBORN_CLONE = 39899, + + NPC_SAVIANA_RAGEFIRE = 39747, + + NPC_GENERAL_ZARITHRIAN = 39746, + NPC_ONYX_FLAMECALLER = 39814, + + NPC_CHARSCALE_INVOKER = 40417, + NPC_CHARSCALE_INVOKER_H = 40418, + NPC_CHARSCALE_ASSAULTER = 40419, + NPC_CHARSCALE_ASSAULTER_H = 40420, + NPC_CHARSCALE_ELITE = 40421, + NPC_CHARSCALE_ELITE_H = 40422, + NPC_CHARSCALE_COMMANDER = 40423, + NPC_CHARSCALE_COMMANDER_H = 40424, + + NPC_HALION = 39863, + NPC_TWILIGHT_HALION = 40142, + NPC_METEOR_STRIKE_MARK = 40029, + NPC_COMBUSTION = 40001, + NPC_LIVING_INFERNO = 40681, + NPC_LIVING_EMBER = 40683, + NPC_ORB_CARRIER = 40081, + NPC_CONSUMPTION = 40135 +}; + +enum GameObjectIdsRS +{ + GO_HALION_PORTAL_1 = 202794, + GO_HALION_PORTAL_2 = 202795, + GO_HALION_PORTAL_3 = 193988, + GO_HALION_PORTAL_4 = 202796 +}; + +enum SpellIdsRS +{ + + SPELL_ENERVATING_BRAND = 74502, + + SPELL_CONFLAGRATION = 74452, + SPELL_FLAME_BEACON = 74453, + + RS_SPELL_TAUNT_WARRIOR = 355, + RS_SPELL_TAUNT_PALADIN = 62124, + RS_SPELL_TAUNT_DK = 56222, + RS_SPELL_TAUNT_DRUID = 6795, + RS_SPELL_PAIN_SUPPRESION = 69910, + RS_SPELL_MAGIC_BARRIER = 38112, + + SPELL_FLAME_BREATH = 74525, + SPELL_FLAME_BREATH_ALT1 = 68970, + SPELL_FLAME_BREATH_ALT2 = 74403, + SPELL_FLAME_BREATH_ALT3 = 74404, + SPELL_FLAME_BREATH_ALT4 = 74526, + SPELL_FLAME_BREATH_ALT5 = 74527, + SPELL_FLAME_BREATH_ALT6 = 74528, + SPELL_CLEAVE = 74524, + SPELL_TAIL_LASH = 74531, + + SPELL_FIERY_COMBUSTION = 74562, + SPELL_MARK_OF_COMBUSTION = 74567, + + SPELL_METEOR_STRIKE_TARGETING = 74638, + SPELL_METEOR_STRIKE = 74637, + + SPELL_TWILIGHT_PHASING = 74808, + + SPELL_TWILIGHT_REALM = 74807, + SPELL_DARK_BREATH = 74806, + SPELL_DARK_BREATH_25N = 75954, + SPELL_DARK_BREATH_10H = 75955, + SPELL_DARK_BREATH_25H = 75956, + SPELL_SOUL_CONSUMPTION = 74792, + SPELL_MARK_OF_CONSUMPTION = 74795, + SPELL_TWILIGHT_CUTTER = 74768, + SPELL_TWILIGHT_PULSE_PERIODIC = 78861, + + RS_SPELL_NITRO_BOOSTS = 54861, + RS_SPELL_EMPOWERED_BLOOD = 70227 +}; + +class RsBaltharusBrandTrigger : public Trigger +{ +public: + RsBaltharusBrandTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs baltharus brand") {} + bool IsActive() override; +}; + +class RsBaltharusTankPositionTrigger : public Trigger +{ +public: + RsBaltharusTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs baltharus tank position") {} + bool IsActive() override; +}; + +class RsBaltharusHealerPositionTrigger : public Trigger +{ +public: + RsBaltharusHealerPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs baltharus healer position") {} + bool IsActive() override; +}; + +class RsBaltharusAvoidFrontTrigger : public Trigger +{ +public: + RsBaltharusAvoidFrontTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs baltharus avoid front") {} + bool IsActive() override; +}; + +class RsSavianaConflagrationTrigger : public Trigger +{ +public: + RsSavianaConflagrationTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs saviana conflagration") {} + bool IsActive() override; +}; + +class RsSavianaTankPositionTrigger : public Trigger +{ +public: + RsSavianaTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs saviana tank position") {} + bool IsActive() override; +}; + +class RsSavianaAvoidFrontTrigger : public Trigger +{ +public: + RsSavianaAvoidFrontTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs saviana avoid front") {} + bool IsActive() override; +}; + +class RsSavianaMeleeSpreadTrigger : public Trigger +{ +public: + RsSavianaMeleeSpreadTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs saviana melee spread") {} + bool IsActive() override; +}; + +class RsZarithrianAddsTrigger : public Trigger +{ +public: + RsZarithrianAddsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs zarithrian adds") {} + bool IsActive() override; +}; + +class RsZarithrianTankTrigger : public Trigger +{ +public: + RsZarithrianTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs zarithrian tank") {} + bool IsActive() override; +}; + +class RsHalionTankPositionTrigger : public Trigger +{ +public: + RsHalionTankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion tank position") {} + bool IsActive() override; +}; + +class RsHalionAvoidConesTrigger : public Trigger +{ +public: + RsHalionAvoidConesTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion avoid cones") {} + bool IsActive() override; +}; + +class RsHalionCombustionTrigger : public Trigger +{ +public: + RsHalionCombustionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion combustion") {} + bool IsActive() override; +}; + +class RsHalionMeteorTrigger : public Trigger +{ +public: + RsHalionMeteorTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion meteor") {} + bool IsActive() override; +}; + +class RsHalionAddsTrigger : public Trigger +{ +public: + RsHalionAddsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion adds") {} + bool IsActive() override; +}; + +class RsHalionAddTankTrigger : public Trigger +{ +public: + RsHalionAddTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion add tank") {} + bool IsActive() override; +}; + +class RsHalionStartPositionTrigger : public Trigger +{ +public: + RsHalionStartPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion start position") {} + bool IsActive() override; +}; + +class RsHalionEnterPortalTrigger : public Trigger +{ +public: + RsHalionEnterPortalTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion enter portal") {} + bool IsActive() override; +}; + +class RsHalionP2TankPositionTrigger : public Trigger +{ +public: + RsHalionP2TankPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion p2 tank position") {} + bool IsActive() override; +}; + +class RsHalionP2AvoidConesTrigger : public Trigger +{ +public: + RsHalionP2AvoidConesTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion p2 avoid cones") {} + bool IsActive() override; +}; + +class RsHalionConsumptionTrigger : public Trigger +{ +public: + RsHalionConsumptionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion consumption") {} + bool IsActive() override; +}; + +class RsHalionCutterTrigger : public Trigger +{ +public: + RsHalionCutterTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion cutter") {} + bool IsActive() override; +}; + +class RsHalionHealConsumptionTrigger : public Trigger +{ +public: + RsHalionHealConsumptionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs halion heal consumption") {} + bool IsActive() override; +}; + +class RsTrashAddsTrigger : public Trigger +{ +public: + RsTrashAddsTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs trash adds") {} + bool IsActive() override; +}; + +class RsTrashMainTankTrigger : public Trigger +{ +public: + RsTrashMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs trash main tank") {} + bool IsActive() override; +}; + +class RsTrashAssistTankTrigger : public Trigger +{ +public: + RsTrashAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs trash assist tank") {} + bool IsActive() override; +}; + +class RsTrashRangedTrigger : public Trigger +{ +public: + RsTrashRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs trash ranged") {} + bool IsActive() override; +}; + +class RsTrashMeleeFlankTrigger : public Trigger +{ +public: + RsTrashMeleeFlankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "rs trash melee flank") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.cpp index bcb48294f..cd1b6eb10 100644 --- a/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.cpp @@ -1,60 +1,91 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "RaidBossHelpers.h" +#include "CellImpl.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" #include "Playerbots.h" #include "RtiTargetValue.h" // Functions to mark targets with raid target icons // Note that these functions do not allow the player to change the icon during the encounter -void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) +bool MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId) { if (!target) - return; + return false; - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != target->GetGUID()) { - ObjectGuid currentGuid = group->GetTargetIcon(iconId); - if (currentGuid != target->GetGUID()) - group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + group->SetTargetIcon(iconId, bot->GetGUID(), target->GetGUID()); + return true; } + + return false; } -void MarkTargetWithSkull(Player* bot, Unit* target) +bool MarkTargetWithSkull(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::skullIndex); } -void MarkTargetWithSquare(Player* bot, Unit* target) +bool MarkTargetWithSquare(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::squareIndex); } -void MarkTargetWithStar(Player* bot, Unit* target) +bool MarkTargetWithStar(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::starIndex); } -void MarkTargetWithCircle(Player* bot, Unit* target) +bool MarkTargetWithCircle(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::circleIndex); } -void MarkTargetWithDiamond(Player* bot, Unit* target) +bool MarkTargetWithDiamond(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::diamondIndex); } -void MarkTargetWithTriangle(Player* bot, Unit* target) +bool MarkTargetWithTriangle(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::triangleIndex); } -void MarkTargetWithCross(Player* bot, Unit* target) +bool MarkTargetWithCross(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::crossIndex); } -void MarkTargetWithMoon(Player* bot, Unit* target) +bool MarkTargetWithMoon(Player* bot, Unit* target) { - MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); + return MarkTargetWithIcon(bot, target, RtiTargetValue::moonIndex); +} + +bool ClearTargetIcon(Player* bot, uint8 iconId) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid currentGuid = group->GetTargetIcon(iconId); + if (currentGuid != ObjectGuid::Empty) + { + group->SetTargetIcon(iconId, bot->GetGUID(), ObjectGuid::Empty); + return true; + } + + return false; } // For bots to set their raid target icon to the specified icon on the specified target @@ -73,70 +104,159 @@ void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target) } } -// Return the first alive DPS bot in the specified instance map, excluding any specified bot -// Intended for purposes of storing and erasing timers and trackers in associative containers -bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude) +// Return the first alive bot in the specified instance map for purposes of assigning +// a single bot to manage associative containers, mark targets, etc. +bool IsMechanicTrackerBot(Player* bot, uint32 mapId) { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member->GetMapId() != mapId || - !GET_PLAYERBOT_AI(member) || !botAI->IsDps(member)) - continue; + Group* group = bot->GetGroup(); + if (!group) + return false; - if (member != exclude) - return member == bot; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->GetMapId() != mapId || + !GET_PLAYERBOT_AI(member)) + { + continue; } + + return member == bot; } return false; } -// Return the first matching alive unit from a cell search of nearby npcs -// More responsive than "find target," but performance cost is much higher -// Re: using the third parameter (false by default), some units are never considered -// to be in combat (e.g., totems) -Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat) +// Requires the main tank to be alive +Player* GetGroupMainTank(PlayerbotAI* botAI, Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + ObjectGuid const mainTankGuid = botAI->GetMainTankGuid(group); + if (mainTankGuid.IsEmpty()) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->IsAlive() && unit->GetEntry() == entry) - { - if (!requireInCombat || unit->IsInCombat()) - return unit; - } + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->GetGUID() == mainTankGuid) + return member; } return nullptr; } -// Return the nearest alive player (human or bot) within the specified radius -Unit* GetNearestPlayerInRadius(Player* bot, float radius) +// Returns the alive assist tank of the specified index (0 = first, 1 = second, etc.) +Player* GetGroupAssistTank(PlayerbotAI* botAI, Player* bot, uint8 index) { - Unit* nearestPlayer = nullptr; + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + ObjectGuid const mainTankGuid = botAI->GetMainTankGuid(group); + if (mainTankGuid.IsEmpty()) + return nullptr; + + uint8 assistantCount = 0; + std::vector nonAssistantTanks; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || !botAI->IsTank(member) || + member->GetGUID() == mainTankGuid) + { + continue; + } + + if (group->IsAssistant(member->GetGUID())) + { + if (assistantCount == index) + return member; + + assistantCount++; + } + else + { + nonAssistantTanks.push_back(member); + } + } + + uint8 nonAssistantIndex = index - assistantCount; + if (nonAssistantIndex < nonAssistantTanks.size()) + return nonAssistantTanks[nonAssistantIndex]; + + return nullptr; +} + +// Return the first matching alive unit from PossibleTargetsValue within sightDistance from config +// Note that PossibleTargetsValue picks up only hostile units +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry) +{ + auto const& units = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + for (auto const& unitGuid : units) + { + Unit* unit = botAI->GetUnit(unitGuid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + return unit; + } + + return nullptr; +} + +// Return the nearest alive player (human or bot) within the specified radius. Distance is +// measured by GetExactDist2d(), which does not take into account player hitboxes (1.5y). +Player* GetNearestPlayerInRadius(Player* bot, float radius) +{ + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* nearestPlayer = nullptr; float nearestDistance = radius; - if (Group* group = bot->GetGroup()) + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || member == bot) - continue; + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot) + continue; - float distance = bot->GetExactDist2d(member); - if (distance < nearestDistance) - { - nearestDistance = distance; - nearestPlayer = member; - } + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; } } return nearestPlayer; } + +// Grid search for dynamic objects for methods to avoid dynobj-based AoE hazards +std::vector GetDynamicObjectPositions(Player* bot, float searchRadius, uint32 spellId) +{ + std::list objs; + Acore::AllWorldObjectsInRange check(bot, searchRadius); + Acore::WorldObjectListSearcher searcher( + bot, objs, check, GRID_MAP_TYPE_MASK_DYNAMICOBJECT); + Cell::VisitObjects(bot, searcher, searchRadius); + + std::vector dynObjs; + for (WorldObject* obj : objs) + { + if (obj->GetTypeId() != TYPEID_DYNAMICOBJECT) + continue; + + DynamicObject* dynObj = static_cast(obj); + if (dynObj->GetSpellId() == spellId) + { + dynObjs.emplace_back( + dynObj->GetPositionX(), dynObj->GetPositionY(), dynObj->GetPositionZ()); + } + } + + return dynObjs; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.h b/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.h index 15c60353e..d4f8ea835 100644 --- a/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.h +++ b/modules/mod-playerbots/src/Ai/Raid/RaidBossHelpers.h @@ -1,21 +1,31 @@ -#ifndef _PLAYERBOT_RAIDBOSSHELPERS_H_ -#define _PLAYERBOT_RAIDBOSSHELPERS_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RAIDBOSSHELPERS_H +#define PLAYERBOTS_RAIDBOSSHELPERS_H #include "AiObject.h" #include "Unit.h" -void MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); -void MarkTargetWithSkull(Player* bot, Unit* target); -void MarkTargetWithSquare(Player* bot, Unit* target); -void MarkTargetWithStar(Player* bot, Unit* target); -void MarkTargetWithCircle(Player* bot, Unit* target); -void MarkTargetWithDiamond(Player* bot, Unit* target); -void MarkTargetWithTriangle(Player* bot, Unit* target); -void MarkTargetWithCross(Player* bot, Unit* target); -void MarkTargetWithMoon(Player* bot, Unit* target); +bool MarkTargetWithIcon(Player* bot, Unit* target, uint8 iconId); +bool MarkTargetWithSkull(Player* bot, Unit* target); +bool MarkTargetWithSquare(Player* bot, Unit* target); +bool MarkTargetWithStar(Player* bot, Unit* target); +bool MarkTargetWithCircle(Player* bot, Unit* target); +bool MarkTargetWithDiamond(Player* bot, Unit* target); +bool MarkTargetWithTriangle(Player* bot, Unit* target); +bool MarkTargetWithCross(Player* bot, Unit* target); +bool MarkTargetWithMoon(Player* bot, Unit* target); +bool ClearTargetIcon(Player* bot, uint8 iconId); void SetRtiTarget(PlayerbotAI* botAI, const std::string& rtiName, Unit* target); -bool IsMechanicTrackerBot(PlayerbotAI* botAI, Player* bot, uint32 mapId, Player* exclude = nullptr); -Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry, bool requireInCombat = false); -Unit* GetNearestPlayerInRadius(Player* bot, float radius); +bool IsMechanicTrackerBot(Player* bot, uint32 mapId); +Player* GetGroupMainTank(PlayerbotAI* botAI, Player* bot); +Player* GetGroupAssistTank(PlayerbotAI* botAI, Player* bot, uint8 index); +Unit* GetFirstAliveUnitByEntry(PlayerbotAI* botAI, uint32 entry); +Player* GetNearestPlayerInRadius(Player* bot, float radius); +std::vector GetDynamicObjectPositions(Player* bot, float searchRadius, uint32 spellId); #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/RaidStrategyContext.h b/modules/mod-playerbots/src/Ai/Raid/RaidStrategyContext.h index 4a040985e..41fd17533 100644 --- a/modules/mod-playerbots/src/Ai/Raid/RaidStrategyContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/RaidStrategyContext.h @@ -1,20 +1,32 @@ -#ifndef _PLAYERBOT_RAIDSTRATEGYCONTEXT_H_ -#define _PLAYERBOT_RAIDSTRATEGYCONTEXT_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_RAIDSTRATEGYCONTEXT_H +#define PLAYERBOTS_RAIDSTRATEGYCONTEXT_H #include "Strategy.h" -#include "RaidAq20Strategy.h" -#include "RaidMcStrategy.h" -#include "RaidBwlStrategy.h" -#include "RaidKarazhanStrategy.h" -#include "RaidMagtheridonStrategy.h" -#include "RaidGruulsLairStrategy.h" -#include "RaidSSCStrategy.h" -#include "RaidOsStrategy.h" -#include "RaidEoEStrategy.h" -#include "RaidVoAStrategy.h" -#include "RaidUlduarStrategy.h" -#include "RaidOnyxiaStrategy.h" -#include "RaidIccStrategy.h" +#include "Aq20Strategy.h" +#include "MCStrategy.h" +#include "BWLStrategy.h" +#include "KaraStrategy.h" +#include "GruulStrategy.h" +#include "MagStrategy.h" +#include "NaxxStrategy.h" +#include "SSCStrategy.h" +#include "TKStrategy.h" +#include "HyjalStrategy.h" +#include "BTStrategy.h" +#include "ZAStrategy.h" +#include "OSStrategy.h" +#include "EoEStrategy.h" +#include "VoAStrategy.h" +#include "UldStrategy.h" +#include "OnyStrategy.h" +#include "ICCStrategy.h" +#include "RSStrategy.h" class RaidStrategyContext : public NamedObjectContext { @@ -25,15 +37,21 @@ public: creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; - creators["magtheridon"] = &RaidStrategyContext::magtheridon; creators["gruulslair"] = &RaidStrategyContext::gruulslair; + creators["magtheridon"] = &RaidStrategyContext::magtheridon; + creators["naxx"] = &RaidStrategyContext::naxx; creators["ssc"] = &RaidStrategyContext::ssc; + creators["tempestkeep"] = &RaidStrategyContext::tempestkeep; + creators["hyjal"] = &RaidStrategyContext::hyjal; + creators["blacktemple"] = &RaidStrategyContext::blacktemple; + creators["zulaman"] = &RaidStrategyContext::zulaman; creators["wotlk-os"] = &RaidStrategyContext::wotlk_os; creators["wotlk-eoe"] = &RaidStrategyContext::wotlk_eoe; creators["voa"] = &RaidStrategyContext::voa; creators["ulduar"] = &RaidStrategyContext::ulduar; creators["onyxia"] = &RaidStrategyContext::onyxia; creators["icc"] = &RaidStrategyContext::icc; + creators["rs"] = &RaidStrategyContext::rs; } private: @@ -41,15 +59,21 @@ private: static Strategy* moltencore(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } static Strategy* bwl(PlayerbotAI* botAI) { return new RaidBwlStrategy(botAI); } static Strategy* karazhan(PlayerbotAI* botAI) { return new RaidKarazhanStrategy(botAI); } - static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } static Strategy* gruulslair(PlayerbotAI* botAI) { return new RaidGruulsLairStrategy(botAI); } + static Strategy* magtheridon(PlayerbotAI* botAI) { return new RaidMagtheridonStrategy(botAI); } + static Strategy* naxx(PlayerbotAI* botAI) { return new RaidNaxxStrategy(botAI); } static Strategy* ssc(PlayerbotAI* botAI) { return new RaidSSCStrategy(botAI); } + static Strategy* tempestkeep(PlayerbotAI* botAI) { return new RaidTempestKeepStrategy(botAI); } + static Strategy* hyjal(PlayerbotAI* botAI) { return new RaidHyjalSummitStrategy(botAI); } + static Strategy* blacktemple(PlayerbotAI* botAI) { return new RaidBlackTempleStrategy(botAI); } + static Strategy* zulaman(PlayerbotAI* botAI) { return new RaidZulAmanStrategy(botAI); } static Strategy* wotlk_os(PlayerbotAI* botAI) { return new RaidOsStrategy(botAI); } static Strategy* wotlk_eoe(PlayerbotAI* botAI) { return new RaidEoEStrategy(botAI); } static Strategy* voa(PlayerbotAI* botAI) { return new RaidVoAStrategy(botAI); } static Strategy* onyxia(PlayerbotAI* botAI) { return new RaidOnyxiaStrategy(botAI); } static Strategy* ulduar(PlayerbotAI* botAI) { return new RaidUlduarStrategy(botAI); } static Strategy* icc(PlayerbotAI* botAI) { return new RaidIccStrategy(botAI); } + static Strategy* rs(PlayerbotAI* botAI) { return new RaidRsStrategy(botAI); } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCActionContext.h similarity index 57% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCActionContext.h index e6dce1694..9bda30215 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCActionContext.h @@ -1,7 +1,13 @@ -#ifndef _PLAYERBOT_RAIDSSCACTIONCONTEXT_H -#define _PLAYERBOT_RAIDSSCACTIONCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidSSCActions.h" +#ifndef PLAYERBOTS_SSCACTIONCONTEXT_H +#define PLAYERBOTS_SSCACTIONCONTEXT_H + +#include "SSCActions.h" #include "NamedObjectContext.h" class RaidSSCActionContext : public NamedObjectContext @@ -161,9 +167,6 @@ public: creators["lady vashj destroy tainted core"] = &RaidSSCActionContext::lady_vashj_destroy_tainted_core; - creators["lady vashj erase core passing trackers"] = - &RaidSSCActionContext::lady_vashj_erase_core_passing_trackers; - creators["lady vashj avoid toxic spores"] = &RaidSSCActionContext::lady_vashj_avoid_toxic_spores; @@ -173,165 +176,170 @@ public: private: // General - static Action* serpent_shrine_cavern_erase_timers_and_trackers( - PlayerbotAI* botAI) { return new SerpentShrineCavernEraseTimersAndTrackersAction(botAI); } + static Action* serpent_shrine_cavern_erase_timers_and_trackers(PlayerbotAI* botAI) { + return new SerpentShrineCavernEraseTimersAndTrackersAction(botAI); + } // Trash - static Action* underbog_colossus_escape_toxic_pool( - PlayerbotAI* botAI) { return new UnderbogColossusEscapeToxicPoolAction(botAI); } - - static Action* greyheart_tidecaller_mark_water_elemental_totem( - PlayerbotAI* botAI) { return new GreyheartTidecallerMarkWaterElementalTotemAction(botAI); } + static Action* underbog_colossus_escape_toxic_pool(PlayerbotAI* botAI) { + return new UnderbogColossusEscapeToxicPoolAction(botAI); + } + static Action* greyheart_tidecaller_mark_water_elemental_totem(PlayerbotAI* botAI) { + return new GreyheartTidecallerMarkWaterElementalTotemAction(botAI); + } // Hydross the Unstable - static Action* hydross_the_unstable_position_frost_tank( - PlayerbotAI* botAI) { return new HydrossTheUnstablePositionFrostTankAction(botAI); } - - static Action* hydross_the_unstable_position_nature_tank( - PlayerbotAI* botAI) { return new HydrossTheUnstablePositionNatureTankAction(botAI); } - - static Action* hydross_the_unstable_prioritize_elemental_adds( - PlayerbotAI* botAI) { return new HydrossTheUnstablePrioritizeElementalAddsAction(botAI); } - - static Action* hydross_the_unstable_frost_phase_spread_out( - PlayerbotAI* botAI) { return new HydrossTheUnstableFrostPhaseSpreadOutAction(botAI); } - - static Action* hydross_the_unstable_misdirect_boss_to_tank( - PlayerbotAI* botAI) { return new HydrossTheUnstableMisdirectBossToTankAction(botAI); } - - static Action* hydross_the_unstable_stop_dps_upon_phase_change( - PlayerbotAI* botAI) { return new HydrossTheUnstableStopDpsUponPhaseChangeAction(botAI); } - - static Action* hydross_the_unstable_manage_timers( - PlayerbotAI* botAI) { return new HydrossTheUnstableManageTimersAction(botAI); } + static Action* hydross_the_unstable_position_frost_tank(PlayerbotAI* botAI) { + return new HydrossTheUnstablePositionFrostTankAction(botAI); + } + static Action* hydross_the_unstable_position_nature_tank(PlayerbotAI* botAI) { + return new HydrossTheUnstablePositionNatureTankAction(botAI); + } + static Action* hydross_the_unstable_prioritize_elemental_adds(PlayerbotAI* botAI) { + return new HydrossTheUnstablePrioritizeElementalAddsAction(botAI); + } + static Action* hydross_the_unstable_frost_phase_spread_out(PlayerbotAI* botAI) { + return new HydrossTheUnstableFrostPhaseSpreadOutAction(botAI); + } + static Action* hydross_the_unstable_misdirect_boss_to_tank(PlayerbotAI* botAI) { + return new HydrossTheUnstableMisdirectBossToTankAction(botAI); + } + static Action* hydross_the_unstable_stop_dps_upon_phase_change(PlayerbotAI* botAI) { + return new HydrossTheUnstableStopDpsUponPhaseChangeAction(botAI); + } + static Action* hydross_the_unstable_manage_timers(PlayerbotAI* botAI) { + return new HydrossTheUnstableManageTimersAction(botAI); + } // The Lurker Below - static Action* the_lurker_below_run_around_behind_boss( - PlayerbotAI* botAI) { return new TheLurkerBelowRunAroundBehindBossAction(botAI); } - - static Action* the_lurker_below_position_main_tank( - PlayerbotAI* botAI) { return new TheLurkerBelowPositionMainTankAction(botAI); } - - static Action* the_lurker_below_spread_ranged_in_arc( - PlayerbotAI* botAI) { return new TheLurkerBelowSpreadRangedInArcAction(botAI); } - - static Action* the_lurker_below_tanks_pick_up_adds( - PlayerbotAI* botAI) { return new TheLurkerBelowTanksPickUpAddsAction(botAI); } - - static Action* the_lurker_below_manage_spout_timer( - PlayerbotAI* botAI) { return new TheLurkerBelowManageSpoutTimerAction(botAI); } + static Action* the_lurker_below_run_around_behind_boss(PlayerbotAI* botAI) { + return new TheLurkerBelowRunAroundBehindBossAction(botAI); + } + static Action* the_lurker_below_position_main_tank(PlayerbotAI* botAI) { + return new TheLurkerBelowPositionMainTankAction(botAI); + } + static Action* the_lurker_below_spread_ranged_in_arc(PlayerbotAI* botAI) { + return new TheLurkerBelowSpreadRangedInArcAction(botAI); + } + static Action* the_lurker_below_tanks_pick_up_adds(PlayerbotAI* botAI) { + return new TheLurkerBelowTanksPickUpAddsAction(botAI); + } + static Action* the_lurker_below_manage_spout_timer(PlayerbotAI* botAI) { + return new TheLurkerBelowManageSpoutTimerAction(botAI); + } // Leotheras the Blind - static Action* leotheras_the_blind_target_spellbinders( - PlayerbotAI* botAI) { return new LeotherasTheBlindTargetSpellbindersAction(botAI); } - - static Action* leotheras_the_blind_demon_form_tank_attack_boss( - PlayerbotAI* botAI) { return new LeotherasTheBlindDemonFormTankAttackBossAction(botAI); } - - static Action* leotheras_the_blind_melee_tanks_dont_attack_demon_form( - PlayerbotAI* botAI) { return new LeotherasTheBlindMeleeTanksDontAttackDemonFormAction(botAI); } - - static Action* leotheras_the_blind_position_ranged( - PlayerbotAI* botAI) { return new LeotherasTheBlindPositionRangedAction(botAI); } - - static Action* leotheras_the_blind_run_away_from_whirlwind( - PlayerbotAI* botAI) { return new LeotherasTheBlindRunAwayFromWhirlwindAction(botAI); } - - static Action* leotheras_the_blind_melee_dps_run_away_from_boss( - PlayerbotAI* botAI) { return new LeotherasTheBlindMeleeDpsRunAwayFromBossAction(botAI); } - - static Action* leotheras_the_blind_destroy_inner_demon( - PlayerbotAI* botAI) { return new LeotherasTheBlindDestroyInnerDemonAction(botAI); } - - static Action* leotheras_the_blind_misdirect_boss_to_demon_form_tank( - PlayerbotAI* botAI) { return new LeotherasTheBlindMisdirectBossToDemonFormTankAction(botAI); } - - static Action* leotheras_the_blind_final_phase_assign_dps_priority( - PlayerbotAI* botAI) { return new LeotherasTheBlindFinalPhaseAssignDpsPriorityAction(botAI); } - - static Action* leotheras_the_blind_manage_dps_wait_timers( - PlayerbotAI* botAI) { return new LeotherasTheBlindManageDpsWaitTimersAction(botAI); } + static Action* leotheras_the_blind_target_spellbinders(PlayerbotAI* botAI) { + return new LeotherasTheBlindTargetSpellbindersAction(botAI); + } + static Action* leotheras_the_blind_demon_form_tank_attack_boss(PlayerbotAI* botAI) { + return new LeotherasTheBlindDemonFormTankAttackBossAction(botAI); + } + static Action* leotheras_the_blind_melee_tanks_dont_attack_demon_form(PlayerbotAI* botAI) { + return new LeotherasTheBlindMeleeTanksDontAttackDemonFormAction(botAI); + } + static Action* leotheras_the_blind_position_ranged(PlayerbotAI* botAI) { + return new LeotherasTheBlindPositionRangedAction(botAI); + } + static Action* leotheras_the_blind_run_away_from_whirlwind(PlayerbotAI* botAI) { + return new LeotherasTheBlindRunAwayFromWhirlwindAction(botAI); + } + static Action* leotheras_the_blind_melee_dps_run_away_from_boss(PlayerbotAI* botAI) { + return new LeotherasTheBlindMeleeDpsRunAwayFromBossAction(botAI); + } + static Action* leotheras_the_blind_destroy_inner_demon(PlayerbotAI* botAI) { + return new LeotherasTheBlindDestroyInnerDemonAction(botAI); + } + static Action* leotheras_the_blind_misdirect_boss_to_demon_form_tank(PlayerbotAI* botAI) { + return new LeotherasTheBlindMisdirectBossToDemonFormTankAction(botAI); + } + static Action* leotheras_the_blind_final_phase_assign_dps_priority(PlayerbotAI* botAI) { + return new LeotherasTheBlindFinalPhaseAssignDpsPriorityAction(botAI); + } + static Action* leotheras_the_blind_manage_dps_wait_timers(PlayerbotAI* botAI) { + return new LeotherasTheBlindManageDpsWaitTimersAction(botAI); + } // Fathom-Lord Karathress - static Action* fathom_lord_karathress_main_tank_position_boss( - PlayerbotAI* botAI) { return new FathomLordKarathressMainTankPositionBossAction(botAI); } - - static Action* fathom_lord_karathress_first_assist_tank_position_caribdis( - PlayerbotAI* botAI) { return new FathomLordKarathressFirstAssistTankPositionCaribdisAction(botAI); } - - static Action* fathom_lord_karathress_second_assist_tank_position_sharkkis( - PlayerbotAI* botAI) { return new FathomLordKarathressSecondAssistTankPositionSharkkisAction(botAI); } - - static Action* fathom_lord_karathress_third_assist_tank_position_tidalvess( - PlayerbotAI* botAI) { return new FathomLordKarathressThirdAssistTankPositionTidalvessAction(botAI); } - - static Action* fathom_lord_karathress_position_caribdis_tank_healer( - PlayerbotAI* botAI) { return new FathomLordKarathressPositionCaribdisTankHealerAction(botAI); } - - static Action* fathom_lord_karathress_misdirect_bosses_to_tanks( - PlayerbotAI* botAI) { return new FathomLordKarathressMisdirectBossesToTanksAction(botAI); } - - static Action* fathom_lord_karathress_assign_dps_priority( - PlayerbotAI* botAI) { return new FathomLordKarathressAssignDpsPriorityAction(botAI); } - - static Action* fathom_lord_karathress_manage_dps_timer( - PlayerbotAI* botAI) { return new FathomLordKarathressManageDpsTimerAction(botAI); } + static Action* fathom_lord_karathress_main_tank_position_boss(PlayerbotAI* botAI) { + return new FathomLordKarathressMainTankPositionBossAction(botAI); + } + static Action* fathom_lord_karathress_first_assist_tank_position_caribdis(PlayerbotAI* botAI) { + return new FathomLordKarathressFirstAssistTankPositionCaribdisAction(botAI); + } + static Action* fathom_lord_karathress_second_assist_tank_position_sharkkis(PlayerbotAI* botAI) { + return new FathomLordKarathressSecondAssistTankPositionSharkkisAction(botAI); + } + static Action* fathom_lord_karathress_third_assist_tank_position_tidalvess(PlayerbotAI* botAI) { + return new FathomLordKarathressThirdAssistTankPositionTidalvessAction(botAI); + } + static Action* fathom_lord_karathress_position_caribdis_tank_healer(PlayerbotAI* botAI) { + return new FathomLordKarathressPositionCaribdisTankHealerAction(botAI); + } + static Action* fathom_lord_karathress_misdirect_bosses_to_tanks(PlayerbotAI* botAI) { + return new FathomLordKarathressMisdirectBossesToTanksAction(botAI); + } + static Action* fathom_lord_karathress_assign_dps_priority(PlayerbotAI* botAI) { + return new FathomLordKarathressAssignDpsPriorityAction(botAI); + } + static Action* fathom_lord_karathress_manage_dps_timer(PlayerbotAI* botAI) { + return new FathomLordKarathressManageDpsTimerAction(botAI); + } // Morogrim Tidewalker - static Action* morogrim_tidewalker_misdirect_boss_to_main_tank( - PlayerbotAI* botAI) { return new MorogrimTidewalkerMisdirectBossToMainTankAction(botAI); } - - static Action* morogrim_tidewalker_move_boss_to_tank_position( - PlayerbotAI* botAI) { return new MorogrimTidewalkerMoveBossToTankPositionAction(botAI); } - - static Action* morogrim_tidewalker_phase_2_reposition_ranged( - PlayerbotAI* botAI) { return new MorogrimTidewalkerPhase2RepositionRangedAction(botAI); } + static Action* morogrim_tidewalker_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new MorogrimTidewalkerMisdirectBossToMainTankAction(botAI); + } + static Action* morogrim_tidewalker_move_boss_to_tank_position(PlayerbotAI* botAI) { + return new MorogrimTidewalkerMoveBossToTankPositionAction(botAI); + } + static Action* morogrim_tidewalker_phase_2_reposition_ranged(PlayerbotAI* botAI) { + return new MorogrimTidewalkerPhase2RepositionRangedAction(botAI); + } // Lady Vashj - static Action* lady_vashj_main_tank_position_boss( - PlayerbotAI* botAI) { return new LadyVashjMainTankPositionBossAction(botAI); } - - static Action* lady_vashj_phase_1_spread_ranged_in_arc( - PlayerbotAI* botAI) { return new LadyVashjPhase1SpreadRangedInArcAction(botAI); } - - static Action* lady_vashj_set_grounding_totem_in_main_tank_group( - PlayerbotAI* botAI) { return new LadyVashjSetGroundingTotemInMainTankGroupAction(botAI); } - - static Action* lady_vashj_static_charge_move_away_from_group( - PlayerbotAI* botAI) { return new LadyVashjStaticChargeMoveAwayFromGroupAction(botAI); } - - static Action* lady_vashj_misdirect_boss_to_main_tank( - PlayerbotAI* botAI) { return new LadyVashjMisdirectBossToMainTankAction(botAI); } - - static Action* lady_vashj_assign_phase_2_and_phase_3_dps_priority( - PlayerbotAI* botAI) { return new LadyVashjAssignPhase2AndPhase3DpsPriorityAction(botAI); } - - static Action* lady_vashj_misdirect_strider_to_first_assist_tank( - PlayerbotAI* botAI) { return new LadyVashjMisdirectStriderToFirstAssistTankAction(botAI); } - - static Action* lady_vashj_tank_attack_and_move_away_strider( - PlayerbotAI* botAI) { return new LadyVashjTankAttackAndMoveAwayStriderAction(botAI); } - - static Action* lady_vashj_teleport_to_tainted_elemental( - PlayerbotAI* botAI) { return new LadyVashjTeleportToTaintedElementalAction(botAI); } - - static Action* lady_vashj_loot_tainted_core( - PlayerbotAI* botAI) { return new LadyVashjLootTaintedCoreAction(botAI); } - - static Action* lady_vashj_pass_the_tainted_core( - PlayerbotAI* botAI) { return new LadyVashjPassTheTaintedCoreAction(botAI); } - - static Action* lady_vashj_destroy_tainted_core( - PlayerbotAI* botAI) { return new LadyVashjDestroyTaintedCoreAction(botAI); } - - static Action* lady_vashj_erase_core_passing_trackers( - PlayerbotAI* botAI) { return new LadyVashjEraseCorePassingTrackersAction(botAI); } - - static Action* lady_vashj_avoid_toxic_spores( - PlayerbotAI* botAI) { return new LadyVashjAvoidToxicSporesAction(botAI); } - - static Action* lady_vashj_use_free_action_abilities( - PlayerbotAI* botAI) { return new LadyVashjUseFreeActionAbilitiesAction(botAI); } + static Action* lady_vashj_main_tank_position_boss(PlayerbotAI* botAI) { + return new LadyVashjMainTankPositionBossAction(botAI); + } + static Action* lady_vashj_phase_1_spread_ranged_in_arc(PlayerbotAI* botAI) { + return new LadyVashjPhase1SpreadRangedInArcAction(botAI); + } + static Action* lady_vashj_set_grounding_totem_in_main_tank_group(PlayerbotAI* botAI) { + return new LadyVashjSetGroundingTotemInMainTankGroupAction(botAI); + } + static Action* lady_vashj_static_charge_move_away_from_group(PlayerbotAI* botAI) { + return new LadyVashjStaticChargeMoveAwayFromGroupAction(botAI); + } + static Action* lady_vashj_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new LadyVashjMisdirectBossToMainTankAction(botAI); + } + static Action* lady_vashj_assign_phase_2_and_phase_3_dps_priority(PlayerbotAI* botAI) { + return new LadyVashjAssignPhase2AndPhase3DpsPriorityAction(botAI); + } + static Action* lady_vashj_misdirect_strider_to_first_assist_tank(PlayerbotAI* botAI) { + return new LadyVashjMisdirectStriderToFirstAssistTankAction(botAI); + } + static Action* lady_vashj_tank_attack_and_move_away_strider(PlayerbotAI* botAI) { + return new LadyVashjTankAttackAndMoveAwayStriderAction(botAI); + } + static Action* lady_vashj_teleport_to_tainted_elemental(PlayerbotAI* botAI) { + return new LadyVashjTeleportToTaintedElementalAction(botAI); + } + static Action* lady_vashj_loot_tainted_core(PlayerbotAI* botAI) { + return new LadyVashjLootTaintedCoreAction(botAI); + } + static Action* lady_vashj_pass_the_tainted_core(PlayerbotAI* botAI) { + return new LadyVashjPassTheTaintedCoreAction(botAI); + } + static Action* lady_vashj_destroy_tainted_core(PlayerbotAI* botAI) { + return new LadyVashjDestroyTaintedCoreAction(botAI); + } + static Action* lady_vashj_avoid_toxic_spores(PlayerbotAI* botAI) { + return new LadyVashjAvoidToxicSporesAction(botAI); + } + static Action* lady_vashj_use_free_action_abilities(PlayerbotAI* botAI) { + return new LadyVashjUseFreeActionAbilitiesAction(botAI); + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCActions.cpp similarity index 66% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCActions.cpp index 7aa3eda35..e97283756 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCActions.cpp @@ -1,5 +1,11 @@ -#include "RaidSSCActions.h" -#include "RaidSSCHelpers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "SSCActions.h" +#include "SSCHelpers.h" #include "AiFactory.h" #include "Corpse.h" #include "LootAction.h" @@ -19,42 +25,40 @@ bool SerpentShrineCavernEraseTimersAndTrackersAction::Execute(Event /*event*/) const ObjectGuid guid = bot->GetGUID(); bool erased = false; - if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + + if (!AI_VALUE2(Unit*, "find target", "hydross the unstable") && + (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0 || + hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0 || + hydrossNatureDpsWaitTimer.erase(instanceId) > 0 || + hydrossFrostDpsWaitTimer.erase(instanceId) > 0)) { - if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) - erased = true; - if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) - erased = true; - if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) - erased = true; - if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "the lurker below")) + + if (!AI_VALUE2(Unit*, "find target", "the lurker below") && + (lurkerRangedPositions.erase(guid) > 0 || + lurkerSpoutTimer.erase(instanceId) > 0)) { - if (lurkerRangedPositions.erase(guid) > 0) - erased = true; - if (lurkerSpoutTimer.erase(instanceId) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && + karathressDpsWaitTimer.erase(instanceId) > 0) { - if (karathressDpsWaitTimer.erase(instanceId) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker") && + (tidewalkerTankStep.erase(guid) > 0 || + tidewalkerRangedStep.erase(guid) > 0)) { - if (tidewalkerTankStep.erase(guid) > 0) - erased = true; - if (tidewalkerRangedStep.erase(guid) > 0) - erased = true; + erased = true; } - if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + + if (!AI_VALUE2(Unit*, "find target", "lady vashj") && + hasReachedVashjRangedPosition.erase(guid) > 0) { - if (vashjRangedPositions.erase(guid) > 0) - erased = true; - if (hasReachedVashjRangedPosition.erase(guid) > 0) - erased = true; + erased = true; } return erased; @@ -74,21 +78,18 @@ bool UnderbogColossusEscapeToxicPoolAction::Execute(Event /*event*/) return false; float radius = dynObj->GetRadius(); - if (radius <= 0.0f) + const SpellInfo* sInfo = sSpellMgr->GetSpellInfo(dynObj->GetSpellId()); + if (radius <= 0.0f && sInfo) { - const SpellInfo* sInfo = sSpellMgr->GetSpellInfo(dynObj->GetSpellId()); - if (sInfo) + for (int e = 0; e < MAX_SPELL_EFFECTS; ++e) { - for (int e = 0; e < MAX_SPELL_EFFECTS; ++e) + auto const& eff = sInfo->Effects[e]; + if (eff.Effect == SPELL_EFFECT_SCHOOL_DAMAGE || + (eff.Effect == SPELL_EFFECT_APPLY_AURA && + eff.ApplyAuraName == SPELL_AURA_PERIODIC_DAMAGE)) { - auto const& eff = sInfo->Effects[e]; - if (eff.Effect == SPELL_EFFECT_SCHOOL_DAMAGE || - (eff.Effect == SPELL_EFFECT_APPLY_AURA && - eff.ApplyAuraName == SPELL_AURA_PERIODIC_DAMAGE)) - { - radius = eff.CalcRadius(); - break; - } + radius = eff.CalcRadius(); + break; } } } @@ -131,10 +132,8 @@ bool UnderbogColossusEscapeToxicPoolAction::Execute(Event /*event*/) bool GreyheartTidecallerMarkWaterElementalTotemAction::Execute(Event /*event*/) { - if (Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM)) - MarkTargetWithSkull(bot, totem); - - return false; + Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM); + return totem && MarkTargetWithSkull(bot, totem); } // Hydross the Unstable @@ -150,10 +149,12 @@ bool HydrossTheUnstablePositionFrostTankAction::Execute(Event /*event*/) if (!hydross->HasAura(SPELL_CORRUPTION) && !HasMarkOfHydrossAt100Percent(bot)) { - MarkTargetWithSquare(bot, hydross); + if (MarkTargetWithSquare(bot, hydross)) + return true; + SetRtiTarget(botAI, "square", hydross); - if (bot->GetTarget() != hydross->GetGUID()) + if (AI_VALUE(Unit*, "current target") != hydross) return Attack(hydross); if (hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) @@ -207,15 +208,13 @@ bool HydrossTheUnstablePositionFrostTankAction::Execute(Event /*event*/) } } - if (hydross->HasAura(SPELL_CORRUPTION)) + const Position& position = HYDROSS_FROST_TANK_POSITION; + if (hydross->HasAura(SPELL_CORRUPTION) && + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) { - const Position& position = HYDROSS_FROST_TANK_POSITION; - if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) - { - return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -232,10 +231,12 @@ bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) if (hydross->HasAura(SPELL_CORRUPTION) && !HasMarkOfCorruptionAt100Percent(bot)) { - MarkTargetWithTriangle(bot, hydross); + if (MarkTargetWithTriangle(bot, hydross)) + return true; + SetRtiTarget(botAI, "triangle", hydross); - if (bot->GetTarget() != hydross->GetGUID()) + if (AI_VALUE(Unit*, "current target") != hydross) return Attack(hydross); if (hydross->GetVictim() == bot && bot->IsWithinMeleeRange(hydross)) @@ -289,15 +290,13 @@ bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) } } - if (!hydross->HasAura(SPELL_CORRUPTION)) + const Position& position = HYDROSS_NATURE_TANK_POSITION; + if (!hydross->HasAura(SPELL_CORRUPTION) && + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) { - const Position& position = HYDROSS_NATURE_TANK_POSITION; - if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) - { - return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, false); - } + return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), + position.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); } return false; @@ -305,25 +304,24 @@ bool HydrossTheUnstablePositionNatureTankAction::Execute(Event /*event*/) bool HydrossTheUnstablePrioritizeElementalAddsAction::Execute(Event /*event*/) { - Unit* waterElemental = GetFirstAliveUnitByEntry(botAI, NPC_PURE_SPAWN_OF_HYDROSS); - if (waterElemental) + if (Unit* waterElemental = GetFirstAliveUnitByEntry(botAI, NPC_PURE_SPAWN_OF_HYDROSS)) { - if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) - MarkTargetWithSkull(bot, waterElemental); + if (MarkTargetWithSkull(bot, waterElemental)) + return true; SetRtiTarget(botAI, "skull", waterElemental); - if (bot->GetTarget() != waterElemental->GetGUID()) + if (AI_VALUE(Unit*, "current target") != waterElemental) return Attack(waterElemental); } else if (Unit* natureElemental = GetFirstAliveUnitByEntry(botAI, NPC_TAINTED_SPAWN_OF_HYDROSS)) { - if (IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr)) - MarkTargetWithSkull(bot, natureElemental); + if (MarkTargetWithSkull(bot, natureElemental)) + return true; SetRtiTarget(botAI, "skull", natureElemental); - if (bot->GetTarget() != natureElemental->GetGUID()) + if (AI_VALUE(Unit*, "current target") != natureElemental) return Attack(natureElemental); } @@ -336,20 +334,10 @@ bool HydrossTheUnstableFrostPhaseSpreadOutAction::Execute(Event /*event*/) if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) return false; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || member == bot || !member->IsAlive()) - continue; - - constexpr float safeDistance = 6.0f; - constexpr uint32 minInterval = 1000; - if (bot->GetExactDist2d(member) < safeDistance) - return FleePosition(member->GetPosition(), safeDistance, minInterval); - } - } + constexpr float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); return false; } @@ -360,33 +348,17 @@ bool HydrossTheUnstableMisdirectBossToTankAction::Execute(Event /*event*/) if (!hydross) return false; - if (Group* group = bot->GetGroup()) - { - if (TryMisdirectToFrostTank(hydross, group)) - return true; - - if (TryMisdirectToNatureTank(hydross, group)) - return true; - } - - return false; + return TryMisdirectToFrostTank(hydross) || TryMisdirectToNatureTank(hydross); } bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToFrostTank( - Unit* hydross, Group* group) + Unit* hydross) { - Player* frostTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - frostTank = member; - break; - } - } + Player* frostTank = GetGroupMainTank(botAI, bot); + if (!frostTank) + return false; - if (HasNoMarkOfHydross(bot) && !hydross->HasAura(SPELL_CORRUPTION) && frostTank) + if (HasNoMarkOfHydross(bot) && !hydross->HasAura(SPELL_CORRUPTION)) { if (botAI->CanCastSpell("misdirection", frostTank)) return botAI->CastSpell("misdirection", frostTank); @@ -399,20 +371,13 @@ bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToFrostTank( } bool HydrossTheUnstableMisdirectBossToTankAction::TryMisdirectToNatureTank( - Unit* hydross, Group* group) + Unit* hydross) { - Player* natureTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && botAI->IsAssistTankOfIndex(member, 0, true)) - { - natureTank = member; - break; - } - } + Player* natureTank = GetGroupAssistTank(botAI, bot, 0); + if (!natureTank) + return false; - if (HasNoMarkOfCorruption(bot) && hydross->HasAura(SPELL_CORRUPTION) && natureTank) + if (HasNoMarkOfCorruption(bot) && hydross->HasAura(SPELL_CORRUPTION)) { if (botAI->CanCastSpell("misdirection", natureTank)) return botAI->CastSpell("misdirection", natureTank); @@ -480,33 +445,28 @@ bool HydrossTheUnstableManageTimersAction::Execute(Event /*event*/) const time_t now = std::time(nullptr); bool changed = false; + if (!hydross->HasAura(SPELL_CORRUPTION)) { - if (hydrossFrostDpsWaitTimer.try_emplace(instanceId, now).second) + if (hydrossFrostDpsWaitTimer.try_emplace(instanceId, now).second || + hydrossNatureDpsWaitTimer.erase(instanceId) > 0 || + hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) changed = true; - if (hydrossNatureDpsWaitTimer.erase(instanceId) > 0) + + if (HasMarkOfHydrossAt100Percent(bot) && + hydrossChangeToNaturePhaseTimer.try_emplace(instanceId, now).second) changed = true; - if (hydrossChangeToFrostPhaseTimer.erase(instanceId) > 0) - changed = true; - if (HasMarkOfHydrossAt100Percent(bot)) - { - if (hydrossChangeToNaturePhaseTimer.try_emplace(instanceId, now).second) - changed = true; - } } else { - if (hydrossNatureDpsWaitTimer.try_emplace(instanceId, now).second) + if (hydrossNatureDpsWaitTimer.try_emplace(instanceId, now).second || + hydrossFrostDpsWaitTimer.erase(instanceId) > 0 || + hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) changed = true; - if (hydrossFrostDpsWaitTimer.erase(instanceId) > 0) + + if (HasMarkOfCorruptionAt100Percent(bot) && + hydrossChangeToFrostPhaseTimer.try_emplace(instanceId, now).second) changed = true; - if (hydrossChangeToNaturePhaseTimer.erase(instanceId) > 0) - changed = true; - if (HasMarkOfCorruptionAt100Percent(bot)) - { - if (hydrossChangeToFrostPhaseTimer.try_emplace(instanceId, now).second) - changed = true; - } } return changed; @@ -521,17 +481,19 @@ bool TheLurkerBelowRunAroundBehindBossAction::Execute(Event /*event*/) if (!lurker) return false; - float radius = frand(20.0f, 21.0f); + float radius = frand(19.0f, 20.0f); float botAngle = std::atan2( bot->GetPositionY() - lurker->GetPositionY(), bot->GetPositionX() - lurker->GetPositionX()); float relativeAngle = Position::NormalizeOrientation(botAngle - lurker->GetOrientation()); constexpr float safeArc = M_PI / 2.0f; - if (std::fabs(Position::NormalizeOrientation(relativeAngle - M_PI)) > safeArc / 2.0f) + if (!botAI->IsMainTank(bot) && + std::fabs(Position::NormalizeOrientation(relativeAngle - M_PI)) > safeArc / 2.0f) { float tangentAngle = botAngle + (relativeAngle > M_PI ? -0.1f : 0.1f); float moveX = lurker->GetPositionX() + radius * std::cos(tangentAngle); float moveY = lurker->GetPositionY() + radius * std::sin(tangentAngle); + botAI->Reset(); return MoveTo(SSC_MAP_ID, moveX, moveY, lurker->GetPositionZ(), false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); @@ -541,6 +503,7 @@ bool TheLurkerBelowRunAroundBehindBossAction::Execute(Event /*event*/) float behindAngle = lurker->GetOrientation() + M_PI + frand(-0.5f, 0.5f) * safeArc; float targetX = lurker->GetPositionX() + radius * std::cos(behindAngle); float targetY = lurker->GetPositionY() + radius * std::sin(behindAngle); + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) { botAI->Reset(); @@ -558,7 +521,7 @@ bool TheLurkerBelowPositionMainTankAction::Execute(Event /*event*/) if (!lurker) return false; - if (bot->GetTarget() != lurker->GetGUID()) + if (AI_VALUE(Unit*, "current target") != lurker) return Attack(lurker); const Position& position = LURKER_MAIN_TANK_POSITION; @@ -611,7 +574,7 @@ bool TheLurkerBelowSpreadRangedInArcAction::Execute(Event /*event*/) float angle = (count == 1) ? arcCenter : (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); - float radius = 28.0f; + constexpr float radius = 27.0f; float targetX = lurker->GetPositionX() + radius * std::sin(angle); float targetY = lurker->GetPositionY() + radius * std::cos(angle); @@ -638,38 +601,24 @@ bool TheLurkerBelowSpreadRangedInArcAction::Execute(Event /*event*/) // the first 3 will each pick up 1 Guardian bool TheLurkerBelowTanksPickUpAddsAction::Execute(Event /*event*/) { - Player* mainTank = nullptr; - Player* firstAssistTank = nullptr; - Player* secondAssistTank = nullptr; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if (!mainTank && botAI->IsMainTank(member)) - mainTank = member; - else if (!firstAssistTank && botAI->IsAssistTankOfIndex(member, 0, true)) - firstAssistTank = member; - else if (!secondAssistTank && botAI->IsAssistTankOfIndex(member, 1, true)) - secondAssistTank = member; - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); if (!mainTank || !firstAssistTank || !secondAssistTank) return false; std::vector guardians; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto guid : npcs) + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + + for (auto guid : attackers) { Unit* unit = botAI->GetUnit(guid); - if (unit && unit->IsAlive() && unit->GetEntry() == NPC_COILFANG_GUARDIAN) + if (unit && unit->IsAlive() && + unit->GetEntry() == NPC_COILFANG_GUARDIAN) + { guardians.push_back(unit); + } } if (guardians.size() < 3) @@ -690,9 +639,12 @@ bool TheLurkerBelowTanksPickUpAddsAction::Execute(Event /*event*/) Unit* guardian = guardians[i]; if (bot == tank) { - MarkTargetWithIcon(bot, guardian, rtiIndices[i]); + if (MarkTargetWithIcon(bot, guardian, rtiIndices[i])) + return true; + SetRtiTarget(botAI, rtiNames[i], guardian); - if (bot->GetTarget() != guardian->GetGUID()) + + if (AI_VALUE(Unit*, "current target") != guardian) return Attack(guardian); } } @@ -709,26 +661,33 @@ bool TheLurkerBelowManageSpoutTimerAction::Execute(Event /*event*/) const uint32 instanceId = lurker->GetMap()->GetInstanceId(); const time_t now = std::time(nullptr); + bool changed = false; + auto it = lurkerSpoutTimer.find(instanceId); if (it != lurkerSpoutTimer.end() && it->second <= now) { lurkerSpoutTimer.erase(it); + changed = true; it = lurkerSpoutTimer.end(); } const time_t spoutCastTime = 20; if (IsLurkerCastingSpout(lurker) && it == lurkerSpoutTimer.end()) + { lurkerSpoutTimer.try_emplace(instanceId, now + spoutCastTime); + changed = true; + } - return false; + return changed; } // Leotheras the Blind bool LeotherasTheBlindTargetSpellbindersAction::Execute(Event /*event*/) { - if (Unit* spellbinder = GetFirstAliveUnitByEntry(botAI, NPC_GREYHEART_SPELLBINDER)) - MarkTargetWithSkull(bot, spellbinder); + Unit* spellbinder = GetFirstAliveUnitByEntry(botAI, NPC_GREYHEART_SPELLBINDER); + if (spellbinder && MarkTargetWithSkull(bot, spellbinder)) + return true; return false; } @@ -737,15 +696,16 @@ bool LeotherasTheBlindTargetSpellbindersAction::Execute(Event /*event*/) // Use tank strategy for Demon Form and DPS strategy for Human Form bool LeotherasTheBlindDemonFormTankAttackBossAction::Execute(Event /*event*/) { + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + Unit* innerDemon = nullptr; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) + for (auto guid : attackers) { Unit* unit = botAI->GetUnit(guid); Creature* creature = unit ? unit->ToCreature() : nullptr; - if (creature && creature->GetEntry() == NPC_INNER_DEMON - && creature->GetSummonerGUID() == bot->GetGUID()) + if (creature && creature->GetEntry() == NPC_INNER_DEMON && + creature->GetSummonerGUID() == bot->GetGUID()) { innerDemon = creature; break; @@ -755,9 +715,11 @@ bool LeotherasTheBlindDemonFormTankAttackBossAction::Execute(Event /*event*/) if (innerDemon) return false; - if (Unit* leotherasDemon = GetActiveLeotherasDemon(botAI)) + if (Unit* leotherasDemon = GetActiveLeotherasDemon(bot)) { - MarkTargetWithSquare(bot, leotherasDemon); + if (MarkTargetWithSquare(bot, leotherasDemon)) + return true; + SetRtiTarget(botAI, "square", leotherasDemon); if (botAI->CanCastSpell("searing pain", leotherasDemon)) @@ -781,7 +743,7 @@ bool LeotherasTheBlindMeleeTanksDontAttackDemonFormAction::Execute(Event /*event bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) { constexpr float safeDistFromBoss = 15.0f; - Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasHuman = GetLeotherasHuman(bot); if (leotherasHuman && bot->GetExactDist2d(leotherasHuman) < safeDistFromBoss && leotherasHuman->GetVictim() != bot) { @@ -789,11 +751,10 @@ bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) return FleePosition(leotherasHuman->GetPosition(), safeDistFromBoss, minInterval); } - Group* group = bot->GetGroup(); - if (!group) + if (!GetActiveLeotherasDemon(bot)) return false; - if (GetActiveLeotherasDemon(botAI)) + if (Group* group = bot->GetGroup()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -804,9 +765,9 @@ bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) constexpr uint32 minInterval = 0; if (GetLeotherasDemonFormTank(bot) == member) { - constexpr float safeDistFromTank = 10.0f; - if (bot->GetExactDist2d(member) < safeDistFromTank) - return FleePosition(member->GetPosition(), safeDistFromTank, minInterval); + constexpr float safeDistFromMember = 10.0f; + if (bot->GetExactDist2d(member) < safeDistFromMember) + return FleePosition(member->GetPosition(), safeDistFromMember, minInterval); } else { @@ -822,7 +783,7 @@ bool LeotherasTheBlindPositionRangedAction::Execute(Event /*event*/) bool LeotherasTheBlindRunAwayFromWhirlwindAction::Execute(Event /*event*/) { - if (Unit* leotherasHuman = GetLeotherasHuman(botAI)) + if (Unit* leotherasHuman = GetLeotherasHuman(bot)) { float currentDistance = bot->GetExactDist2d(leotherasHuman); constexpr float safeDistance = 25.0f; @@ -843,7 +804,7 @@ bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) if (botAI->CanCastSpell("cloak of shadows", bot)) return botAI->CastSpell("cloak of shadows", bot); - Unit* leotheras = GetPhase2LeotherasDemon(botAI); + Unit* leotheras = GetPhase2LeotherasDemon(bot); if (!leotheras) return false; @@ -853,12 +814,8 @@ bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) float currentDistance = bot->GetExactDist2d(demonVictim); constexpr float safeDistance = 10.0f; - if (currentDistance < safeDistance) - { - botAI->Reset(); - if (demonVictim != bot) - return MoveAway(demonVictim, safeDistance - currentDistance); - } + if (currentDistance < safeDistance && demonVictim != bot) + return MoveAway(demonVictim, safeDistance - currentDistance); return false; } @@ -866,15 +823,16 @@ bool LeotherasTheBlindMeleeDpsRunAwayFromBossAction::Execute(Event /*event*/) // Hardcoded actions for healers and bear tanks to kill Inner Demons bool LeotherasTheBlindDestroyInnerDemonAction::Execute(Event /*event*/) { + auto const& attackers = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + Unit* innerDemon = nullptr; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) + for (auto guid : attackers) { Unit* unit = botAI->GetUnit(guid); Creature* creature = unit ? unit->ToCreature() : nullptr; - if (creature && creature->GetEntry() == NPC_INNER_DEMON - && creature->GetSummonerGUID() == bot->GetGUID()) + if (creature && creature->GetEntry() == NPC_INNER_DEMON && + creature->GetSummonerGUID() == bot->GetGUID()) { innerDemon = creature; break; @@ -891,7 +849,7 @@ bool LeotherasTheBlindDestroyInnerDemonAction::Execute(Event /*event*/) // Roles without a strategy need to affirmatively attack their Inner Demons // Because DPS assist is disabled via multipliers - if (bot->GetTarget() != innerDemon->GetGUID()) + if (AI_VALUE(Unit*, "current target") != innerDemon) return Attack(innerDemon); } @@ -910,37 +868,34 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleFeralTankStrategy(Unit* inn bot->RemoveAura(SPELL_BEAR_FORM); bool casted = false; - if (!bot->HasAura(SPELL_CAT_FORM) && botAI->CanCastSpell("cat form", bot)) - { - if (botAI->CastSpell("cat form", bot)) - casted = true; - } - if (botAI->CanCastSpell("berserk", bot)) - { - if (botAI->CastSpell("berserk", bot)) - casted = true; - } - if (bot->GetPower(POWER_ENERGY) < 30 && botAI->CanCastSpell("tiger's fury", bot)) - { - if (botAI->CastSpell("tiger's fury", bot)) - casted = true; - } - if (bot->GetComboPoints() >= 4 && botAI->CanCastSpell("ferocious bite", innerDemon)) - { - if (botAI->CastSpell("ferocious bite", innerDemon)) - casted = true; - } + + if (!bot->HasAura(SPELL_CAT_FORM) && + botAI->CanCastSpell("cat form", bot) && + botAI->CastSpell("cat form", bot)) + casted = true; + + if (botAI->CanCastSpell("berserk", bot) && + botAI->CastSpell("berserk", bot)) + casted = true; + + if (bot->GetPower(POWER_ENERGY) < 30 && + botAI->CanCastSpell("tiger's fury", bot) && + botAI->CastSpell("tiger's fury", bot)) + casted = true; + + if (bot->GetComboPoints() >= 4 && + botAI->CanCastSpell("ferocious bite", innerDemon) && + botAI->CastSpell("ferocious bite", innerDemon)) + casted = true; + if (bot->GetComboPoints() == 0 && innerDemon->GetHealthPct() > 25.0f && - botAI->CanCastSpell("rake", innerDemon)) - { - if (botAI->CastSpell("rake", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("mangle (cat)", innerDemon)) - { - if (botAI->CastSpell("mangle (cat)", innerDemon)) - casted = true; - } + botAI->CanCastSpell("rake", innerDemon) && + botAI->CastSpell("rake", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("mangle (cat)", innerDemon) && + botAI->CastSpell("mangle (cat)", innerDemon)) + casted = true; return casted; } @@ -953,52 +908,44 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerD bot->RemoveAura(SPELL_TREE_OF_LIFE); bool casted = false; - if (botAI->CanCastSpell("barkskin", bot)) - { - if (botAI->CastSpell("barkskin", bot)) - casted = true; - } - if (botAI->CanCastSpell("wrath", innerDemon)) - { - if (botAI->CastSpell("wrath", innerDemon)) - casted = true; - } + + if (botAI->CanCastSpell("barkskin", bot) && + botAI->CastSpell("barkskin", bot)) + casted = true; + + if (botAI->CanCastSpell("wrath", innerDemon) && + botAI->CastSpell("wrath", innerDemon)) + casted = true; return casted; } else if (bot->getClass() == CLASS_PALADIN) { bool casted = false; - if (botAI->CanCastSpell("avenging wrath", bot)) - { - if (botAI->CastSpell("avenging wrath", bot)) - casted = true; - } - if (botAI->CanCastSpell("consecration", bot)) - { - if (botAI->CastSpell("consecration", bot)) - casted = true; - } - if (botAI->CanCastSpell("exorcism", innerDemon)) - { - if (botAI->CastSpell("exorcism", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("hammer of wrath", innerDemon)) - { - if (botAI->CastSpell("hammer of wrath", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("holy shock", innerDemon)) - { - if (botAI->CastSpell("holy shock", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("judgment of light", innerDemon)) - { - if (botAI->CastSpell("judgment of light", innerDemon)) - casted = true; - } + + if (botAI->CanCastSpell("avenging wrath", bot) && + botAI->CastSpell("avenging wrath", bot)) + casted = true; + + if (botAI->CanCastSpell("consecration", bot) && + botAI->CastSpell("consecration", bot)) + casted = true; + + if (botAI->CanCastSpell("exorcism", innerDemon) && + botAI->CastSpell("exorcism", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("hammer of wrath", innerDemon) && + botAI->CastSpell("hammer of wrath", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("holy shock", innerDemon) && + botAI->CastSpell("holy shock", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("judgement of light", innerDemon) && + botAI->CastSpell("judgement of light", innerDemon)) + casted = true; return casted; } @@ -1010,21 +957,18 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerD else if (bot->getClass() == CLASS_SHAMAN) { bool casted = false; - if (botAI->CanCastSpell("earth shock", innerDemon)) - { - if (botAI->CastSpell("earth shock", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("chain lightning", innerDemon)) - { - if (botAI->CastSpell("chain lightning", innerDemon)) - casted = true; - } - if (botAI->CanCastSpell("lightning bolt", innerDemon)) - { - if (botAI->CastSpell("lightning bolt", innerDemon)) - casted = true; - } + + if (botAI->CanCastSpell("earth shock", innerDemon) && + botAI->CastSpell("earth shock", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("chain lightning", innerDemon) && + botAI->CastSpell("chain lightning", innerDemon)) + casted = true; + + if (botAI->CanCastSpell("lightning bolt", innerDemon) && + botAI->CastSpell("lightning bolt", innerDemon)) + casted = true; return casted; } @@ -1035,33 +979,39 @@ bool LeotherasTheBlindDestroyInnerDemonAction::HandleHealerStrategy(Unit* innerD // Everybody except the Warlock tank should focus on Leotheras in Phase 3 bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/) { - Unit* leotherasHuman = GetLeotherasHuman(botAI); + Unit* leotherasHuman = GetLeotherasHuman(bot); if (!leotherasHuman) return false; - MarkTargetWithStar(bot, leotherasHuman); + if (MarkTargetWithStar(bot, leotherasHuman)) + return true; + SetRtiTarget(botAI, "star", leotherasHuman); - if (bot->GetTarget() != leotherasHuman->GetGUID()) + if (AI_VALUE(Unit*, "current target") != leotherasHuman) return Attack(leotherasHuman); - Unit* leotherasDemon = GetPhase3LeotherasDemon(botAI); - if (leotherasDemon) - { - if (leotherasHuman->GetVictim() == bot) - { - Unit* demonTarget = leotherasDemon->GetVictim(); - if (demonTarget && leotherasHuman->GetExactDist2d(demonTarget) < 20.0f) - { - float angle = atan2(bot->GetPositionY() - demonTarget->GetPositionY(), - bot->GetPositionX() - demonTarget->GetPositionX()); - float targetX = bot->GetPositionX() + 25.0f * std::cos(angle); - float targetY = bot->GetPositionY() + 25.0f * std::sin(angle); + Unit* leotherasDemon = GetPhase3LeotherasDemon(bot); + if (!leotherasDemon) + return false; - return MoveTo(SSC_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, - false, false, MovementPriority::MOVEMENT_FORCED, true, false); - } - } + if (leotherasHuman->GetVictim() != bot) + return false; + + Unit* demonTarget = leotherasDemon->GetVictim(); + if (!demonTarget) + return false; + + constexpr float safeDistanceFromDemon = 20.0f; + if (leotherasHuman->GetExactDist2d(demonTarget) < safeDistanceFromDemon) + { + float angle = atan2(bot->GetPositionY() - demonTarget->GetPositionY(), + bot->GetPositionX() - demonTarget->GetPositionX()); + float targetX = bot->GetPositionX() + safeDistanceFromDemon * std::cos(angle); + float targetY = bot->GetPositionY() + safeDistanceFromDemon * std::sin(angle); + + return MoveTo(SSC_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_FORCED, true, false); } return false; @@ -1070,28 +1020,13 @@ bool LeotherasTheBlindFinalPhaseAssignDpsPriorityAction::Execute(Event /*event*/ // Misdirect to Warlock tank or to main tank if there is no Warlock tank bool LeotherasTheBlindMisdirectBossToDemonFormTankAction::Execute(Event /*event*/) { - Unit* leotherasDemon = GetActiveLeotherasDemon(botAI); + Unit* leotherasDemon = GetActiveLeotherasDemon(bot); if (!leotherasDemon) return false; - Player* demonFormTank = GetLeotherasDemonFormTank(bot); - Player* targetTank = demonFormTank; - + Player* targetTank = GetLeotherasDemonFormTank(bot); if (!targetTank) - { - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - targetTank = member; - break; - } - } - } - } + targetTank = GetGroupMainTank(botAI, bot); if (!targetTank) return false; @@ -1117,43 +1052,37 @@ bool LeotherasTheBlindManageDpsWaitTimersAction::Execute(Event /*event*/) bool changed = false; // Encounter start/reset: clear all timers - if (leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + if (leotheras->HasAura(SPELL_LEOTHERAS_BANISHED) && + (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0 || + leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0 || + leotherasFinalPhaseDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (leotherasFinalPhaseDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } // Human Phase - Unit* leotherasHuman = GetLeotherasHuman(botAI); - Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); - if (leotherasHuman && !leotherasPhase3Demon) + Unit* leotherasHuman = GetLeotherasHuman(bot); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(bot); + if (leotherasHuman && !leotherasPhase3Demon && + (leotherasHumanFormDpsWaitTimer.try_emplace(instanceId, now).second || + leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasHumanFormDpsWaitTimer.try_emplace(instanceId, now).second) - changed = true; - if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } // Demon Phase - else if (Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI)) + else if (GetPhase2LeotherasDemon(bot) && + (leotherasDemonFormDpsWaitTimer.try_emplace(instanceId, now).second || + leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasDemonFormDpsWaitTimer.try_emplace(instanceId, now).second) - changed = true; - if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } // Final Phase (<15% HP) - else if (leotherasHuman && leotherasPhase3Demon) + else if (leotherasHuman && leotherasPhase3Demon && + (leotherasFinalPhaseDpsWaitTimer.try_emplace(instanceId, now).second || + leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0 || + leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0)) { - if (leotherasFinalPhaseDpsWaitTimer.try_emplace(instanceId, now).second) - changed = true; - if (leotherasHumanFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; - if (leotherasDemonFormDpsWaitTimer.erase(instanceId) > 0) - changed = true; + changed = true; } return changed; @@ -1170,10 +1099,12 @@ bool FathomLordKarathressMainTankPositionBossAction::Execute(Event /*event*/) if (!karathress) return false; - MarkTargetWithTriangle(bot, karathress); + if (MarkTargetWithTriangle(bot, karathress)) + return true; + SetRtiTarget(botAI, "triangle", karathress); - if (bot->GetTarget() != karathress->GetGUID()) + if (AI_VALUE(Unit*, "current target") != karathress) return Attack(karathress); if (karathress->GetVictim() == bot && bot->IsWithinMeleeRange(karathress)) @@ -1206,10 +1137,12 @@ bool FathomLordKarathressFirstAssistTankPositionCaribdisAction::Execute(Event /* if (!caribdis) return false; - MarkTargetWithDiamond(bot, caribdis); + if (MarkTargetWithDiamond(bot, caribdis)) + return true; + SetRtiTarget(botAI, "diamond", caribdis); - if (bot->GetTarget() != caribdis->GetGUID()) + if (AI_VALUE(Unit*, "current target") != caribdis) return Attack(caribdis); if (caribdis->GetVictim() == bot) @@ -1241,10 +1174,12 @@ bool FathomLordKarathressSecondAssistTankPositionSharkkisAction::Execute(Event / if (!sharkkis) return false; - MarkTargetWithStar(bot, sharkkis); + if (MarkTargetWithStar(bot, sharkkis)) + return true; + SetRtiTarget(botAI, "star", sharkkis); - if (bot->GetTarget() != sharkkis->GetGUID()) + if (AI_VALUE(Unit*, "current target") != sharkkis) return Attack(sharkkis); if (sharkkis->GetVictim() == bot && bot->IsWithinMeleeRange(sharkkis)) @@ -1276,10 +1211,12 @@ bool FathomLordKarathressThirdAssistTankPositionTidalvessAction::Execute(Event / if (!tidalvess) return false; - MarkTargetWithCircle(bot, tidalvess); + if (MarkTargetWithCircle(bot, tidalvess)) + return true; + SetRtiTarget(botAI, "circle", tidalvess); - if (bot->GetTarget() != tidalvess->GetGUID()) + if (AI_VALUE(Unit*, "current target") != tidalvess) return Attack(tidalvess); if (tidalvess->GetVictim() == bot && bot->IsWithinMeleeRange(tidalvess)) @@ -1367,41 +1304,17 @@ bool FathomLordKarathressMisdirectBossesToTanksAction::Execute(Event /*event*/) if (hunterIndex == 0) { bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 0, false)) - { - tankTarget = member; - break; - } - } + tankTarget = GetGroupAssistTank(botAI, bot, 0); } else if (hunterIndex == 1) { bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 2, false)) - { - tankTarget = member; - break; - } - } + tankTarget = GetGroupAssistTank(botAI, bot, 2); } else if (hunterIndex == 2) { bossTarget = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsAssistTankOfIndex(member, 1, false)) - { - tankTarget = member; - break; - } - } + tankTarget = GetGroupAssistTank(botAI, bot, 1); } if (!bossTarget || !tankTarget) @@ -1424,10 +1337,12 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* totem = GetFirstAliveUnitByEntry(botAI, NPC_SPITFIRE_TOTEM); if (totem && botAI->IsMelee(bot) && botAI->IsDps(bot)) { - MarkTargetWithSkull(bot, totem); + if (MarkTargetWithSkull(bot, totem)) + return true; + SetRtiTarget(botAI, "skull", totem); - if (bot->GetTarget() != totem->GetGUID()) + if (AI_VALUE(Unit*, "current target") != totem) return Attack(totem); // Direct movement order due to path between Sharkkis and totem sometimes being screwy @@ -1445,10 +1360,12 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* tidalvess = AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); if (tidalvess) { - MarkTargetWithCircle(bot, tidalvess); + if (MarkTargetWithCircle(bot, tidalvess)) + return true; + SetRtiTarget(botAI, "circle", tidalvess); - if (bot->GetTarget() != tidalvess->GetGUID()) + if (AI_VALUE(Unit*, "current target") != tidalvess) return Attack(tidalvess); return false; @@ -1458,7 +1375,9 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); if (botAI->IsRangedDps(bot) && caribdis) { - MarkTargetWithDiamond(bot, caribdis); + if (MarkTargetWithDiamond(bot, caribdis)) + return true; + SetRtiTarget(botAI, "diamond", caribdis); const Position& position = CARIBDIS_RANGED_DPS_POSITION; @@ -1468,7 +1387,7 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) position.GetPositionZ(), 8.0f, MovementPriority::MOVEMENT_COMBAT); } - if (bot->GetTarget() != caribdis->GetGUID()) + if (AI_VALUE(Unit*, "current target") != caribdis) return Attack(caribdis); return false; @@ -1478,10 +1397,12 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* sharkkis = AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); if (sharkkis) { - MarkTargetWithStar(bot, sharkkis); + if (MarkTargetWithStar(bot, sharkkis)) + return true; + SetRtiTarget(botAI, "star", sharkkis); - if (bot->GetTarget() != sharkkis->GetGUID()) + if (AI_VALUE(Unit*, "current target") != sharkkis) return Attack(sharkkis); return false; @@ -1491,10 +1412,12 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* fathomSporebat = AI_VALUE2(Unit*, "find target", "fathom sporebat"); if (fathomSporebat && botAI->IsMelee(bot)) { - MarkTargetWithCross(bot, fathomSporebat); + if (MarkTargetWithCross(bot, fathomSporebat)) + return true; + SetRtiTarget(botAI, "cross", fathomSporebat); - if (bot->GetTarget() != fathomSporebat->GetGUID()) + if (AI_VALUE(Unit*, "current target") != fathomSporebat) return Attack(fathomSporebat); return false; @@ -1503,10 +1426,12 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* fathomLurker = AI_VALUE2(Unit*, "find target", "fathom lurker"); if (fathomLurker && botAI->IsMelee(bot)) { - MarkTargetWithSquare(bot, fathomLurker); + if (MarkTargetWithSquare(bot, fathomLurker)) + return true; + SetRtiTarget(botAI, "square", fathomLurker); - if (bot->GetTarget() != fathomLurker->GetGUID()) + if (AI_VALUE(Unit*, "current target") != fathomLurker) return Attack(fathomLurker); return false; @@ -1516,10 +1441,12 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); if (karathress) { - MarkTargetWithTriangle(bot, karathress); + if (MarkTargetWithTriangle(bot, karathress)) + return true; + SetRtiTarget(botAI, "triangle", karathress); - if (bot->GetTarget() != karathress->GetGUID()) + if (AI_VALUE(Unit*, "current target") != karathress) return Attack(karathress); } @@ -1529,11 +1456,9 @@ bool FathomLordKarathressAssignDpsPriorityAction::Execute(Event /*event*/) bool FathomLordKarathressManageDpsTimerAction::Execute(Event /*event*/) { Unit* karathress = AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); - if (!karathress) - return false; - - karathressDpsWaitTimer.try_emplace( - karathress->GetMap()->GetInstanceId(), std::time(nullptr)); + if (karathress && karathressDpsWaitTimer.try_emplace( + karathress->GetMap()->GetInstanceId(), std::time(nullptr)).second) + return true; return false; } @@ -1546,20 +1471,7 @@ bool MorogrimTidewalkerMisdirectBossToMainTankAction::Execute(Event /*event*/) if (!tidewalker) return false; - Player* mainTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); if (!mainTank) return false; @@ -1580,22 +1492,22 @@ bool MorogrimTidewalkerMoveBossToTankPositionAction::Execute(Event /*event*/) if (!tidewalker) return false; - if (bot->GetTarget() != tidewalker->GetGUID()) + if (AI_VALUE(Unit*, "current target") != tidewalker) return Attack(tidewalker); if (tidewalker->GetVictim() == bot && bot->IsWithinMeleeRange(tidewalker)) { if (tidewalker->GetHealthPct() > 26.0f) - return MoveToPhase1TankPosition(tidewalker); + return MoveToPhase1TankPosition(); else - return MoveToPhase2TankPosition(tidewalker); + return MoveToPhase2TankPosition(); } return false; } // Phase 1: tank position is up against the Northeast pillar -bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition(Unit* tidewalker) +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition() { const Position& phase1 = TIDEWALKER_PHASE_1_TANK_POSITION; float distToPhase1 = bot->GetExactDist2d(phase1.GetPositionX(), phase1.GetPositionY()); @@ -1615,7 +1527,7 @@ bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase1TankPosition(Un } // Phase 2: move in two steps to get around the pillar and back up into the Northeast corner -bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase2TankPosition(Unit* tidewalker) +bool MorogrimTidewalkerMoveBossToTankPositionAction::MoveToPhase2TankPosition() { const Position& phase2 = TIDEWALKER_PHASE_2_TANK_POSITION; const Position& transition = TIDEWALKER_PHASE_TRANSITION_WAYPOINT; @@ -1730,7 +1642,7 @@ bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) if (!vashj) return false; - if (bot->GetTarget() != vashj->GetGUID()) + if (AI_VALUE(Unit*, "current target") != vashj) return Attack(vashj); if (vashj->GetVictim() == bot && bot->IsWithinMeleeRange(vashj)) @@ -1757,8 +1669,7 @@ bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) // Phase 3: No fixed position, but move Vashj away from Enchanted Elementals else if (IsLadyVashjInPhase3(botAI)) { - Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); - if (enchanted) + if (Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental")) { float currentDistance = bot->GetExactDist2d(enchanted); constexpr float safeDistance = 10.0f; @@ -1775,70 +1686,52 @@ bool LadyVashjMainTankPositionBossAction::Execute(Event /*event*/) bool LadyVashjPhase1SpreadRangedInArcAction::Execute(Event /*event*/) { std::vector spreadMembers; - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && GET_PLAYERBOT_AI(member)) - { - if (botAI->IsRanged(member)) - spreadMembers.push_back(member); - } - } + Player* member = ref->GetSource(); + if (member && GET_PLAYERBOT_AI(member) && botAI->IsRanged(member)) + spreadMembers.push_back(member); } const ObjectGuid guid = bot->GetGUID(); - - auto itPos = vashjRangedPositions.find(guid); auto itReached = hasReachedVashjRangedPosition.find(guid); - if (itPos == vashjRangedPositions.end()) - { - auto it = std::find(spreadMembers.begin(), spreadMembers.end(), bot); - size_t botIndex = (it != spreadMembers.end()) ? - std::distance(spreadMembers.begin(), it) : 0; - size_t count = spreadMembers.size(); - if (count == 0) - return false; - const Position& center = VASHJ_PLATFORM_CENTER_POSITION; - constexpr float minRadius = 20.0f; - constexpr float maxRadius = 30.0f; - - constexpr float arcCenter = M_PI / 2.0f; // North - constexpr float arcSpan = M_PI; // 180° - constexpr float arcStart = arcCenter - arcSpan / 2.0f; - - float angle; - if (count == 1) - angle = arcCenter; - else - angle = arcStart + (static_cast(botIndex) / (count - 1)) * arcSpan; - - float radius = frand(minRadius, maxRadius); - float targetX = center.GetPositionX() + radius * std::cos(angle); - float targetY = center.GetPositionY() + radius * std::sin(angle); - - auto res = vashjRangedPositions.try_emplace(guid, Position(targetX, targetY, center.GetPositionZ())); - itPos = res.first; - hasReachedVashjRangedPosition.try_emplace(guid, false); - itReached = hasReachedVashjRangedPosition.find(guid); - } - - if (itPos == vashjRangedPositions.end()) + auto it = std::find(spreadMembers.begin(), spreadMembers.end(), bot); + size_t botIndex = (it != spreadMembers.end()) ? + std::distance(spreadMembers.begin(), it) : 0; + size_t count = spreadMembers.size(); + if (count == 0) return false; - Position position = itPos->second; + constexpr float arcCenter = M_PI / 2.0f; // North + constexpr float arcSpan = M_PI; // 180° + constexpr float arcStart = arcCenter - arcSpan / 2.0f; + + float angle; + if (count == 1) + angle = arcCenter; + else + angle = arcStart + (static_cast(botIndex) / (count - 1)) * arcSpan; + + const Position& center = VASHJ_PLATFORM_CENTER_POSITION; + float radius = 25.0f; + float targetX = center.GetPositionX() + radius * std::cos(angle); + float targetY = center.GetPositionY() + radius * std::sin(angle); + float targetZ = center.GetPositionZ(); + if (itReached == hasReachedVashjRangedPosition.end() || !(itReached->second)) { - if (bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()) > 2.0f) + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) { - return MoveTo(SSC_MAP_ID, position.GetPositionX(), position.GetPositionY(), - position.GetPositionZ(), false, false, false, false, + hasReachedVashjRangedPosition.try_emplace(guid, false); + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); } - if (itReached != hasReachedVashjRangedPosition.end()) - itReached->second = true; + hasReachedVashjRangedPosition[guid] = true; } return false; @@ -1847,37 +1740,19 @@ bool LadyVashjPhase1SpreadRangedInArcAction::Execute(Event /*event*/) // For absorbing Shock Burst bool LadyVashjSetGroundingTotemInMainTankGroupAction::Execute(Event /*event*/) { - Player* mainTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); if (!mainTank) return false; - if (bot->GetExactDist2d(mainTank) > 25.0f) - { - return MoveInside(SSC_MAP_ID, mainTank->GetPositionX(), mainTank->GetPositionY(), - mainTank->GetPositionZ(), 20.0f, MovementPriority::MOVEMENT_COMBAT); - } + if (mainTank->HasAura(SPELL_GROUNDING_TOTEM_EFFECT)) + return false; - if (!botAI->HasStrategy("grounding", BotState::BOT_STATE_COMBAT)) - botAI->ChangeStrategy("+grounding", BotState::BOT_STATE_COMBAT); + constexpr float distFromTank = 25.0f; + if (bot->GetDistance(mainTank) > distFromTank) + return MoveTo(mainTank, distFromTank, MovementPriority::MOVEMENT_COMBAT); - if (!bot->HasAura(SPELL_GROUNDING_TOTEM_EFFECT) && - botAI->CanCastSpell("grounding totem", bot)) - return botAI->CastSpell("grounding totem", bot); - - return false; + return botAI->CanCastSpell("grounding totem", bot) && + botAI->CastSpell("grounding totem", bot); } bool LadyVashjMisdirectBossToMainTankAction::Execute(Event /*event*/) @@ -1886,20 +1761,7 @@ bool LadyVashjMisdirectBossToMainTankAction::Execute(Event /*event*/) if (!vashj) return false; - Player* mainTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member)) - { - mainTank = member; - break; - } - } - } - + Player* mainTank = GetGroupMainTank(botAI, bot); if (!mainTank) return false; @@ -1919,19 +1781,8 @@ bool LadyVashjStaticChargeMoveAwayFromGroupAction::Execute(Event /*event*/) return false; // If the main tank has Static Charge, other group members should move away - Player* mainTank = nullptr; - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->IsAlive() && botAI->IsMainTank(member) && - member->HasAura(SPELL_STATIC_CHARGE)) - { - mainTank = member; - break; - } - } - - if (mainTank && bot != mainTank) + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && bot != mainTank && mainTank->HasAura(SPELL_STATIC_CHARGE)) { float currentDistance = bot->GetExactDist2d(mainTank); constexpr float safeDistance = 11.0f; @@ -1979,7 +1830,7 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) } auto const& attackers = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); Unit* target = nullptr; Unit* enchanted = nullptr; Unit* elite = nullptr; @@ -2019,7 +1870,7 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) break; case NPC_TOXIC_SPOREBAT: - if (!sporebat || bot->GetExactDist2d(unit) < bot->GetExactDist2d(sporebat)) + if (!sporebat || bot->GetDistance(unit) < bot->GetDistance(sporebat)) sporebat = unit; break; @@ -2063,14 +1914,16 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) { if (botAI->IsMainTank(bot)) { - MarkTargetWithDiamond(bot, vashj); + if (MarkTargetWithDiamond(bot, vashj)) + return true; + SetRtiTarget(botAI, "diamond", vashj); targets = { vashj }; } - else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + else if (botAI->HasCheat(BotCheatMask::raid) && + botAI->IsAssistTankOfIndex(bot, 0, true)) { - if (botAI->HasCheat(BotCheatMask::raid)) - targets = { strider, elite, enchanted, vashj }; + targets = { strider, elite, enchanted, vashj }; } else targets = { elite, strider, enchanted, vashj }; @@ -2110,26 +1963,13 @@ bool LadyVashjAssignPhase2AndPhase3DpsPriorityAction::Execute(Event /*event*/) currentTarget = nullptr; } - if (target && currentTarget != target && bot->GetTarget() != target->GetGUID()) + if (target && currentTarget != target && + AI_VALUE(Unit*, "current target") != target) return Attack(target); // If bots have wandered too far from the center, move them back - if (bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > 55.0f) - { - Player* designatedLooter = GetDesignatedCoreLooter(bot->GetGroup(), botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(bot->GetGroup(), botAI); - // A bot will not move back to the middle if (1) there is a Tainted Elemental, and - // (2) the bot is either the designated looter or the first core passer - if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) - { - if ((designatedLooter && designatedLooter == bot) || - (firstCorePasser && firstCorePasser == bot)) - return false; - } - - return MoveInside(SSC_MAP_ID, center.GetPositionX(), center.GetPositionY(), - center.GetPositionZ(), 40.0f, MovementPriority::MOVEMENT_COMBAT); - } + if (bot->GetExactDist2d(vashj) > maxPursueRange) + return MoveTo(vashj, maxPursueRange - 10.0f, MovementPriority::MOVEMENT_FORCED); return false; } @@ -2144,24 +1984,11 @@ bool LadyVashjMisdirectStriderToFirstAssistTankAction::Execute(Event /*event*/) if (bot->getClass() != CLASS_HUNTER) return false; - Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + Unit* strider = AI_VALUE2(Unit*, "find target", "coilfang strider"); if (!strider) return false; - Player* firstAssistTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && botAI->IsAssistTankOfIndex(member, 0, true)) - { - firstAssistTank = member; - break; - } - } - } - + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); if (!firstAssistTank || strider->GetVictim() == firstAssistTank) return false; @@ -2180,7 +2007,7 @@ bool LadyVashjTankAttackAndMoveAwayStriderAction::Execute(Event /*event*/) if (!vashj) return false; - Unit* strider = GetFirstAliveUnitByEntry(botAI, NPC_COILFANG_STRIDER); + Unit* strider = AI_VALUE2(Unit*, "find target", "coilfang strider"); if (!strider) return false; @@ -2193,52 +2020,41 @@ bool LadyVashjTankAttackAndMoveAwayStriderAction::Execute(Event /*event*/) bot->AddAura(SPELL_FEAR_WARD, bot); if (botAI->IsAssistTankOfIndex(bot, 0, true) && - bot->GetTarget() != strider->GetGUID()) + AI_VALUE(Unit*, "current target") != strider) return Attack(strider); - if (strider->GetVictim() == bot) - { - float currentDistance = bot->GetExactDist2d(vashj); - constexpr float safeDistance = 28.0f; - - if (currentDistance < safeDistance) - return MoveAway(vashj, safeDistance - currentDistance); - } + float currentDistance = bot->GetExactDist2d(vashj); + constexpr float safeDistance = 28.0f; + if (strider->GetVictim() == bot && currentDistance < safeDistance) + return MoveAway(vashj, safeDistance - currentDistance); return false; } // Don't move away if raid cheats are enabled, or in any case if the bot is a tank - if (!botAI->HasCheat(BotCheatMask::raid) || !botAI->IsTank(bot)) + if (!botAI->HasCheat(BotCheatMask::raid)) { float currentDistance = bot->GetExactDist2d(strider); constexpr float safeDistance = 20.0f; - if (currentDistance < safeDistance) + if (!botAI->IsTank(bot) && currentDistance < safeDistance) return MoveAway(strider, safeDistance - currentDistance); - } - - // Try to root/slow the Strider if it is not tankable (poor man's kiting strategy) - if (!botAI->HasCheat(BotCheatMask::raid)) - { - if (!strider->HasAura(SPELL_HEAVY_NETHERWEAVE_NET)) - { - Item* net = bot->GetItemByEntry(ITEM_HEAVY_NETHERWEAVE_NET); - if (net && botAI->HasItemInInventory(ITEM_HEAVY_NETHERWEAVE_NET) && - botAI->CanCastSpell("heavy netherweave net", strider)) - return botAI->CastSpell("heavy netherweave net", strider); - } + // Try to root/slow the Strider if it is not tankable (poor man's kiting strategy) if (!botAI->HasAura("frost shock", strider) && bot->getClass() == CLASS_SHAMAN && botAI->CanCastSpell("frost shock", strider)) + { return botAI->CastSpell("frost shock", strider); - - if (!strider->HasAura(SPELL_CURSE_OF_EXHAUSTION) && bot->getClass() == CLASS_WARLOCK && - botAI->CanCastSpell("curse of exhaustion", strider)) + } + else if (!strider->HasAura(SPELL_CURSE_OF_EXHAUSTION) && bot->getClass() == CLASS_WARLOCK && + botAI->CanCastSpell("curse of exhaustion", strider)) + { return botAI->CastSpell("curse of exhaustion", strider); - - if (!strider->HasAura(SPELL_SLOW) && bot->getClass() == CLASS_MAGE && - botAI->CanCastSpell("slow", strider)) + } + else if (!strider->HasAura(SPELL_SLOW) && bot->getClass() == CLASS_MAGE && + botAI->CanCastSpell("slow", strider)) + { return botAI->CastSpell("slow", strider); + } } return false; @@ -2252,7 +2068,7 @@ bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) if (!tainted) return false; - if (bot->GetExactDist2d(tainted) >= 10.0f) + if (bot->GetExactDist2d(tainted) > 10.0f) { bot->AttackStop(); bot->InterruptNonMeleeSpells(true); @@ -2260,9 +2076,11 @@ bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) tainted->GetPositionZ(), tainted->GetOrientation()); } - if (bot->GetTarget() != tainted->GetGUID()) + if (AI_VALUE(Unit*, "current target") != tainted) { - MarkTargetWithStar(bot, tainted); + if (MarkTargetWithStar(bot, tainted)) + return true; + SetRtiTarget(botAI, "star", tainted); return Attack(tainted); } @@ -2276,87 +2094,7 @@ bool LadyVashjTeleportToTaintedElementalAction::Execute(Event /*event*/) return false; } -bool LadyVashjLootTaintedCoreAction::Execute(Event) -{ - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - if (!vashj) - return false; - - auto const& corpses = context->GetValue("nearest corpses")->Get(); - const float maxLootRange = sPlayerbotAIConfig.lootDistance; - - for (auto const& guid : corpses) - { - LootObject loot(bot, guid); - if (!loot.IsLootPossible(bot)) - continue; - - WorldObject* object = loot.GetWorldObject(bot); - if (!object) - continue; - - Creature* creature = object->ToCreature(); - if (!creature || creature->GetEntry() != NPC_TAINTED_ELEMENTAL || creature->IsAlive()) - continue; - - context->GetValue("loot target")->Set(loot); - - float dist = bot->GetDistance(object); - if (dist > maxLootRange) - return MoveTo(object, 2.0f, MovementPriority::MOVEMENT_FORCED); - - OpenLootAction open(botAI); - if (!open.Execute(Event())) - return false; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) - return true; - } - } - - const ObjectGuid botGuid = bot->GetGUID(); - const ObjectGuid corpseGuid = guid; - constexpr uint8 coreIndex = 0; - - botAI->AddTimedEvent([botGuid, corpseGuid, coreIndex, vashj]() - { - Player* receiver = botGuid.IsEmpty() ? nullptr : ObjectAccessor::FindPlayer(botGuid); - if (!receiver) - return; - - if (Group* group = receiver->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) - return; - } - } - - receiver->SetLootGUID(corpseGuid); - - WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); - *packet << coreIndex; - receiver->GetSession()->QueuePacket(packet); - - const uint32 instanceId = vashj->GetMap()->GetInstanceId(); - const time_t now = std::time(nullptr); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - }, 600); - - return true; - } - - return false; -} - -bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) +bool LadyVashjLootTaintedCoreAction::Execute(Event /*event*/) { Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); if (!vashj) @@ -2366,36 +2104,78 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) if (!group) return false; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - if (!designatedLooter) + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return false; + } + + constexpr float searchRadius = 150.0f; + Creature* elemental = bot->FindNearestCreature(NPC_TAINTED_ELEMENTAL, searchRadius, false); + + if (!elemental || elemental->IsAlive()) return false; - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); - Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + LootObject loot(bot, elemental->GetGUID()); + if (!loot.IsLootPossible(bot)) + return false; + + context->GetValue("loot target")->Set(loot); + + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + constexpr float distFromObject = 2.0f; + + if (bot->GetDistance(elemental) > maxLootRange) + return MoveTo(elemental, distFromObject, MovementPriority::MOVEMENT_FORCED); + + OpenLootAction open(botAI); + if (!open.Execute(Event())) + return false; + + bot->SetLootGUID(elemental->GetGUID()); + constexpr uint8 coreIndex = 0; + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << coreIndex; + bot->GetSession()->QueuePacket(packet); + + const time_t now = std::time(nullptr); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + + return true; +} + +bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) +{ + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); + Player* secondCorePasser = GetSecondTaintedCorePasser(botAI, bot); + Player* thirdCorePasser = GetThirdTaintedCorePasser(botAI, bot); + Player* fourthCorePasser = GetFourthTaintedCorePasser(botAI, bot); + const uint32 instanceId = vashj->GetMap()->GetInstanceId(); Unit* closestTrigger = nullptr; - if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental")) + if (Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + (closestTrigger = GetNearestActiveShieldGeneratorTriggerByEntry(tainted))) { - closestTrigger = GetNearestActiveShieldGeneratorTriggerByEntry(tainted); - if (closestTrigger) - nearestTriggerGuid.insert_or_assign(instanceId, closestTrigger->GetGUID()); + nearestTriggerGuid.try_emplace(instanceId, closestTrigger->GetGUID()); } auto itSnap = nearestTriggerGuid.find(instanceId); if (itSnap != nearestTriggerGuid.end() && !itSnap->second.IsEmpty()) { - Unit* snapUnit = botAI->GetUnit(itSnap->second); - if (snapUnit) + if (Unit* snapUnit = botAI->GetUnit(itSnap->second)) closestTrigger = snapUnit; else nearestTriggerGuid.erase(instanceId); } - if (!firstCorePasser || !secondCorePasser || !thirdCorePasser || - !fourthCorePasser || !closestTrigger) + if (!closestTrigger) return false; // Not gated behind CheatMask because the auto application of Fear Ward is necessary @@ -2408,63 +2188,57 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) if (!item || !botAI->HasItemInInventory(ITEM_TAINTED_CORE)) { // Passer order: HealAssistantOfIndex 0, 1, 2, then RangedDpsAssistantOfIndex 0 - if (bot == firstCorePasser) + if (bot == firstCorePasser && + LineUpFirstCorePasser(designatedLooter)) { - if (LineUpFirstCorePasser(designatedLooter, closestTrigger)) - return true; + return true; } - else if (bot == secondCorePasser) + else if (bot == secondCorePasser && + LineUpSecondCorePasser(firstCorePasser, closestTrigger)) { - if (LineUpSecondCorePasser(firstCorePasser, closestTrigger)) - return true; + return true; } - else if (bot == thirdCorePasser) + else if (bot == thirdCorePasser && LineUpThirdCorePasser( + designatedLooter, firstCorePasser, secondCorePasser, closestTrigger)) { - if (LineUpThirdCorePasser(designatedLooter, firstCorePasser, - secondCorePasser, closestTrigger)) - return true; + return true; } - else if (bot == fourthCorePasser) + else if (bot == fourthCorePasser && LineUpFourthCorePasser( + firstCorePasser, secondCorePasser, thirdCorePasser, closestTrigger)) { - if (LineUpFourthCorePasser(firstCorePasser, secondCorePasser, - thirdCorePasser, closestTrigger)) - return true; + return true; } } else if (item && botAI->HasItemInInventory(ITEM_TAINTED_CORE)) { // Designated core looter logic // Applicable only if cheat mode is on and thus looter is a bot - if (bot == designatedLooter) - { - if (IsFirstCorePasserInIntendedPosition( - designatedLooter, firstCorePasser, closestTrigger)) - { - const time_t now = std::time(nullptr); - auto it = lastImbueAttempt.find(instanceId); - if (it == lastImbueAttempt.end() || (now - it->second) >= 2) - { - lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - botAI->ImbueItem(item, firstCorePasser); - intendedLineup.erase(bot->GetGUID()); - ScheduleTransferCoreAfterImbue(botAI, bot, firstCorePasser); - return true; - } - } - } - // First core passer: receive core from looter at the top of the stairs, - // pass to second core passer - else if (bot == firstCorePasser) + if (bot == designatedLooter && + IsFirstCorePasserInPosition(firstCorePasser)) + { + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) + { + lastImbueAttempt.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, firstCorePasser); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + ScheduleTransferCoreAfterImbue(botAI, bot, firstCorePasser); + return true; + } + } + // First core passer: receive core from looter at the top of the stairs, + // pass to second core passer + else if (bot == firstCorePasser && + IsSecondCorePasserInPosition(secondCorePasser)) { const time_t now = std::time(nullptr); auto it = lastImbueAttempt.find(instanceId); if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); botAI->ImbueItem(item, secondCorePasser); - intendedLineup.erase(bot->GetGUID()); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); ScheduleTransferCoreAfterImbue(botAI, bot, secondCorePasser); return true; } @@ -2472,76 +2246,72 @@ bool LadyVashjPassTheTaintedCoreAction::Execute(Event /*event*/) // Second core passer: if closest usable generator is within passing distance // of the first passer, move to the generator; otherwise, move as close as // possible to the generator while staying in passing range - else if (bot == secondCorePasser) + else if (bot == secondCorePasser && !UseCoreOnNearestGenerator(instanceId) && + IsThirdCorePasserInPosition(thirdCorePasser)) { - if (!UseCoreOnNearestGenerator(instanceId)) + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { - if (IsThirdCorePasserInIntendedPosition( - secondCorePasser, thirdCorePasser, closestTrigger)) - { - const time_t now = std::time(nullptr); - auto it = lastImbueAttempt.find(instanceId); - if (it == lastImbueAttempt.end() || (now - it->second) >= 2) - { - lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - botAI->ImbueItem(item, thirdCorePasser); - intendedLineup.erase(bot->GetGUID()); - ScheduleTransferCoreAfterImbue(botAI, bot, thirdCorePasser); - return true; - } - } + lastImbueAttempt.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, thirdCorePasser); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + ScheduleTransferCoreAfterImbue(botAI, bot, thirdCorePasser); + return true; } } // Third core passer: if closest usable generator is within passing distance // of the second passer, move to the generator; otherwise, move as close as // possible to the generator while staying in passing range - else if (bot == thirdCorePasser) + else if (bot == thirdCorePasser && !UseCoreOnNearestGenerator(instanceId) && + IsFourthCorePasserInPosition(fourthCorePasser)) { - if (!UseCoreOnNearestGenerator(instanceId)) + const time_t now = std::time(nullptr); + auto it = lastImbueAttempt.find(instanceId); + if (it == lastImbueAttempt.end() || (now - it->second) >= 2) { - if (IsFourthCorePasserInIntendedPosition( - thirdCorePasser, fourthCorePasser, closestTrigger)) - { - const time_t now = std::time(nullptr); - auto it = lastImbueAttempt.find(instanceId); - if (it == lastImbueAttempt.end() || (now - it->second) >= 2) - { - lastImbueAttempt.insert_or_assign(instanceId, now); - lastCoreInInventoryTime.insert_or_assign(instanceId, now); - botAI->ImbueItem(item, fourthCorePasser); - intendedLineup.erase(bot->GetGUID()); - ScheduleTransferCoreAfterImbue(botAI, bot, fourthCorePasser); - return true; - } - } + lastImbueAttempt.insert_or_assign(instanceId, now); + botAI->ImbueItem(item, fourthCorePasser); + lastCoreInInventoryTime.insert_or_assign(bot->GetGUID(), now); + ScheduleTransferCoreAfterImbue(botAI, bot, fourthCorePasser); + return true; } } // Fourth core passer: the fourth passer is rarely needed and no more than // four ever should be, so it should use the Core on the nearest generator - else if (bot == fourthCorePasser) - UseCoreOnNearestGenerator(instanceId); + else if (bot == fourthCorePasser && UseCoreOnNearestGenerator(instanceId)) + return true; } return false; } bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( - Player* designatedLooter, Unit* closestTrigger) + Player* designatedLooter) { const float centerX = VASHJ_PLATFORM_CENTER_POSITION.GetPositionX(); const float centerY = VASHJ_PLATFORM_CENTER_POSITION.GetPositionY(); constexpr float radius = 57.5f; - float mx = designatedLooter->GetPositionX(); - float my = designatedLooter->GetPositionY(); - float angle = atan2(my - centerY, mx - centerX); + auto it = intendedLineup.find(bot->GetGUID()); + if (it == intendedLineup.end()) + { + float mx = designatedLooter->GetPositionX(); + float my = designatedLooter->GetPositionY(); + float angle = atan2(my - centerY, mx - centerX); - float targetX = centerX + radius * std::cos(angle); - float targetY = centerY + radius * std::sin(angle); - constexpr float targetZ = 41.097f; + float targetX = centerX + radius * std::cos(angle); + float targetY = centerY + radius * std::sin(angle); + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, targetZ)); + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + it = intendedLineup.find(bot->GetGUID()); + } + + const Position& pos = it->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); @@ -2552,144 +2322,181 @@ bool LadyVashjPassTheTaintedCoreAction::LineUpFirstCorePasser( bool LadyVashjPassTheTaintedCoreAction::LineUpSecondCorePasser( Player* firstCorePasser, Unit* closestTrigger) { - float fx = firstCorePasser->GetPositionX(); - float fy = firstCorePasser->GetPositionY(); - - float dx = closestTrigger->GetPositionX() - fx; - float dy = closestTrigger->GetPositionY() - fy; - float distToTrigger = firstCorePasser->GetExactDist2d(closestTrigger); - - if (distToTrigger == 0.0f) + auto itFirst = intendedLineup.find(firstCorePasser->GetGUID()); + if (itFirst == intendedLineup.end()) return false; - dx /= distToTrigger; dy /= distToTrigger; - - // Target is on a line between firstCorePasser and closestTrigger - float targetX, targetY, targetZ; - // If firstCorePasser is within thresholdDist of closestTrigger, - // go to nearTriggerDist short of closestTrigger - constexpr float thresholdDist = 40.0f; - constexpr float nearTriggerDist = 1.5f; - // If firstCorePasser is not thresholdDist yards from closestTrigger, - // go to farDistance from firstCorePasser - constexpr float farDistance = 38.0f; - - if (distToTrigger <= thresholdDist) + auto itSecond = intendedLineup.find(bot->GetGUID()); + if (itSecond == intendedLineup.end()) { - float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); - targetX = fx + dx * moveDist; - targetY = fy + dy * moveDist; - } - else - { - targetX = fx + dx * farDistance; - targetY = fy + dy * farDistance; + float fx = itFirst->second.GetPositionX(); + float fy = itFirst->second.GetPositionY(); + + float dx = closestTrigger->GetPositionX() - fx; + float dy = closestTrigger->GetPositionY() - fy; + float distToTrigger = std::sqrt(dx * dx + dy * dy); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + float targetX, targetY; + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = fx + dx * moveDist; + targetY = fy + dy * moveDist; + } + else + { + targetX = fx + dx * farDistance; + targetY = fy + dy * farDistance; + } + + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + itSecond = intendedLineup.find(bot->GetGUID()); } - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + const Position& pos = itSecond->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); } bool LadyVashjPassTheTaintedCoreAction::LineUpThirdCorePasser( - Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) + Player*, Player* firstCorePasser, + Player* secondCorePasser, Unit* closestTrigger) { - // Wait to move until it is clear that a third passer is needed - bool needThird = - (IsFirstCorePasserInIntendedPosition(designatedLooter, firstCorePasser, closestTrigger) && + bool needThirdPasser = + (IsFirstCorePasserInPosition(firstCorePasser) && firstCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || - (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + (IsSecondCorePasserInPosition(secondCorePasser) && secondCorePasser->GetExactDist2d(closestTrigger) > 4.0f); - if (!needThird) + if (!needThirdPasser) return false; - float sx = secondCorePasser->GetPositionX(); - float sy = secondCorePasser->GetPositionY(); - - float dx = closestTrigger->GetPositionX() - sx; - float dy = closestTrigger->GetPositionY() - sy; - float distToTrigger = secondCorePasser->GetExactDist2d(closestTrigger); - - if (distToTrigger == 0.0f) + auto itSecond = intendedLineup.find(secondCorePasser->GetGUID()); + if (itSecond == intendedLineup.end()) return false; - dx /= distToTrigger; dy /= distToTrigger; - - float targetX, targetY, targetZ; - constexpr float thresholdDist = 40.0f; - constexpr float nearTriggerDist = 1.5f; - constexpr float farDistance = 38.0f; - - if (distToTrigger <= thresholdDist) + auto itThird = intendedLineup.find(bot->GetGUID()); + if (itThird == intendedLineup.end()) { - float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); - targetX = sx + dx * moveDist; - targetY = sy + dy * moveDist; - } - else - { - targetX = sx + dx * farDistance; - targetY = sy + dy * farDistance; + float sx = itSecond->second.GetPositionX(); + float sy = itSecond->second.GetPositionY(); + + float dx = closestTrigger->GetPositionX() - sx; + float dy = closestTrigger->GetPositionY() - sy; + float distToTrigger = std::sqrt(dx * dx + dy * dy); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + float targetX, targetY; + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; + constexpr float thresholdDist = 40.0f; + constexpr float nearTriggerDist = 1.5f; + constexpr float farDistance = 38.0f; + + if (distToTrigger <= thresholdDist) + { + float moveDist = std::max(distToTrigger - nearTriggerDist, 0.0f); + targetX = sx + dx * moveDist; + targetY = sy + dy * moveDist; + } + else + { + targetX = sx + dx * farDistance; + targetY = sy + dy * farDistance; + } + + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + itThird = intendedLineup.find(bot->GetGUID()); } - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + const Position& pos = itThird->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); - - return false; } bool LadyVashjPassTheTaintedCoreAction::LineUpFourthCorePasser( - Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) + Player*, Player* secondCorePasser, + Player* thirdCorePasser, Unit* closestTrigger) { - // Wait to move until it is clear that a fourth passer is needed - bool needFourth = - (IsSecondCorePasserInIntendedPosition(firstCorePasser, secondCorePasser, closestTrigger) && + bool needFourthPasser = + (IsSecondCorePasserInPosition(secondCorePasser) && secondCorePasser->GetExactDist2d(closestTrigger) > 42.0f) || - (IsThirdCorePasserInIntendedPosition(secondCorePasser, thirdCorePasser, closestTrigger) && + (IsThirdCorePasserInPosition(thirdCorePasser) && thirdCorePasser->GetExactDist2d(closestTrigger) > 4.0f); - if (!needFourth) + if (!needFourthPasser) return false; - float sx = thirdCorePasser->GetPositionX(); - float sy = thirdCorePasser->GetPositionY(); - - float tx = closestTrigger->GetPositionX(); - float ty = closestTrigger->GetPositionY(); - - float dx = tx - sx; - float dy = ty - sy; - float distToTrigger = thirdCorePasser->GetExactDist2d(closestTrigger); - - if (distToTrigger == 0.0f) + auto itThird = intendedLineup.find(thirdCorePasser->GetGUID()); + if (itThird == intendedLineup.end()) return false; - dx /= distToTrigger; dy /= distToTrigger; + auto itFourth = intendedLineup.find(bot->GetGUID()); + if (itFourth == intendedLineup.end()) + { + float sx = itThird->second.GetPositionX(); + float sy = itThird->second.GetPositionY(); - constexpr float nearTriggerDist = 1.5f; - float targetX = tx - dx * nearTriggerDist; - float targetY = ty - dy * nearTriggerDist; + float tx = closestTrigger->GetPositionX(); + float ty = closestTrigger->GetPositionY(); - intendedLineup.insert_or_assign(bot->GetGUID(), Position(targetX, targetY, VASHJ_PLATFORM_Z)); + float dx = tx - sx; + float dy = ty - sy; + float distToTrigger = std::sqrt(dx * dx + dy * dy); + + if (distToTrigger == 0.0f) + return false; + + dx /= distToTrigger; dy /= distToTrigger; + + constexpr float nearTriggerDist = 1.5f; + float targetX = tx - dx * nearTriggerDist; + float targetY = ty - dy * nearTriggerDist; + constexpr float targetZ = VASHJ_PLATFORM_CENTER_Z; + + intendedLineup.try_emplace(bot->GetGUID(), Position(targetX, targetY, targetZ)); + itFourth = intendedLineup.find(bot->GetGUID()); + } + + const Position& pos = itFourth->second; + float targetX = pos.GetPositionX(); + float targetY = pos.GetPositionY(); + float targetZ = pos.GetPositionZ(); bot->AttackStop(); bot->InterruptNonMeleeSpells(true); - return MoveTo(SSC_MAP_ID, targetX, targetY, VASHJ_PLATFORM_Z, false, false, false, true, + return MoveTo(SSC_MAP_ID, targetX, targetY, targetZ, false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); } // The next four functions check if the respective passer is <= 2 yards of their intended // position and are used to determine when the prior bot in the chain can pass the core -bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInIntendedPosition( - Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInPosition(Player* firstCorePasser) { auto itSnap = intendedLineup.find(firstCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2702,8 +2509,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsFirstCorePasserInIntendedPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInIntendedPosition( - Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInPosition(Player* secondCorePasser) { auto itSnap = intendedLineup.find(secondCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2716,8 +2522,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsSecondCorePasserInIntendedPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInIntendedPosition( - Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInPosition(Player* thirdCorePasser) { auto itSnap = intendedLineup.find(thirdCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2730,8 +2535,7 @@ bool LadyVashjPassTheTaintedCoreAction::IsThirdCorePasserInIntendedPosition( return false; } -bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInIntendedPosition( - Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger) +bool LadyVashjPassTheTaintedCoreAction::IsFourthCorePasserInPosition(Player* fourthCorePasser) { auto itSnap = intendedLineup.find(fourthCorePasser->GetGUID()); if (itSnap != intendedLineup.end()) @@ -2799,17 +2603,11 @@ bool LadyVashjPassTheTaintedCoreAction::UseCoreOnNearestGenerator(const uint32 i return false; GameObject* generator = botAI->GetGameObject(nearestGen->guid); - if (!generator) - return false; - - if (bot->GetExactDist2d(generator) > 4.5f) + if (!generator || bot->GetExactDist2d(generator) > 4.5f) return false; Item* core = bot->GetItemByEntry(ITEM_TAINTED_CORE); - if (!core) - return false; - - if (bot->CanUseItem(core) != EQUIP_ERR_OK) + if (!core || bot->CanUseItem(core) != EQUIP_ERR_OK) return false; if (bot->IsNonMeleeSpellCast(false)) @@ -2845,9 +2643,18 @@ bool LadyVashjPassTheTaintedCoreAction::UseCoreOnNearestGenerator(const uint32 i packet << generator->GetGUID().WriteAsPacked(); bot->GetSession()->HandleUseItemOpcode(packet); - nearestTriggerGuid.erase(instanceId); + lastImbueAttempt.erase(instanceId); - lastCoreInInventoryTime.erase(instanceId); + auto coreHandlers = GetCoreHandlers(botAI, bot); + for (Player* handler : coreHandlers) + { + if (handler) + { + intendedLineup.erase(handler->GetGUID()); + lastCoreInInventoryTime.erase(handler->GetGUID()); + } + } + return true; } @@ -2864,35 +2671,12 @@ bool LadyVashjDestroyTaintedCoreAction::Execute(Event /*event*/) return false; } -// This needs to be separate from the general map erasing logic because -// Bots may end up out of combat during the Vashj encounter -bool LadyVashjEraseCorePassingTrackersAction::Execute(Event /*event*/) -{ - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - if (!vashj) - return false; - - const uint32 instanceId = vashj->GetMap()->GetInstanceId(); - - bool erased = false; - if (nearestTriggerGuid.erase(instanceId) > 0) - erased = true; - if (lastImbueAttempt.erase(instanceId) > 0) - erased = true; - if (lastCoreInInventoryTime.erase(instanceId) > 0) - erased = true; - if (intendedLineup.erase(bot->GetGUID()) > 0) - erased = true; - - return erased; -} - // The standard "avoid aoe" strategy does work for Toxic Spores, but this method // provides more buffer distance and limits the area in which bots can move // so that they do not go down the stairs bool LadyVashjAvoidToxicSporesAction::Execute(Event /*event*/) { - auto const& spores = GetAllSporeDropTriggers(botAI, bot); + auto const& spores = GetAllSporeDropTriggers(bot); if (spores.empty()) return false; @@ -2910,16 +2694,21 @@ bool LadyVashjAvoidToxicSporesAction::Execute(Event /*event*/) if (!inDanger) return false; + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) + return false; + const Position& vashjCenter = VASHJ_PLATFORM_CENTER_POSITION; constexpr float maxRadius = 60.0f; Position safestPos = FindSafestNearbyPosition(spores, vashjCenter, maxRadius, hazardRadius); + bool backwards = vashj->GetVictim() == bot; + MovementPriority priority = backwards ? + MovementPriority::MOVEMENT_FORCED : MovementPriority::MOVEMENT_COMBAT; - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - bool backwards = (vashj && vashj->GetVictim() == bot); return MoveTo(SSC_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), safestPos.GetPositionZ(), false, false, false, true, - MovementPriority::MOVEMENT_COMBAT, true, backwards); + priority, true, backwards); } Position LadyVashjAvoidToxicSporesAction::FindSafestNearbyPosition( @@ -3015,19 +2804,18 @@ bool LadyVashjAvoidToxicSporesAction::IsPathSafeFromSpores(const Position& start // When Toxic Sporebats spit poison, they summon "Spore Drop Trigger" NPCs // that create the toxic pools -std::vector LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers( - PlayerbotAI* botAI, Player* bot) +std::vector LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(Player* bot) { std::vector sporeDropTriggers; - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); - for (auto const& npcGuid : npcs) + std::list creatureList; + constexpr float searchRadius = 50.0f; + + bot->GetCreatureListWithEntryInGrid(creatureList, NPC_SPORE_DROP_TRIGGER, searchRadius); + + for (Creature* creature : creatureList) { - constexpr float maxSearchRadius = 40.0f; - Unit* unit = botAI->GetUnit(npcGuid); - if (unit && unit->GetEntry() == NPC_SPORE_DROP_TRIGGER && - bot->GetExactDist2d(unit) < maxSearchRadius) - sporeDropTriggers.push_back(unit); + if (creature && creature->IsAlive()) + sporeDropTriggers.push_back(creature); } return sporeDropTriggers; @@ -3040,7 +2828,7 @@ bool LadyVashjUseFreeActionAbilitiesAction::Execute(Event /*event*/) return false; auto const& spores = - LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(botAI, bot); + LadyVashjAvoidToxicSporesAction::GetAllSporeDropTriggers(bot); constexpr float toxicSporeRadius = 6.0f; // If Rogues are Entangled and either have Static Charge or @@ -3109,19 +2897,13 @@ bool LadyVashjUseFreeActionAbilitiesAction::Execute(Event /*event*/) { // Priority 1: Entangled in Toxic Spores (prefer main tank) Player* toxicTarget = mainTankToxic ? mainTankToxic : anyToxic; - if (toxicTarget) - { - if (botAI->CanCastSpell("hand of freedom", toxicTarget)) - return botAI->CastSpell("hand of freedom", toxicTarget); - } + if (toxicTarget && botAI->CanCastSpell("hand of freedom", toxicTarget)) + return botAI->CastSpell("hand of freedom", toxicTarget); // Priority 2: Entangled with Static Charge (prefer main tank) Player* staticTarget = mainTankStatic ? mainTankStatic : anyStatic; - if (staticTarget) - { - if (botAI->CanCastSpell("hand of freedom", staticTarget)) - return botAI->CastSpell("hand of freedom", staticTarget); - } + if (staticTarget && botAI->CanCastSpell("hand of freedom", staticTarget)) + return botAI->CastSpell("hand of freedom", staticTarget); } return false; diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCActions.h similarity index 93% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCActions.h index cbd237402..58b2e2f9f 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Action/RaidSSCActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDSSCACTIONS_H -#define _PLAYERBOT_RAIDSSCACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SSCACTIONS_H +#define PLAYERBOTS_SSCACTIONS_H #include "Action.h" #include "AttackAction.h" @@ -75,8 +81,8 @@ public: bool Execute(Event event) override; private: - bool TryMisdirectToFrostTank(Unit* hydross, Group* group); - bool TryMisdirectToNatureTank(Unit* hydross, Group* group); + bool TryMisdirectToFrostTank(Unit* hydross); + bool TryMisdirectToNatureTank(Unit* hydross); }; class HydrossTheUnstableStopDpsUponPhaseChangeAction : public Action @@ -307,8 +313,8 @@ public: bool Execute(Event event) override; private: - bool MoveToPhase1TankPosition(Unit* tidewalker); - bool MoveToPhase2TankPosition(Unit* tidewalker); + bool MoveToPhase1TankPosition(); + bool MoveToPhase2TankPosition(); }; class MorogrimTidewalkerPhase2RepositionRangedAction : public MovementAction @@ -409,14 +415,14 @@ public: bool Execute(Event event) override; private: - bool LineUpFirstCorePasser(Player* designatedLooter, Unit* closestTrigger); + bool LineUpFirstCorePasser(Player* designatedLooter); bool LineUpSecondCorePasser(Player* firstCorePasser, Unit* closestTrigger); bool LineUpThirdCorePasser(Player* designatedLooter, Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); bool LineUpFourthCorePasser(Player* firstCorePasser, Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); - bool IsFirstCorePasserInIntendedPosition(Player* designatedLooter, Player* firstCorePasser, Unit* closestTrigger); - bool IsSecondCorePasserInIntendedPosition(Player* firstCorePasser, Player* secondCorePasser, Unit* closestTrigger); - bool IsThirdCorePasserInIntendedPosition(Player* secondCorePasser, Player* thirdCorePasser, Unit* closestTrigger); - bool IsFourthCorePasserInIntendedPosition(Player* thirdCorePasser, Player* fourthCorePasser, Unit* closestTrigger); + bool IsFirstCorePasserInPosition(Player* firstCorePasser); + bool IsSecondCorePasserInPosition(Player* secondCorePasser); + bool IsThirdCorePasserInPosition(Player* thirdCorePasser); + bool IsFourthCorePasserInPosition(Player* fourthCorePasser); void ScheduleTransferCoreAfterImbue(PlayerbotAI* botAI, Player* giver, Player* receiver); bool UseCoreOnNearestGenerator(const uint32 instanceId); }; @@ -428,19 +434,12 @@ public: bool Execute(Event event) override; }; -class LadyVashjEraseCorePassingTrackersAction : public Action -{ -public: - LadyVashjEraseCorePassingTrackersAction(PlayerbotAI* botAI, std::string const name = "lady vashj erase core passing trackers") : Action(botAI, name) {} - bool Execute(Event event) override; -}; - class LadyVashjAvoidToxicSporesAction : public MovementAction { public: LadyVashjAvoidToxicSporesAction(PlayerbotAI* botAI, std::string const name = "lady vashj avoid toxic spores") : MovementAction(botAI, name) {} bool Execute(Event event) override; - static std::vector GetAllSporeDropTriggers(PlayerbotAI* botAI, Player* bot); + static std::vector GetAllSporeDropTriggers(Player* bot); private: Position FindSafestNearbyPosition(const std::vector& spores, const Position& position, float maxRadius, float hazardRadius); diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCHelpers.cpp similarity index 65% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCHelpers.cpp index 7bda085be..832f6c1b8 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCHelpers.cpp @@ -1,4 +1,10 @@ -#include "RaidSSCHelpers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "SSCHelpers.h" #include "AiFactory.h" #include "Creature.h" #include "ObjectAccessor.h" @@ -79,61 +85,54 @@ namespace SerpentShrineCavernHelpers std::unordered_map leotherasDemonFormDpsWaitTimer; std::unordered_map leotherasFinalPhaseDpsWaitTimer; - Unit* GetLeotherasHuman(PlayerbotAI* botAI) + Unit* GetLeotherasHuman(Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && - unit->IsInCombat() && !unit->HasAura(SPELL_METAMORPHOSIS)) - return unit; - } + constexpr float searchRadius = 100.0f; + Creature* leotheras = + bot->FindNearestCreature(NPC_LEOTHERAS_THE_BLIND, searchRadius, true); + + if (leotheras && leotheras->IsInCombat() && + !leotheras->HasAura(SPELL_METAMORPHOSIS)) + return leotheras; + return nullptr; } - Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI) + Unit* GetPhase2LeotherasDemon(Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == NPC_LEOTHERAS_THE_BLIND && - unit->HasAura(SPELL_METAMORPHOSIS)) - return unit; - } + constexpr float searchRadius = 100.0f; + Creature* leotheras = + bot->FindNearestCreature(NPC_LEOTHERAS_THE_BLIND, searchRadius, true); + + if (leotheras && leotheras->HasAura(SPELL_METAMORPHOSIS)) + return leotheras; + return nullptr; } - Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI) + Unit* GetPhase3LeotherasDemon(Player* bot) { - auto const& npcs = - botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); - for (auto const& guid : npcs) - { - Unit* unit = botAI->GetUnit(guid); - if (unit && unit->GetEntry() == NPC_SHADOW_OF_LEOTHERAS) - return unit; - } - return nullptr; + constexpr float searchRadius = 100.0f; + return bot->FindNearestCreature(NPC_SHADOW_OF_LEOTHERAS, searchRadius, true); } - Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI) + Unit* GetActiveLeotherasDemon(Player* bot) { - Unit* phase2 = GetPhase2LeotherasDemon(botAI); - Unit* phase3 = GetPhase3LeotherasDemon(botAI); + Unit* phase2 = GetPhase2LeotherasDemon(bot); + Unit* phase3 = GetPhase3LeotherasDemon(bot); return phase2 ? phase2 : phase3; } + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot Player* GetLeotherasDemonFormTank(Player* bot) { Group* group = bot->GetGroup(); if (!group) return nullptr; - // (1) First loop: Return the first assistant Warlock (real player or bot) + Player* fallbackWarlock = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); @@ -142,21 +141,12 @@ namespace SerpentShrineCavernHelpers if (group->IsAssistant(member->GetGUID())) return member; + + if (!fallbackWarlock && GET_PLAYERBOT_AI(member)) + fallbackWarlock = member; } - // (2) Fall back to first found bot Warlock - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - member->getClass() != CLASS_WARLOCK) - continue; - - return member; - } - - // (3) Return nullptr if none found - return nullptr; + return fallbackWarlock; } // Fathom-Lord Karathress @@ -182,16 +172,15 @@ namespace SerpentShrineCavernHelpers // Lady Vashj - const Position VASHJ_PLATFORM_CENTER_POSITION = { 29.634f, -923.541f, 42.985f }; + const Position VASHJ_PLATFORM_CENTER_POSITION = { 29.634f, -923.541f, 42.902f }; - std::unordered_map vashjRangedPositions; std::unordered_map hasReachedVashjRangedPosition; std::unordered_map nearestTriggerGuid; std::unordered_map intendedLineup; std::unordered_map lastImbueAttempt; - std::unordered_map lastCoreInInventoryTime; + std::unordered_map lastCoreInInventoryTime; - bool IsMainTankInSameSubgroup(Player* bot) + bool IsMainTankInSameSubgroup(PlayerbotAI* botAI, Player* bot) { Group* group = bot->GetGroup(); if (!group || !group->isRaidGroup()) @@ -210,11 +199,8 @@ namespace SerpentShrineCavernHelpers if (group->GetMemberGroup(member->GetGUID()) != botSubGroup) continue; - if (PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member)) - { - if (memberAI->IsMainTank(member)) - return true; - } + if (botAI->IsMainTank(member)) + return true; } return false; @@ -224,35 +210,24 @@ namespace SerpentShrineCavernHelpers { Unit* vashj = botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); - if (!vashj) - return false; - Creature* vashjCreature = vashj->ToCreature(); - return vashjCreature && vashjCreature->GetHealthPct() > 70.0f && - vashjCreature->GetReactState() != REACT_PASSIVE; + return vashj && vashj->GetHealthPct() > 70.0f; } bool IsLadyVashjInPhase2(PlayerbotAI* botAI) { Unit* vashj = botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); - if (!vashj) - return false; - Creature* vashjCreature = vashj->ToCreature(); - return vashjCreature && vashjCreature->GetReactState() == REACT_PASSIVE; + return vashj && vashj->GetHealthPct() <= 70.0f && vashj->HasAura(SPELL_MAGIC_BARRIER); } bool IsLadyVashjInPhase3(PlayerbotAI* botAI) { Unit* vashj = botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); - if (!vashj) - return false; - Creature* vashjCreature = vashj->ToCreature(); - return vashjCreature && vashjCreature->GetHealthPct() <= 50.0f && - vashjCreature->GetReactState() != REACT_PASSIVE; + return vashj && vashj->GetHealthPct() <= 70.0f && !vashj->HasAura(SPELL_MAGIC_BARRIER); } bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI) @@ -277,38 +252,9 @@ namespace SerpentShrineCavernHelpers return false; } - bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds) - { - Unit* vashj = - botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); - if (!vashj) - return false; - - if (group) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasItemCount(ITEM_TAINTED_CORE, 1, false)) - return true; - } - } - - const uint32 instanceId = vashj->GetMap()->GetInstanceId(); - const time_t now = std::time(nullptr); - - auto it = lastCoreInInventoryTime.find(instanceId); - if (it != lastCoreInInventoryTime.end()) - { - if ((now - it->second) <= static_cast(graceSeconds)) - return true; - } - - return false; - } - - Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI) + Player* GetDesignatedCoreLooter(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; @@ -317,10 +263,15 @@ namespace SerpentShrineCavernHelpers if (!leaderGuid.IsEmpty()) leader = ObjectAccessor::FindPlayer(leaderGuid); + // If cheats are disabled, the group leader will be the designated looter if (!botAI->HasCheat(BotCheatMask::raid)) return leader; - Player* fallback = leader; + // Priority: (1) assistant melee DPS, (2) other melee DPS, (3) any ranged DPS + Player* meleeDpsAssistant = nullptr; + Player* meleeDps = nullptr; + Player* rangedDps = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); @@ -331,22 +282,36 @@ namespace SerpentShrineCavernHelpers if (!memberAI) continue; - if (memberAI->IsMelee(member) && memberAI->IsDps(member)) - return member; + if (!meleeDpsAssistant && memberAI->IsMelee(member) && + memberAI->IsDps(member) && group->IsAssistant(member->GetGUID())) + { + meleeDpsAssistant = member; + break; + } - if (!fallback && memberAI->IsRangedDps(member)) - fallback = member; + if (!meleeDps && memberAI->IsMelee(member) && memberAI->IsDps(member)) + meleeDps = member; + + if (!rangedDps && memberAI->IsRangedDps(member)) + rangedDps = member; } - return fallback ? fallback : leader; + if (meleeDpsAssistant) + return meleeDpsAssistant; + if (meleeDps) + return meleeDps; + if (rangedDps) + return rangedDps; + return leader; } - Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetFirstTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -355,32 +320,29 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistHealOfIndex(member, 0, true)) + if (memberAI && memberAI->IsAssistHealOfIndex(member, 0, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter) + return member; } return nullptr; } - Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetSecondTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -390,34 +352,31 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistHealOfIndex(member, 1, true)) + if (memberAI && memberAI->IsAssistHealOfIndex(member, 1, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter || - member == firstCorePasser) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter && + member != firstCorePasser) + return member; } return nullptr; } - Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetThirdTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); + Player* secondCorePasser = GetSecondTaintedCorePasser(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -427,35 +386,32 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistHealOfIndex(member, 2, true)) + if (memberAI && memberAI->IsAssistHealOfIndex(member, 2, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter || - member == firstCorePasser || member == secondCorePasser) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter && + member != firstCorePasser && member != secondCorePasser) + return member; } return nullptr; } - Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI) + Player* GetFourthTaintedCorePasser(PlayerbotAI* botAI, Player* bot) { + Group* group = bot->GetGroup(); if (!group) return nullptr; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); + Player* designatedLooter = GetDesignatedCoreLooter(botAI, bot); + Player* firstCorePasser = GetFirstTaintedCorePasser(botAI, bot); + Player* secondCorePasser = GetSecondTaintedCorePasser(botAI, bot); + Player* thirdCorePasser = GetThirdTaintedCorePasser(botAI, bot); for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { @@ -466,27 +422,75 @@ namespace SerpentShrineCavernHelpers continue; PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (memberAI->IsAssistRangedDpsOfIndex(member, 0, true)) + if (memberAI && memberAI->IsAssistRangedDpsOfIndex(member, 0, true)) return member; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member || !member->IsAlive() || !GET_PLAYERBOT_AI(member) || - botAI->IsTank(member) || member == designatedLooter || - member == firstCorePasser || member == secondCorePasser || - member == thirdCorePasser) - continue; - return member; + if (member && member->IsAlive() && GET_PLAYERBOT_AI(member) && + !botAI->IsTank(member) && member != designatedLooter && + member != firstCorePasser && member != secondCorePasser && + member != thirdCorePasser) + return member; } return nullptr; } + std::array GetCoreHandlers(PlayerbotAI* botAI, Player* bot) + { + return + { + GetDesignatedCoreLooter(botAI, bot), + GetFirstTaintedCorePasser(botAI, bot), + GetSecondTaintedCorePasser(botAI, bot), + GetThirdTaintedCorePasser(botAI, bot), + GetFourthTaintedCorePasser(botAI, bot) + }; + } + + // Checks if any bot from earlier in the passing sequence has the Tainted Core or + // had it within the prior 3 seconds so the chain is not broken when the Core is in transit + bool AnyRecentCoreInInventory(PlayerbotAI* botAI, Player* bot) + { + Unit* vashj = + botAI->GetAiObjectContext()->GetValue("find target", "lady vashj")->Get(); + if (!vashj) + return false; + + auto coreHandlers = GetCoreHandlers(botAI, bot); + + int8 myIndex = -1; + for (int8 i = 0; i < 5; ++i) + if (coreHandlers[i] && coreHandlers[i] == bot) + myIndex = i; + + if (myIndex == -1) + return false; + + const time_t now = std::time(nullptr); + constexpr uint8 lookbackSeconds = 3; + + for (int8 i = 0; i <= myIndex; ++i) + { + Player* handler = coreHandlers[i]; + if (!handler) + continue; + + if (handler->HasItemCount(ITEM_TAINTED_CORE, 1, false)) + return true; + + auto it = lastCoreInInventoryTime.find(handler->GetGUID()); + if (it != lastCoreInInventoryTime.end() && + (now - it->second) <= static_cast(lookbackSeconds)) + return true; + } + + return false; + } + const std::vector SHIELD_GENERATOR_DB_GUIDS = { 47482, // NW @@ -510,10 +514,7 @@ namespace SerpentShrineCavernHelpers continue; GameObject* go = bounds.first->second; - if (!go) - continue; - - if (go->GetGoState() != GO_STATE_READY) + if (!go || go->GetGoState() != GO_STATE_READY) continue; GeneratorInfo info; @@ -529,7 +530,7 @@ namespace SerpentShrineCavernHelpers // Returns the nearest active Shield Generator to the bot // Active generators are powered by NPC_WORLD_INVISIBLE_TRIGGER creatures, - // which depawn after use + // which despawn after use Unit* GetNearestActiveShieldGeneratorTriggerByEntry(Unit* reference) { if (!reference) diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCHelpers.h similarity index 82% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCHelpers.h index a725e28fc..96d7a0aa1 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Util/RaidSSCHelpers.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCHelpers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDSSCHELPERS_H_ -#define _PLAYERBOT_RAIDSSCHELPERS_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SSCHELPERS_H +#define PLAYERBOTS_SSCHELPERS_H #include #include @@ -43,6 +49,7 @@ namespace SerpentShrineCavernHelpers // Lady Vashj SPELL_FEAR_WARD = 6346, + SPELL_MAGIC_BARRIER = 38112, SPELL_POISON_BOLT = 38253, SPELL_STATIC_CHARGE = 38280, SPELL_ENTANGLE = 38316, @@ -64,9 +71,6 @@ namespace SerpentShrineCavernHelpers // Warlock SPELL_CURSE_OF_EXHAUSTION = 18223, - - // Item - SPELL_HEAVY_NETHERWEAVE_NET = 31368, }; enum SerpentShrineCavernNPCs @@ -105,9 +109,6 @@ namespace SerpentShrineCavernHelpers { // Lady Vashj ITEM_TAINTED_CORE = 31088, - - // Tailoring - ITEM_HEAVY_NETHERWEAVE_NET = 24269, }; constexpr uint32 SSC_MAP_ID = 548; @@ -134,10 +135,10 @@ namespace SerpentShrineCavernHelpers extern std::unordered_map leotherasHumanFormDpsWaitTimer; extern std::unordered_map leotherasDemonFormDpsWaitTimer; extern std::unordered_map leotherasFinalPhaseDpsWaitTimer; - Unit* GetLeotherasHuman(PlayerbotAI* botAI); - Unit* GetPhase2LeotherasDemon(PlayerbotAI* botAI); - Unit* GetPhase3LeotherasDemon(PlayerbotAI* botAI); - Unit* GetActiveLeotherasDemon(PlayerbotAI* botAI); + Unit* GetLeotherasHuman(Player* bot); + Unit* GetPhase2LeotherasDemon(Player* bot); + Unit* GetPhase3LeotherasDemon(Player* bot); + Unit* GetActiveLeotherasDemon(Player* bot); Player* GetLeotherasDemonFormTank(Player* bot); // Fathom-Lord Karathress @@ -158,25 +159,26 @@ namespace SerpentShrineCavernHelpers extern std::unordered_map tidewalkerRangedStep; // Lady Vashj - constexpr float VASHJ_PLATFORM_Z = 42.985f; + constexpr float VASHJ_PLATFORM_CENTER_Z = 42.902f; + constexpr float VASHJ_PLATFORM_EDGE_Z = 41.097f; extern const Position VASHJ_PLATFORM_CENTER_POSITION; - extern std::unordered_map vashjRangedPositions; extern std::unordered_map hasReachedVashjRangedPosition; extern std::unordered_map nearestTriggerGuid; extern std::unordered_map intendedLineup; extern std::unordered_map lastImbueAttempt; - extern std::unordered_map lastCoreInInventoryTime; - bool IsMainTankInSameSubgroup(Player* bot); + extern std::unordered_map lastCoreInInventoryTime; + bool IsMainTankInSameSubgroup(PlayerbotAI* botAI, Player* bot); bool IsLadyVashjInPhase1(PlayerbotAI* botAI); bool IsLadyVashjInPhase2(PlayerbotAI* botAI); bool IsLadyVashjInPhase3(PlayerbotAI* botAI); bool IsValidLadyVashjCombatNpc(Unit* unit, PlayerbotAI* botAI); - bool AnyRecentCoreInInventory(Group* group, PlayerbotAI* botAI, uint32 graceSeconds = 3); - Player* GetDesignatedCoreLooter(Group* group, PlayerbotAI* botAI); - Player* GetFirstTaintedCorePasser(Group* group, PlayerbotAI* botAI); - Player* GetSecondTaintedCorePasser(Group* group, PlayerbotAI* botAI); - Player* GetThirdTaintedCorePasser(Group* group, PlayerbotAI* botAI); - Player* GetFourthTaintedCorePasser(Group* group, PlayerbotAI* botAI); + Player* GetDesignatedCoreLooter(PlayerbotAI* botAI, Player* bot); + Player* GetFirstTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + Player* GetSecondTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + Player* GetThirdTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + Player* GetFourthTaintedCorePasser(PlayerbotAI* botAI, Player* bot); + std::array GetCoreHandlers(PlayerbotAI* botAI, Player* bot); + bool AnyRecentCoreInInventory(PlayerbotAI* botAI, Player* bot); struct GeneratorInfo { ObjectGuid guid; float x, y, z; }; extern const std::vector SHIELD_GENERATOR_DB_GUIDS; std::vector GetAllGeneratorInfosByDbGuids( diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCMultipliers.cpp similarity index 62% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCMultipliers.cpp index c99cafa3c..a06959da8 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCMultipliers.cpp @@ -1,6 +1,12 @@ -#include "RaidSSCMultipliers.h" -#include "RaidSSCActions.h" -#include "RaidSSCHelpers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "SSCMultipliers.h" +#include "SSCActions.h" +#include "SSCHelpers.h" #include "ChooseTargetActions.h" #include "DestroyItemAction.h" #include "DKActions.h" @@ -28,12 +34,10 @@ using namespace SerpentShrineCavernHelpers; float UnderbogColossusEscapeToxicPoolMultiplier::GetValue(Action* action) { - if (bot->HasAura(SPELL_TOXIC_POOL)) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + if (bot->HasAura(SPELL_TOXIC_POOL) && + dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -53,16 +57,16 @@ float HydrossTheUnstableDisableTankActionsMultiplier::GetValue(Action* action) dynamic_cast(action)) return 0.0f; + if ((botAI->IsMainTank(bot) && !hydross->HasAura(SPELL_CORRUPTION)) || + (botAI->IsAssistTankOfIndex(bot, 0, true) && hydross->HasAura(SPELL_CORRUPTION))) + return 1.0f; + if (dynamic_cast(action) || dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action) && !dynamic_cast(action))) - { - if ((botAI->IsMainTank(bot) && hydross->HasAura(SPELL_CORRUPTION)) || - (botAI->IsAssistTankOfIndex(bot, 0, true) && !hydross->HasAura(SPELL_CORRUPTION))) - return 0.0f; - } + return 0.0f; return 1.0f; } @@ -97,13 +101,13 @@ float HydrossTheUnstableWaitForDpsMultiplier::GetValue(Action* action) bool aboutToChange = (itPhase != hydrossChangeToFrostPhaseTimer.end() && (now - itPhase->second) > phaseChangeWaitSeconds); - if (justChanged || aboutToChange) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } + if (!justChanged && !aboutToChange) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } if (hydross->HasAura(SPELL_CORRUPTION) && !botAI->IsAssistTankOfIndex(bot, 0, true)) @@ -116,13 +120,13 @@ float HydrossTheUnstableWaitForDpsMultiplier::GetValue(Action* action) bool aboutToChange = (itPhase != hydrossChangeToNaturePhaseTimer.end() && (now - itPhase->second) > phaseChangeWaitSeconds); - if (justChanged || aboutToChange) - { - if (dynamic_cast(action) || - (dynamic_cast(action) && - !dynamic_cast(action))) - return 0.0f; - } + if (!justChanged && !aboutToChange) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; } return 1.0f; @@ -133,11 +137,9 @@ float HydrossTheUnstableControlMisdirectionMultiplier::GetValue(Action* action) if (bot->getClass() != CLASS_HUNTER) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "hydross the unstable")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (AI_VALUE2(Unit*, "find target", "hydross the unstable") && + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -175,14 +177,14 @@ float TheLurkerBelowMaintainRangedSpreadMultiplier::GetValue(Action* action) if (!botAI->IsRanged(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "the lurker below")) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "the lurker below")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -215,11 +217,11 @@ float TheLurkerBelowDisableTankAssistMultiplier::GetValue(Action* action) ++tankCount; } - if (tankCount >= 3) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (tankCount < 3) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -234,22 +236,18 @@ float LeotherasTheBlindAvoidWhirlwindMultiplier::GetValue(Action* action) if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return 1.0f; - Unit* leotherasHuman = GetLeotherasHuman(botAI); - if (!leotherasHuman) + Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!leotheras || (!leotheras->HasAura(SPELL_WHIRLWIND) && + !leotheras->HasAura(SPELL_WHIRLWIND_CHANNEL))) return 1.0f; - if (!leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && - (leotherasHuman->HasAura(SPELL_WHIRLWIND) || - leotherasHuman->HasAura(SPELL_WHIRLWIND_CHANNEL))) - { - if (dynamic_cast(action)) - return 0.0f; + if (dynamic_cast(action)) + return 0.0f; - if (dynamic_cast(action) && - !dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -262,10 +260,10 @@ float LeotherasTheBlindDisableTankActionsMultiplier::GetValue(Action* action) if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return 1.0f; - if (GetPhase2LeotherasDemon(botAI) && dynamic_cast(action)) + if (GetPhase2LeotherasDemon(bot) && dynamic_cast(action)) return 0.0f; - if (!GetPhase3LeotherasDemon(botAI) && dynamic_cast(action)) + if (!GetPhase3LeotherasDemon(bot) && dynamic_cast(action)) return 0.0f; return 1.0f; @@ -273,21 +271,21 @@ float LeotherasTheBlindDisableTankActionsMultiplier::GetValue(Action* action) float LeotherasTheBlindFocusOnInnerDemonMultiplier::GetValue(Action* action) { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -297,19 +295,19 @@ float LeotherasTheBlindMeleeDpsAvoidChaosBlastMultiplier::GetValue(Action* actio if (botAI->IsRanged(bot) || botAI->IsTank(bot)) return 1.0f; - if (!GetPhase2LeotherasDemon(botAI)) + if (!GetPhase2LeotherasDemon(bot)) return 1.0f; Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); - if (chaosBlast && chaosBlast->GetStackAmount() >= 5) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!chaosBlast || chaosBlast->GetStackAmount() < 5) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -330,8 +328,8 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) const time_t now = std::time(nullptr); constexpr uint8 dpsWaitSecondsPhase1 = 5; - Unit* leotherasHuman = GetLeotherasHuman(botAI); - Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(botAI); + Unit* leotherasHuman = GetLeotherasHuman(bot); + Unit* leotherasPhase3Demon = GetPhase3LeotherasDemon(bot); if (leotherasHuman && !leotherasHuman->HasAura(SPELL_LEOTHERAS_BANISHED) && !leotherasPhase3Demon) { @@ -345,12 +343,12 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } } constexpr uint8 dpsWaitSecondsPhase2 = 12; - Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(botAI); + Unit* leotherasPhase2Demon = GetPhase2LeotherasDemon(bot); Player* demonFormTank = GetLeotherasDemonFormTank(bot); if (leotherasPhase2Demon) { @@ -367,7 +365,7 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } } @@ -384,7 +382,7 @@ float LeotherasTheBlindWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } } @@ -398,12 +396,12 @@ float LeotherasTheBlindDelayBloodlustAndHeroismMultiplier::GetValue(Action* acti return 1.0f; Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); - if (leotheras && leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) - { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!leotheras || !leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -429,7 +427,8 @@ float FathomLordKarathressDisableTankActionsMultiplier::GetValue(Action* action) dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || dynamic_cast(action) || @@ -447,14 +446,12 @@ float FathomLordKarathressDisableAoeMultiplier::GetValue(Action* action) if (!botAI->IsDps(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) - { - if (auto castSpellAction = dynamic_cast(action)) - { - if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) - return 0.0f; - } - } + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (castSpellAction && castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return 0.0f; return 1.0f; } @@ -464,11 +461,11 @@ float FathomLordKarathressControlMisdirectionMultiplier::GetValue(Action* action if (bot->getClass() != CLASS_HUNTER) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -494,7 +491,7 @@ float FathomLordKarathressWaitForDpsMultiplier::GetValue(Action* action) if (dynamic_cast(action) || (dynamic_cast(action) && !dynamic_cast(action))) - return 0.0f; + return 0.0f; } return 1.0f; @@ -505,12 +502,12 @@ float FathomLordKarathressCaribdisTankHealerMaintainPositionMultiplier::GetValue if (!botAI->IsAssistHealOfIndex(bot, 0, true)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "fathom-guard caribdis")) - { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "fathom-guard caribdis")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -526,12 +523,12 @@ float MorogrimTidewalkerDelayBloodlustAndHeroismMultiplier::GetValue(Action* act if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) return 1.0f; - if (!AI_VALUE2(Unit*, "find target", "tidewalker lurker")) - { - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (AI_VALUE2(Unit*, "find target", "tidewalker lurker")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -541,11 +538,11 @@ float MorogrimTidewalkerDisableTankActionsMultiplier::GetValue(Action* action) if (!botAI->IsMainTank(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "morogrim tidewalker")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -556,17 +553,14 @@ float MorogrimTidewalkerMaintainPhase2StackingMultiplier::GetValue(Action* actio return 1.0f; Unit* tidewalker = AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); - if (!tidewalker) + if (!tidewalker || tidewalker->GetHealthPct() > 25.0f) return 1.0f; - if (tidewalker->GetHealthPct() < 25.0f) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -580,41 +574,59 @@ float LadyVashjDelayCooldownsMultiplier::GetValue(Action* action) if (!AI_VALUE2(Unit*, "find target", "lady vashj")) return 1.0f; - if (bot->getClass() == CLASS_SHAMAN) - { - if (IsLadyVashjInPhase3(botAI)) - return 1.0f; + if (bot->getClass() == CLASS_SHAMAN && + !IsLadyVashjInPhase3(botAI) && + (dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; - if (dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!botAI->IsDps(bot) || !IsLadyVashjInPhase1(botAI)) + return 1.0f; - if (botAI->IsDps(bot) && IsLadyVashjInPhase1(botAI)) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (!IsMainTankInSameSubgroup(botAI, bot)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -624,15 +636,15 @@ float LadyVashjMaintainPhase1RangedSpreadMultiplier::GetValue(Action* action) if (!botAI->IsRanged(bot)) return 1.0f; - if (AI_VALUE2(Unit*, "find target", "lady vashj") && - IsLadyVashjInPhase1(botAI)) - { - if (dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action) || - dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || + !IsLadyVashjInPhase1(botAI)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -658,19 +670,18 @@ float LadyVashjStaticChargeStayAwayFromGroupMultiplier::GetValue(Action* action) // Bots should not loot the core with normal looting logic float LadyVashjDoNotLootTheTaintedCoreMultiplier::GetValue(Action* action) { - if (AI_VALUE2(Unit*, "find target", "lady vashj")) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; return 1.0f; } float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* action) { - if (!AI_VALUE2(Unit*, "find target", "lady vashj") || - !IsLadyVashjInPhase2(botAI)) + if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI)) return 1.0f; if (dynamic_cast(action) || @@ -678,65 +689,41 @@ float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* acti dynamic_cast(action)) return 1.0f; - Group* group = bot->GetGroup(); - if (!group) - return 1.0f; + auto coreHandlers = GetCoreHandlers(botAI, bot); - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); - Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); + bool isCoreHandler = false; + for (int i = 0; i < static_cast(coreHandlers.size()); ++i) + { + if (coreHandlers[i] && coreHandlers[i] == bot) + { + isCoreHandler = true; + } + } + if (!isCoreHandler) + return 1.0f; auto hasCore = [](Player* player) { return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); }; - if (hasCore(bot)) - { - if (!dynamic_cast(action)) - return 0.0f; - } + // If the bot actually has the core, only allow core handling + if (hasCore(bot) && !dynamic_cast(action)) + return 0.0f; - if (bot == designatedLooter) - { - if (!hasCore(bot)) - return 1.0f; - } - else if (bot == firstCorePasser) - { - if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || - hasCore(fourthCorePasser)) - return 1.0f; - } - else if (bot == secondCorePasser) - { - if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) - return 1.0f; - } - else if (bot == thirdCorePasser) - { - if (hasCore(fourthCorePasser)) - return 1.0f; - } - else if (bot != fourthCorePasser) - return 1.0f; + // First and second passers block movement when the looter teleports to the elemental + Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (tainted && coreHandlers[0]->GetExactDist2d(tainted) < 5.0f && + (bot == coreHandlers[1] || bot == coreHandlers[2]) && + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; - if (AI_VALUE2(Unit*, "find target", "tainted elemental") && - (bot == firstCorePasser || bot == secondCorePasser)) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } - - if (AnyRecentCoreInInventory(group, botAI)) - { - if (dynamic_cast(action) && - !dynamic_cast(action)) - return 0.0f; - } + // If any prior handler (including self) recently had the core, block other movement + if (AnyRecentCoreInInventory(botAI, bot) && + dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; return 1.0f; } @@ -745,7 +732,8 @@ float LadyVashjCorePassersPrioritizePositioningMultiplier::GetValue(Action* acti // So the standard target selection system must be disabled float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *action) { - if (!AI_VALUE2(Unit*, "find target", "lady vashj")) + Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); + if (!vashj) return 1.0f; if (dynamic_cast(action)) @@ -755,24 +743,26 @@ float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *ac { if (dynamic_cast(action) || dynamic_cast(action) || - dynamic_cast(action) || dynamic_cast(action)) return 0.0f; + if (bot->GetExactDist2d(vashj) < 60.0f && + dynamic_cast(action)) + return 0.0f; + if (!botAI->IsHeal(bot) && dynamic_cast(action)) return 0.0f; Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); - if (enchanted && bot->GetVictim() == enchanted) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (enchanted && AI_VALUE(Unit*, "current target") == enchanted && + dynamic_cast(action)) + return 0.0f; } if (IsLadyVashjInPhase3(botAI)) { - if (dynamic_cast(action)) + if (dynamic_cast(action) || + dynamic_cast(action)) return 0.0f; Unit* enchanted = AI_VALUE2(Unit*, "find target", "enchanted elemental"); @@ -780,16 +770,13 @@ float LadyVashjDisableAutomaticTargetingAndMovementModifier::GetValue(Action *ac Unit* elite = AI_VALUE2(Unit*, "find target", "coilfang elite"); if (enchanted || strider || elite) { - if (dynamic_cast(action) || - dynamic_cast(action) || + if (dynamic_cast(action) || dynamic_cast(action)) return 0.0f; - if (enchanted && bot->GetVictim() == enchanted) - { - if (dynamic_cast(action)) - return 0.0f; - } + if (enchanted && AI_VALUE(Unit*, "current target") == enchanted && + dynamic_cast(action)) + return 0.0f; } else if (dynamic_cast(action)) return 0.0f; diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCMultipliers.h similarity index 92% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCMultipliers.h index 6630dc206..1c4c1c857 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Multiplier/RaidSSCMultipliers.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCMultipliers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDSSCMULTIPLIERS_H -#define _PLAYERBOT_RAIDSSCMULTIPLIERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SSCMULTIPLIERS_H +#define PLAYERBOTS_SSCMULTIPLIERS_H #include "Multiplier.h" @@ -193,6 +199,14 @@ public: virtual float GetValue(Action* action); }; +class LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier : public Multiplier +{ +public: + LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "lady vashj main tank group shaman use grounding totem") {} + virtual float GetValue(Action* action); +}; + class LadyVashjMaintainPhase1RangedSpreadMultiplier : public Multiplier { public: diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCStrategy.cpp similarity index 96% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCStrategy.cpp index 139667dc6..29299e55b 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCStrategy.cpp @@ -1,5 +1,11 @@ -#include "RaidSSCStrategy.h" -#include "RaidSSCMultipliers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "SSCStrategy.h" +#include "SSCMultipliers.h" void RaidSSCStrategy::InitTriggers(std::vector& triggers) { @@ -144,9 +150,6 @@ void RaidSSCStrategy::InitTriggers(std::vector& triggers) triggers.push_back(new TriggerNode("lady vashj tainted core is unusable", { NextAction("lady vashj destroy tainted core", ACTION_EMERGENCY + 1) })); - triggers.push_back(new TriggerNode("lady vashj need to reset core passing trackers", { - NextAction("lady vashj erase core passing trackers", ACTION_EMERGENCY + 10) })); - triggers.push_back(new TriggerNode("lady vashj adds spawn in phase 2 and phase 3", { NextAction("lady vashj assign phase 2 and phase 3 dps priority", ACTION_RAID + 1) })); @@ -198,6 +201,7 @@ void RaidSSCStrategy::InitMultipliers(std::vector& multipliers) // Lady Vashj multipliers.push_back(new LadyVashjDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new LadyVashjMainTankGroupShamanUseGroundingTotemMultiplier(botAI)); multipliers.push_back(new LadyVashjMaintainPhase1RangedSpreadMultiplier(botAI)); multipliers.push_back(new LadyVashjStaticChargeStayAwayFromGroupMultiplier(botAI)); multipliers.push_back(new LadyVashjDoNotLootTheTaintedCoreMultiplier(botAI)); diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCStrategy.h similarity index 52% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCStrategy.h index 3c2c05f58..c68e4e32a 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Strategy/RaidSSCStrategy.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCStrategy.h @@ -1,8 +1,13 @@ -#ifndef _PLAYERBOT_RAIDSSCSTRATEGY_H_ -#define _PLAYERBOT_RAIDSSCSTRATEGY_H_ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SSCSTRATEGY_H +#define PLAYERBOTS_SSCSTRATEGY_H #include "Strategy.h" -#include "Multiplier.h" class RaidSSCStrategy : public Strategy { diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggerContext.h similarity index 59% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggerContext.h index 13135bf3e..a1af2004e 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggerContext.h @@ -1,8 +1,14 @@ -#ifndef _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidSSCTriggers.h" -#include "AiObjectContext.h" +#ifndef PLAYERBOTS_SSCTRIGGERCONTEXT_H +#define PLAYERBOTS_SSCTRIGGERCONTEXT_H + +#include "SSCTriggers.h" +#include "NamedObjectContext.h" class RaidSSCTriggerContext : public NamedObjectContext { @@ -155,9 +161,6 @@ public: creators["lady vashj tainted core is unusable"] = &RaidSSCTriggerContext::lady_vashj_tainted_core_is_unusable; - creators["lady vashj need to reset core passing trackers"] = - &RaidSSCTriggerContext::lady_vashj_need_to_reset_core_passing_trackers; - creators["lady vashj toxic sporebats are spewing poison clouds"] = &RaidSSCTriggerContext::lady_vashj_toxic_sporebats_are_spewing_poison_clouds; @@ -167,159 +170,164 @@ public: private: // General - static Trigger* serpent_shrine_cavern_bot_is_not_in_combat( - PlayerbotAI* botAI) { return new SerpentShrineCavernBotIsNotInCombatTrigger(botAI); } + static Trigger* serpent_shrine_cavern_bot_is_not_in_combat(PlayerbotAI* botAI) { + return new SerpentShrineCavernBotIsNotInCombatTrigger(botAI); + } // Trash - static Trigger* underbog_colossus_spawned_toxic_pool_after_death( - PlayerbotAI* botAI) { return new UnderbogColossusSpawnedToxicPoolAfterDeathTrigger(botAI); } - - static Trigger* greyheart_tidecaller_water_elemental_totem_spawned( - PlayerbotAI* botAI) { return new GreyheartTidecallerWaterElementalTotemSpawnedTrigger(botAI); } + static Trigger* underbog_colossus_spawned_toxic_pool_after_death(PlayerbotAI* botAI) { + return new UnderbogColossusSpawnedToxicPoolAfterDeathTrigger(botAI); + } + static Trigger* greyheart_tidecaller_water_elemental_totem_spawned(PlayerbotAI* botAI) { + return new GreyheartTidecallerWaterElementalTotemSpawnedTrigger(botAI); + } // Hydross the Unstable - static Trigger* hydross_the_unstable_bot_is_frost_tank( - PlayerbotAI* botAI) { return new HydrossTheUnstableBotIsFrostTankTrigger(botAI); } - - static Trigger* hydross_the_unstable_bot_is_nature_tank( - PlayerbotAI* botAI) { return new HydrossTheUnstableBotIsNatureTankTrigger(botAI); } - - static Trigger* hydross_the_unstable_elementals_spawned( - PlayerbotAI* botAI) { return new HydrossTheUnstableElementalsSpawnedTrigger(botAI); } - - static Trigger* hydross_the_unstable_danger_from_water_tombs( - PlayerbotAI* botAI) { return new HydrossTheUnstableDangerFromWaterTombsTrigger(botAI); } - - static Trigger* hydross_the_unstable_tank_needs_aggro_upon_phase_change( - PlayerbotAI* botAI) { return new HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger(botAI); } - - static Trigger* hydross_the_unstable_aggro_resets_upon_phase_change( - PlayerbotAI* botAI) { return new HydrossTheUnstableAggroResetsUponPhaseChangeTrigger(botAI); } - - static Trigger* hydross_the_unstable_need_to_manage_timers( - PlayerbotAI* botAI) { return new HydrossTheUnstableNeedToManageTimersTrigger(botAI); } + static Trigger* hydross_the_unstable_bot_is_frost_tank(PlayerbotAI* botAI) { + return new HydrossTheUnstableBotIsFrostTankTrigger(botAI); + } + static Trigger* hydross_the_unstable_bot_is_nature_tank(PlayerbotAI* botAI) { + return new HydrossTheUnstableBotIsNatureTankTrigger(botAI); + } + static Trigger* hydross_the_unstable_elementals_spawned(PlayerbotAI* botAI) { + return new HydrossTheUnstableElementalsSpawnedTrigger(botAI); + } + static Trigger* hydross_the_unstable_danger_from_water_tombs(PlayerbotAI* botAI) { + return new HydrossTheUnstableDangerFromWaterTombsTrigger(botAI); + } + static Trigger* hydross_the_unstable_tank_needs_aggro_upon_phase_change(PlayerbotAI* botAI) { + return new HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger(botAI); + } + static Trigger* hydross_the_unstable_aggro_resets_upon_phase_change(PlayerbotAI* botAI) { + return new HydrossTheUnstableAggroResetsUponPhaseChangeTrigger(botAI); + } + static Trigger* hydross_the_unstable_need_to_manage_timers(PlayerbotAI* botAI) { + return new HydrossTheUnstableNeedToManageTimersTrigger(botAI); + } // The Lurker Below - static Trigger* the_lurker_below_spout_is_active( - PlayerbotAI* botAI) { return new TheLurkerBelowSpoutIsActiveTrigger(botAI); } - - static Trigger* the_lurker_below_boss_is_active_for_main_tank( - PlayerbotAI* botAI) { return new TheLurkerBelowBossIsActiveForMainTankTrigger(botAI); } - - static Trigger* the_lurker_below_boss_casts_geyser( - PlayerbotAI* botAI) { return new TheLurkerBelowBossCastsGeyserTrigger(botAI); } - - static Trigger* the_lurker_below_boss_is_submerged( - PlayerbotAI* botAI) { return new TheLurkerBelowBossIsSubmergedTrigger(botAI); } - - static Trigger* the_lurker_below_need_to_prepare_timer_for_spout( - PlayerbotAI* botAI) { return new TheLurkerBelowNeedToPrepareTimerForSpoutTrigger(botAI); } + static Trigger* the_lurker_below_spout_is_active(PlayerbotAI* botAI) { + return new TheLurkerBelowSpoutIsActiveTrigger(botAI); + } + static Trigger* the_lurker_below_boss_is_active_for_main_tank(PlayerbotAI* botAI) { + return new TheLurkerBelowBossIsActiveForMainTankTrigger(botAI); + } + static Trigger* the_lurker_below_boss_casts_geyser(PlayerbotAI* botAI) { + return new TheLurkerBelowBossCastsGeyserTrigger(botAI); + } + static Trigger* the_lurker_below_boss_is_submerged(PlayerbotAI* botAI) { + return new TheLurkerBelowBossIsSubmergedTrigger(botAI); + } + static Trigger* the_lurker_below_need_to_prepare_timer_for_spout(PlayerbotAI* botAI) { + return new TheLurkerBelowNeedToPrepareTimerForSpoutTrigger(botAI); + } // Leotheras the Blind - static Trigger* leotheras_the_blind_boss_is_inactive( - PlayerbotAI* botAI) { return new LeotherasTheBlindBossIsInactiveTrigger(botAI); } - - static Trigger* leotheras_the_blind_boss_transformed_into_demon_form( - PlayerbotAI* botAI) { return new LeotherasTheBlindBossTransformedIntoDemonFormTrigger(botAI); } - - static Trigger* leotheras_the_blind_only_warlock_should_tank_demon_form( - PlayerbotAI* botAI) { return new LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger(botAI); } - - static Trigger* leotheras_the_blind_boss_engaged_by_ranged( - PlayerbotAI* botAI) { return new LeotherasTheBlindBossEngagedByRangedTrigger(botAI); } - - static Trigger* leotheras_the_blind_boss_channeling_whirlwind( - PlayerbotAI* botAI) { return new LeotherasTheBlindBossChannelingWhirlwindTrigger(botAI); } - - static Trigger* leotheras_the_blind_bot_has_too_many_chaos_blast_stacks( - PlayerbotAI* botAI) { return new LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger(botAI); } - - static Trigger* leotheras_the_blind_inner_demon_has_awakened( - PlayerbotAI* botAI) { return new LeotherasTheBlindInnerDemonHasAwakenedTrigger(botAI); } - - static Trigger* leotheras_the_blind_entered_final_phase( - PlayerbotAI* botAI) { return new LeotherasTheBlindEnteredFinalPhaseTrigger(botAI); } - - static Trigger* leotheras_the_blind_demon_form_tank_needs_aggro( - PlayerbotAI* botAI) { return new LeotherasTheBlindDemonFormTankNeedsAggro(botAI); } - - static Trigger* leotheras_the_blind_boss_wipes_aggro_upon_phase_change( - PlayerbotAI* botAI) { return new LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger(botAI); } + static Trigger* leotheras_the_blind_boss_is_inactive(PlayerbotAI* botAI) { + return new LeotherasTheBlindBossIsInactiveTrigger(botAI); + } + static Trigger* leotheras_the_blind_boss_transformed_into_demon_form(PlayerbotAI* botAI) { + return new LeotherasTheBlindBossTransformedIntoDemonFormTrigger(botAI); + } + static Trigger* leotheras_the_blind_only_warlock_should_tank_demon_form(PlayerbotAI* botAI) { + return new LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger(botAI); + } + static Trigger* leotheras_the_blind_boss_engaged_by_ranged(PlayerbotAI* botAI) { + return new LeotherasTheBlindBossEngagedByRangedTrigger(botAI); + } + static Trigger* leotheras_the_blind_boss_channeling_whirlwind(PlayerbotAI* botAI) { + return new LeotherasTheBlindBossChannelingWhirlwindTrigger(botAI); + } + static Trigger* leotheras_the_blind_bot_has_too_many_chaos_blast_stacks(PlayerbotAI* botAI) { + return new LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger(botAI); + } + static Trigger* leotheras_the_blind_inner_demon_has_awakened(PlayerbotAI* botAI) { + return new LeotherasTheBlindInnerDemonHasAwakenedTrigger(botAI); + } + static Trigger* leotheras_the_blind_entered_final_phase(PlayerbotAI* botAI) { + return new LeotherasTheBlindEnteredFinalPhaseTrigger(botAI); + } + static Trigger* leotheras_the_blind_demon_form_tank_needs_aggro(PlayerbotAI* botAI) { + return new LeotherasTheBlindDemonFormTankNeedsAggro(botAI); + } + static Trigger* leotheras_the_blind_boss_wipes_aggro_upon_phase_change(PlayerbotAI* botAI) { + return new LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger(botAI); + } // Fathom-Lord Karathress - static Trigger* fathom_lord_karathress_boss_engaged_by_main_tank( - PlayerbotAI* botAI) { return new FathomLordKarathressBossEngagedByMainTankTrigger(botAI); } - - static Trigger* fathom_lord_karathress_caribdis_engaged_by_first_assist_tank( - PlayerbotAI* botAI) { return new FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger(botAI); } - - static Trigger* fathom_lord_karathress_sharkkis_engaged_by_second_assist_tank( - PlayerbotAI* botAI) { return new FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger(botAI); } - - static Trigger* fathom_lord_karathress_tidalvess_engaged_by_third_assist_tank( - PlayerbotAI* botAI) { return new FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger(botAI); } - - static Trigger* fathom_lord_karathress_caribdis_tank_needs_dedicated_healer( - PlayerbotAI* botAI) { return new FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger(botAI); } - - static Trigger* fathom_lord_karathress_pulling_bosses( - PlayerbotAI* botAI) { return new FathomLordKarathressPullingBossesTrigger(botAI); } - - static Trigger* fathom_lord_karathress_determining_kill_order( - PlayerbotAI* botAI) { return new FathomLordKarathressDeterminingKillOrderTrigger(botAI); } - - static Trigger* fathom_lord_karathress_tanks_need_to_establish_aggro( - PlayerbotAI* botAI) { return new FathomLordKarathressTanksNeedToEstablishAggroTrigger(botAI); } + static Trigger* fathom_lord_karathress_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new FathomLordKarathressBossEngagedByMainTankTrigger(botAI); + } + static Trigger* fathom_lord_karathress_caribdis_engaged_by_first_assist_tank(PlayerbotAI* botAI) { + return new FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger(botAI); + } + static Trigger* fathom_lord_karathress_sharkkis_engaged_by_second_assist_tank(PlayerbotAI* botAI) { + return new FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger(botAI); + } + static Trigger* fathom_lord_karathress_tidalvess_engaged_by_third_assist_tank(PlayerbotAI* botAI) { + return new FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger(botAI); + } + static Trigger* fathom_lord_karathress_caribdis_tank_needs_dedicated_healer(PlayerbotAI* botAI) { + return new FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger(botAI); + } + static Trigger* fathom_lord_karathress_pulling_bosses(PlayerbotAI* botAI) { + return new FathomLordKarathressPullingBossesTrigger(botAI); + } + static Trigger* fathom_lord_karathress_determining_kill_order(PlayerbotAI* botAI) { + return new FathomLordKarathressDeterminingKillOrderTrigger(botAI); + } + static Trigger* fathom_lord_karathress_tanks_need_to_establish_aggro(PlayerbotAI* botAI) { + return new FathomLordKarathressTanksNeedToEstablishAggroTrigger(botAI); + } // Morogrim Tidewalker - static Trigger* morogrim_tidewalker_boss_engaged_by_main_tank( - PlayerbotAI* botAI) { return new MorogrimTidewalkerBossEngagedByMainTankTrigger(botAI); } - - static Trigger* morogrim_tidewalker_pulling_boss( - PlayerbotAI* botAI) { return new MorogrimTidewalkerPullingBossTrigger(botAI); } - - static Trigger* morogrim_tidewalker_water_globules_are_incoming( - PlayerbotAI* botAI) { return new MorogrimTidewalkerWaterGlobulesAreIncomingTrigger(botAI); } + static Trigger* morogrim_tidewalker_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new MorogrimTidewalkerBossEngagedByMainTankTrigger(botAI); + } + static Trigger* morogrim_tidewalker_pulling_boss(PlayerbotAI* botAI) { + return new MorogrimTidewalkerPullingBossTrigger(botAI); + } + static Trigger* morogrim_tidewalker_water_globules_are_incoming(PlayerbotAI* botAI) { + return new MorogrimTidewalkerWaterGlobulesAreIncomingTrigger(botAI); + } // Lady Vashj - static Trigger* lady_vashj_boss_engaged_by_main_tank( - PlayerbotAI* botAI) { return new LadyVashjBossEngagedByMainTankTrigger(botAI); } - - static Trigger* lady_vashj_boss_engaged_by_ranged_in_phase_1( - PlayerbotAI* botAI) { return new LadyVashjBossEngagedByRangedInPhase1Trigger(botAI); } - - static Trigger* lady_vashj_casts_shock_blast_on_highest_aggro( - PlayerbotAI* botAI) { return new LadyVashjCastsShockBlastOnHighestAggroTrigger(botAI); } - - static Trigger* lady_vashj_bot_has_static_charge( - PlayerbotAI* botAI) { return new LadyVashjBotHasStaticChargeTrigger(botAI); } - - static Trigger* lady_vashj_pulling_boss_in_phase_1_and_phase_3( - PlayerbotAI* botAI) { return new LadyVashjPullingBossInPhase1AndPhase3Trigger(botAI); } - - static Trigger* lady_vashj_adds_spawn_in_phase_2_and_phase_3( - PlayerbotAI* botAI) { return new LadyVashjAddsSpawnInPhase2AndPhase3Trigger(botAI); } - - static Trigger* lady_vashj_coilfang_strider_is_approaching( - PlayerbotAI* botAI) { return new LadyVashjCoilfangStriderIsApproachingTrigger(botAI); } - - static Trigger* lady_vashj_tainted_elemental_cheat( - PlayerbotAI* botAI) { return new LadyVashjTaintedElementalCheatTrigger(botAI); } - - static Trigger* lady_vashj_tainted_core_was_looted( - PlayerbotAI* botAI) { return new LadyVashjTaintedCoreWasLootedTrigger(botAI); } - - static Trigger* lady_vashj_tainted_core_is_unusable( - PlayerbotAI* botAI) { return new LadyVashjTaintedCoreIsUnusableTrigger(botAI); } - - static Trigger* lady_vashj_need_to_reset_core_passing_trackers( - PlayerbotAI* botAI) { return new LadyVashjNeedToResetCorePassingTrackersTrigger(botAI); } - - static Trigger* lady_vashj_toxic_sporebats_are_spewing_poison_clouds( - PlayerbotAI* botAI) { return new LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger(botAI); } - - static Trigger* lady_vashj_bot_is_entangled_in_toxic_spores_or_static_charge( - PlayerbotAI* botAI) { return new LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger(botAI); } + static Trigger* lady_vashj_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new LadyVashjBossEngagedByMainTankTrigger(botAI); + } + static Trigger* lady_vashj_boss_engaged_by_ranged_in_phase_1(PlayerbotAI* botAI) { + return new LadyVashjBossEngagedByRangedInPhase1Trigger(botAI); + } + static Trigger* lady_vashj_casts_shock_blast_on_highest_aggro(PlayerbotAI* botAI) { + return new LadyVashjCastsShockBlastOnHighestAggroTrigger(botAI); + } + static Trigger* lady_vashj_bot_has_static_charge(PlayerbotAI* botAI) { + return new LadyVashjBotHasStaticChargeTrigger(botAI); + } + static Trigger* lady_vashj_pulling_boss_in_phase_1_and_phase_3(PlayerbotAI* botAI) { + return new LadyVashjPullingBossInPhase1AndPhase3Trigger(botAI); + } + static Trigger* lady_vashj_adds_spawn_in_phase_2_and_phase_3(PlayerbotAI* botAI) { + return new LadyVashjAddsSpawnInPhase2AndPhase3Trigger(botAI); + } + static Trigger* lady_vashj_coilfang_strider_is_approaching(PlayerbotAI* botAI) { + return new LadyVashjCoilfangStriderIsApproachingTrigger(botAI); + } + static Trigger* lady_vashj_tainted_elemental_cheat(PlayerbotAI* botAI) { + return new LadyVashjTaintedElementalCheatTrigger(botAI); + } + static Trigger* lady_vashj_tainted_core_was_looted(PlayerbotAI* botAI) { + return new LadyVashjTaintedCoreWasLootedTrigger(botAI); + } + static Trigger* lady_vashj_tainted_core_is_unusable(PlayerbotAI* botAI) { + return new LadyVashjTaintedCoreIsUnusableTrigger(botAI); + } + static Trigger* lady_vashj_toxic_sporebats_are_spewing_poison_clouds(PlayerbotAI* botAI) { + return new LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger(botAI); + } + static Trigger* lady_vashj_bot_is_entangled_in_toxic_spores_or_static_charge(PlayerbotAI* botAI) { + return new LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger(botAI); + } }; #endif diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggers.cpp similarity index 61% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggers.cpp index e77e63642..ffd4959ab 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggers.cpp @@ -1,6 +1,12 @@ -#include "RaidSSCTriggers.h" -#include "RaidSSCHelpers.h" -#include "RaidSSCActions.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "SSCTriggers.h" +#include "SSCHelpers.h" +#include "SSCActions.h" #include "AiFactory.h" #include "Corpse.h" #include "LootObjectStack.h" @@ -13,7 +19,7 @@ using namespace SerpentShrineCavernHelpers; // General bool SerpentShrineCavernBotIsNotInCombatTrigger::IsActive() { - return !bot->IsInCombat(); + return bot->GetMapId() == SSC_MAP_ID && !AI_VALUE2(bool, "combat", "self target"); } // Trash Mobs @@ -26,35 +32,37 @@ bool UnderbogColossusSpawnedToxicPoolAfterDeathTrigger::IsActive() bool GreyheartTidecallerWaterElementalTotemSpawnedTrigger::IsActive() { return botAI->IsDps(bot) && - GetFirstAliveUnitByEntry(botAI, NPC_WATER_ELEMENTAL_TOTEM); + AI_VALUE2(Unit*, "find target", "greyheart tidecaller"); } // Hydross the Unstable bool HydrossTheUnstableBotIsFrostTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "hydross the unstable") && - botAI->IsMainTank(bot); + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); } bool HydrossTheUnstableBotIsNatureTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "hydross the unstable") && - botAI->IsAssistTankOfIndex(bot, 0, true); + return botAI->IsAssistTankOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); } bool HydrossTheUnstableElementalsSpawnedTrigger::IsActive() { + if (botAI->IsHeal(bot)) + return false; + Unit* hydross = AI_VALUE2(Unit*, "find target", "hydross the unstable"); - if (hydross && hydross->GetHealthPct() < 10.0f) + if (!hydross || hydross->GetHealthPct() < 10.0f) return false; - if (!AI_VALUE2(Unit*, "find target", "pure spawn of hydross") && - !AI_VALUE2(Unit*, "find target", "tainted spawn of hydross")) + if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true)) return false; - return !botAI->IsHeal(bot) && !botAI->IsMainTank(bot) && - !botAI->IsAssistTankOfIndex(bot, 0, true); + return AI_VALUE2(Unit*, "find target", "pure spawn of hydross") || + AI_VALUE2(Unit*, "find target", "tainted spawn of hydross"); } bool HydrossTheUnstableDangerFromWaterTombsTrigger::IsActive() @@ -71,19 +79,19 @@ bool HydrossTheUnstableTankNeedsAggroUponPhaseChangeTrigger::IsActive() bool HydrossTheUnstableAggroResetsUponPhaseChangeTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "hydross the unstable")) + if (bot->getClass() == CLASS_HUNTER || + botAI->IsHeal(bot) || + botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, true)) return false; - return bot->getClass() != CLASS_HUNTER && - !botAI->IsHeal(bot) && - !botAI->IsMainTank(bot) && - !botAI->IsAssistTankOfIndex(bot, 0, true); + return AI_VALUE2(Unit*, "find target", "hydross the unstable"); } bool HydrossTheUnstableNeedToManageTimersTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "hydross the unstable") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "hydross the unstable"); } // The Lurker Below @@ -102,11 +110,11 @@ bool TheLurkerBelowSpoutIsActiveTrigger::IsActive() bool TheLurkerBelowBossIsActiveForMainTankTrigger::IsActive() { - Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); - if (!lurker) + if (!botAI->IsMainTank(bot)) return false; - if (!botAI->IsMainTank(bot)) + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); + if (!lurker) return false; const time_t now = std::time(nullptr); @@ -135,35 +143,16 @@ bool TheLurkerBelowBossCastsGeyserTrigger::IsActive() // Trigger will be active only if there are at least 3 tanks in the raid bool TheLurkerBelowBossIsSubmergedTrigger::IsActive() { + if (!botAI->IsTank(bot)) + return false; + Unit* lurker = AI_VALUE2(Unit*, "find target", "the lurker below"); if (!lurker || lurker->getStandState() != UNIT_STAND_STATE_SUBMERGED) return false; - Player* mainTank = nullptr; - Player* firstAssistTank = nullptr; - Player* secondAssistTank = nullptr; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - PlayerbotAI* memberAI = GET_PLAYERBOT_AI(member); - if (!memberAI) - continue; - - if (!mainTank && memberAI->IsMainTank(member)) - mainTank = member; - else if (!firstAssistTank && memberAI->IsAssistTankOfIndex(member, 0, true)) - firstAssistTank = member; - else if (!secondAssistTank && memberAI->IsAssistTankOfIndex(member, 1, true)) - secondAssistTank = member; - } + Player* mainTank = GetGroupMainTank(botAI, bot); + Player* firstAssistTank = GetGroupAssistTank(botAI, bot, 0); + Player* secondAssistTank = GetGroupAssistTank(botAI, bot, 1); if (!mainTank || !firstAssistTank || !secondAssistTank) return false; @@ -173,51 +162,55 @@ bool TheLurkerBelowBossIsSubmergedTrigger::IsActive() bool TheLurkerBelowNeedToPrepareTimerForSpoutTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "the lurker below") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "the lurker below"); } // Leotheras the Blind bool LeotherasTheBlindBossIsInactiveTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "greyheart spellbinder"); + return IsMechanicTrackerBot(bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "greyheart spellbinder"); } bool LeotherasTheBlindBossTransformedIntoDemonFormTrigger::IsActive() { + if (bot->getClass() != CLASS_WARLOCK) + return false; + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; if (GetLeotherasDemonFormTank(bot) != bot) return false; - return GetActiveLeotherasDemon(botAI); + return GetActiveLeotherasDemon(bot); } bool LeotherasTheBlindOnlyWarlockShouldTankDemonFormTrigger::IsActive() { - if (botAI->IsRanged(bot) || !botAI->IsTank(bot)) + if (!botAI->IsTank(bot)) + return false; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) - return false; - if (!GetLeotherasDemonFormTank(bot)) return false; - return GetPhase2LeotherasDemon(botAI); + return GetPhase2LeotherasDemon(bot); } bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (!botAI->IsRanged(bot)) return false; - if (!botAI->IsRanged(bot)) + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); @@ -231,14 +224,14 @@ bool LeotherasTheBlindBossEngagedByRangedTrigger::IsActive() bool LeotherasTheBlindBossChannelingWhirlwindTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) - return false; - - if (botAI->IsTank(bot) && botAI->IsMelee(bot)) + if (botAI->IsTank(bot)) return false; Unit* leotheras = AI_VALUE2(Unit*, "find target", "leotheras the blind"); - if (!leotheras || leotheras->HasAura(SPELL_LEOTHERAS_BANISHED)) + if (!leotheras) + return false; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; return leotheras->HasAura(SPELL_WHIRLWIND) || @@ -247,10 +240,13 @@ bool LeotherasTheBlindBossChannelingWhirlwindTrigger::IsActive() bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + if (botAI->IsRanged(bot)) return false; - if (botAI->IsRanged(bot)) + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) return false; Aura* chaosBlast = bot->GetAura(SPELL_CHAOS_BLAST); @@ -260,7 +256,7 @@ bool LeotherasTheBlindBotHasTooManyChaosBlastStacksTrigger::IsActive() if (!GetLeotherasDemonFormTank(bot) && botAI->IsMainTank(bot)) return false; - return GetPhase2LeotherasDemon(botAI); + return GetPhase2LeotherasDemon(bot); } bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive() @@ -271,89 +267,68 @@ bool LeotherasTheBlindInnerDemonHasAwakenedTrigger::IsActive() bool LeotherasTheBlindEnteredFinalPhaseTrigger::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) - return false; - if (botAI->IsHeal(bot)) return false; - if (GetLeotherasDemonFormTank(bot) == bot) + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) return false; - return GetPhase3LeotherasDemon(botAI) && - GetLeotherasHuman(botAI); + if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) + return false; + + if (bot->getClass() == CLASS_WARLOCK && GetLeotherasDemonFormTank(bot) == bot) + return false; + + return GetPhase3LeotherasDemon(bot); } bool LeotherasTheBlindDemonFormTankNeedsAggro::IsActive() { - if (bot->HasAura(SPELL_INSIDIOUS_WHISPER)) - return false; - if (bot->getClass() != CLASS_HUNTER) return false; - return AI_VALUE2(Unit*, "find target", "leotheras the blind"); + if (!AI_VALUE2(Unit*, "find target", "leotheras the blind")) + return false; + + return !bot->HasAura(SPELL_INSIDIOUS_WHISPER); } bool LeotherasTheBlindBossWipesAggroUponPhaseChangeTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "leotheras the blind") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "leotheras the blind"); } // Fathom-Lord Karathress bool FathomLordKarathressBossEngagedByMainTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && - botAI->IsMainTank(bot); + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); } bool FathomLordKarathressCaribdisEngagedByFirstAssistTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-guard caribdis") && - botAI->IsAssistTankOfIndex(bot, 0, false); + return botAI->IsAssistTankOfIndex(bot, 0, false) && + AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); } bool FathomLordKarathressSharkkisEngagedBySecondAssistTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis") && - botAI->IsAssistTankOfIndex(bot, 1, false); + return botAI->IsAssistTankOfIndex(bot, 1, false) && + AI_VALUE2(Unit*, "find target", "fathom-guard sharkkis"); } bool FathomLordKarathressTidalvessEngagedByThirdAssistTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess") && - botAI->IsAssistTankOfIndex(bot, 2, false); + return botAI->IsAssistTankOfIndex(bot, 2, false) && + AI_VALUE2(Unit*, "find target", "fathom-guard tidalvess"); } bool FathomLordKarathressCaribdisTankNeedsDedicatedHealerTrigger::IsActive() { - Unit* caribdis = AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); - if (!caribdis) - return false; - - if (!botAI->IsAssistHealOfIndex(bot, 0, true)) - return false; - - Player* firstAssistTank = nullptr; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->IsAlive()) - continue; - - if (botAI->IsAssistTankOfIndex(member, 0, false)) - { - firstAssistTank = member; - break; - } - } - } - - return firstAssistTank; + return botAI->IsAssistHealOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "fathom-guard caribdis"); } bool FathomLordKarathressPullingBossesTrigger::IsActive() @@ -367,10 +342,10 @@ bool FathomLordKarathressPullingBossesTrigger::IsActive() bool FathomLordKarathressDeterminingKillOrderTrigger::IsActive() { - if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) + if (botAI->IsHeal(bot)) return false; - if (botAI->IsHeal(bot)) + if (!AI_VALUE2(Unit*, "find target", "fathom-lord karathress")) return false; if (botAI->IsDps(bot)) @@ -387,8 +362,8 @@ bool FathomLordKarathressDeterminingKillOrderTrigger::IsActive() bool FathomLordKarathressTanksNeedToEstablishAggroTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "fathom-lord karathress") && - IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr); + return IsMechanicTrackerBot(bot, SSC_MAP_ID) && + AI_VALUE2(Unit*, "find target", "fathom-lord karathress"); } // Morogrim Tidewalker @@ -404,8 +379,8 @@ bool MorogrimTidewalkerPullingBossTrigger::IsActive() bool MorogrimTidewalkerBossEngagedByMainTankTrigger::IsActive() { - return AI_VALUE2(Unit*, "find target", "morogrim tidewalker") && - botAI->IsMainTank(bot); + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "morogrim tidewalker"); } bool MorogrimTidewalkerWaterGlobulesAreIncomingTrigger::IsActive() @@ -421,8 +396,11 @@ bool MorogrimTidewalkerWaterGlobulesAreIncomingTrigger::IsActive() bool LadyVashjBossEngagedByMainTankTrigger::IsActive() { + if (!botAI->IsMainTank(bot)) + return false; + return AI_VALUE2(Unit*, "find target", "lady vashj") && - !IsLadyVashjInPhase2(botAI) && botAI->IsMainTank(bot); + !IsLadyVashjInPhase2(botAI); } bool LadyVashjBossEngagedByRangedInPhase1Trigger::IsActive() @@ -439,10 +417,7 @@ bool LadyVashjCastsShockBlastOnHighestAggroTrigger::IsActive() IsLadyVashjInPhase2(botAI)) return false; - if (!IsMainTankInSameSubgroup(bot)) - return false; - - return true; + return IsMainTankInSameSubgroup(botAI, bot); } bool LadyVashjBotHasStaticChargeTrigger::IsActive() @@ -450,14 +425,15 @@ bool LadyVashjBotHasStaticChargeTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "lady vashj")) return false; - if (Group* group = bot->GetGroup()) + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (member && member->HasAura(SPELL_STATIC_CHARGE)) - return true; - } + Player* member = ref->GetSource(); + if (member && member->HasAura(SPELL_STATIC_CHARGE)) + return true; } return false; @@ -500,9 +476,10 @@ bool LadyVashjTaintedElementalCheatTrigger::IsActive() return false; bool taintedPresent = false; - Unit* taintedUnit = AI_VALUE2(Unit*, "find target", "tainted elemental"); - if (taintedUnit) + if (AI_VALUE2(Unit*, "find target", "tainted elemental")) + { taintedPresent = true; + } else { GuidVector corpses = AI_VALUE(GuidVector, "nearest corpses"); @@ -513,13 +490,11 @@ bool LadyVashjTaintedElementalCheatTrigger::IsActive() if (!object) continue; - if (Creature* creature = object->ToCreature()) + if (Creature* creature = object->ToCreature(); + creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive()) { - if (creature->GetEntry() == NPC_TAINTED_ELEMENTAL && !creature->IsAlive()) - { - taintedPresent = true; - break; - } + taintedPresent = true; + break; } } } @@ -527,12 +502,8 @@ bool LadyVashjTaintedElementalCheatTrigger::IsActive() if (!taintedPresent) return false; - Group* group = bot->GetGroup(); - if (!group) - return false; - - return (GetDesignatedCoreLooter(group, botAI) == bot && - !bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)); + return GetDesignatedCoreLooter(botAI, bot) == bot && + !bot->HasItemCount(ITEM_TAINTED_CORE, 1, false); } bool LadyVashjTaintedCoreWasLootedTrigger::IsActive() @@ -540,54 +511,24 @@ bool LadyVashjTaintedCoreWasLootedTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "lady vashj") || !IsLadyVashjInPhase2(botAI)) return false; - Group* group = bot->GetGroup(); - if (!group) + auto coreHandlers = GetCoreHandlers(botAI, bot); + + bool isCoreHandler = false; + for (Player* handler : coreHandlers) + if (handler == bot) + isCoreHandler = true; + + if (!isCoreHandler) return false; - Player* designatedLooter = GetDesignatedCoreLooter(group, botAI); - Player* firstCorePasser = GetFirstTaintedCorePasser(group, botAI); - Player* secondCorePasser = GetSecondTaintedCorePasser(group, botAI); - Player* thirdCorePasser = GetThirdTaintedCorePasser(group, botAI); - Player* fourthCorePasser = GetFourthTaintedCorePasser(group, botAI); - - auto hasCore = [](Player* player) -> bool - { - return player && player->HasItemCount(ITEM_TAINTED_CORE, 1, false); - }; - - if (bot == designatedLooter) - { - if (!hasCore(bot)) - return false; - } - else if (bot == firstCorePasser) - { - if (hasCore(secondCorePasser) || hasCore(thirdCorePasser) || - hasCore(fourthCorePasser)) - return false; - } - else if (bot == secondCorePasser) - { - if (hasCore(thirdCorePasser) || hasCore(fourthCorePasser)) - return false; - } - else if (bot == thirdCorePasser) - { - if (hasCore(fourthCorePasser)) - return false; - } - else if (bot != fourthCorePasser) - return false; - - if (AnyRecentCoreInInventory(group, botAI)) - return true; - // First and second passers move to positions as soon as the elemental appears - if (AI_VALUE2(Unit*, "find target", "tainted elemental") && - (bot == firstCorePasser || bot == secondCorePasser)) + Unit* tainted = AI_VALUE2(Unit*, "find target", "tainted elemental"); + if (tainted && coreHandlers[0]->GetExactDist2d(tainted) < 5.0f && + (bot == coreHandlers[1] || bot == coreHandlers[2])) return true; - return false; + // Main logic: run if core is in play for this bot or a prior handler + return AnyRecentCoreInInventory(botAI, bot); } bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() @@ -599,18 +540,7 @@ bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() if (!IsLadyVashjInPhase2(botAI)) return bot->HasItemCount(ITEM_TAINTED_CORE, 1, false); - Group* group = bot->GetGroup(); - if (!group) - return false; - - Player* coreHandlers[] = - { - GetDesignatedCoreLooter(group, botAI), - GetFirstTaintedCorePasser(group, botAI), - GetSecondTaintedCorePasser(group, botAI), - GetThirdTaintedCorePasser(group, botAI), - GetFourthTaintedCorePasser(group, botAI) - }; + auto coreHandlers = GetCoreHandlers(botAI, bot); if (bot->HasItemCount(ITEM_TAINTED_CORE, 1, false)) { @@ -625,24 +555,6 @@ bool LadyVashjTaintedCoreIsUnusableTrigger::IsActive() return false; } -bool LadyVashjNeedToResetCorePassingTrackersTrigger::IsActive() -{ - Unit* vashj = AI_VALUE2(Unit*, "find target", "lady vashj"); - if (!vashj || IsLadyVashjInPhase2(botAI)) - return false; - - Group* group = bot->GetGroup(); - if (!group) - return false; - - return IsMechanicTrackerBot(botAI, bot, SSC_MAP_ID, nullptr) || - GetDesignatedCoreLooter(group, botAI) == bot || - GetFirstTaintedCorePasser(group, botAI) == bot || - GetSecondTaintedCorePasser(group, botAI) == bot || - GetThirdTaintedCorePasser(group, botAI) == bot || - GetFourthTaintedCorePasser(group, botAI) == bot; -} - bool LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger::IsActive() { return IsLadyVashjInPhase3(botAI); @@ -653,17 +565,18 @@ bool LadyVashjBotIsEntangledInToxicSporesOrStaticChargeTrigger::IsActive() if (!AI_VALUE2(Unit*, "find target", "lady vashj")) return false; - if (Group* group = bot->GetGroup()) - { - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member || !member->HasAura(SPELL_ENTANGLE)) - continue; + Group* group = bot->GetGroup(); + if (!group) + return false; - if (botAI->IsMelee(member)) - return true; - } + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->HasAura(SPELL_ENTANGLE)) + continue; + + if (botAI->IsMelee(member)) + return true; } return false; diff --git a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggers.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggers.h index e106b58f3..8b97117d9 100644 --- a/modules/mod-playerbots/src/Ai/Raid/SerpentshrineCavern/Trigger/RaidSSCTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/SSC/SSCTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDSSCTRIGGERS_H -#define _PLAYERBOT_RAIDSSCTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_SSCTRIGGERS_H +#define PLAYERBOTS_SSCTRIGGERS_H #include "Trigger.h" @@ -387,14 +393,6 @@ public: bool IsActive() override; }; -class LadyVashjNeedToResetCorePassingTrackersTrigger : public Trigger -{ -public: - LadyVashjNeedToResetCorePassingTrackersTrigger( - PlayerbotAI* botAI) : Trigger(botAI, "lady vashj need to reset core passing trackers") {} - bool IsActive() override; -}; - class LadyVashjToxicSporebatsAreSpewingPoisonCloudsTrigger : public Trigger { public: diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKActionContext.h b/modules/mod-playerbots/src/Ai/Raid/TK/TKActionContext.h new file mode 100644 index 000000000..0e6022190 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKActionContext.h @@ -0,0 +1,300 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKACTIONCONTEXT_H +#define PLAYERBOTS_TKACTIONCONTEXT_H + +#include "TKActions.h" +#include "NamedObjectContext.h" + +class RaidTempestKeepActionContext : public NamedObjectContext +{ +public: + RaidTempestKeepActionContext() + { + // Trash + creators["crimson hand centurion cast polymorph"] = + &RaidTempestKeepActionContext::crimson_hand_centurion_cast_polymorph; + + // Al'ar + creators["al'ar misdirect boss to main tank"] = + &RaidTempestKeepActionContext::alar_misdirect_boss_to_main_tank; + + creators["al'ar boss tanks move between platforms"] = + &RaidTempestKeepActionContext::alar_boss_tanks_move_between_platforms; + + creators["al'ar melee dps move between platforms"] = + &RaidTempestKeepActionContext::alar_melee_dps_move_between_platforms; + + creators["al'ar ranged and ember tank move under platforms"] = + &RaidTempestKeepActionContext::alar_ranged_and_ember_tank_move_under_platforms; + + creators["al'ar assist tanks pick up embers"] = + &RaidTempestKeepActionContext::alar_assist_tanks_pick_up_embers; + + creators["al'ar ranged dps prioritize embers"] = + &RaidTempestKeepActionContext::alar_ranged_dps_prioritize_embers; + + creators["al'ar jump from platform"] = + &RaidTempestKeepActionContext::alar_jump_from_platform; + + creators["al'ar move away from rebirth"] = + &RaidTempestKeepActionContext::alar_move_away_from_rebirth; + + creators["al'ar swap tanks on boss"] = + &RaidTempestKeepActionContext::alar_swap_tanks_on_boss; + + creators["al'ar avoid flame patches and dive bombs"] = + &RaidTempestKeepActionContext::alar_avoid_flame_patches_and_dive_bombs; + + creators["al'ar return to room center"] = + &RaidTempestKeepActionContext::alar_return_to_room_center; + + creators["al'ar manage phase tracker"] = + &RaidTempestKeepActionContext::alar_manage_phase_tracker; + + // Void Reaver + creators["void reaver tanks position boss"] = + &RaidTempestKeepActionContext::void_reaver_tanks_position_boss; + + creators["void reaver use aggro dump ability"] = + &RaidTempestKeepActionContext::void_reaver_use_aggro_dump_ability; + + creators["void reaver spread ranged"] = + &RaidTempestKeepActionContext::void_reaver_spread_ranged; + + creators["void reaver avoid arcane orb"] = + &RaidTempestKeepActionContext::void_reaver_avoid_arcane_orb; + + creators["void reaver erase trackers"] = + &RaidTempestKeepActionContext::void_reaver_erase_trackers; + + // High Astromancer Solarian + creators["high astromancer solarian ranged leave space for melee"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_ranged_leave_space_for_melee; + + creators["high astromancer solarian move away from group"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_move_away_from_group; + + creators["high astromancer solarian stack for aoe"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_stack_for_aoe; + + creators["high astromancer solarian target solarium priests"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_target_solarium_priests; + + creators["high astromancer solarian cast fear ward on main tank"] = + &RaidTempestKeepActionContext::high_astromancer_solarian_cast_fear_ward_on_main_tank; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider kite thaladred"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_kite_thaladred; + + creators["kael'thas sunstrider misdirect advisors to tanks"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_misdirect_advisors_to_tanks; + + creators["kael'thas sunstrider main tank position sanguinar"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_sanguinar; + + creators["kael'thas sunstrider cast fear ward on sanguinar tank"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank; + + creators["kael'thas sunstrider warlock tank position capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_warlock_tank_position_capernian; + + creators["kael'thas sunstrider spread and move away from capernian"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_and_move_away_from_capernian; + + creators["kael'thas sunstrider first assist tank position telonicus"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_first_assist_tank_position_telonicus; + + creators["kael'thas sunstrider handle advisor roles in phase 3"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_advisor_roles_in_phase_3; + + creators["kael'thas sunstrider assign advisor dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_advisor_dps_priority; + + creators["kael'thas sunstrider manage advisor dps timer"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_manage_advisor_dps_timer; + + creators["kael'thas sunstrider assign legendary weapon dps priority"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_assign_legendary_weapon_dps_priority; + + creators["kael'thas sunstrider move devastation away"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_move_devastation_away; + + creators["kael'thas sunstrider loot legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_loot_legendary_weapons; + + creators["kael'thas sunstrider use legendary weapons"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_use_legendary_weapons; + + creators["kael'thas sunstrider reequip gear"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_reequip_gear; + + creators["kael'thas sunstrider main tank position boss"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_main_tank_position_boss; + + creators["kael'thas sunstrider avoid flame strike"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_avoid_flame_strike; + + creators["kael'thas sunstrider handle phoenixes and eggs"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_handle_phoenixes_and_eggs; + + creators["kael'thas sunstrider break mind control"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_mind_control; + + creators["kael'thas sunstrider break through shock barrier"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_break_through_shock_barrier; + + creators["kael'thas sunstrider spread out in midair"] = + &RaidTempestKeepActionContext::kaelthas_sunstrider_spread_out_in_midair; + } + +private: + // Trash + static Action* crimson_hand_centurion_cast_polymorph(PlayerbotAI* botAI) { + return new CrimsonHandCenturionCastPolymorphAction(botAI); + } + + // Al'ar + static Action* alar_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new AlarMisdirectBossToMainTankAction(botAI); + } + static Action* alar_boss_tanks_move_between_platforms(PlayerbotAI* botAI) { + return new AlarBossTanksMoveBetweenPlatformsAction(botAI); + } + static Action* alar_melee_dps_move_between_platforms(PlayerbotAI* botAI) { + return new AlarMeleeDpsMoveBetweenPlatformsAction(botAI); + } + static Action* alar_ranged_and_ember_tank_move_under_platforms(PlayerbotAI* botAI) { + return new AlarRangedAndEmberTankMoveUnderPlatformsAction(botAI); + } + static Action* alar_assist_tanks_pick_up_embers(PlayerbotAI* botAI) { + return new AlarAssistTanksPickUpEmbersAction(botAI); + } + static Action* alar_ranged_dps_prioritize_embers(PlayerbotAI* botAI) { + return new AlarRangedDpsPrioritizeEmbersAction(botAI); + } + static Action* alar_jump_from_platform(PlayerbotAI* botAI) { + return new AlarJumpFromPlatformAction(botAI); + } + static Action* alar_move_away_from_rebirth(PlayerbotAI* botAI) { + return new AlarMoveAwayFromRebirthAction(botAI); + } + static Action* alar_swap_tanks_on_boss(PlayerbotAI* botAI) { + return new AlarSwapTanksOnBossAction(botAI); + } + static Action* alar_avoid_flame_patches_and_dive_bombs(PlayerbotAI* botAI) { + return new AlarAvoidFlamePatchesAndDiveBombsAction(botAI); + } + static Action* alar_return_to_room_center(PlayerbotAI* botAI) { + return new AlarReturnToRoomCenterAction(botAI); + } + static Action* alar_manage_phase_tracker(PlayerbotAI* botAI) { + return new AlarManagePhaseTrackerAction(botAI); + } + + // Void Reaver + static Action* void_reaver_tanks_position_boss(PlayerbotAI* botAI) { + return new VoidReaverTanksPositionBossAction(botAI); + } + static Action* void_reaver_use_aggro_dump_ability(PlayerbotAI* botAI) { + return new VoidReaverUseAggroDumpAbilityAction(botAI); + } + static Action* void_reaver_spread_ranged(PlayerbotAI* botAI) { + return new VoidReaverSpreadRangedAction(botAI); + } + static Action* void_reaver_avoid_arcane_orb(PlayerbotAI* botAI) { + return new VoidReaverAvoidArcaneOrbAction(botAI); + } + static Action* void_reaver_erase_trackers(PlayerbotAI* botAI) { + return new VoidReaverEraseTrackersAction(botAI); + } + + // High Astromancer Solarian + static Action* high_astromancer_solarian_ranged_leave_space_for_melee(PlayerbotAI* botAI) { + return new HighAstromancerSolarianRangedLeaveSpaceForMeleeAction(botAI); + } + static Action* high_astromancer_solarian_move_away_from_group(PlayerbotAI* botAI) { + return new HighAstromancerSolarianMoveAwayFromGroupAction(botAI); + } + static Action* high_astromancer_solarian_stack_for_aoe(PlayerbotAI* botAI) { + return new HighAstromancerSolarianStackForAoeAction(botAI); + } + static Action* high_astromancer_solarian_target_solarium_priests(PlayerbotAI* botAI) { + return new HighAstromancerSolarianTargetSolariumPriestsAction(botAI); + } + static Action* high_astromancer_solarian_cast_fear_ward_on_main_tank(PlayerbotAI* botAI) { + return new HighAstromancerSolarianCastFearWardOnMainTankAction(botAI); + } + + // Kael'thas Sunstrider + static Action* kaelthas_sunstrider_kite_thaladred(PlayerbotAI* botAI) { + return new KaelthasSunstriderKiteThaladredAction(botAI); + } + static Action* kaelthas_sunstrider_misdirect_advisors_to_tanks(PlayerbotAI* botAI) { + return new KaelthasSunstriderMisdirectAdvisorsToTanksAction(botAI); + } + static Action* kaelthas_sunstrider_main_tank_position_sanguinar(PlayerbotAI* botAI) { + return new KaelthasSunstriderMainTankPositionSanguinarAction(botAI); + } + static Action* kaelthas_sunstrider_cast_fear_ward_on_sanguinar_tank(PlayerbotAI* botAI) { + return new KaelthasSunstriderCastFearWardOnSanguinarTankAction(botAI); + } + static Action* kaelthas_sunstrider_warlock_tank_position_capernian(PlayerbotAI* botAI) { + return new KaelthasSunstriderWarlockTankPositionCapernianAction(botAI); + } + static Action* kaelthas_sunstrider_spread_and_move_away_from_capernian(PlayerbotAI* botAI) { + return new KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction(botAI); + } + static Action* kaelthas_sunstrider_first_assist_tank_position_telonicus(PlayerbotAI* botAI) { + return new KaelthasSunstriderFirstAssistTankPositionTelonicusAction(botAI); + } + static Action* kaelthas_sunstrider_handle_advisor_roles_in_phase_3(PlayerbotAI* botAI) { + return new KaelthasSunstriderHandleAdvisorRolesInPhase3Action(botAI); + } + static Action* kaelthas_sunstrider_assign_advisor_dps_priority(PlayerbotAI* botAI) { + return new KaelthasSunstriderAssignAdvisorDpsPriorityAction(botAI); + } + static Action* kaelthas_sunstrider_manage_advisor_dps_timer(PlayerbotAI* botAI) { + return new KaelthasSunstriderManageAdvisorDpsTimerAction(botAI); + } + static Action* kaelthas_sunstrider_assign_legendary_weapon_dps_priority(PlayerbotAI* botAI) { + return new KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction(botAI); + } + static Action* kaelthas_sunstrider_move_devastation_away(PlayerbotAI* botAI) { + return new KaelthasSunstriderMoveDevastationAwayAction(botAI); + } + static Action* kaelthas_sunstrider_loot_legendary_weapons(PlayerbotAI* botAI) { + return new KaelthasSunstriderLootLegendaryWeaponsAction(botAI); + } + static Action* kaelthas_sunstrider_use_legendary_weapons(PlayerbotAI* botAI) { + return new KaelthasSunstriderUseLegendaryWeaponsAction(botAI); + } + static Action* kaelthas_sunstrider_reequip_gear(PlayerbotAI* botAI) { + return new KaelthasSunstriderReequipGearAction(botAI); + } + static Action* kaelthas_sunstrider_main_tank_position_boss(PlayerbotAI* botAI) { + return new KaelthasSunstriderMainTankPositionBossAction(botAI); + } + static Action* kaelthas_sunstrider_avoid_flame_strike(PlayerbotAI* botAI) { + return new KaelthasSunstriderAvoidFlameStrikeAction(botAI); + } + static Action* kaelthas_sunstrider_handle_phoenixes_and_eggs(PlayerbotAI* botAI) { + return new KaelthasSunstriderHandlePhoenixesAndEggsAction(botAI); + } + static Action* kaelthas_sunstrider_break_mind_control(PlayerbotAI* botAI) { + return new KaelthasSunstriderBreakMindControlAction(botAI); + } + static Action* kaelthas_sunstrider_break_through_shock_barrier(PlayerbotAI* botAI) { + return new KaelthasSunstriderBreakThroughShockBarrierAction(botAI); + } + static Action* kaelthas_sunstrider_spread_out_in_midair(PlayerbotAI* botAI) { + return new KaelthasSunstriderSpreadOutInMidairAction(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKActions.cpp b/modules/mod-playerbots/src/Ai/Raid/TK/TKActions.cpp new file mode 100644 index 000000000..922cd3bd5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKActions.cpp @@ -0,0 +1,2100 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TKActions.h" +#include "TKHelpers.h" +#include "TKKaelthasBossAI.h" +#include "AiFactory.h" +#include "EquipAction.h" +#include "LootAction.h" +#include "LootObjectStack.h" +#include "ObjectAccessor.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastPolymorphAction::Execute(Event /*event*/) +{ + Unit* centurion = AI_VALUE2(Unit*, "find target", "crimson hand centurion"); + if (!centurion) + return false; + + if (centurion->GetHealth() == centurion->GetMaxHealth() && + !botAI->HasAura("polymorph", centurion) && + botAI->CanCastSpell("polymorph", centurion)) + { + return botAI->CastSpell("polymorph", centurion); + } + else if (centurion->HasAura(SPELL_ARCANE_FLURRY)) + { + botAI->Reset(); + return botAI->CastSpell("polymorph", centurion); + } + + return false; +} + +// Al'ar + +bool AlarMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", alar)) + return botAI->CastSpell("steady shot", alar); + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (MarkTargetWithStar(bot, alar)) + return true; + + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (botAI->IsMainTank(bot)) + return PositionMainTank(alar, locationIndex); + else + return PositionAssistTank(alar, locationIndex); +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionMainTank( + Unit* alar, int8 locationIndex) +{ + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_3_IDX) + ? PLATFORM_POSITIONS[0] : PLATFORM_POSITIONS[2]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if ((locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_2_IDX) && + AI_VALUE(Unit*, "current target") != alar) + return Attack(alar); + } + + return false; +} + +bool AlarBossTanksMoveBetweenPlatformsAction::PositionAssistTank( + Unit* alar, int8 locationIndex) +{ + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = + (locationIndex == PLATFORM_0_IDX || locationIndex == PLATFORM_1_IDX) + ? PLATFORM_POSITIONS[1] : PLATFORM_POSITIONS[3]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + else if ((locationIndex == PLATFORM_1_IDX || locationIndex == PLATFORM_3_IDX) && + AI_VALUE(Unit*, "current target") != alar) + return Attack(alar); + } + + return false; +} + +bool AlarMeleeDpsMoveBetweenPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsMelee(bot) || !botAI->IsDps(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + SetRtiTarget(botAI, "star", alar); + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& target = PLATFORM_POSITIONS[locationIndex]; + + if (bot->GetExactDist2d(target.GetPositionX(), target.GetPositionY()) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, target.GetPositionX(), target.GetPositionY(), + target.GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (AI_VALUE(Unit*, "current target") != alar) + return Attack(alar); + } + + return false; +} + +bool AlarRangedAndEmberTankMoveUnderPlatformsAction::Execute(Event /*event*/) +{ + if (!botAI->IsRanged(bot) && !botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + + constexpr float distRangedFromTarget = 8.0f; + constexpr float distTankFromTarget = 20.0f; + if (botAI->IsRanged(bot) && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > distRangedFromTarget) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + distRangedFromTarget, MovementPriority::MOVEMENT_COMBAT); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && + !AI_VALUE2(Unit*, "find target", "ember of al'ar") && bot->GetExactDist2d( + groundTarget.GetPositionX(), groundTarget.GetPositionY()) > distTankFromTarget) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, groundTarget.GetPositionX(), + groundTarget.GetPositionY(), groundTarget.GetPositionZ(), + distTankFromTarget, MovementPriority::MOVEMENT_COMBAT); + } + } + + return false; +} + +bool AlarAssistTanksPickUpEmbersAction::Execute(Event /*event*/) +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (!isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return HandlePhase1Embers(alar); + else + return HandlePhase2Embers(); +} + +// Embers will be tanked by only the second assist tank in Phase 1 +bool AlarAssistTanksPickUpEmbersAction::HandlePhase1Embers(Unit* alar) +{ + if (!botAI->IsAssistTankOfIndex(bot, 1, true)) + return false; + + if (Unit* ember = AI_VALUE2(Unit*, "find target", "ember of al'ar")) + { + if (MarkTargetWithSquare(bot, ember)) + return true; + + SetRtiTarget(botAI, "square", ember); + + if (AI_VALUE(Unit*, "current target") != ember) + return Attack(ember); + + if (ember->GetVictim() == bot) + { + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + if (locationIndex >= PLATFORM_0_IDX && locationIndex <= PLATFORM_3_IDX) + { + const Position& groundTarget = GROUND_POSITIONS[locationIndex]; + const Position& center = ALAR_POINT_MIDDLE; + float dx = center.GetPositionX() - groundTarget.GetPositionX(); + float dy = center.GetPositionY() - groundTarget.GetPositionY(); + float distToCenter = + groundTarget.GetExactDist2d(center.GetPositionX(), center.GetPositionY()); + + constexpr float moveDist = 25.0f; + float targetX = groundTarget.GetPositionX() + (dx / distToCenter) * moveDist; + float targetY = groundTarget.GetPositionY() + (dy / distToCenter) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, groundTarget.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + constexpr float safeDistance = 16.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (!bot->IsWithinMeleeRange(ember)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ember->GetPositionX(), ember->GetPositionY(), + ember->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +// One Ember will be tanked by the second assist tank in Phase 2, and the other by +// the main tank or first assist tank (whichever is not tanking Al'ar) +bool AlarAssistTanksPickUpEmbersAction::HandlePhase2Embers() +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + if (botAI->IsAssistTankOfIndex(bot, 1, true) && firstEmber) + { + if (MarkTargetWithSquare(bot, firstEmber)) + return true; + + SetRtiTarget(botAI, "square", firstEmber); + + if (firstEmber->GetVictim() != bot) + { + if (AI_VALUE(Unit*, "current target") != firstEmber) + return Attack(firstEmber); + + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + else if (bot->IsWithinMeleeRange(firstEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + else if (GetSecondEmberTank(botAI) == bot && secondEmber) + { + if (MarkTargetWithCircle(bot, secondEmber)) + return true; + + SetRtiTarget(botAI, "circle", secondEmber); + + if (secondEmber->GetVictim() != bot) + { + if (AI_VALUE(Unit*, "current target") != secondEmber) + return Attack(secondEmber); + + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + else if (bot->IsWithinMeleeRange(secondEmber)) + { + constexpr float safeDistance = 16.0f; + if (GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + } + } + + return false; +} + +bool AlarRangedDpsPrioritizeEmbersAction::Execute(Event /*event*/) +{ + auto [firstEmber, secondEmber] = GetFirstTwoEmbersOfAlar(botAI); + + constexpr float safeDistance = 16.0f; + if (firstEmber) + { + if (bot->GetDistance2d(firstEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(firstEmber, safeDistance - bot->GetDistance2d(firstEmber)); + } + + SetRtiTarget(botAI, "square", firstEmber); + if (AI_VALUE(Unit*, "current target") != firstEmber) + return Attack(firstEmber); + } + else if (secondEmber) + { + if (bot->GetDistance2d(secondEmber) < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(secondEmber, safeDistance - bot->GetDistance2d(secondEmber)); + } + + SetRtiTarget(botAI, "circle", secondEmber); + if (AI_VALUE(Unit*, "current target") != secondEmber) + return Attack(secondEmber); + } + else if (Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar")) + { + SetRtiTarget(botAI, "star", alar); + if (AI_VALUE(Unit*, "current target") != alar) + return Attack(alar); + } + + return false; +} + +// Jump from platform during Flame Quills and wait at assigned position after landing +bool AlarJumpFromPlatformAction::Execute(Event /*event*/) +{ + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + constexpr float distAlarTankFromPosition = 5.0f; + constexpr float distEmberTankFromPos = 25.0f; + constexpr float distMeleeDpsFromPos = 5.0f; + constexpr float distRangedFromPos = 10.0f; + + if (botAI->IsMainTank(bot) && + bot->GetExactDist2d(ALAR_SW_RAMP_BASE.GetPositionX(), + ALAR_SW_RAMP_BASE.GetPositionY()) > distAlarTankFromPosition) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SW_RAMP_BASE.GetPositionX(), + ALAR_SW_RAMP_BASE.GetPositionY(), ALAR_SW_RAMP_BASE.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true) && + bot->GetExactDist2d(ALAR_SE_RAMP_BASE.GetPositionX(), + ALAR_SE_RAMP_BASE.GetPositionY()) > distAlarTankFromPosition) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, ALAR_SE_RAMP_BASE.GetPositionX(), + ALAR_SE_RAMP_BASE.GetPositionY(), ALAR_SE_RAMP_BASE.GetPositionZ(), + false, false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && + bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY()) > distEmberTankFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY(), ALAR_POINT_MIDDLE.GetPositionZ(), + distEmberTankFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsMelee(bot) && + bot->GetExactDist2d(ALAR_ROOM_S_CENTER.GetPositionX(), + ALAR_ROOM_S_CENTER.GetPositionY()) > distMeleeDpsFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_ROOM_S_CENTER.GetPositionX(), + ALAR_ROOM_S_CENTER.GetPositionY(), ALAR_ROOM_S_CENTER.GetPositionZ(), + distMeleeDpsFromPos, MovementPriority::MOVEMENT_FORCED); + } + else if (botAI->IsRanged(bot) && + bot->GetExactDist2d(ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY()) > distRangedFromPos) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, ALAR_POINT_MIDDLE.GetPositionX(), + ALAR_POINT_MIDDLE.GetPositionY(), ALAR_POINT_MIDDLE.GetPositionZ(), + distRangedFromPos, MovementPriority::MOVEMENT_FORCED); + } + } + + return false; +} + +bool AlarMoveAwayFromRebirthAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (bot->GetPositionZ() > ALAR_BALCONY_Z) + { + int8 closestPlatform; + Position ground; + GetClosestPlatformAndGround(bot->GetPosition(), closestPlatform, ground); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return JumpTo(TEMPEST_KEEP_MAP_ID, ground.GetPositionX(), ground.GetPositionY(), + ground.GetPositionZ(), MovementPriority::MOVEMENT_FORCED); + } + else + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + return MoveAway(alar, safeDistance - currentDistance); + } + + return false; +} + +// Main tank and first assist tank will swap tanking Al'ar when Melt Armor is applied +bool AlarSwapTanksOnBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (alar->GetHealth() == alar->GetMaxHealth()) + { + SetRtiTarget(botAI, "star", alar); + if (AI_VALUE(Unit*, "current target") != alar) + return Attack(alar); + } + + Player* secondEmberTank = GetSecondEmberTank(botAI); + if (secondEmberTank && secondEmberTank != bot) + { + SetRtiTarget(botAI, "star", alar); + if (AI_VALUE(Unit*, "current target") != alar) + return Attack(alar); + else if (alar->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + return AvoidFlamePatch() || HandleDiveBomb(alar); +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::AvoidFlamePatch() +{ + constexpr float searchRadius = 40.0f; + constexpr float hazardRadius = 8.0f; + + std::vector flamePatches = + GetAllHazardTriggers(bot, NPC_FLAME_PATCH, searchRadius); + + for (Unit* flamePatch : flamePatches) + { + if (bot->GetExactDist2d(flamePatch) < hazardRadius) + { + Position safestPos = FindSafestNearbyPosition(bot, flamePatches, hazardRadius); + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_FORCED, true, false); + } + } + + return false; +} + +bool AlarAvoidFlamePatchesAndDiveBombsAction::HandleDiveBomb(Unit* alar) +{ + if ((alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_DIVE)) || + !alar->IsVisible()) + { + float currentDistance = bot->GetDistance2d(alar); + constexpr float safeDistance = 20.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(alar, safeDistance - currentDistance); + } + } + else + { + Position dest; + if (GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX) + { + constexpr float safeDistance = 10.0f; + constexpr uint32 minInterval = 0; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + } + } + + return false; +} + +// For Phase 2, ensure that bots don't get too far away and become inactive +bool AlarReturnToRoomCenterAction::Execute(Event /*event*/) +{ + constexpr float distFromCenter = 45.0f; + const Position& center = ALAR_ROOM_CENTER; + if (AI_VALUE(Unit*, "current target") == nullptr && + bot->GetExactDist2d(center.GetPositionX(), center.GetPositionY()) > distFromCenter) + { + return MoveInside(TEMPEST_KEEP_MAP_ID, center.GetPositionX(), center.GetPositionY(), + center.GetPositionZ(), distFromCenter - 5.0f, + MovementPriority::MOVEMENT_COMBAT); + } + + return false; +} + +bool AlarManagePhaseTrackerAction::Execute(Event /*event*/) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + const uint32 instanceId = alar->GetMap()->GetInstanceId(); + + if (alar->GetHealthPct() > 99.5f && alar->GetPositionZ() >= ALAR_BALCONY_Z) + { + isAlarInPhase2.erase(instanceId); + lastRebirthState.erase(instanceId); + } + + bool rebirthActive = alar->HasUnitState(UNIT_STATE_CASTING) && + alar->FindCurrentSpellBySpellId(SPELL_REBIRTH_PHASE2); + bool lastRebirth = lastRebirthState[instanceId]; + + if (lastRebirth && !rebirthActive) + isAlarInPhase2[instanceId] = true; + + lastRebirthState[instanceId] = rebirthActive; + + return false; +} + +// Void Reaver + +bool VoidReaverTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + const Position& position = VOID_REAVER_TANK_POSITION; + + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float distToPosition = bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (bot->IsWithinMeleeRange(voidReaver) && distToPosition > 2.0f) + { + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + + return false; +} + +bool VoidReaverUseAggroDumpAbilityAction::Execute(Event /*event*/) +{ + botAI->Reset(); + static const std::array spells = + { + "divine protection", + "fade", + "feign death", + "ice block", + "soulshatter", + "vanish", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, bot) && + botAI->CastSpell(spell, bot)) + return true; + } + + return false; +} + +bool VoidReaverSpreadRangedAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + ObjectGuid guid = bot->GetGUID(); + + if (!hasReachedVoidReaverPosition[guid]) + { + int healerCount = 0, rangedDpsCount = 0; + int healerIndex = GetHealerIndex(group, healerCount); + int rangedDpsIndex = GetRangedDpsIndex(group, rangedDpsCount); + + // Void Reaver's hitbox is 15 yards (GetDistance2d() of 16.5 yards for non-Tauren) + constexpr float radius = 45.0f; + float targetX = 0.0f; + float targetY = 0.0f; + + if (healerIndex != -1 && healerCount > 0) + { + float angle = 2 * M_PI * healerIndex / healerCount; + targetX = voidReaver->GetPositionX() + radius * std::cos(angle); + targetY = voidReaver->GetPositionY() + radius * std::sin(angle); + } + else if (rangedDpsIndex != -1 && rangedDpsCount > 0) + { + float angle = 2 * M_PI * rangedDpsIndex / rangedDpsCount; + if (healerCount > 0) + angle += M_PI / rangedDpsCount; + + targetX = voidReaver->GetPositionX() + radius * std::cos(angle); + targetY = voidReaver->GetPositionY() + radius * std::sin(angle); + } + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + else + { + hasReachedVoidReaverPosition[guid] = true; + } + } + else + { + constexpr float safeDistance = 20.0f; + constexpr uint32 minInterval = 1000; + if (bot->GetDistance2d(voidReaver) < safeDistance) + return FleePosition(voidReaver->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +int VoidReaverSpreadRangedAction::GetHealerIndex(Group* group, int& healerCount) +{ + std::vector healers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsHeal(member)) + continue; + + healers.push_back(member); + } + + healerCount = healers.size(); + auto it = std::find(healers.begin(), healers.end(), bot); + return (it != healers.end()) ? std::distance(healers.begin(), it) : -1; +} + +int VoidReaverSpreadRangedAction::GetRangedDpsIndex(Group* group, int& rangedDpsCount) +{ + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member) || botAI->IsHeal(member)) + continue; + + rangedDps.push_back(member); + } + + rangedDpsCount = rangedDps.size(); + auto it = std::find(rangedDps.begin(), rangedDps.end(), bot); + return (it != rangedDps.end()) ? std::distance(rangedDps.begin(), it) : -1; +} + +bool VoidReaverAvoidArcaneOrbAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver) + return false; + + auto it = voidReaverArcaneOrbs.find(bot->GetMap()->GetInstanceId()); + if (it == voidReaverArcaneOrbs.end() || it->second.empty()) + return false; + + uint32 currentTime = getMSTime(); + constexpr uint32 orbDuration = 7000; + constexpr float safeDistance = 22.0f; + bool shouldFlee = false; + Position fleeDest; + + for (auto const& orb : it->second) + { + if (getMSTimeDiff(orb.castTime, currentTime) <= orbDuration) + { + if (bot->GetExactDist2d(orb.destination.GetPositionX(), + orb.destination.GetPositionY()) < safeDistance) + { + shouldFlee = true; + fleeDest = orb.destination; + break; + } + } + } + + it->second.erase(std::remove_if(it->second.begin(), it->second.end(), + [currentTime](const ArcaneOrbData& orb) { + return getMSTimeDiff(orb.castTime, currentTime) > orbDuration; + }), it->second.end()); + + if (shouldFlee) + { + constexpr uint32 minInterval = 0; + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return FleePosition(fleeDest, safeDistance, minInterval); + } + + return false; +} + +bool VoidReaverEraseTrackersAction::Execute(Event /*event*/) +{ + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (voidReaver) + return false; + + bool erased = false; + + if (voidReaverArcaneOrbs.erase(bot->GetMap()->GetInstanceId())) + erased = true; + + if (hasReachedVoidReaverPosition.erase(bot->GetGUID())) + erased = true; + + return erased; +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianRangedLeaveSpaceForMeleeAction::Execute(Event /*event*/) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + float currentDistance = bot->GetExactDist2d(astromancer); + constexpr float minDistance = 20.0f; + if (currentDistance < minDistance) + return MoveAway(astromancer, minDistance - currentDistance); + + return false; +} + +bool HighAstromancerSolarianMoveAwayFromGroupAction::Execute(Event /*event*/) +{ + constexpr float safeDistance = 15.0f; + if (GetNearestPlayerInRadius(bot, safeDistance)) + { + botAI->Reset(); + return MoveFromGroup(safeDistance); + } + + return false; +} + +bool HighAstromancerSolarianStackForAoeAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* stackTarget = nullptr; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsRanged(member)) + { + stackTarget = member; + break; + } + } + + if (stackTarget && bot != stackTarget && bot->GetExactDist2d(stackTarget) > 5.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, stackTarget->GetPositionX(), stackTarget->GetPositionY(), + stackTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +// Split melee into two groups, one on each Solarium Priest +bool HighAstromancerSolarianTargetSolariumPriestsAction::Execute(Event /*event*/) +{ + auto priestsPair = GetSolariumPriests(botAI); + if (!priestsPair.first || !priestsPair.second) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + auto meleeMembers = GetMeleeBots(group); + if (meleeMembers.empty()) + return false; + + Unit* targetPriest = AssignSolariumPriestsToBots(priestsPair, meleeMembers); + if (!targetPriest) + return false; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return false; + + if (targetPriest == priestsPair.first) + { + if (MarkTargetWithSquare(bot, targetPriest)) + return true; + + SetRtiTarget(botAI, "square", targetPriest); + } + else + { + if (MarkTargetWithStar(bot, targetPriest)) + return true; + + SetRtiTarget(botAI, "star", targetPriest); + } + + if (AI_VALUE(Unit*, "current target") != targetPriest) + return Attack(targetPriest); + + return false; +} + +std::pair HighAstromancerSolarianTargetSolariumPriestsAction::GetSolariumPriests(PlayerbotAI* botAI) +{ + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->GetEntry() == NPC_SOLARIUM_PRIEST) + { + if (!lowest || unit->GetGUID().GetRawValue() < lowest->GetGUID().GetRawValue()) + lowest = unit; + + if (!highest || unit->GetGUID().GetRawValue() > highest->GetGUID().GetRawValue()) + highest = unit; + } + } + + return {lowest, highest}; +} + +std::vector HighAstromancerSolarianTargetSolariumPriestsAction::GetMeleeBots(Group* group) +{ + std::vector meleeMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && botAI->IsMelee(member) && GET_PLAYERBOT_AI(member)) + meleeMembers.push_back(member); + } + + return meleeMembers; +} + +Unit* HighAstromancerSolarianTargetSolariumPriestsAction::AssignSolariumPriestsToBots( + const std::pair& priestsPair, const std::vector& meleeMembers) +{ + if (!priestsPair.first || !priestsPair.second || meleeMembers.empty()) + return nullptr; + + auto it = std::find(meleeMembers.begin(), meleeMembers.end(), bot); + if (it == meleeMembers.end()) + return nullptr; + + size_t botIndex = std::distance(meleeMembers.begin(), it); + size_t totalMelee = meleeMembers.size(); + + if (totalMelee == 1) + return priestsPair.first; + + size_t split = totalMelee / 2; + + if (botIndex < split) + return priestsPair.first; + else + return priestsPair.second; +} + +bool HighAstromancerSolarianCastFearWardOnMainTankAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderKiteThaladredAction::Execute(Event /*event*/) +{ + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred) + return false; + + float currentDistance = bot->GetExactDist2d(thaladred); + constexpr float safeDistance = 25.0f; + if (currentDistance < safeDistance) + return MoveAway(thaladred, safeDistance - currentDistance); + + return false; +} + +// Misdirect order: (1) Capernian, (2) Telonicus, (3) Capernian (again for good measure) +bool KaelthasSunstriderMisdirectAdvisorsToTanksAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector hunters; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && member->IsAlive() && member->getClass() == CLASS_HUNTER && GET_PLAYERBOT_AI(member)) + hunters.push_back(member); + + if (hunters.size() >= 3) + break; + } + + int8 hunterIndex = -1; + for (size_t i = 0; i < hunters.size(); ++i) + { + if (hunters[i] == bot) + { + hunterIndex = static_cast(i); + break; + } + } + if (hunterIndex == -1) + return false; + + Unit* advisorTarget = nullptr; + Player* tankTarget = nullptr; + if (hunterIndex == 0 || hunterIndex == 2) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + tankTarget = GetCapernianTank(bot); + } + else if (hunterIndex == 1) + { + advisorTarget = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + tankTarget = GetGroupAssistTank(botAI, bot, 0); + } + + if (!advisorTarget || + advisorTarget->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + advisorTarget->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) || + advisorTarget->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + if (!tankTarget || !tankTarget->IsAlive()) + return false; + + if (botAI->CanCastSpell("misdirection", tankTarget)) + return botAI->CastSpell("misdirection", tankTarget); + + if (bot->HasAura(SPELL_MISDIRECTION) && botAI->CanCastSpell("steady shot", advisorTarget)) + return botAI->CastSpell("steady shot", advisorTarget); + + return false; +} + +bool KaelthasSunstriderMainTankPositionSanguinarAction::Execute(Event /*event*/) +{ + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (!sanguinar) + return false; + + if (MarkTargetWithStar(bot, sanguinar)) + return true; + + SetRtiTarget(botAI, "star", sanguinar); + + if (AI_VALUE(Unit*, "current target") != sanguinar) + return Attack(sanguinar); + + if (sanguinar->GetVictim() == bot && bot->IsWithinMeleeRange(sanguinar)) + { + const Position& position = SANGUINAR_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderCastFearWardOnSanguinarTankAction::Execute(Event /*event*/) +{ + Player* mainTank = GetGroupMainTank(botAI, bot); + if (mainTank && botAI->CanCastSpell("fear ward", mainTank)) + return botAI->CastSpell("fear ward", mainTank); + + return false; +} + +bool KaelthasSunstriderWarlockTankPositionCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + if (MarkTargetWithCircle(bot, capernian)) + return true; + + SetRtiTarget(botAI, "circle", capernian); + + if (AI_VALUE(Unit*, "current target") != capernian && + botAI->CanCastSpell("searing pain", capernian) && + botAI->CastSpell("searing pain", capernian)) + return true; + + if (capernian->GetVictim() == bot) + { + float currentDist = bot->GetDistance2d(capernian); + constexpr float minDistance = 28.0f; + if (currentDist < minDistance) + return MoveAway(capernian, minDistance - currentDist); + } + + if (botAI->CanCastSpell("searing pain", capernian)) + return botAI->CastSpell("searing pain", capernian); + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::Execute(Event /*event*/) +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (botAI->IsRanged(bot) && capernian->GetVictim() != bot && + RangedBotsDisperse(kaelAI, capernian)) + { + return true; + } + else if (botAI->IsMelee(bot) && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR && + MeleeStayBackFromCapernian(capernian)) + { + return true; + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::RangedBotsDisperse(boss_kaelthas* kaelAI, Unit* capernian) +{ + if (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR) + { + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector healers; + std::vector rangedDps; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + if (botAI->IsHeal(member)) + healers.push_back(member); + else + rangedDps.push_back(member); + } + + if (healers.empty() && rangedDps.empty()) + return false; + + size_t count = healers.size() + rangedDps.size(); + size_t botIndex = 0; + float radius = 0.0f; + float angle = 0.0f; + + // Spread is 90-degree arc for healers and 120-degree arc for ranged DPS + float arcSpan = botAI->IsHeal(bot) ? M_PI / 2.0f : 2.0f * M_PI / 3.0f; + constexpr float arcCenter = 2.9f; + float arcStart = arcCenter - arcSpan / 2.0f; + + // Capernian's hitbox is 4.5 yards (GetDistance2d of 6.0f for non-Tauren) + if (botAI->IsHeal(bot)) + { + auto findIt = std::find(healers.begin(), healers.end(), bot); + botIndex = (findIt != healers.end()) ? std::distance(healers.begin(), findIt) : 0; + radius = 42.0f; + count = healers.size(); + } + else + { + auto findIt = std::find(rangedDps.begin(), rangedDps.end(), bot); + botIndex = (findIt != rangedDps.end()) ? std::distance(rangedDps.begin(), findIt) : 0; + radius = 34.0f; + count = rangedDps.size(); + } + + angle = (count == 1) ? arcCenter : + (arcStart + arcSpan * static_cast(botIndex) / static_cast(count - 1)); + + float targetX = capernian->GetPositionX() + radius * std::cos(angle); + float targetY = capernian->GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 1.0f) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + else + { + if (AI_VALUE2(Unit*, "find target", "thaladred the darkener")) + return false; + + const float safeDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, safeDistance)) + return FleePosition(nearestPlayer->GetPosition(), safeDistance, minInterval); + } + + return false; +} + +bool KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction::MeleeStayBackFromCapernian(Unit* capernian) +{ + // Main tank purposely stays in range to bait Conflagration in Phase 1 + if (botAI->IsMainTank(bot)) + { + // MoveTo called for a WorldObj is a GetDistance() check so both hitboxes are account for + constexpr float desiredDist = 15.0f; + botAI->Reset(); + return MoveTo(capernian, desiredDist, MovementPriority::MOVEMENT_FORCED); + } + else + { + constexpr float safeDistance = 42.0f; + float currentDistance = bot->GetDistance2d(capernian); + if (currentDistance < safeDistance) + { + botAI->Reset(); + return MoveAway(capernian, safeDistance - currentDistance); + } + else + { + return true; + } + } +} + +bool KaelthasSunstriderFirstAssistTankPositionTelonicusAction::Execute(Event /*event*/) +{ + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (!telonicus) + return false; + + if (MarkTargetWithTriangle(bot, telonicus)) + return true; + + SetRtiTarget(botAI, "triangle", telonicus); + + if (AI_VALUE(Unit*, "current target") != telonicus) + return Attack(telonicus); + + if (telonicus->GetVictim() == bot && bot->IsWithinMeleeRange(telonicus)) + { + const Position& position = TELONICUS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool KaelthasSunstriderHandleAdvisorRolesInPhase3Action::Execute(Event /*event*/) +{ + const Position* movePosition = nullptr; + if (botAI->IsAssistHealOfIndex(bot, 0, true)) + { + movePosition = &ADVISOR_HEAL_POSITION; + } + else if (botAI->IsMainTank(bot)) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + if (sanguinar && sanguinar->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &SANGUINAR_WAITING_POSITION; + } + else if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + if (telonicus && telonicus->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &TELONICUS_WAITING_POSITION; + } + else if (GetCapernianTank(bot) == bot) + { + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (capernian && capernian->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + movePosition = &CAPERNIAN_WAITING_POSITION; + } + + if (movePosition && + bot->GetExactDist2d(movePosition->GetPositionX(), movePosition->GetPositionY()) > 2.0f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, movePosition->GetPositionX(), movePosition->GetPositionY(), + movePosition->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool KaelthasSunstriderReequipGearAction::Execute(Event /*event*/) +{ + return botAI->DoSpecificAction("equip upgrade", Event(), true); +} + +bool KaelthasSunstriderAssignAdvisorDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Thaladred, except Capernian tank + Player* capernianTank = GetCapernianTank(bot); + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + + if ((!capernianTank || bot != capernianTank) && + thaladred && !thaladred->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + if (MarkTargetWithSquare(bot, thaladred)) + return true; + + SetRtiTarget(botAI, "square", thaladred); + + if (AI_VALUE(Unit*, "current target") != thaladred) + return Attack(thaladred); + + return false; + } + + // Target priority 2: Capernian for ranged only (excluding longbow tank) + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + + if (botAI->IsRangedDps(bot) && !IsDebuffHunter(bot) && + capernian && !capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "circle", capernian); + + if (AI_VALUE(Unit*, "current target") != capernian) + return Attack(capernian); + + return false; + } + + // Target priority 3: Sanguinar (debuff hunter and melee move here after Thaladred) + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + + if (sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "star", sanguinar); + + if (AI_VALUE(Unit*, "current target") != sanguinar) + return Attack(sanguinar); + + return false; + } + + // Target priority 4: Telonicus + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + if (telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + { + SetRtiTarget(botAI, "triangle", telonicus); + if (AI_VALUE(Unit*, "current target") != telonicus) + return Attack(telonicus); + + // Melee DPS need to stay at max-ish melee range behind Telonicus to avoid bombs + if (botAI->IsMelee(bot) && botAI->IsDps(bot) && telonicus->GetVictim() != bot) + { + float desiredDist = bot->GetMeleeRange(telonicus); + float behindAngle = Position::NormalizeOrientation(telonicus->GetOrientation() + M_PI); + float targetX = telonicus->GetPositionX() + desiredDist * std::cos(behindAngle); + float targetY = telonicus->GetPositionY() + desiredDist * std::sin(behindAngle); + + if (bot->GetExactDist2d(targetX, targetY) > 0.25f) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, targetX, targetY, telonicus->GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_FORCED, true, false); + } + } + } + + return false; +} + +bool KaelthasSunstriderManageAdvisorDpsTimerAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + static const std::array advisorNames = + { + "grand astromancer capernian", + "master engineer telonicus", + "lord sanguinar" + }; + + for (const char* name : advisorNames) + { + Unit* advisor = AI_VALUE2(Unit*, "find target", name); + if (!advisor) + continue; + + if (advisor->GetHealth() == advisor->GetMaxHealth() && + !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE)) + { + const time_t now = std::time(nullptr); + advisorDpsWaitTimer.insert_or_assign(kaelthas->GetMap()->GetInstanceId(), now); + return true; + } + } + + return false; +} + +bool KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTank(bot)) + SetRtiTarget(botAI, "moon", nullptr); + + // Priority 0: Everybody other than the main tank needs to stay away from the axe + // But this applies to assist tanks only after they get aggro on the mace, dagger, or sword + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + Unit* mace = AI_VALUE2(Unit*, "find target", "cosmic infuser"); + Unit* dagger = AI_VALUE2(Unit*, "find target", "infinity blades"); + Unit* sword = AI_VALUE2(Unit*, "find target", "warp slicer"); + + if (axe) + { + bool hasAggroFromWeapon = (mace && mace->GetVictim() == bot) || + (dagger && dagger->GetVictim() == bot) || + (sword && sword->GetVictim() == bot); + if (!botAI->IsTank(bot) || + (botAI->IsAssistTank(bot) && hasAggroFromWeapon)) + { + float currentDistance = bot->GetExactDist2d(axe); + const float safeDistance = botAI->IsAssistTank(bot) ? 17.0f : 13.0f; + if (currentDistance < safeDistance) + return MoveAway(axe, safeDistance - currentDistance); + } + } + + if (botAI->IsDps(bot)) + { + // Priority 1: Staff of Disintegration (Skull) + if (Unit* staff = AI_VALUE2(Unit*, "find target", "staff of disintegration")) + { + if (MarkTargetWithSkull(bot, staff)) + return true; + + SetRtiTarget(botAI, "skull", staff); + + if (AI_VALUE(Unit*, "current target") != staff) + return Attack(staff); + } + // Priority 2: Cosmic Infuser (Skull) + else if (mace) + { + if (MarkTargetWithSkull(bot, mace)) + return true; + + SetRtiTarget(botAI, "skull", mace); + + if (AI_VALUE(Unit*, "current target") != mace) + return Attack(mace); + } + // Priority 3: Warp Slicer (Skull) + else if (sword) + { + if (MarkTargetWithSkull(bot, sword)) + return true; + + SetRtiTarget(botAI, "skull", sword); + + if (AI_VALUE(Unit*, "current target") != sword) + return Attack(sword); + } + // Priority 4: Infinity Blades (Skull) + else if (dagger) + { + if (MarkTargetWithSkull(bot, dagger)) + return true; + + SetRtiTarget(botAI, "skull", dagger); + + if (AI_VALUE(Unit*, "current target") != dagger) + return Attack(dagger); + } + // Priority 5: Devastation - ranged only (Diamond--marked in other method by main tank) + else if (axe && botAI->IsRangedDps(bot)) + { + SetRtiTarget(botAI, "diamond", axe); + + if (AI_VALUE(Unit*, "current target") != axe) + return Attack(axe); + } + // Priority 6: Netherstrand Longbow (Skull) + else if (Unit* longbow = AI_VALUE2(Unit*, "find target", "netherstrand longbow")) + { + if (MarkTargetWithSkull(bot, longbow)) + return true; + + SetRtiTarget(botAI, "skull", longbow); + + if (AI_VALUE(Unit*, "current target") != longbow) + return Attack(longbow); + } + // Priority 7: Phaseshift Bulwark (Skull) + else if (Unit* shield = AI_VALUE2(Unit*, "find target", "phaseshift bulwark")) + { + if (MarkTargetWithSkull(bot, shield)) + return true; + + SetRtiTarget(botAI, "skull", shield); + + if (AI_VALUE(Unit*, "current target") != shield) + return Attack(shield); + } + } + + return false; +} + +bool KaelthasSunstriderMoveDevastationAwayAction::Execute(Event /*event*/) +{ + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (!axe) + return false; + + if (MarkTargetWithDiamond(bot, axe)) + return true; + + SetRtiTarget(botAI, "diamond", axe); + + if (AI_VALUE(Unit*, "current target") != axe) + return Attack(axe); + + constexpr float safeDistance = 13.0f; + if (axe->GetVictim() == bot && GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::Execute(Event /*event*/) +{ + struct WeaponInfo + { + uint32 npcEntry; + uint32 itemId; + }; + + static const std::array weapons = + { + WeaponInfo{ NPC_NETHERSTRAND_LONGBOW, ITEM_NETHERSTRAND_LONGBOW }, + WeaponInfo{ NPC_COSMIC_INFUSER, ITEM_COSMIC_INFUSER }, + WeaponInfo{ NPC_DEVASTATION, ITEM_DEVASTATION }, + WeaponInfo{ NPC_INFINITY_BLADES, ITEM_INFINITY_BLADE }, + WeaponInfo{ NPC_WARP_SLICER, ITEM_WARP_SLICER }, + WeaponInfo{ NPC_STAFF_OF_DISINTEGRATION, ITEM_STAFF_OF_DISINTEGRATION }, + WeaponInfo{ NPC_PHASESHIFT_BULWARK, ITEM_PHASESHIFT_BULWARK } + }; + + for (auto const& weapon : weapons) + { + if (ShouldBotLootWeapon(weapon.npcEntry)) + { + if (bot->HasItemCount(weapon.itemId, 1, false)) + { + EquipAction* equipAction = + dynamic_cast(botAI->GetAiObjectContext()->GetAction("equip")); + if (equipAction) + { + ItemIds ids; + ids.insert(weapon.itemId); + equipAction->EquipItems(ids); + } + continue; + } + return LootWeapon(weapon.npcEntry, weapon.itemId); + } + } + + return false; +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::ShouldBotLootWeapon(uint32 weaponEntry) +{ + uint8 tab = AiFactory::GetPlayerSpecTab(bot); + switch (weaponEntry) + { + case NPC_NETHERSTRAND_LONGBOW: + return bot->getClass() == CLASS_HUNTER; + + case NPC_COSMIC_INFUSER: + return botAI->IsHeal(bot); + + // Fury Warriors could use the axe, but their DPS is terrible at appropriate gear levels + // So IMO they're better off looting only the dagger to MH it and break MCs + case NPC_DEVASTATION: + return (bot->getClass() == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) || + (bot->getClass() == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) || + (botAI->IsDps(bot) && bot->getClass() == CLASS_DEATH_KNIGHT); + + case NPC_INFINITY_BLADES: + return bot->getClass() == CLASS_ROGUE || + bot->getClass() == CLASS_HUNTER || + (bot->getClass() == CLASS_SHAMAN && tab == SHAMAN_TAB_ENHANCEMENT) || + (bot->getClass() == CLASS_WARRIOR && tab != WARRIOR_TAB_ARMS); + + case NPC_WARP_SLICER: + return (bot->getClass() == CLASS_ROGUE && tab != ROGUE_TAB_ASSASSINATION) || + (botAI->IsTank(bot) && + (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_PALADIN)); + + case NPC_STAFF_OF_DISINTEGRATION: + return (botAI->IsRangedDps(bot) && bot->getClass() != CLASS_HUNTER) || + (bot->getClass() == CLASS_DRUID && tab == DRUID_TAB_FERAL); + + case NPC_PHASESHIFT_BULWARK: + return botAI->IsTank(bot) && + (bot->getClass() == CLASS_PALADIN || + bot->getClass() == CLASS_WARRIOR || + bot->getClass() == CLASS_DEATH_KNIGHT); + + default: + return false; + } +} + +bool KaelthasSunstriderLootLegendaryWeaponsAction::LootWeapon( + uint32 weaponEntry, uint32 itemId) +{ + constexpr float searchRadius = 150.0f; + Creature* weapon = bot->FindNearestCreature(weaponEntry, searchRadius, false); + + if (!weapon || weapon->IsAlive()) + return false; + + LootObject loot(bot, weapon->GetGUID()); + if (!loot.IsLootPossible(bot)) + return false; + + context->GetValue("loot target")->Set(loot); + + const float maxLootRange = sPlayerbotAIConfig.lootDistance; + constexpr float distFromObject = 2.0f; + + if (bot->GetDistance(weapon) > maxLootRange) + return MoveTo(weapon, distFromObject, MovementPriority::MOVEMENT_COMBAT); + + OpenLootAction open(botAI); + bool opened = open.Execute(Event()); + if (!opened) + return opened; + + if (bot->HasItemCount(itemId, 1, false)) + return false; + + bot->SetLootGUID(weapon->GetGUID()); + + constexpr uint8 weaponIndex = 0; + WorldPacket* packet = new WorldPacket(CMSG_AUTOSTORE_LOOT_ITEM, 1); + *packet << weaponIndex; + bot->GetSession()->QueuePacket(packet); + + return true; +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::Execute(Event /*event*/) +{ + return UsePhaseshiftBulwark() || + UseStaffOfDisintegration() || + UseNetherstrandLongbow(); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UsePhaseshiftBulwark() +{ + Item* offHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND); + if (!offHand || offHand->GetEntry() != ITEM_PHASESHIFT_BULWARK) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas || !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + return false; + + if (bot->HasAura(SPELL_ARCANE_BARRIER) || bot->CanUseItem(offHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(offHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseStaffOfDisintegration() +{ + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (!mainHand || mainHand->GetEntry() != ITEM_STAFF_OF_DISINTEGRATION) + return false; + + if (bot->HasAura(SPELL_MENTAL_PROTECTION_FIELD) || + bot->CanUseItem(mainHand) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(mainHand); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseNetherstrandLongbow() +{ + Item* ranged = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!ranged || ranged->GetEntry() != ITEM_NETHERSTRAND_LONGBOW) + return false; + + if (bot->HasItemCount(ITEM_NETHER_SPIKES, 1, false) || + bot->CanUseItem(ranged) != EQUIP_ERR_OK) + return false; + + return UseEquippedItemWithPacket(ranged); +} + +bool KaelthasSunstriderUseLegendaryWeaponsAction::UseEquippedItemWithPacket(Item* item) +{ + if (!item || bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true)) + return false; + + uint8 bagIndex = item->GetBagSlot(); + uint8 slot = item->GetSlot(); + uint8 cast_count = 1; + ObjectGuid item_guid = item->GetGUID(); + uint32 glyphIndex = 0; + uint8 castFlags = 0; + uint32 spellId = 0; + + for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (item->GetTemplate()->Spells[i].SpellId > 0 && + item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) + { + spellId = item->GetTemplate()->Spells[i].SpellId; + break; + } + } + + if (!spellId) + return false; + + WorldPacket packet(CMSG_USE_ITEM); + packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; + + uint32 targetFlag = TARGET_FLAG_UNIT; + packet << targetFlag << bot->GetPackGUID(); + + bot->GetSession()->HandleUseItemOpcode(packet); + return true; +} + +bool KaelthasSunstriderMainTankPositionBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (MarkTargetWithStar(bot, kaelthas)) + return true; + + SetRtiTarget(botAI, "star", kaelthas); + + if (AI_VALUE(Unit*, "current target") != kaelthas) + return Attack(kaelthas); + + if (kaelthas->GetVictim() == bot && bot->IsWithinMeleeRange(kaelthas)) + { + const Position& position = KAELTHAS_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 4.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(5.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(TEMPEST_KEEP_MAP_ID, moveX, moveY, position.GetPositionZ(), false, + false, false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool KaelthasSunstriderAvoidFlameStrikeAction::Execute(Event /*event*/) +{ + constexpr float searchRadius = 40.0f; + std::vector flameStrikes = + GetAllHazardTriggers(bot, NPC_FLAME_STRIKE_TRIGGER, searchRadius); + + if (flameStrikes.empty()) + return false; + + constexpr float hazardRadius = 12.0f; + bool inDanger = false; + for (Unit* flameStrike : flameStrikes) + { + if (bot->GetExactDist2d(flameStrike) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + Position safestPos = FindSafestNearbyPosition(bot, flameStrikes, hazardRadius); + + botAI->Reset(); + return MoveTo(TEMPEST_KEEP_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + safestPos.GetPositionZ(), false, false, false, true, + MovementPriority::MOVEMENT_COMBAT, true, false); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::Execute(Event /*event*/) +{ + if (botAI->IsAssistTankOfIndex(bot, 0, true) || botAI->IsAssistTankOfIndex(bot, 1, true)) + return AssistTanksPickUpPhoenixes(); + else + return NonTanksDestroyEggsAndAvoidPhoenixes(); +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::AssistTanksPickUpPhoenixes() +{ + std::vector phoenixes; + auto const& npcs = botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + for (auto const& npcGuid : npcs) + { + Unit* unit = botAI->GetUnit(npcGuid); + if (unit && unit->GetEntry() == NPC_PHOENIX && unit->IsAlive()) + phoenixes.push_back(unit); + } + + if (phoenixes.empty()) + return false; + + std::sort(phoenixes.begin(), phoenixes.end(), + [](Unit* first, Unit* second) { return first->GetGUID() < second->GetGUID(); }); + + Unit* targetPhoenix = nullptr; + if (botAI->IsAssistTankOfIndex(bot, 0, true)) + { + targetPhoenix = phoenixes[0]; + + if (MarkTargetWithSquare(bot, targetPhoenix)) + return true; + + SetRtiTarget(botAI, "square", targetPhoenix); + } + else if (botAI->IsAssistTankOfIndex(bot, 1, true) && phoenixes.size() >= 2) + { + targetPhoenix = phoenixes[1]; + + if (MarkTargetWithCircle(bot, targetPhoenix)) + return true; + + SetRtiTarget(botAI, "circle", targetPhoenix); + } + + if (!targetPhoenix) + return false; + + if (AI_VALUE(Unit*, "current target") != targetPhoenix) + return Attack(targetPhoenix); + + constexpr float safeDistance = 12.0f; + if (targetPhoenix->GetVictim() == bot && + GetNearestNonTankPlayerInRadius(botAI, bot, safeDistance)) + return MoveFromGroup(safeDistance); + + return false; +} + +bool KaelthasSunstriderHandlePhoenixesAndEggsAction::NonTanksDestroyEggsAndAvoidPhoenixes() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsDps(bot) && !kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + if (Unit* phoenixEgg = GetFirstAliveUnitByEntry(botAI, NPC_PHOENIX_EGG)) + { + if (MarkTargetWithDiamond(bot, phoenixEgg)) + return true; + + SetRtiTarget(botAI, "diamond", phoenixEgg); + + if (AI_VALUE(Unit*, "current target") != phoenixEgg) + return Attack(phoenixEgg); + } + } + else if (botAI->IsDps(bot)) + return false; + + if (Unit* phoenix = AI_VALUE2(Unit*, "find target", "phoenix")) + { + float currentDistance = bot->GetExactDist2d(phoenix); + constexpr float safeDistance = 12.0f; + if (currentDistance < safeDistance) + return MoveAway(phoenix, safeDistance - currentDistance); + } + + return false; +} + +bool KaelthasSunstriderBreakMindControlAction::Execute(Event /*event*/) +{ + Player* mcTarget = nullptr; + float closestDist = std::numeric_limits::max(); + + Group* group = bot->GetGroup(); + if (!group) + return false; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + { + float dist = bot->GetExactDist2d(member); + if (dist < closestDist) + { + closestDist = dist; + mcTarget = member; + } + } + } + + if (!mcTarget) + return false; + + if (!bot->IsWithinMeleeRange(mcTarget)) + { + return MoveTo(TEMPEST_KEEP_MAP_ID, mcTarget->GetPositionX(), mcTarget->GetPositionY(), + mcTarget->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_COMBAT, true, false); + } + + if (bot->getClass() == CLASS_ROGUE && + AiFactory::GetPlayerSpecTab(bot) != ROGUE_TAB_COMBAT && + botAI->CanCastSpell("sinister strike", mcTarget)) + { + return botAI->CastSpell("sinister strike", mcTarget); + } + else + { + static const std::array spells = + { + "hamstring", + "wing clip", + "shiv", + "stormstrike" + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, mcTarget)) + return botAI->CastSpell(spell, mcTarget); + } + } + + return false; +} + +// Shock Barrier needs to be #1 focus, even if there is a Phoenix Egg up +bool KaelthasSunstriderBreakThroughShockBarrierAction::Execute(Event /*event*/) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!kaelthas->HasAura(SPELL_SHOCK_BARRIER)) + { + static const std::array spells = + { + "bash", + "counterspell", + "kick", + "mind freeze", + "pummel", + "shield bash", + "silencing shot", + "wind shear", + }; + for (const char* spell : spells) + { + if (botAI->CanCastSpell(spell, kaelthas)) + return botAI->CastSpell(spell, kaelthas); + } + } + else if (AI_VALUE(Unit*, "current target") != kaelthas) + { + SetRtiTarget(botAI, "star", kaelthas); + return Attack(kaelthas); + } + + return false; +} + +bool KaelthasSunstriderSpreadOutInMidairAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + constexpr float minSpreadDistance = 16.0f; + + std::vector nearbyPlayers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || member == bot || !member->IsAlive()) + continue; + + if (bot->IsWithinDist3d(member, minSpreadDistance * 1.0f)) + nearbyPlayers.push_back(member); + } + + if (nearbyPlayers.empty()) + return false; + + Player* closestPlayer = nullptr; + float closestDist = std::numeric_limits::max(); + for (Player* player : nearbyPlayers) + { + float distToPlayer = bot->GetExactDist(player); + if (distToPlayer < closestDist) + { + closestDist = distToPlayer; + closestPlayer = player; + } + } + + if (closestPlayer && closestDist < minSpreadDistance) + { + float angle = bot->GetAngle(closestPlayer) + M_PI; + float distance = minSpreadDistance - closestDist; + + float x = bot->GetPositionX() + std::cos(angle) * distance; + float y = bot->GetPositionY() + std::sin(angle) * distance; + + return MoveTo(TEMPEST_KEEP_MAP_ID, x, y, bot->GetPositionZ(), false, false, + false, true, MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKActions.h b/modules/mod-playerbots/src/Ai/Raid/TK/TKActions.h new file mode 100644 index 000000000..6c3d28230 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKActions.h @@ -0,0 +1,419 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKACTIONS_H +#define PLAYERBOTS_TKACTIONS_H + +#include "TKHelpers.h" +#include "TKKaelthasBossAI.h" +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +using namespace TempestKeepHelpers; + +// Trash + +class CrimsonHandCenturionCastPolymorphAction : public Action +{ +public: + CrimsonHandCenturionCastPolymorphAction( + PlayerbotAI* botAI, std::string const name = "crimson hand centurion cast polymorph") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Al'ar + +class AlarMisdirectBossToMainTankAction : public AttackAction +{ +public: + AlarMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "al'ar misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarBossTanksMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarBossTanksMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar boss tanks move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool PositionMainTank(Unit* alar, int8 locationIndex); + bool PositionAssistTank(Unit* alar, int8 locationIndex); +}; + +class AlarMeleeDpsMoveBetweenPlatformsAction : public AttackAction +{ +public: + AlarMeleeDpsMoveBetweenPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar melee dps move between platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarRangedAndEmberTankMoveUnderPlatformsAction : public AttackAction +{ +public: + AlarRangedAndEmberTankMoveUnderPlatformsAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged and ember tank move under platforms") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAssistTanksPickUpEmbersAction : public AttackAction +{ +public: + AlarAssistTanksPickUpEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar assist tanks pick up embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool HandlePhase1Embers(Unit* alar); + bool HandlePhase2Embers(); +}; + +class AlarRangedDpsPrioritizeEmbersAction : public AttackAction +{ +public: + AlarRangedDpsPrioritizeEmbersAction( + PlayerbotAI* botAI, std::string const name = "al'ar ranged dps prioritize embers") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarJumpFromPlatformAction : public MovementAction +{ +public: + AlarJumpFromPlatformAction( + PlayerbotAI* botAI, std::string const name = "al'ar jump from platform") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarMoveAwayFromRebirthAction : public MovementAction +{ +public: + AlarMoveAwayFromRebirthAction( + PlayerbotAI* botAI, std::string const name = "al'ar move away from rebirth") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarSwapTanksOnBossAction : public AttackAction +{ +public: + AlarSwapTanksOnBossAction( + PlayerbotAI* botAI, std::string const name = "al'ar swap tanks on boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarAvoidFlamePatchesAndDiveBombsAction : public MovementAction +{ +public: + AlarAvoidFlamePatchesAndDiveBombsAction( + PlayerbotAI* botAI, std::string const name = "al'ar avoid flame patches and dive bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AvoidFlamePatch(); + bool HandleDiveBomb(Unit* alar); +}; + +class AlarReturnToRoomCenterAction : public MovementAction +{ +public: + AlarReturnToRoomCenterAction( + PlayerbotAI* botAI, std::string const name = "al'ar return to room center") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AlarManagePhaseTrackerAction : public Action +{ +public: + AlarManagePhaseTrackerAction( + PlayerbotAI* botAI, std::string const name = "al'ar manage phase tracker") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Void Reaver + +class VoidReaverTanksPositionBossAction : public AttackAction +{ +public: + VoidReaverTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "void reaver tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverUseAggroDumpAbilityAction : public Action +{ +public: + VoidReaverUseAggroDumpAbilityAction( + PlayerbotAI* botAI, std::string const name = "void reaver use aggro dump ability") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverSpreadRangedAction : public MovementAction +{ +public: + VoidReaverSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "void reaver spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + int GetHealerIndex(Group* group, int& healerCount); + int GetRangedDpsIndex(Group* group, int& rangedDpsCount); +}; + +class VoidReaverAvoidArcaneOrbAction : public MovementAction +{ +public: + VoidReaverAvoidArcaneOrbAction( + PlayerbotAI* botAI, std::string const name = "void reaver avoid arcane orb") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class VoidReaverEraseTrackersAction : public Action +{ +public: + VoidReaverEraseTrackersAction( + PlayerbotAI* botAI, std::string const name = "void reaver erase trackers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianRangedLeaveSpaceForMeleeAction : public MovementAction +{ +public: + HighAstromancerSolarianRangedLeaveSpaceForMeleeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian ranged leave space for melee") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianMoveAwayFromGroupAction : public MovementAction +{ +public: + HighAstromancerSolarianMoveAwayFromGroupAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian move away from group") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianStackForAoeAction : public MovementAction +{ +public: + HighAstromancerSolarianStackForAoeAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian stack for aoe") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HighAstromancerSolarianTargetSolariumPriestsAction : public AttackAction +{ +public: + HighAstromancerSolarianTargetSolariumPriestsAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian target solarium priests") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + std::pair GetSolariumPriests(PlayerbotAI* botAI); + std::vector GetMeleeBots(Group* group); + Unit* AssignSolariumPriestsToBots(const std::pair& priestsPair, const std::vector& meleeMembers); +}; + +class HighAstromancerSolarianCastFearWardOnMainTankAction : public Action +{ +public: + HighAstromancerSolarianCastFearWardOnMainTankAction( + PlayerbotAI* botAI, std::string const name = "high astromancer solarian cast fear ward on main tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderKiteThaladredAction : public MovementAction +{ +public: + KaelthasSunstriderKiteThaladredAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider kite thaladred") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMisdirectAdvisorsToTanksAction : public AttackAction +{ +public: + KaelthasSunstriderMisdirectAdvisorsToTanksAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider misdirect advisors to tanks") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionSanguinarAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionSanguinarAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position sanguinar") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderCastFearWardOnSanguinarTankAction : public Action +{ +public: + KaelthasSunstriderCastFearWardOnSanguinarTankAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider cast fear ward on sanguinar tank") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderWarlockTankPositionCapernianAction : public AttackAction +{ +public: + KaelthasSunstriderWarlockTankPositionCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider warlock tank position capernian") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadAndMoveAwayFromCapernianAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread and move away from capernian") : MovementAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool RangedBotsDisperse(boss_kaelthas* kaelAI, Unit* capernian); + bool MeleeStayBackFromCapernian(Unit* capernian); +}; + +class KaelthasSunstriderFirstAssistTankPositionTelonicusAction : public AttackAction +{ +public: + KaelthasSunstriderFirstAssistTankPositionTelonicusAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider first assist tank position telonicus") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandleAdvisorRolesInPhase3Action : public MovementAction +{ +public: + KaelthasSunstriderHandleAdvisorRolesInPhase3Action( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle advisor roles in phase 3") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignAdvisorDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignAdvisorDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign advisor dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderManageAdvisorDpsTimerAction : public Action +{ +public: + KaelthasSunstriderManageAdvisorDpsTimerAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider manage advisor dps timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction : public AttackAction +{ +public: + KaelthasSunstriderAssignLegendaryWeaponDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider assign legendary weapon dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMoveDevastationAwayAction : public AttackAction +{ +public: + KaelthasSunstriderMoveDevastationAwayAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider move devastation away") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderLootLegendaryWeaponsAction : public MovementAction +{ +public: + KaelthasSunstriderLootLegendaryWeaponsAction( + PlayerbotAI* botAI) : MovementAction(botAI, "kael'thas sunstrider loot legendary weapons") {} + bool Execute(Event event) override; + +private: + bool ShouldBotLootWeapon(uint32 weaponEntry); + bool LootWeapon(uint32 weaponEntry, uint32 itemId); +}; + +class KaelthasSunstriderUseLegendaryWeaponsAction : public Action +{ +public: + KaelthasSunstriderUseLegendaryWeaponsAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider use legendary weapons") {} + bool Execute(Event event) override; + +private: + bool UsePhaseshiftBulwark(); + bool UseStaffOfDisintegration(); + bool UseNetherstrandLongbow(); + bool UseEquippedItemWithPacket(Item* item); +}; + +class KaelthasSunstriderReequipGearAction : public Action +{ +public: + KaelthasSunstriderReequipGearAction( + PlayerbotAI* botAI) : Action(botAI, "kael'thas sunstrider reequip gear") {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderMainTankPositionBossAction : public AttackAction +{ +public: + KaelthasSunstriderMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderAvoidFlameStrikeAction : public MovementAction +{ +public: + KaelthasSunstriderAvoidFlameStrikeAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider avoid flame strike") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderHandlePhoenixesAndEggsAction : public AttackAction +{ +public: + KaelthasSunstriderHandlePhoenixesAndEggsAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider handle phoenixes and eggs") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool AssistTanksPickUpPhoenixes(); + bool NonTanksDestroyEggsAndAvoidPhoenixes(); +}; + +class KaelthasSunstriderBreakMindControlAction : public AttackAction +{ +public: + KaelthasSunstriderBreakMindControlAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break mind control") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderBreakThroughShockBarrierAction : public AttackAction +{ +public: + KaelthasSunstriderBreakThroughShockBarrierAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider break through shock barrier") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class KaelthasSunstriderSpreadOutInMidairAction : public MovementAction +{ +public: + KaelthasSunstriderSpreadOutInMidairAction( + PlayerbotAI* botAI, std::string const name = "kael'thas sunstrider spread out in midair") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.cpp new file mode 100644 index 000000000..670e7e8c5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.cpp @@ -0,0 +1,429 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TKMultipliers.h" +#include "TKActions.h" +#include "TKHelpers.h" +#include "TKKaelthasBossAI.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidActions.h" +#include "DruidBearActions.h" +#include "EquipAction.h" +#include "FollowActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +// Al'ar + +float AlarMoveBetweenPlatformsMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsDps(bot) && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "al'ar")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarStayAwayFromRebirthMultiplier::GetValue(Action* action) +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return 1.0f; + + Creature* alarCreature = alar->ToCreature(); + if (!alarCreature || alarCreature->GetReactState() != REACT_PASSIVE) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AlarPhase2NoTankingIfArmorMeltedMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_MELT_ARMOR)) + return 1.0f; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || AI_VALUE(Unit*, "current target") != alar) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Void Reaver + +float VoidReaverMaintainPositionsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "void reaver")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// High Astromancer Solarian + +float HighAstromancerSolarianMaintainPositionMultiplier::GetValue(Action* action) +{ + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer || astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM)) + return 1.0f; + + if (botAI->IsRanged(bot) && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (!bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return 1.0f; + + if (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float HighAstromancerSolarianDisableTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (!botAI->IsTank(bot)) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "solarium priest")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Kael'thas Sunstrider + +float KaelthasSunstriderWaitForDpsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + if (dynamic_cast(action)) + return 1.0f; + + const time_t now = std::time(nullptr); + constexpr uint8 dpsWaitSeconds = 10; + + auto it = advisorDpsWaitTimer.find(kaelthas->GetMap()->GetInstanceId()); + if (it == advisorDpsWaitTimer.end() || (now - it->second) < dpsWaitSeconds) + { + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + + auto isAdvisorActive = [](Unit* advisor) + { + return advisor && !advisor->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !advisor->HasAura(SPELL_PERMANENT_FEIGN_DEATH); + }; + + if ((isAdvisorActive(sanguinar) && botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && (botAI->IsMainTank(bot) || GetCapernianTank(bot) == bot))) + return 1.0f; + + bool shouldHoldDps = + (isAdvisorActive(sanguinar) && !botAI->IsMainTank(bot)) || + (isAdvisorActive(telonicus) && !botAI->IsAssistTankOfIndex(bot, 0, true)) || + (isAdvisorActive(capernian) && !botAI->IsMainTank(bot) && GetCapernianTank(bot) != bot); + + if (shouldHoldDps && + (dynamic_cast(action) || + (dynamic_cast(action) && + !dynamic_cast(action)))) + return 0.0f; + } + + return 1.0f; +} + +float KaelthasSunstriderKiteThaladredMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS) + return 1.0f; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot || + thaladred->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() == PHASE_FINAL) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderKeepDistanceFromCapernianMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR) + return 1.0f; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderManageWeaponTankingMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_WEAPONS && + dynamic_cast(action)) + return 0.0f; + + if (!botAI->IsMainTank(bot)) + return 1.0f; + + // Try to keep main tank from grabbing aggro on any weapon other than the axe + if (kaelAI->GetPhase() == PHASE_WEAPONS && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderDisableAdvisorTankAssistMultiplier::GetValue(Action* action) +{ + if (bot->GetVictim() == nullptr || !botAI->IsTank(bot)) + return 1.0f; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return 1.0f; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderDisableDisperseMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Bloodlust/Heroism and other major cooldowns should be used at the start of Phase 3 +float KaelthasSunstriderDelayCooldownsMultiplier::GetValue(Action* action) +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return 1.0f; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() == PHASE_ALL_ADVISORS || + kaelAI->GetPhase() == PHASE_FINAL) + return 1.0f; + + if (bot->getClass() == CLASS_SHAMAN && + (dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + if (botAI->IsDps(bot) && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier::GetValue(Action* action) +{ + if (!bot->HasAura(SPELL_GRAVITY_LAPSE)) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.h new file mode 100644 index 000000000..b8c8e4a96 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKMultipliers.h @@ -0,0 +1,156 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKMULTIPLIERS_H +#define PLAYERBOTS_TKMULTIPLIERS_H + +#include "Multiplier.h" + +// Al'ar + +class AlarMoveBetweenPlatformsMultiplier : public Multiplier +{ +public: + AlarMoveBetweenPlatformsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar move between platforms multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableDisperseMultiplier : public Multiplier +{ +public: + AlarDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarDisableTankAssistMultiplier : public Multiplier +{ +public: + AlarDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarStayAwayFromRebirthMultiplier : public Multiplier +{ +public: + AlarStayAwayFromRebirthMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar stay away from rebirth multiplier") {} + virtual float GetValue(Action* action); +}; + +class AlarPhase2NoTankingIfArmorMeltedMultiplier : public Multiplier +{ +public: + AlarPhase2NoTankingIfArmorMeltedMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "al'ar phase 2 no tanking if armor melted multiplier") {} + virtual float GetValue(Action* action); +}; + +// Void Reaver + +class VoidReaverMaintainPositionsMultiplier : public Multiplier +{ +public: + VoidReaverMaintainPositionsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "void reaver maintain positions multiplier") {} + virtual float GetValue(Action* action); +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianDisableTankAssistMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianDisableTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian disable tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class HighAstromancerSolarianMaintainPositionMultiplier : public Multiplier +{ +public: + HighAstromancerSolarianMaintainPositionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "high astromancer solarian maintain position multiplier") {} + virtual float GetValue(Action* action); +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderWaitForDpsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderWaitForDpsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider wait for dps multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKiteThaladredMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKiteThaladredMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider kite thaladred multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderControlMisdirectionMultiplier : public Multiplier +{ +public: + KaelthasSunstriderControlMisdirectionMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider control misdirection multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderKeepDistanceFromCapernianMultiplier : public Multiplier +{ +public: + KaelthasSunstriderKeepDistanceFromCapernianMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider keep distance from capernian multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderManageWeaponTankingMultiplier : public Multiplier +{ +public: + KaelthasSunstriderManageWeaponTankingMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider manage weapon tanking multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableAdvisorTankAssistMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableAdvisorTankAssistMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable advisor tank assist multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDisableDisperseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDisableDisperseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider disable disperse multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderDelayCooldownsMultiplier : public Multiplier +{ +public: + KaelthasSunstriderDelayCooldownsMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider delay cooldowns multiplier") {} + virtual float GetValue(Action* action); +}; + +class KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier : public Multiplier +{ +public: + KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier( + PlayerbotAI* botAI) : Multiplier(botAI, "kael'thas sunstrider stay spread during gravity lapse multiplier") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.cpp new file mode 100644 index 000000000..d0260dc61 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.cpp @@ -0,0 +1,168 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TKStrategy.h" +#include "TKMultipliers.h" + +void RaidTempestKeepStrategy::InitTriggers(std::vector& triggers) +{ + // Trash + triggers.push_back(new TriggerNode("crimson hand centurion casts arcane volley", { + NextAction("crimson hand centurion cast polymorph", ACTION_RAID + 1) })); + + // Al'ar + triggers.push_back(new TriggerNode("al'ar pulling boss", { + NextAction("al'ar misdirect boss to main tank", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar boss is flying between platforms", { + NextAction("al'ar boss tanks move between platforms", ACTION_RAID + 1), + NextAction("al'ar melee dps move between platforms", ACTION_RAID + 1), + NextAction("al'ar ranged and ember tank move under platforms", ACTION_RAID + 4) })); + + triggers.push_back(new TriggerNode("al'ar embers of al'ar explode upon death", { + NextAction("al'ar assist tanks pick up embers", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("al'ar killing embers of al'ar damages boss", { + NextAction("al'ar ranged dps prioritize embers", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("al'ar incoming flame quills", { + NextAction("al'ar jump from platform", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar rising from the ashes", { + NextAction("al'ar move away from rebirth", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("al'ar everything is on fire in phase 2", { + NextAction("al'ar swap tanks on boss", ACTION_EMERGENCY + 2), + NextAction("al'ar avoid flame patches and dive bombs", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("al'ar phase 2 encounter is at room center", { + NextAction("al'ar return to room center", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("al'ar strategy changes between phases", { + NextAction("al'ar manage phase tracker", ACTION_EMERGENCY + 10) })); + + // Void Reaver + triggers.push_back(new TriggerNode("void reaver boss casts pounding", { + NextAction("void reaver tanks position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver knock away reduces tank aggro", { + NextAction("void reaver use aggro dump ability", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("void reaver boss launches arcane orbs", { + NextAction("void reaver spread ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("void reaver arcane orb is incoming", { + NextAction("void reaver avoid arcane orb", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("void reaver bot is not in combat", { + NextAction("void reaver erase trackers", ACTION_EMERGENCY + 11) })); + + // High Astromancer Solarian + triggers.push_back(new TriggerNode("high astromancer solarian boss casts wrath of the astromancer", { + NextAction("high astromancer solarian ranged leave space for melee", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian bot has wrath of the astromancer", { + NextAction("high astromancer solarian move away from group", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss has vanished", { + NextAction("high astromancer solarian stack for aoe", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian solarium priests spawned", { + NextAction("high astromancer solarian target solarium priests", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("high astromancer solarian boss casts psychic scream", { + NextAction("high astromancer solarian cast fear ward on main tank", ACTION_RAID + 2) })); + + // Kael'thas Sunstrider + triggers.push_back(new TriggerNode("kael'thas sunstrider thaladred is fixated on bot", { + NextAction("kael'thas sunstrider kite thaladred", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider pulling tankable advisors", { + NextAction("kael'thas sunstrider misdirect advisors to tanks", ACTION_EMERGENCY + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar engaged by main tank", { + NextAction("kael'thas sunstrider main tank position sanguinar", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider sanguinar casts bellowing roar", { + NextAction("kael'thas sunstrider cast fear ward on sanguinar tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian should be tanked by a warlock", { + NextAction("kael'thas sunstrider warlock tank position capernian", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider capernian casts arcane burst and conflagration", { + NextAction("kael'thas sunstrider spread and move away from capernian", ACTION_RAID + 3) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider telonicus engaged by first assist tank", { + NextAction("kael'thas sunstrider first assist tank position telonicus", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider bots have specific roles in phase 3", { + NextAction("kael'thas sunstrider handle advisor roles in phase 3", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider determining advisor kill order", { + NextAction("kael'thas sunstrider assign advisor dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider waiting for tanks to get aggro on advisors", { + NextAction("kael'thas sunstrider manage advisor dps timer", ACTION_EMERGENCY + 10) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are alive", { + NextAction("kael'thas sunstrider assign legendary weapon dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary axe casts whirlwind", { + NextAction("kael'thas sunstrider move devastation away", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are dead and lootable", { + NextAction("kael'thas sunstrider loot legendary weapons", ACTION_RAID) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons are equipped", { + NextAction("kael'thas sunstrider use legendary weapons", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider legendary weapons were lost", { + NextAction("kael'thas sunstrider reequip gear", ACTION_EMERGENCY + 11) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss has entered the fight", { + NextAction("kael'thas sunstrider main tank position boss", ACTION_RAID + 1), + NextAction("kael'thas sunstrider avoid flame strike", ACTION_EMERGENCY + 8) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider phoenixes and eggs are spawning", { + NextAction("kael'thas sunstrider handle phoenixes and eggs", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider raid member is mind controlled", { + NextAction("kael'thas sunstrider break mind control", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is casting pyroblast", { + NextAction("kael'thas sunstrider break through shock barrier", ACTION_EMERGENCY + 7) })); + + triggers.push_back(new TriggerNode("kael'thas sunstrider boss is manipulating gravity", { + NextAction("kael'thas sunstrider spread out in midair", ACTION_EMERGENCY + 1) })); +} + +void RaidTempestKeepStrategy::InitMultipliers(std::vector& multipliers) +{ + // Alar + multipliers.push_back(new AlarMoveBetweenPlatformsMultiplier(botAI)); + multipliers.push_back(new AlarDisableDisperseMultiplier(botAI)); + multipliers.push_back(new AlarDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new AlarStayAwayFromRebirthMultiplier(botAI)); + multipliers.push_back(new AlarPhase2NoTankingIfArmorMeltedMultiplier(botAI)); + + // Void Reaver + multipliers.push_back(new VoidReaverMaintainPositionsMultiplier(botAI)); + + // High Astromancer Solarian + multipliers.push_back(new HighAstromancerSolarianDisableTankAssistMultiplier(botAI)); + multipliers.push_back(new HighAstromancerSolarianMaintainPositionMultiplier(botAI)); + + // Kael'thas Sunstrider + multipliers.push_back(new KaelthasSunstriderWaitForDpsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderKiteThaladredMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderControlMisdirectionMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderKeepDistanceFromCapernianMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderManageWeaponTankingMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableAdvisorTankAssistMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDisableDisperseMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderDelayCooldownsMultiplier(botAI)); + multipliers.push_back(new KaelthasSunstriderStaySpreadDuringGravityLapseMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.h b/modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.h new file mode 100644 index 000000000..1f8828e3a --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKStrategy.h @@ -0,0 +1,23 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKSTRATEGY_H +#define PLAYERBOTS_TKSTRATEGY_H + +#include "Strategy.h" + +class RaidTempestKeepStrategy : public Strategy +{ +public: + RaidTempestKeepStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "tempestkeep"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggerContext.h new file mode 100644 index 000000000..845d982e4 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggerContext.h @@ -0,0 +1,276 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKTRIGGERCONTEXT_H +#define PLAYERBOTS_TKTRIGGERCONTEXT_H + +#include "TKTriggers.h" +#include "NamedObjectContext.h" + +class RaidTempestKeepTriggerContext : public NamedObjectContext +{ +public: + RaidTempestKeepTriggerContext() + { + // Trash + creators["crimson hand centurion casts arcane volley"] = + &RaidTempestKeepTriggerContext::crimson_hand_centurion_casts_arcane_volley; + + // Al'ar + creators["al'ar pulling boss"] = + &RaidTempestKeepTriggerContext::alar_pulling_boss; + + creators["al'ar boss is flying between platforms"] = + &RaidTempestKeepTriggerContext::alar_boss_is_flying_between_platforms; + + creators["al'ar embers of al'ar explode upon death"] = + &RaidTempestKeepTriggerContext::alar_embers_of_alar_explode_upon_death; + + creators["al'ar killing embers of al'ar damages boss"] = + &RaidTempestKeepTriggerContext::alar_killing_embers_of_alar_damages_boss; + + creators["al'ar incoming flame quills"] = + &RaidTempestKeepTriggerContext::alar_incoming_flame_quills; + + creators["al'ar rising from the ashes"] = + &RaidTempestKeepTriggerContext::alar_rising_from_the_ashes; + + creators["al'ar everything is on fire in phase 2"] = + &RaidTempestKeepTriggerContext::alar_everything_is_on_fire_in_phase_2; + + creators["al'ar phase 2 encounter is at room center"] = + &RaidTempestKeepTriggerContext::alar_phase_2_encounter_is_at_room_center; + + creators["al'ar strategy changes between phases"] = + &RaidTempestKeepTriggerContext::alar_strategy_changes_between_phases; + + // Void Reaver + creators["void reaver boss casts pounding"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_casts_pounding; + + creators["void reaver knock away reduces tank aggro"] = + &RaidTempestKeepTriggerContext::void_reaver_knock_away_reduces_tank_aggro; + + creators["void reaver boss launches arcane orbs"] = + &RaidTempestKeepTriggerContext::void_reaver_boss_launches_arcane_orbs; + + creators["void reaver arcane orb is incoming"] = + &RaidTempestKeepTriggerContext::void_reaver_arcane_orb_is_incoming; + + creators["void reaver bot is not in combat"] = + &RaidTempestKeepTriggerContext::void_reaver_bot_is_not_in_combat; + + // High Astromancer Solarian + creators["high astromancer solarian boss casts wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_wrath_of_the_astromancer; + + creators["high astromancer solarian bot has wrath of the astromancer"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_bot_has_wrath_of_the_astromancer; + + creators["high astromancer solarian boss has vanished"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_has_vanished; + + creators["high astromancer solarian solarium priests spawned"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_solarium_priests_spawned; + + creators["high astromancer solarian boss casts psychic scream"] = + &RaidTempestKeepTriggerContext::high_astromancer_solarian_boss_casts_psychic_scream; + + // Kael'thas Sunstrider + creators["kael'thas sunstrider thaladred is fixated on bot"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_thaladred_is_fixated_on_bot; + + creators["kael'thas sunstrider pulling tankable advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_pulling_tankable_advisors; + + creators["kael'thas sunstrider sanguinar engaged by main tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_engaged_by_main_tank; + + creators["kael'thas sunstrider sanguinar casts bellowing roar"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_sanguinar_casts_bellowing_roar; + + creators["kael'thas sunstrider capernian should be tanked by a warlock"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock; + + creators["kael'thas sunstrider capernian casts arcane burst and conflagration"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration; + + creators["kael'thas sunstrider telonicus engaged by first assist tank"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank; + + creators["kael'thas sunstrider bots have specific roles in phase 3"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_bots_have_specific_roles_in_phase_3; + + creators["kael'thas sunstrider determining advisor kill order"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_determining_advisor_kill_order; + + creators["kael'thas sunstrider waiting for tanks to get aggro on advisors"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors; + + creators["kael'thas sunstrider legendary weapons are alive"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_alive; + + creators["kael'thas sunstrider legendary axe casts whirlwind"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_axe_casts_whirlwind; + + creators["kael'thas sunstrider legendary weapons are dead and lootable"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable; + + creators["kael'thas sunstrider legendary weapons are equipped"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_are_equipped; + + creators["kael'thas sunstrider legendary weapons were lost"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_legendary_weapons_were_lost; + + creators["kael'thas sunstrider boss has entered the fight"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_has_entered_the_fight; + + creators["kael'thas sunstrider raid member is mind controlled"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_raid_member_is_mind_controlled; + + creators["kael'thas sunstrider phoenixes and eggs are spawning"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_phoenixes_and_eggs_are_spawning; + + creators["kael'thas sunstrider boss is casting pyroblast"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_casting_pyroblast; + + creators["kael'thas sunstrider boss is manipulating gravity"] = + &RaidTempestKeepTriggerContext::kaelthas_sunstrider_boss_is_manipulating_gravity; + } + +private: + // Trash + static Trigger* crimson_hand_centurion_casts_arcane_volley(PlayerbotAI* botAI) { + return new CrimsonHandCenturionCastsArcaneVolleyTrigger(botAI); + } + + // Al'ar + static Trigger* alar_pulling_boss(PlayerbotAI* botAI) { + return new AlarPullingBossTrigger(botAI); + } + static Trigger* alar_boss_is_flying_between_platforms(PlayerbotAI* botAI) { + return new AlarBossIsFlyingBetweenPlatformsTrigger(botAI); + } + static Trigger* alar_embers_of_alar_explode_upon_death(PlayerbotAI* botAI) { + return new AlarEmbersOfAlarExplodeUponDeathTrigger(botAI); + } + static Trigger* alar_killing_embers_of_alar_damages_boss(PlayerbotAI* botAI) { + return new AlarKillingEmbersOfAlarDamagesBossTrigger(botAI); + } + static Trigger* alar_incoming_flame_quills(PlayerbotAI* botAI) { + return new AlarIncomingFlameQuillsTrigger(botAI); + } + static Trigger* alar_rising_from_the_ashes(PlayerbotAI* botAI) { + return new AlarRisingFromTheAshesTrigger(botAI); + } + static Trigger* alar_everything_is_on_fire_in_phase_2(PlayerbotAI* botAI) { + return new AlarEverythingIsOnFireInPhase2Trigger(botAI); + } + static Trigger* alar_phase_2_encounter_is_at_room_center(PlayerbotAI* botAI) { + return new AlarPhase2EncounterIsAtRoomCenterTrigger(botAI); + } + static Trigger* alar_strategy_changes_between_phases(PlayerbotAI* botAI) { + return new AlarStrategyChangesBetweenPhasesTrigger(botAI); + } + + // Void Reaver + static Trigger* void_reaver_boss_casts_pounding(PlayerbotAI* botAI) { + return new VoidReaverBossCastsPoundingTrigger(botAI); + } + static Trigger* void_reaver_knock_away_reduces_tank_aggro(PlayerbotAI* botAI) { + return new VoidReaverKnockAwayReducesTankAggroTrigger(botAI); + } + static Trigger* void_reaver_boss_launches_arcane_orbs(PlayerbotAI* botAI) { + return new VoidReaverBossLaunchesArcaneOrbsTrigger(botAI); + } + static Trigger* void_reaver_arcane_orb_is_incoming(PlayerbotAI* botAI) { + return new VoidReaverArcaneOrbIsIncomingTrigger(botAI); + } + static Trigger* void_reaver_bot_is_not_in_combat(PlayerbotAI* botAI) { + return new VoidReaverBotIsNotInCombatTrigger(botAI); + } + + // High Astromancer Solarian + static Trigger* high_astromancer_solarian_boss_casts_wrath_of_the_astromancer(PlayerbotAI* botAI) { + return new HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger(botAI); + } + static Trigger* high_astromancer_solarian_bot_has_wrath_of_the_astromancer(PlayerbotAI* botAI) { + return new HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger(botAI); + } + static Trigger* high_astromancer_solarian_boss_has_vanished(PlayerbotAI* botAI) { + return new HighAstromancerSolarianBossHasVanishedTrigger(botAI); + } + static Trigger* high_astromancer_solarian_solarium_priests_spawned(PlayerbotAI* botAI) { + return new HighAstromancerSolarianSolariumPriestsSpawnedTrigger(botAI); + } + static Trigger* high_astromancer_solarian_boss_casts_psychic_scream(PlayerbotAI* botAI) { + return new HighAstromancerSolarianBossCastsPsychicScreamTrigger(botAI); + } + + // Kael'thas Sunstrider + static Trigger* kaelthas_sunstrider_thaladred_is_fixated_on_bot(PlayerbotAI* botAI) { + return new KaelthasSunstriderThaladredIsFixatedOnBotTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_pulling_tankable_advisors(PlayerbotAI* botAI) { + return new KaelthasSunstriderPullingTankableAdvisorsTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_sanguinar_engaged_by_main_tank(PlayerbotAI* botAI) { + return new KaelthasSunstriderSanguinarEngagedByMainTankTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_sanguinar_casts_bellowing_roar(PlayerbotAI* botAI) { + return new KaelthasSunstriderSanguinarCastsBellowingRoarTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_capernian_should_be_tanked_by_a_warlock(PlayerbotAI* botAI) { + return new KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_capernian_casts_arcane_burst_and_conflagration(PlayerbotAI* botAI) { + return new KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_telonicus_engaged_by_first_assist_tank(PlayerbotAI* botAI) { + return new KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_bots_have_specific_roles_in_phase_3(PlayerbotAI* botAI) { + return new KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger(botAI); + } + static Trigger* kaelthas_sunstrider_determining_advisor_kill_order(PlayerbotAI* botAI) { + return new KaelthasSunstriderDeterminingAdvisorKillOrderTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_waiting_for_tanks_to_get_aggro_on_advisors(PlayerbotAI* botAI) { + return new KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_legendary_weapons_are_alive(PlayerbotAI* botAI) { + return new KaelthasSunstriderLegendaryWeaponsAreAliveTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_legendary_axe_casts_whirlwind(PlayerbotAI* botAI) { + return new KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_legendary_weapons_are_dead_and_lootable(PlayerbotAI* botAI) { + return new KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_legendary_weapons_are_equipped(PlayerbotAI* botAI) { + return new KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_legendary_weapons_were_lost(PlayerbotAI* botAI) { + return new KaelthasSunstriderLegendaryWeaponsWereLostTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_boss_has_entered_the_fight(PlayerbotAI* botAI) { + return new KaelthasSunstriderBossHasEnteredTheFightTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_raid_member_is_mind_controlled(PlayerbotAI* botAI) { + return new KaelthasSunstriderRaidMemberIsMindControlledTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_phoenixes_and_eggs_are_spawning(PlayerbotAI* botAI) { + return new KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_boss_is_casting_pyroblast(PlayerbotAI* botAI) { + return new KaelthasSunstriderBossIsCastingPyroblastTrigger(botAI); + } + static Trigger* kaelthas_sunstrider_boss_is_manipulating_gravity(PlayerbotAI* botAI) { + return new KaelthasSunstriderBossIsManipulatingGravityTrigger(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.cpp new file mode 100644 index 000000000..82239c9e8 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.cpp @@ -0,0 +1,532 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TKTriggers.h" +#include "TKHelpers.h" +#include "TKActions.h" +#include "TKKaelthasBossAI.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace TempestKeepHelpers; + +// Trash + +bool CrimsonHandCenturionCastsArcaneVolleyTrigger::IsActive() +{ + return bot->getClass() == CLASS_MAGE && + AI_VALUE2(Unit*, "find target", "crimson hand centurion"); +} + +// Al'ar + +bool AlarPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && alar->GetHealthPct() > 98.0f; +} + +bool AlarBossIsFlyingBetweenPlatformsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !alar->IsVisible()) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + int8 locationIndex = GetAlarCurrentLocationIndex(alar); + if (locationIndex == LOCATION_NONE) + { + Position dest; + locationIndex = GetAlarDestinationLocationIndex(alar, dest); + } + + return locationIndex != POINT_QUILL_OR_DIVE_IDX && + locationIndex != POINT_MIDDLE_IDX; +} + +bool AlarEmbersOfAlarExplodeUponDeathTrigger::IsActive() +{ + return botAI->IsTank(bot) && AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarKillingEmbersOfAlarDamagesBossTrigger::IsActive() +{ + return botAI->IsRangedDps(bot) && + AI_VALUE2(Unit*, "find target", "ember of al'ar"); +} + +bool AlarIncomingFlameQuillsTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) == POINT_QUILL_OR_DIVE_IDX || + GetAlarDestinationLocationIndex(alar, dest) == POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarRisingFromTheAshesTrigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar) + return false; + + if (isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + return alarCreature && alarCreature->GetReactState() == REACT_PASSIVE; +} + +bool AlarEverythingIsOnFireInPhase2Trigger::IsActive() +{ + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + return alar && isAlarInPhase2[alar->GetMap()->GetInstanceId()]; +} + +bool AlarPhase2EncounterIsAtRoomCenterTrigger::IsActive() +{ + if (bot->GetVictim()) + return false; + + Unit* alar = AI_VALUE2(Unit*, "find target", "al'ar"); + if (!alar || !isAlarInPhase2[alar->GetMap()->GetInstanceId()]) + return false; + + Creature* alarCreature = alar->ToCreature(); + if (alarCreature && alarCreature->GetReactState() == REACT_PASSIVE) + return false; + + Position dest; + return GetAlarCurrentLocationIndex(alar) != POINT_QUILL_OR_DIVE_IDX && + GetAlarDestinationLocationIndex(alar, dest) != POINT_QUILL_OR_DIVE_IDX; +} + +bool AlarStrategyChangesBetweenPhasesTrigger::IsActive() +{ + return IsMechanicTrackerBot(bot, TEMPEST_KEEP_MAP_ID) && + AI_VALUE2(Unit*, "find target", "al'ar"); +} + +// Void Reaver + +bool VoidReaverBossCastsPoundingTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverKnockAwayReducesTankAggroTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + if (bot->getClass() == CLASS_DEATH_KNIGHT || + bot->getClass() == CLASS_DRUID || + bot->getClass() == CLASS_SHAMAN || + bot->getClass() == CLASS_WARRIOR) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() == bot; +} + +bool VoidReaverBossLaunchesArcaneOrbsTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + return voidReaver && voidReaver->GetVictim() != bot; +} + +bool VoidReaverArcaneOrbIsIncomingTrigger::IsActive() +{ + if (botAI->IsTank(bot)) + return false; + + Unit* voidReaver = AI_VALUE2(Unit*, "find target", "void reaver"); + if (!voidReaver || voidReaver->GetVictim() == bot) + return false; + + auto it = voidReaverArcaneOrbs.find(bot->GetMap()->GetInstanceId()); + if (it == voidReaverArcaneOrbs.end() || it->second.empty()) + return false; + + uint32 currentTime = getMSTime(); + constexpr uint32 orbDuration = 7000; + constexpr float safeDistance = 22.0f; + + for (auto const& orb : it->second) + { + if (getMSTimeDiff(orb.castTime, currentTime) <= orbDuration && + bot->GetExactDist2d(orb.destination.GetPositionX(), + orb.destination.GetPositionY()) < safeDistance) + return true; + } + + return false; +} + +bool VoidReaverBotIsNotInCombatTrigger::IsActive() +{ + return bot->GetMapId() == TEMPEST_KEEP_MAP_ID && + !AI_VALUE2(bool, "combat", "self target") && + !AI_VALUE2(Unit*, "find target", "void reaver"); +} + +// High Astromancer Solarian + +bool HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + if (!botAI->IsRanged(bot)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && !astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +bool HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger::IsActive() +{ + return bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER); +} + +bool HighAstromancerSolarianBossHasVanishedTrigger::IsActive() +{ + if (bot->HasAura(SPELL_WRATH_OF_THE_ASTROMANCER)) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + if (!astromancer) + return false; + + Creature* astromancerCreature = astromancer->ToCreature(); + return astromancerCreature && + astromancerCreature->GetReactState() == REACT_PASSIVE; +} + +bool HighAstromancerSolarianSolariumPriestsSpawnedTrigger::IsActive() +{ + return botAI->IsMelee(bot) && AI_VALUE2(Unit*, "find target", "solarium priest"); +} + +bool HighAstromancerSolarianBossCastsPsychicScreamTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* astromancer = AI_VALUE2(Unit*, "find target", "high astromancer solarian"); + return astromancer && astromancer->HasAura(SPELL_SOLARIAN_TRANSFORM); +} + +// Kael'thas Sunstrider + +bool KaelthasSunstriderThaladredIsFixatedOnBotTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + Unit* thaladred = AI_VALUE2(Unit*, "find target", "thaladred the darkener"); + if (!thaladred || thaladred->GetVictim() != bot) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + return !(botAI->IsTank(bot) && kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderPullingTankableAdvisorsTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderSanguinarEngagedByMainTankTrigger::IsActive() +{ + if (!botAI->IsMainTank(bot)) + return false; + + Unit* sanguinar = AI_VALUE2(Unit*, "find target", "lord sanguinar"); + return sanguinar && !sanguinar->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !sanguinar->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderSanguinarCastsBellowingRoarTrigger::IsActive() +{ + if (bot->getClass() != CLASS_PRIEST) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI) + return false; + + if (kaelAI->GetPhase() != PHASE_SINGLE_ADVISOR && + kaelAI->GetPhase() != PHASE_TRANSITION && + kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank || mainTank->HasAura(SPELL_FEAR_WARD)) + return false; + + return botAI->CanCastSpell("fear ward", mainTank); +} + +bool KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger::IsActive() +{ + if (bot->getClass() != CLASS_WARLOCK) + return false; + + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + return GetCapernianTank(bot) == bot; +} + +bool KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger::IsActive() +{ + Unit* capernian = AI_VALUE2(Unit*, "find target", "grand astromancer capernian"); + if (!capernian || capernian->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) || + capernian->HasAura(SPELL_PERMANENT_FEIGN_DEATH)) + return false; + + return GetCapernianTank(bot) != bot; +} + +bool KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger::IsActive() +{ + if (!botAI->IsAssistTankOfIndex(bot, 0, false)) + return false; + + Unit* telonicus = AI_VALUE2(Unit*, "find target", "master engineer telonicus"); + return telonicus && !telonicus->HasUnitFlag(UNIT_FLAG_NON_ATTACKABLE) && + !telonicus->HasAura(SPELL_PERMANENT_FEIGN_DEATH); +} + +bool KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (!AI_VALUE2(Unit*, "find target", "master engineer telonicus") && + !AI_VALUE2(Unit*, "find target", "lord sanguinar")) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || kaelAI->GetPhase() != PHASE_ALL_ADVISORS) + return false; + + return botAI->IsAssistHealOfIndex(bot, 0, true) || + botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true) || + (bot->getClass() == CLASS_WARLOCK && GetCapernianTank(bot) == bot); +} + +bool KaelthasSunstriderDeterminingAdvisorKillOrderTrigger::IsActive() +{ + if (botAI->IsHeal(bot) || + botAI->IsMainTank(bot) || + botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && (kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR || + kaelAI->GetPhase() == PHASE_ALL_ADVISORS); +} + +bool KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger::IsActive() +{ + if (!IsMechanicTrackerBot(bot, TEMPEST_KEEP_MAP_ID)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_SINGLE_ADVISOR; +} + +bool KaelthasSunstriderLegendaryWeaponsAreAliveTrigger::IsActive() +{ + if (botAI->IsMainTank(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_WEAPONS; +} + +bool KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "devastation"); +} + +bool KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + if (!kaelAI || + (kaelAI->GetPhase() != PHASE_WEAPONS && kaelAI->GetPhase() != PHASE_ALL_ADVISORS)) + return false; + + Unit* axe = AI_VALUE2(Unit*, "find target", "devastation"); + if (axe && axe->GetVictim() == bot) + return false; + + return IsAnyLegendaryWeaponDead(bot); +} + +bool KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "kael'thas sunstrider")) + return false; + + return bot->HasItemCount(ITEM_STAFF_OF_DISINTEGRATION, 1, false) || + bot->HasItemCount(ITEM_NETHERSTRAND_LONGBOW, 1, false) || + bot->HasItemCount(ITEM_PHASESHIFT_BULWARK, 1, false); +} + +bool KaelthasSunstriderLegendaryWeaponsWereLostTrigger::IsActive() +{ + if (bot->GetMapId() != TEMPEST_KEEP_MAP_ID) + return false; + + Map* map = bot->GetMap(); + if (!map) + return false; + + constexpr uint32 KAELTHAS_DB_GUID = 158218; + auto const& creatureStore = map->GetCreatureBySpawnIdStore(); + auto it = creatureStore.find(KAELTHAS_DB_GUID); + if (it == creatureStore.end()) + return false; + + Creature* kaelthas = it->second; + if (!kaelthas || bot->GetExactDist2d(kaelthas) > 150.0f) + return false; + + const std::array weaponSlots = + { + EQUIPMENT_SLOT_MAINHAND, + EQUIPMENT_SLOT_OFFHAND, + EQUIPMENT_SLOT_RANGED + }; + + for (uint8 slot : weaponSlots) + { + if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot) && + HasEquippableItemForSlot(bot, slot)) + return true; + } + + return false; +} + +bool KaelthasSunstriderBossHasEnteredTheFightTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + boss_kaelthas* kaelAI = dynamic_cast(kaelthas->GetAI()); + return kaelAI && kaelAI->GetPhase() == PHASE_FINAL; +} + +bool KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + return AI_VALUE2(Unit*, "find target", "phoenix") || + AI_VALUE2(Unit*, "find target", "phoenix egg"); +} + +bool KaelthasSunstriderRaidMemberIsMindControlledTrigger::IsActive() +{ + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + if (!kaelthas) + return false; + + if (botAI->IsTank(bot) && kaelthas->GetVictim() == bot) + return false; + + if (!bot->HasItemCount(ITEM_INFINITY_BLADE, 1, true)) + return false; + + if (Group* group = bot->GetGroup()) + { + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(SPELL_KAELTHAS_MIND_CONTROL)) + return true; + } + } + + return false; +} + +bool KaelthasSunstriderBossIsCastingPyroblastTrigger::IsActive() +{ + if (!botAI->IsDps(bot)) + return false; + + Unit* kaelthas = AI_VALUE2(Unit*, "find target", "kael'thas sunstrider"); + return kaelthas && kaelthas->HasAura(SPELL_SHOCK_BARRIER); +} + +bool KaelthasSunstriderBossIsManipulatingGravityTrigger::IsActive() +{ + return bot->HasAura(SPELL_GRAVITY_LAPSE); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.h b/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.h new file mode 100644 index 000000000..8dba5988e --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/TKTriggers.h @@ -0,0 +1,344 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKTRIGGERS_H +#define PLAYERBOTS_TKTRIGGERS_H + +#include "Trigger.h" + +// General + +// Trash + +class CrimsonHandCenturionCastsArcaneVolleyTrigger : public Trigger +{ +public: + CrimsonHandCenturionCastsArcaneVolleyTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "crimson hand centurion casts arcane volley") {} + bool IsActive() override; +}; + +// Al'ar + +class AlarPullingBossTrigger : public Trigger +{ +public: + AlarPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar pulling boss") {} + bool IsActive() override; +}; + +class AlarBossIsFlyingBetweenPlatformsTrigger : public Trigger +{ +public: + AlarBossIsFlyingBetweenPlatformsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar boss is flying between platforms") {} + bool IsActive() override; +}; + +class AlarEmbersOfAlarExplodeUponDeathTrigger : public Trigger +{ +public: + AlarEmbersOfAlarExplodeUponDeathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar embers of al'ar explode upon death") {} + bool IsActive() override; +}; + +class AlarKillingEmbersOfAlarDamagesBossTrigger : public Trigger +{ +public: + AlarKillingEmbersOfAlarDamagesBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar killing embers of al'ar damages boss") {} + bool IsActive() override; +}; + +class AlarIncomingFlameQuillsTrigger : public Trigger +{ +public: + AlarIncomingFlameQuillsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar incoming flame quills") {} + bool IsActive() override; +}; + +class AlarRisingFromTheAshesTrigger : public Trigger +{ +public: + AlarRisingFromTheAshesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar rising from the ashes") {} + bool IsActive() override; +}; + +class AlarEverythingIsOnFireInPhase2Trigger : public Trigger +{ +public: + AlarEverythingIsOnFireInPhase2Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar everything is on fire in phase 2") {} + bool IsActive() override; +}; + +class AlarPhase2EncounterIsAtRoomCenterTrigger : public Trigger +{ +public: + AlarPhase2EncounterIsAtRoomCenterTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar phase 2 encounter is at room center") {} + bool IsActive() override; +}; + +class AlarStrategyChangesBetweenPhasesTrigger : public Trigger +{ +public: + AlarStrategyChangesBetweenPhasesTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "al'ar strategy changes between phases") {} + bool IsActive() override; +}; + +// Void Reaver + +class VoidReaverBossCastsPoundingTrigger : public Trigger +{ +public: + VoidReaverBossCastsPoundingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss casts pounding") {} + bool IsActive() override; +}; + +class VoidReaverKnockAwayReducesTankAggroTrigger : public Trigger +{ +public: + VoidReaverKnockAwayReducesTankAggroTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver knock away reduces tank aggro") {} + bool IsActive() override; +}; + +class VoidReaverBossLaunchesArcaneOrbsTrigger : public Trigger +{ +public: + VoidReaverBossLaunchesArcaneOrbsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver boss launches arcane orbs") {} + bool IsActive() override; +}; + +class VoidReaverArcaneOrbIsIncomingTrigger : public Trigger +{ +public: + VoidReaverArcaneOrbIsIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver arcane orb is incoming") {} + bool IsActive() override; +}; + +class VoidReaverBotIsNotInCombatTrigger : public Trigger +{ +public: + VoidReaverBotIsNotInCombatTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "void reaver bot is not in combat") {} + bool IsActive() override; +}; + +// High Astromancer Solarian + +class HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss casts wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger : public Trigger +{ +public: + HighAstromancerSolarianBotHasWrathOfTheAstromancerTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian bot has wrath of the astromancer") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossHasVanishedTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossHasVanishedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian boss has vanished") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianSolariumPriestsSpawnedTrigger : public Trigger +{ +public: + HighAstromancerSolarianSolariumPriestsSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer solarian solarium priests spawned") {} + bool IsActive() override; +}; + +class HighAstromancerSolarianBossCastsPsychicScreamTrigger : public Trigger +{ +public: + HighAstromancerSolarianBossCastsPsychicScreamTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "high astromancer boss casts psychic scream") {} + bool IsActive() override; +}; + +// Kael'thas Sunstrider + +class KaelthasSunstriderThaladredIsFixatedOnBotTrigger : public Trigger +{ +public: + KaelthasSunstriderThaladredIsFixatedOnBotTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider thaladred is fixated on bot") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPullingTankableAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderPullingTankableAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider pulling tankable advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarEngagedByMainTankTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar engaged by main tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderSanguinarCastsBellowingRoarTrigger : public Trigger +{ +public: + KaelthasSunstriderSanguinarCastsBellowingRoarTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider sanguinar casts bellowing roar") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianShouldBeTankedByAWarlockTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian should be tanked by a warlock") {} + bool IsActive() override; +}; + +class KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger : public Trigger +{ +public: + KaelthasSunstriderCapernianCastsArcaneBurstAndConflagrationTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider capernian casts arcane burst and conflagration") {} + bool IsActive() override; +}; + +class KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger : public Trigger +{ +public: + KaelthasSunstriderTelonicusEngagedByFirstAssistTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider telonicus engaged by first assist tank") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger : public Trigger +{ +public: + KaelthasSunstriderBotsHaveSpecificRolesInPhase3Trigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider bots have specific roles in phase 3") {} + bool IsActive() override; +}; + +class KaelthasSunstriderDeterminingAdvisorKillOrderTrigger : public Trigger +{ +public: + KaelthasSunstriderDeterminingAdvisorKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider determining advisor kill order") {} + bool IsActive() override; +}; + +class KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger : public Trigger +{ +public: + KaelthasSunstriderWaitingForTanksToGetAggroOnAdvisorsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider waiting for tanks to get aggro on advisors") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreAliveTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreAliveTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are alive") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryAxeCastsWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary axe casts whirlwind") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreDeadAndLootableTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are dead and lootable") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsAreEquippedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons are equipped") {} + bool IsActive() override; +}; + +class KaelthasSunstriderLegendaryWeaponsWereLostTrigger : public Trigger +{ +public: + KaelthasSunstriderLegendaryWeaponsWereLostTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider legendary weapons were lost") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossHasEnteredTheFightTrigger : public Trigger +{ +public: + KaelthasSunstriderBossHasEnteredTheFightTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss has entered the fight") {} + bool IsActive() override; +}; + +class KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger : public Trigger +{ +public: + KaelthasSunstriderPhoenixesAndEggsAreSpawningTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider phoenixes and eggs are spawning") {} + bool IsActive() override; +}; + +class KaelthasSunstriderRaidMemberIsMindControlledTrigger : public Trigger +{ +public: + KaelthasSunstriderRaidMemberIsMindControlledTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider raid member is mind controlled") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsCastingPyroblastTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsCastingPyroblastTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is casting pyroblast") {} + bool IsActive() override; +}; + +class KaelthasSunstriderBossIsManipulatingGravityTrigger : public Trigger +{ +public: + KaelthasSunstriderBossIsManipulatingGravityTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "kael'thas sunstrider boss is manipulating gravity") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.cpp new file mode 100644 index 000000000..b1ebf7206 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.cpp @@ -0,0 +1,431 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TKHelpers.h" +#include "TKActions.h" +#include "LootObjectStack.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +namespace TempestKeepHelpers +{ + // General + + Unit* GetNearestNonTankPlayerInRadius(PlayerbotAI* botAI, Player* bot, float radius) + { + Player* nearestPlayer = nullptr; + float nearestDistance = radius; + + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member == bot || botAI->IsTank(member)) + continue; + + float distance = bot->GetExactDist2d(member); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestPlayer = member; + } + } + + return nearestPlayer; + } + + std::vector GetAllHazardTriggers(Player* bot, uint32 npcEntry, float searchRadius) + { + std::vector hazardTriggers; + + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, npcEntry, searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + hazardTriggers.push_back(creature); + } + + return hazardTriggers; + } + + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float hazardRadius, const Position* center) + { + constexpr float searchStep = M_PI / 8.0f; + constexpr float minDistance = 2.0f; + constexpr float maxDistance = 30.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = minDistance; distance <= maxDistance; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + const Position& searchCenter = center ? *center : bot->GetPosition(); + float x = searchCenter.GetPositionX() + distance * std::cos(angle); + float y = searchCenter.GetPositionY() + distance * std::sin(angle); + + bool isSafe = true; + for (Unit* hazard : hazards) + { + if (hazard->GetExactDist2d(x, y) < hazardRadius) + { + isSafe = false; + break; + } + } + + if (!isSafe) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, hazardRadius); + if (pathSafe || !foundSafe) + { + float moveDistance = bot->GetExactDist2d(x, y); + + if (pathSafe && (!foundSafe || moveDistance < minMoveDistance)) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = true; + } + else if (!foundSafe && moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + } + } + } + + if (foundSafe) + break; + } + + return bestPos; + } + + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, float hazardRadius) + { + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + for (Unit* hazard : hazards) + { + float distToHazard = hazard->GetExactDist2d(checkX, checkY); + if (distToHazard < hazardRadius) + return false; + } + } + + return true; + } + + // Al'ar + + const Position ALAR_PLATFORM_0 = { 335.638f, 59.4879f, 17.9319f }; // West Platform + const Position ALAR_PLATFORM_1 = { 388.751f, 31.7312f, 20.2636f }; // Northwest Platform + const Position ALAR_PLATFORM_2 = { 388.791f, -33.1059f, 20.2636f }; // Northeast Platform + const Position ALAR_PLATFORM_3 = { 332.723f, -61.159f, 17.9791f }; // East Platform + const std::array PLATFORM_POSITIONS = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3 + }; + const Position ALAR_GROUND_0 = { 336.439f, 48.181f, -2.389f }; // Ground counterpart to West Platform + const Position ALAR_GROUND_1 = { 379.122f, 25.146f, -2.385f }; // Ground counterpart to Northwest Platform + const Position ALAR_GROUND_2 = { 378.583f, -27.481f, -2.385f }; // Ground counterpart to Northeast Platform + const Position ALAR_GROUND_3 = { 331.631f, -49.716f, -2.389f }; // Ground counterpart to East Platform + const std::array GROUND_POSITIONS = + { + ALAR_GROUND_0, + ALAR_GROUND_1, + ALAR_GROUND_2, + ALAR_GROUND_3 + }; + const Position ALAR_ROOM_CENTER = { 330.611f, -2.540f, -2.389f }; + const Position ALAR_POINT_QUILL_OR_DIVE = { 332.000f, 0.010f, 43.000f }; + const Position ALAR_POINT_MIDDLE = { 331.000f, 0.010f, -2.380f }; + const Position ALAR_SE_RAMP_BASE = { 281.064f, -36.590f, -2.389f }; + const Position ALAR_SW_RAMP_BASE = { 281.064f, 36.590f, -2.389f }; + const Position ALAR_ROOM_S_CENTER = { 281.064f, 0.000f, -2.389f }; + + std::unordered_map lastRebirthState; + std::unordered_map isAlarInPhase2; + + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest) + { + if (!alar) + return LOCATION_NONE; + + float x, y, z; + if (!alar->GetMotionMaster()->GetDestination(x, y, z)) + return LOCATION_NONE; + + dest.Relocate(x, y, z); + + const std::array locations = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < TOTAL_ALAR_LOCATIONS; ++i) + { + float dist = dest.GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + int8 GetAlarCurrentLocationIndex(Unit* alar) + { + if (!alar) + return LOCATION_NONE; + + const std::array locations = + { + ALAR_PLATFORM_0, + ALAR_PLATFORM_1, + ALAR_PLATFORM_2, + ALAR_PLATFORM_3, + ALAR_POINT_QUILL_OR_DIVE, + ALAR_POINT_MIDDLE, + }; + + float minDist = std::numeric_limits::max(); + int8 locationIndex = LOCATION_NONE; + for (int8 i = 0; i < TOTAL_ALAR_LOCATIONS; ++i) + { + float dist = alar->GetPosition().GetExactDist2d(&locations[i]); + if (dist < minDist) + { + minDist = dist; + locationIndex = i; + } + } + if (minDist > 0.1f) + return LOCATION_NONE; + + return locationIndex; + } + + void GetClosestPlatformAndGround(const Position& botPos, int8& closestPlatform, Position& ground) + { + float minDist = std::numeric_limits::max(); + closestPlatform = -1; + for (int8 i = 0; i < 4; ++i) + { + float dist = botPos.GetExactDist2d(&PLATFORM_POSITIONS[i]); + if (dist < minDist) + { + minDist = dist; + closestPlatform = i; + } + } + ground = GROUND_POSITIONS[closestPlatform]; + } + + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI) + { + Unit* firstEmber = nullptr; + Unit* secondEmber = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_EMBER_OF_ALAR) + { + if (!firstEmber) + { + firstEmber = unit; + } + else if (!secondEmber) + { + secondEmber = unit; + break; + } + } + } + + return { firstEmber, secondEmber }; + } + + Player* GetSecondEmberTank(PlayerbotAI* botAI) + { + Player* mainTank = GetGroupMainTank(botAI, botAI->GetBot()); + Player* assistTank = GetGroupAssistTank(botAI, botAI->GetBot(), 0); + + bool mainTankHasMelt = mainTank && mainTank->HasAura(SPELL_MELT_ARMOR); + bool assistTankHasMelt = assistTank && assistTank->HasAura(SPELL_MELT_ARMOR); + + if (mainTankHasMelt) + return mainTank; + + if (assistTankHasMelt || (!mainTankHasMelt && !assistTankHasMelt)) + return assistTank; + + return nullptr; + } + + // Void Reaver + + const Position VOID_REAVER_TANK_POSITION = { 423.845f, 371.733f, 14.897f }; + + std::unordered_map hasReachedVoidReaverPosition; + std::unordered_map> voidReaverArcaneOrbs; + + // Kael'thas Sunstrider + + const Position SANGUINAR_TANK_POSITION = { 775.478f, 39.888f, 46.780f }; + const Position SANGUINAR_WAITING_POSITION = { 761.850f, 27.459f, 46.779f }; + const Position TELONICUS_TANK_POSITION = { 773.717f, 44.091f, 46.780f }; + const Position TELONICUS_WAITING_POSITION = { 754.347f, 31.739f, 46.796f }; + const Position ADVISOR_HEAL_POSITION = { 752.171f, 19.494f, 46.779f }; + const Position CAPERNIAN_WAITING_POSITION = { 743.897f, -11.575f, 46.779f }; + const Position KAELTHAS_TANK_POSITION = { 799.390f, -0.837f, 48.729f }; + + std::unordered_map advisorDpsWaitTimer; + + // (1) First priority is an assistant Warlock (real player or bot) + // (2) If no assistant Warlock, then look for any Warlock bot + Player* GetCapernianTank(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + Player* fallbackWarlock = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_WARLOCK) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member; + + if (!fallbackWarlock && GET_PLAYERBOT_AI(member)) + fallbackWarlock = member; + } + + return fallbackWarlock; + } + + // One Hunter will start on Sanguinar in Phase 3 with Melee to apply Armor Disruption + // (1) First priority is an assistant Hunter (real player or bot) + // (2) If no assistant Hunter, then look for any Hunter bot + bool IsDebuffHunter(Player* bot) + { + if (bot->getClass() != CLASS_HUNTER || !bot->IsAlive()) + return false; + + Group* group = bot->GetGroup(); + if (!group) + return false; + + Player* fallbackHunter = nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive() || member->getClass() != CLASS_HUNTER) + continue; + + if (group->IsAssistant(member->GetGUID())) + return member == bot; + + if (!fallbackHunter && GET_PLAYERBOT_AI(member)) + fallbackHunter = member; + } + + return fallbackHunter == bot; + } + + bool IsAnyLegendaryWeaponDead(Player* bot) + { + static const std::array weaponEntries = + { + NPC_STAFF_OF_DISINTEGRATION, + NPC_COSMIC_INFUSER, + NPC_INFINITY_BLADES, + NPC_WARP_SLICER, + NPC_PHASESHIFT_BULWARK, + NPC_NETHERSTRAND_LONGBOW, + NPC_DEVASTATION + }; + + constexpr float searchRadius = 100.0f; + + for (uint32 entry : weaponEntries) + { + Creature* weapon = bot->FindNearestCreature(entry, searchRadius, false); + + if (weapon && !weapon->IsAlive()) + return true; + } + + return false; + } + + bool HasEquippableItemForSlot(Player* bot, uint8 slot) + { + for (uint8 i = 0; i < 5; ++i) + { + uint8 bag = (i == 0) ? INVENTORY_SLOT_BAG_0 : (INVENTORY_SLOT_BAG_START + i - 1); + uint8 startSlot = (bag == INVENTORY_SLOT_BAG_0) ? INVENTORY_SLOT_ITEM_START : 0; + uint8 endSlot = (bag == INVENTORY_SLOT_BAG_0) ? INVENTORY_SLOT_ITEM_END : + (bot->GetBagByPos(bag) ? bot->GetBagByPos(bag)->GetBagSize() : 0); + + for (uint8 bagSlot = startSlot; bagSlot < endSlot; ++bagSlot) + { + Item* item = bot->GetItemByPos(bag, bagSlot); + if (!item || !item->GetTemplate()) + continue; + + uint16 dest = 0; + if (bot->CanEquipItem(slot, dest, item, false) == EQUIP_ERR_OK) + return true; + } + } + + return false; + } +} diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.h b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.h new file mode 100644 index 000000000..a4055a1dd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKHelpers.h @@ -0,0 +1,164 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKHELPERS_H +#define PLAYERBOTS_TKHELPERS_H + +#include +#include +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace TempestKeepHelpers +{ + enum TempestKeepSpells + { + // Trash + SPELL_ARCANE_FLURRY = 37268, + + // Al'ar + SPELL_REBIRTH_PHASE2 = 34342, + SPELL_REBIRTH_DIVE = 35369, + SPELL_MELT_ARMOR = 35410, + + // Void Reaver + SPELL_ARCANE_ORB = 34172, + + // High Astromancer Solarian + SPELL_SOLARIAN_TRANSFORM = 39117, + SPELL_WRATH_OF_THE_ASTROMANCER = 42783, + + // Kael'thas Sunstrider + SPELL_PERMANENT_FEIGN_DEATH = 29266, + SPELL_GRAVITY_LAPSE = 39432, + SPELL_KAEL_FULL_POWER = 36187, + SPELL_MENTAL_PROTECTION_FIELD = 36480, // Staff of Disintegration + SPELL_ARCANE_BARRIER = 36481, // Phaseshift Bulwark + SPELL_KAELTHAS_MIND_CONTROL = 36797, + SPELL_SHOCK_BARRIER = 36815, + SPELL_STAFF_FROSTBOLT = 36990, + + // Hunter + SPELL_MISDIRECTION = 35079, + + // Priest + SPELL_FEAR_WARD = 6346, + }; + + enum TempestKeepNPCs + { + // Al'ar + NPC_EMBER_OF_ALAR = 19551, + NPC_FLAME_PATCH = 20602, + + // High Astromancer Solarian + NPC_SOLARIUM_PRIEST = 18806, + + // Kael'thas Sunstrider + NPC_KAELTHAS_SUNSTRIDER = 19622, + NPC_NETHERSTRAND_LONGBOW = 21268, + NPC_DEVASTATION = 21269, + NPC_COSMIC_INFUSER = 21270, + NPC_INFINITY_BLADES = 21271, // Item is singular, but NPC is plural + NPC_WARP_SLICER = 21272, + NPC_PHASESHIFT_BULWARK = 21273, + NPC_STAFF_OF_DISINTEGRATION = 21274, + // NPC_NETHER_VAPOR = 21002, // Unimplemented in AC; method needed if fixed + NPC_PHOENIX = 21362, + NPC_PHOENIX_EGG = 21364, + NPC_FLAME_STRIKE_TRIGGER = 21369, + }; + + enum TempestKeepItems + { + // Kael'thas Sunstrider + ITEM_WARP_SLICER = 30311, + ITEM_INFINITY_BLADE = 30312, + ITEM_STAFF_OF_DISINTEGRATION = 30313, + ITEM_PHASESHIFT_BULWARK = 30314, + ITEM_DEVASTATION = 30316, + ITEM_COSMIC_INFUSER = 30317, + ITEM_NETHERSTRAND_LONGBOW = 30318, + ITEM_NETHER_SPIKES = 30319, + }; + + // General + constexpr uint32 TEMPEST_KEEP_MAP_ID = 550; + Unit* GetNearestNonTankPlayerInRadius(PlayerbotAI* botAI, Player* bot, float radius); + std::vector GetAllHazardTriggers(Player* bot, uint32 npcEntry, float searchRadius); + Position FindSafestNearbyPosition(Player* bot, const std::vector& hazards, + float hazardRadius, const Position* center = nullptr); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, const std::vector& hazards, + float hazardRadius); + + // Al'ar + enum AlarLocationIndex + { + PLATFORM_0_IDX, + PLATFORM_1_IDX, + PLATFORM_2_IDX, + PLATFORM_3_IDX, + POINT_QUILL_OR_DIVE_IDX, + POINT_MIDDLE_IDX, + LOCATION_NONE = -1 + }; + constexpr float ALAR_BALCONY_Z = 17.0f; + extern const Position ALAR_PLATFORM_0; + extern const Position ALAR_PLATFORM_1; + extern const Position ALAR_PLATFORM_2; + extern const Position ALAR_PLATFORM_3; + extern const std::array PLATFORM_POSITIONS; + extern const Position ALAR_GROUND_0; + extern const Position ALAR_GROUND_1; + extern const Position ALAR_GROUND_2; + extern const Position ALAR_GROUND_3; + extern const std::array GROUND_POSITIONS; + extern const Position ALAR_ROOM_CENTER; + extern const Position ALAR_POINT_QUILL_OR_DIVE; + extern const Position ALAR_POINT_MIDDLE; + extern const Position ALAR_SE_RAMP_BASE; + extern const Position ALAR_SW_RAMP_BASE; + extern const Position ALAR_ROOM_S_CENTER; + constexpr uint8 TOTAL_ALAR_LOCATIONS = 6; + extern std::unordered_map lastRebirthState; + extern std::unordered_map isAlarInPhase2; + int8 GetAlarDestinationLocationIndex(Unit* alar, Position& dest); + int8 GetAlarCurrentLocationIndex(Unit* alar); + void GetClosestPlatformAndGround( + const Position& botPos, int8& closestPlatform, Position& ground); + std::pair GetFirstTwoEmbersOfAlar(PlayerbotAI* botAI); + Player* GetSecondEmberTank(PlayerbotAI* botAI); + + // Void Reaver + extern const Position VOID_REAVER_TANK_POSITION; + extern std::unordered_map hasReachedVoidReaverPosition; + struct ArcaneOrbData + { + Position destination; + uint32 castTime; + }; + extern std::unordered_map> voidReaverArcaneOrbs; + + // Kael'thas Sunstrider + extern const Position SANGUINAR_TANK_POSITION; + extern const Position SANGUINAR_WAITING_POSITION; + extern const Position TELONICUS_TANK_POSITION; + extern const Position TELONICUS_WAITING_POSITION; + extern const Position CAPERNIAN_WAITING_POSITION; + extern const Position ADVISOR_HEAL_POSITION; + extern const Position KAELTHAS_TANK_POSITION; + extern std::unordered_map advisorDpsWaitTimer; + Player* GetCapernianTank(Player* bot); + bool IsDebuffHunter(Player* bot); + bool IsAnyLegendaryWeaponDead(Player* bot); + bool HasEquippableItemForSlot(Player* bot, uint8 slot); +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKKaelthasBossAI.h b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKKaelthasBossAI.h new file mode 100644 index 000000000..f23474ee5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKKaelthasBossAI.h @@ -0,0 +1,60 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_TKKAELTHASBOSSAI_H +#define PLAYERBOTS_TKKAELTHASBOSSAI_H + +#include "ScriptedCreature.h" + +enum KTYells +{ +}; + +enum KTPhases +{ + PHASE_NONE = 0, + PHASE_SINGLE_ADVISOR = 1, + PHASE_WEAPONS = 2, + PHASE_TRANSITION = 3, + PHASE_ALL_ADVISORS = 4, + PHASE_FINAL = 5 +}; + +enum KTActions +{ +}; + +struct boss_kaelthas : public BossAI +{ + boss_kaelthas(Creature* creature); + + void PrepareAdvisors(); + void SetRoomState(GOState state); + void Reset() override; + void AttackStart(Unit* who) override; + void MoveInLineOfSight(Unit* who) override; + void KilledUnit(Unit* victim) override; + void JustSummoned(Creature* summon) override; + void SpellHit(Unit* caster, SpellInfo const* spell) override; + void MovementInform(uint32 type, uint32 point) override; + void ExecuteMiddleEvent(); + void IntroduceNewAdvisor(KTYells talkIntroduction, KTActions kaelAction); + void PhaseEnchantedWeaponsExecute(); + void PhaseAllAdvisorsExecute(); + void PhaseKaelExecute(); + void UpdateAI(uint32 diff) override; + bool CheckEvadeIfOutOfCombatArea() const override; + void JustDied(Unit* killer) override; + + uint32 GetPhase() const { return _phase; } // This is the only addition to the existing class + +private: + uint32 _phase; + uint8 _advisorsAlive; + bool _transitionSceneReached = false; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKScripts.cpp b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKScripts.cpp new file mode 100644 index 000000000..74a894b9f --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/TK/Util/TKScripts.cpp @@ -0,0 +1,53 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "TKHelpers.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "Spell.h" +#include "Timer.h" + +using namespace TempestKeepHelpers; + +class BossListenerScript : public AllSpellScript +{ +public: + BossListenerScript() : AllSpellScript("BossListenerScript") { } + + void OnSpellCast(Spell* spell, Unit* caster, SpellInfo const* spellInfo, bool /*skipCheck*/) override + { + if (spellInfo->Id != SPELL_ARCANE_ORB) + return; + + std::list const& targets = *spell->GetUniqueTargetInfo(); + if (targets.empty()) + return; + + Player* target = ObjectAccessor::GetPlayer(*caster, targets.front().targetGUID); + if (!target) + return; + + auto& orbs = voidReaverArcaneOrbs[caster->GetMap()->GetInstanceId()]; + uint32 currentTime = getMSTime(); + + ArcaneOrbData orbData; + orbData.destination = target->GetPosition(); + orbData.castTime = currentTime; + + orbs.push_back(orbData); + + orbs.erase(std::remove_if(orbs.begin(), orbs.end(), + [currentTime](const ArcaneOrbData& orb) { + return getMSTimeDiff(orb.castTime, currentTime) > 5000; + }), orbs.end()); + } +}; + +void AddSC_TempestKeepBotScripts() +{ + new BossListenerScript(); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/RaidUlduarActionContext.h b/modules/mod-playerbots/src/Ai/Raid/Uld/UldActionContext.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/RaidUlduarActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldActionContext.h index 84c1bb850..2c756e070 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/RaidUlduarActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldActionContext.h @@ -1,14 +1,15 @@ -// /* -// * 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. -// */ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#ifndef _PLAYERBOT_RAIDULDUARACTIONCONTEXT_H -#define _PLAYERBOT_RAIDULDUARACTIONCONTEXT_H +#ifndef PLAYERBOTS_ULDACTIONCONTEXT_H +#define PLAYERBOTS_ULDACTIONCONTEXT_H #include "Action.h" #include "NamedObjectContext.h" -#include "RaidUlduarActions.h" +#include "UldActions.h" #include "BossAuraActions.h" class RaidUlduarActionContext : public NamedObjectContext diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp b/modules/mod-playerbots/src/Ai/Raid/Uld/UldActions.cpp similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldActions.cpp index 15db518a7..aeea94f23 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldActions.cpp @@ -1,5 +1,10 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#include "RaidUlduarActions.h" +#include "UldActions.h" #include #include @@ -16,8 +21,8 @@ #include "PlayerbotAIConfig.h" #include "Playerbots.h" #include "Position.h" -#include "RaidUlduarBossHelper.h" -#include "RaidUlduarScripts.h" +#include "UldBossHelper.h" +#include "UldScripts.h" #include "RtiValue.h" #include "ScriptedCreature.h" #include "ServerFacade.h" @@ -72,9 +77,7 @@ bool FlameLeviathanVehicleAction::Execute(Event /*event*/) continue; } if (!target || bot->GetExactDist(target) > bot->GetExactDist(unit)) - { target = unit; - } } // Flame Leviathan is chasing me if (flame && flame->GetVictim() == vehicleBase_) @@ -106,7 +109,7 @@ bool FlameLeviathanVehicleAction::MoveAvoidChasing(Unit* target) return false; if (avoidChaseIdx == -1) { - for (int i = 0; i < corners.size(); i++) + for (uint32 i = 0; i < corners.size(); i++) { if (bot->GetExactDist(corners[i]) > target->GetExactDist(corners[i])) continue; @@ -419,9 +422,7 @@ bool RazorscaleAvoidDevouringFlameAction::Execute(Event /*event*/) RazorscaleBossHelper razorscaleHelper(botAI); if (!razorscaleHelper.UpdateBossAI()) - { return false; - } bool isMainTank = botAI->IsMainTank(bot); const float flameRadius = 3.5f; @@ -433,9 +434,7 @@ bool RazorscaleAvoidDevouringFlameAction::Execute(Event /*event*/) // Get the boss Unit* boss = AI_VALUE2(Unit*, "find target", "razorscale"); if (!boss) - { return false; - } GuidVector npcs = AI_VALUE(GuidVector, "nearest hostile npcs"); Unit* closestFlame = nullptr; @@ -458,15 +457,12 @@ bool RazorscaleAvoidDevouringFlameAction::Execute(Event /*event*/) // Off tanks are following the main tank during grounded and should prioritise stacking if (razorscaleHelper.IsGroundPhase() && (botAI->IsTank(bot) && !botAI->IsMainTank(bot))) - { return false; - } // Handle movement from flames if (closestDistance < safeDistance) - { return MoveAway(closestFlame, safeDistance); - } + return false; } @@ -486,9 +482,7 @@ bool RazorscaleAvoidDevouringFlameAction::isUseful() { float distance = bot->GetDistance2d(unit); if (distance < safeDistance) - { return true; // Bot is within the danger distance - } } } @@ -522,9 +516,7 @@ bool RazorscaleAvoidSentinelAction::Execute(Event /*event*/) // Move away if ranged and too close if (isRanged && bot->GetDistance2d(unit) < radius) - { movedAway = MoveAway(unit, radius) || movedAway; - } } } @@ -547,9 +539,7 @@ bool RazorscaleAvoidSentinelAction::Execute(Event /*event*/) // If there's no skull set yet, or the skull is on a different target, set the sentinel if (!currentSkullTarget || (lowestHealthSentinel->GetGUID() != currentSkullTarget)) - { group->SetTargetIcon(skullIndex, bot->GetGUID(), lowestHealthSentinel->GetGUID()); - } } break; // Stop after finding the first valid bot tank } @@ -565,9 +555,7 @@ bool RazorscaleAvoidSentinelAction::Execute(Event /*event*/) // If there's no skull set yet, or the skull is on a different target, set the sentinel if (!currentSkullTarget || (lowestHealthSentinel->GetGUID() != currentSkullTarget)) - { group->SetTargetIcon(skullIndex, bot->GetGUID(), lowestHealthSentinel->GetGUID()); - } } } @@ -582,9 +570,7 @@ bool RazorscaleAvoidSentinelAction::isUseful() // If this bot is the main tank, it should always try to mark if (isMainTank) - { return true; - } // If the main tank is a human, check if this bot is one of the first three valid bot tanks if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player @@ -592,9 +578,7 @@ bool RazorscaleAvoidSentinelAction::isUseful() for (int i = 0; i < 3; ++i) { if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Bot is a valid tank - { return true; // This bot should assist with marking - } } } @@ -608,9 +592,7 @@ bool RazorscaleAvoidSentinelAction::isUseful() if (unit && unit->GetEntry() == RazorscaleBossHelper::UNIT_DARK_RUNE_SENTINEL) { if (isRanged && bot->GetDistance2d(unit) < radius) - { return true; - } } } @@ -633,9 +615,7 @@ bool RazorscaleAvoidWhirlwindAction::Execute(Event /*event*/) { float currentDistance = bot->GetDistance2d(unit); if (currentDistance < radius) - { return MoveAway(unit, radius); - } } } return false; @@ -690,23 +670,17 @@ bool RazorscaleIgnoreBossAction::isUseful() } if (!botAI->IsTank(bot)) - { return false; - } Group* group = bot->GetGroup(); if (!group) - { return false; - } // Check if the boss is already set as the moon marker int8 moonIndex = 4; // Moon marker index ObjectGuid currentMoonTarget = group->GetTargetIcon(moonIndex); if (currentMoonTarget == boss->GetGUID()) - { return false; // Moon marker is already correctly set, no further action needed - } // Proceed to tank-specific logic Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); @@ -714,9 +688,7 @@ bool RazorscaleIgnoreBossAction::isUseful() // If this bot is the main tank, it needs to set the moon marker if (mainTankUnit == bot) - { return true; - } // If the main tank is a human, check if this bot is the lowest-indexed bot tank if (mainTank && !GET_PLAYERBOT_AI(mainTank)) // Main tank is a human player @@ -724,9 +696,7 @@ bool RazorscaleIgnoreBossAction::isUseful() for (int i = 0; i < 3; ++i) // Only iterate through the first 3 indexes { if (botAI->IsAssistTankOfIndex(bot, i) && GET_PLAYERBOT_AI(bot)) // Valid bot tank - { return true; // This bot should assign the marker - } } } } @@ -1156,9 +1126,7 @@ bool IronAssemblyLightningTendrilsAction::Execute(Event /*event*/) float currentDistance = bot->GetDistance2d(boss); if (currentDistance < radius) - { return MoveAway(boss, radius - currentDistance); - } return false; } @@ -1180,9 +1148,7 @@ bool IronAssemblyOverloadAction::Execute(Event /*event*/) float currentDistance = bot->GetDistance2d(boss); if (currentDistance < radius) - { return MoveAway(boss, radius - currentDistance); - } return false; } @@ -1230,7 +1196,6 @@ bool KologarnMarkDpsTargetAction::Execute(Event /*event*/) if (!target) continue; - uint32 creatureId = target->GetEntry(); if (target->GetEntry() == NPC_RUBBLE && target->IsAlive()) { targetToMark = target; @@ -1252,21 +1217,15 @@ bool KologarnMarkDpsTargetAction::Execute(Event /*event*/) { Unit* boss = AI_VALUE2(Unit*, "find target", "kologarn"); if (boss && boss->IsAlive()) - { targetToMark = boss; - } } if (!targetToMark) - { return false; // No target to mark - } Unit* leftArm = AI_VALUE2(Unit*, "find target", "left arm"); if (leftArm && leftArm->IsAlive()) - { targetToCcMark = leftArm; - } bool isMainTank = botAI->IsMainTank(bot); Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); @@ -1284,13 +1243,10 @@ bool KologarnMarkDpsTargetAction::Execute(Event /*event*/) { group->SetTargetIcon(skullIndex, bot->GetGUID(), targetToMark->GetGUID()); if (targetToCcMark) - { group->SetTargetIcon(moonIndex, bot->GetGUID(), targetToCcMark->GetGUID()); - } + if (additionalTargetToMark) - { group->SetTargetIcon(crossIndex, bot->GetGUID(), additionalTargetToMark->GetGUID()); - } return true; } @@ -1305,13 +1261,11 @@ bool KologarnMarkDpsTargetAction::Execute(Event /*event*/) { group->SetTargetIcon(skullIndex, bot->GetGUID(), targetToMark->GetGUID()); if (targetToCcMark) - { group->SetTargetIcon(moonIndex, bot->GetGUID(), targetToCcMark->GetGUID()); - } + if (additionalTargetToMark) - { group->SetTargetIcon(crossIndex, bot->GetGUID(), additionalTargetToMark->GetGUID()); - } + return true; } } @@ -1326,13 +1280,11 @@ bool KologarnMarkDpsTargetAction::Execute(Event /*event*/) { group->SetTargetIcon(skullIndex, bot->GetGUID(), targetToMark->GetGUID()); if (targetToCcMark) - { group->SetTargetIcon(moonIndex, bot->GetGUID(), targetToCcMark->GetGUID()); - } + if (additionalTargetToMark) - { group->SetTargetIcon(crossIndex, bot->GetGUID(), additionalTargetToMark->GetGUID()); - } + return true; } break; // Stop after finding the first valid bot tank @@ -1379,17 +1331,11 @@ bool KologarnEyebeamAction::Execute(Event /*event*/) bool runToLeftSide; if (!distanceToLeftPoint) - { runToLeftSide = true; - } else if (!distanceToRightPoint) - { runToLeftSide = false; - } else - { runToLeftSide = distanceToRightPoint > distanceToLeftPoint; - } bool teleportedToPoint; KologarnEyebeamTrigger kologarnEyebeamTrigger(botAI); @@ -1418,9 +1364,7 @@ bool KologarnEyebeamAction::isUseful() { KologarnEyebeamTrigger kologarnEyebeamTrigger(botAI); if (!kologarnEyebeamTrigger.IsActive()) - { return false; - } return botAI->HasCheat(BotCheatMask::raid); } @@ -1447,9 +1391,7 @@ bool KologarnCrunchArmorAction::isUseful() { KologarnCrunchArmorTrigger kologarnCrunchArmorTrigger(botAI); if (!kologarnCrunchArmorTrigger.IsActive()) - { return false; - } return botAI->HasCheat(BotCheatMask::raid); } @@ -1482,15 +1424,11 @@ bool HodirMoveSnowpackedIcicleAction::isUseful() // Check boss and it is alive Unit* boss = AI_VALUE2(Unit*, "find target", "hodir"); if (!boss || !boss->IsAlive()) - { return false; - } // Check if boss is casting Flash Freeze if (!boss->HasUnitState(UNIT_STATE_CASTING) || !boss->FindCurrentSpellBySpellId(SPELL_FLASH_FREEZE)) - { return false; - } // Find the nearest Snowpacked Icicle Target Creature* target = bot->FindNearestCreature(NPC_SNOWPACKED_ICICLE, 100.0f); @@ -1499,9 +1437,7 @@ bool HodirMoveSnowpackedIcicleAction::isUseful() // Check that bot is stacked on Snowpacked Icicle if (bot->GetDistance2d(target->GetPositionX(), target->GetPositionY()) <= 5.0f) - { return false; - } return true; } @@ -1612,42 +1548,26 @@ bool FreyaMarkDpsTargetAction::Execute(Event /*event*/) continue; if (target->GetEntry() == NPC_EONARS_GIFT) - { eonarsGift = target; - } else if (target->GetEntry() == NPC_ANCIENT_CONSERVATOR) - { ancientConservator = target; - } else if (target->GetEntry() == NPC_SNAPLASHER) - { snaplasher = target; - } else if (target->GetEntry() == NPC_ANCIENT_WATER_SPIRIT) - { ancientWaterSpirit = target; - } else if (target->GetEntry() == NPC_STORM_LASHER) - { stormLasher = target; - } else if (target->GetEntry() == NPC_DETONATING_LASHER && !firstDetonatingLasher) - { firstDetonatingLasher = target; - } } // Check that eonars gift is need to be mark if (eonarsGift) - { targetToMark = eonarsGift; - } // Check that ancient conservator is need to be mark if (ancientConservator && !targetToMark) - { targetToMark = ancientConservator; - } // Check that trio of adds is need to be mark if ((snaplasher || ancientWaterSpirit || stormLasher) && !targetToMark) @@ -1679,14 +1599,10 @@ bool FreyaMarkDpsTargetAction::Execute(Event /*event*/) // Check that detonating lasher is need to be mark if (firstDetonatingLasher && !targetToMark) - { targetToMark = firstDetonatingLasher; - } if (!targetToMark) - { return false; // No target to mark - } bool isMainTank = botAI->IsMainTank(bot); Unit* mainTankUnit = AI_VALUE(Unit*, "main tank"); @@ -1778,9 +1694,7 @@ bool ThorimUnbalancingStrikeAction::isUseful() { ThorimUnbalancingStrikeTrigger thorimUnbalancingStrikeTrigger(botAI); if (!thorimUnbalancingStrikeTrigger.IsActive()) - { return false; - } return botAI->HasCheat(BotCheatMask::raid); } @@ -2188,23 +2102,15 @@ bool MimironShockBlastAction::Execute(Event /*event*/) continue; if (target->GetEntry() == NPC_LEVIATHAN_MKII) - { leviathanMkII = target; - } else if (target->GetEntry() == NPC_VX001) - { vx001 = target; - } else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) - { aerialCommandUnit = target; - } } if (!leviathanMkII) - { return false; - } if (!vx001 && !aerialCommandUnit) { @@ -2213,9 +2119,8 @@ bool MimironShockBlastAction::Execute(Event /*event*/) MoveAway(leviathanMkII, radius - currentDistance); if (botAI->IsMelee(bot)) - { botAI->SetNextCheckDelay(100); - } + return true; } else @@ -2261,15 +2166,11 @@ bool MimironP3Wx2LaserBarrageAction::Execute(Event /*event*/) { auto master = botAI->GetMaster(); if (!master || !master->IsAlive()) - { return false; - } if (bot->GetDistance2d(master) > 15.0f) - { return bot->TeleportTo(master->GetMapId(), master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(), master->GetOrientation()); - } return MoveTo(master->GetMapId(), master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_COMBAT, true); @@ -2294,18 +2195,14 @@ bool MimironRapidBurstAction::Execute(Event /*event*/) continue; if (target->GetEntry() == NPC_LEVIATHAN_MKII) - { leviathanMkII = target; - } } Position targetPosition; Group* group = bot->GetGroup(); if (!group) - { return false; - } uint32 memberSpotNumber = 0; for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) @@ -2359,28 +2256,21 @@ bool MimironRapidBurstAction::Execute(Event /*event*/) memberSpotNumber++; if (memberSpotNumber == 3) - { memberSpotNumber = 0; - } } MoveTo(bot->GetMapId(), targetPosition.GetPositionX(), targetPosition.GetPositionY(), targetPosition.GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_FORCED, true, false); if (AI_VALUE(float, "disperse distance") != 0.0f) - { SET_AI_VALUE(float, "disperse distance", 0.0f); - } TankFaceStrategy tankFaceStrategy(botAI); if (botAI->HasStrategy(tankFaceStrategy.getName(), BotState::BOT_STATE_COMBAT)) - { botAI->ChangeStrategy(REMOVE_STRATEGY_CHAR + tankFaceStrategy.getName(), BotState::BOT_STATE_COMBAT); - } + if (botAI->HasStrategy(tankFaceStrategy.getName(), BotState::BOT_STATE_NON_COMBAT)) - { botAI->ChangeStrategy(REMOVE_STRATEGY_CHAR + tankFaceStrategy.getName(), BotState::BOT_STATE_NON_COMBAT); - } if (bot->GetDistance(targetPosition) > 1.0f) return false; @@ -2403,53 +2293,38 @@ bool MimironAerialCommandUnitAction::Execute(Event /*event*/) continue; if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) - { boss = target; - } else if (target->GetEntry() == NPC_BOMB_BOT) - { bombBot = target; - } else if (target->GetEntry() == NPC_ASSAULT_BOT) - { assaultBot = target; - } } if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0)) { Group* group = bot->GetGroup(); if (!group) - { return false; - } if (bombBot) - { group->SetTargetIcon(RtiTargetValue::crossIndex, bot->GetGUID(), bombBot->GetGUID()); - } else if (boss) - { group->SetTargetIcon(RtiTargetValue::crossIndex, bot->GetGUID(), boss->GetGUID()); - } if (assaultBot) { ObjectGuid skullTarget = group->GetTargetIcon(RtiTargetValue::skullIndex); Unit* skullUnit = botAI->GetUnit(skullTarget); if (!skullTarget || !skullUnit || !skullUnit->IsAlive()) - { group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), assaultBot->GetGUID()); - } } return true; } if (AI_VALUE(float, "disperse distance") != 5.0f) - { SET_AI_VALUE(float, "disperse distance", 5.0f); - } + botAI->GetAiObjectContext()->GetValue("rti")->Set("cross"); return true; } @@ -2462,7 +2337,6 @@ bool MimironRocketStrikeAction::isUseful() bool MimironRocketStrikeAction::Execute(Event /*event*/) { - Unit* leviathanMkII = nullptr; Unit* vx001 = nullptr; Unit* aerialCommandUnit = nullptr; @@ -2474,12 +2348,8 @@ bool MimironRocketStrikeAction::Execute(Event /*event*/) if (!target || !target->IsAlive()) continue; - if (target->GetEntry() == NPC_LEVIATHAN_MKII) - leviathanMkII = target; - - else if (target->GetEntry() == NPC_VX001) + if (target->GetEntry() == NPC_VX001) vx001 = target; - else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) aerialCommandUnit = target; } @@ -2536,23 +2406,15 @@ bool MimironPhase4MarkDpsAction::Execute(Event /*event*/) continue; if (target->GetEntry() == NPC_LEVIATHAN_MKII) - { leviathanMkII = target; - } else if (target->GetEntry() == NPC_VX001) - { vx001 = target; - } else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) - { aerialCommandUnit = target; - } } if (!leviathanMkII || !vx001 || !aerialCommandUnit) - { return false; - } if (botAI->IsMainTank(bot)) { @@ -2563,31 +2425,27 @@ bool MimironPhase4MarkDpsAction::Execute(Event /*event*/) highestHealth = leviathanMkII->GetHealth(); highestHealthUnit = leviathanMkII; } + if (vx001 && vx001->GetHealth() > highestHealth) { highestHealth = vx001->GetHealth(); highestHealthUnit = vx001; } + if (aerialCommandUnit && aerialCommandUnit->GetHealth() > highestHealth) - { highestHealthUnit = aerialCommandUnit; - } group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), highestHealthUnit->GetGUID()); if (highestHealthUnit == leviathanMkII) { if (AI_VALUE(std::string, "rti") == "skull") - { botAI->GetAiObjectContext()->GetValue("rti")->Set("skull"); - } } else { group->SetTargetIcon(RtiTargetValue::crossIndex, bot->GetGUID(), leviathanMkII->GetGUID()); if (AI_VALUE(std::string, "rti") != "cross") - { botAI->GetAiObjectContext()->GetValue("rti")->Set("cross"); - } } botAI->DoSpecificAction("attack rti target"); @@ -2614,13 +2472,9 @@ bool MimironCheatAction::Execute(Event /*event*/) continue; if (unit->GetEntry() == NPC_PROXIMITY_MINE) - { unit->Kill(bot, unit); - } else if (unit->GetEntry() == NPC_BOMB_BOT) - { unit->Kill(bot, unit); - } } return true; @@ -2631,9 +2485,7 @@ bool VezaxCheatAction::Execute(Event /*event*/) // Restore bot's mana to full uint32 maxMana = bot->GetMaxPower(POWER_MANA); if (maxMana > 0) - { bot->SetPower(POWER_MANA, maxMana); - } return true; } @@ -2643,9 +2495,7 @@ bool VezaxShadowCrashAction::Execute(Event /*event*/) // Find General Vezax boss Unit* boss = AI_VALUE2(Unit*, "find target", "general vezax"); if (!boss || !boss->IsAlive()) - { return false; - } // Get bot's current position relative to boss float bossX = boss->GetPositionX(); @@ -2664,9 +2514,7 @@ bool VezaxShadowCrashAction::Execute(Event /*event*/) // If too close or too far, adjust distance first if (currentDistance < desiredDistance - 2.0f || currentDistance > desiredDistance + 2.0f) - { currentDistance = desiredDistance; - } // Calculate movement increment - move in increments around the boss float angleIncrement = M_PI / 10; @@ -2696,15 +2544,11 @@ bool YoggSaronOminousCloudCheatAction::Execute(Event /*event*/) Unit* boss = yoggSaronTrigger.GetSaraIfAlive(); if (!boss) - { return false; - } Creature* target = boss->FindNearestCreature(NPC_OMINOUS_CLOUD, 25.0f); if (!target || !target->IsAlive()) - { return false; - } target->Kill(bot, target); return true; @@ -2730,9 +2574,7 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) - { return false; - } YoggSaronTrigger yoggSaronTrigger(botAI); if (yoggSaronTrigger.IsPhase2()) @@ -2741,9 +2583,7 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) { Unit* crusherTentacle = bot->FindNearestCreature(NPC_CRUSHER_TENTACLE, 200.0f, true); if (crusherTentacle) - { crusherTentacle->Kill(bot, crusherTentacle); - } } ObjectGuid currentMoonTarget = group->GetTargetIcon(RtiTargetValue::moonIndex); @@ -2761,9 +2601,7 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) { nextPossibleTarget = bot->FindNearestCreature(NPC_CORRUPTOR_TENTACLE, 200.0f, true); if (!nextPossibleTarget) - { return false; - } } if (currentSkullTarget) @@ -2772,9 +2610,7 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) if (currentSkullUnit && currentSkullUnit->IsAlive() && currentSkullUnit->GetGUID() == nextPossibleTarget->GetGUID()) - { return false; - } } group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), nextPossibleTarget->GetGUID()); @@ -2783,15 +2619,11 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) { TankFaceStrategy tankFaceStrategy(botAI); if (botAI->HasStrategy(tankFaceStrategy.getName(), BotState::BOT_STATE_COMBAT)) - { botAI->ChangeStrategy(REMOVE_STRATEGY_CHAR + tankFaceStrategy.getName(), BotState::BOT_STATE_COMBAT); - } TankAssistStrategy tankAssistStrategy(botAI); if (!botAI->HasStrategy(tankAssistStrategy.getName(), BotState::BOT_STATE_COMBAT)) - { botAI->ChangeStrategy(ADD_STRATEGY_CHAR + tankAssistStrategy.getName(), BotState::BOT_STATE_COMBAT); - } GuidVector targets = AI_VALUE(GuidVector, "nearest npcs"); @@ -2801,14 +2633,12 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) { Unit* unit = botAI->GetUnit(guid); if (!unit || !unit->IsAlive()) - { continue; - } if ((unit->GetEntry() == NPC_IMMORTAL_GUARDIAN || unit->GetEntry() == NPC_MARKED_IMMORTAL_GUARDIAN) && unit->GetHealthPct() > 10) { - if (unit->GetHealth() < lowestHealth) + if (unit->GetHealth() < uint32(lowestHealth)) { lowestHealth = unit->GetHealth(); lowestHealthUnit = unit; @@ -2821,13 +2651,9 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) // Added because lunatic gaze freeze all bots and they can't attack // If someone fix it then this cheat can be removed if (botAI->HasCheat(BotCheatMask::raid)) - { lowestHealthUnit->Kill(bot, lowestHealthUnit); - } else - { group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), lowestHealthUnit->GetGUID()); - } return true; } @@ -2835,9 +2661,7 @@ bool YoggSaronMarkTargetAction::Execute(Event /*event*/) ObjectGuid currentSkullTarget = group->GetTargetIcon(RtiTargetValue::skullIndex); Unit* currentSkullUnit = nullptr; if (currentSkullTarget) - { currentSkullUnit = botAI->GetUnit(currentSkullTarget); - } if (!currentSkullUnit || currentSkullUnit->GetEntry() != NPC_YOGG_SARON) { @@ -2859,17 +2683,13 @@ bool YoggSaronBrainLinkAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) - { return false; - } for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* player = gref->GetSource(); if (player && player->IsAlive() && player->HasAura(SPELL_BRAIN_LINK) && player->GetGUID() != bot->GetGUID()) - { return MoveNear(player, 10.0f, MovementPriority::MOVEMENT_FORCED); - } } return false; @@ -2879,17 +2699,13 @@ bool YoggSaronMoveToEnterPortalAction::Execute(Event /*event*/) { Group* group = bot->GetGroup(); if (!group) - { return false; - } bool isInBrainRoomTeam = false; int portalNumber = 0; int brainRoomTeamCount = 10; if (bot->GetRaidDifficulty() == Difficulty::RAID_DIFFICULTY_10MAN_NORMAL) - { brainRoomTeamCount = 4; - } Player* master = botAI->GetMaster(); if (master && !botAI->IsTank(master)) @@ -2902,9 +2718,7 @@ bool YoggSaronMoveToEnterPortalAction::Execute(Event /*event*/) { Player* member = gref->GetSource(); if (!member || !member->IsAlive() || botAI->IsTank(member) || botAI->GetMaster()->GetGUID() == member->GetGUID()) - { continue; - } portalNumber++; if (member->GetGUID() == bot->GetGUID()) @@ -2915,15 +2729,11 @@ bool YoggSaronMoveToEnterPortalAction::Execute(Event /*event*/) brainRoomTeamCount--; if (brainRoomTeamCount == 0) - { break; - } } if (!isInBrainRoomTeam) - { return false; - } Position assignedPortalPosition = yoggPortalLoc[portalNumber - 1]; @@ -2980,33 +2790,24 @@ bool YoggSaronBossRoomMovementCheatAction::Execute(Event /*event*/) { FollowMasterStrategy followMasterStrategy(botAI); if (botAI->HasStrategy(followMasterStrategy.getName(), BotState::BOT_STATE_NON_COMBAT)) - { botAI->ChangeStrategy(REMOVE_STRATEGY_CHAR + followMasterStrategy.getName(), BotState::BOT_STATE_NON_COMBAT); - } if (!botAI->HasCheat(BotCheatMask::raid)) - { return false; - } Group* group = bot->GetGroup(); if (!group) - { return false; - } + ObjectGuid currentSkullTarget = group->GetTargetIcon(RtiTargetValue::skullIndex); if (!currentSkullTarget) - { return false; - } Unit* currentSkullUnit = botAI->GetUnit(currentSkullTarget); if (!currentSkullUnit || !currentSkullUnit->IsAlive()) - { return false; - } return bot->TeleportTo(bot->GetMapId(), currentSkullUnit->GetPositionX(), currentSkullUnit->GetPositionY(), currentSkullUnit->GetPositionZ(), bot->GetOrientation()); @@ -3014,19 +2815,15 @@ bool YoggSaronBossRoomMovementCheatAction::Execute(Event /*event*/) bool YoggSaronUsePortalAction::Execute(Event /*event*/) { - Creature* assignedPortal = bot->FindNearestCreature(NPC_DESCEND_INTO_MADNESS, 2.0f, true); - if (!assignedPortal) - { - return false; - } + Creature* assignedPortal = bot->FindNearestCreature(NPC_DESCEND_INTO_MADNESS, 2.0f, true); + if (!assignedPortal) + return false; - FollowMasterStrategy followMasterStrategy(botAI); - if (botAI->HasStrategy(followMasterStrategy.getName(), BotState::BOT_STATE_NON_COMBAT)) - { - botAI->ChangeStrategy(ADD_STRATEGY_CHAR + followMasterStrategy.getName(), BotState::BOT_STATE_NON_COMBAT); - } + FollowMasterStrategy followMasterStrategy(botAI); + if (botAI->HasStrategy(followMasterStrategy.getName(), BotState::BOT_STATE_NON_COMBAT)) + botAI->ChangeStrategy(ADD_STRATEGY_CHAR + followMasterStrategy.getName(), BotState::BOT_STATE_NON_COMBAT); - return assignedPortal->HandleSpellClick(bot); + return assignedPortal->HandleSpellClick(bot); } bool YoggSaronIllusionRoomAction::Execute(Event /*event*/) @@ -3067,15 +2864,11 @@ bool YoggSaronIllusionRoomAction::SetIllusionRtiTarget(YoggSaronTrigger yoggSaro { Unit* currentRtiTarget = yoggSaronTrigger.GetIllusionRoomRtiTarget(); if (currentRtiTarget) - { return false; - } Unit* nextRtiTarget = yoggSaronTrigger.GetNextIllusionRoomRtiTarget(); if (!nextRtiTarget) - { return false; - } // If proper adds handling in illusion room will be implemented, then this can be removed if (botAI->HasCheat(BotCheatMask::raid)) @@ -3090,9 +2883,7 @@ bool YoggSaronIllusionRoomAction::SetIllusionRtiTarget(YoggSaronTrigger yoggSaro { Group* group = bot->GetGroup(); if (!group) - { return false; - } uint8 rtiIndex = RtiTargetValue::GetRtiIndex(AI_VALUE(std::string, "rti")); group->SetTargetIcon(rtiIndex, bot->GetGUID(), nextRtiTarget->GetGUID()); @@ -3104,23 +2895,17 @@ bool YoggSaronIllusionRoomAction::SetIllusionRtiTarget(YoggSaronTrigger yoggSaro bool YoggSaronIllusionRoomAction::SetBrainRtiTarget(YoggSaronTrigger yoggSaronTrigger) { if (AI_VALUE(std::string, "rti") == "square" || !yoggSaronTrigger.IsMasterIsInBrainRoom()) - { return false; - } botAI->GetAiObjectContext()->GetValue("rti")->Set("square"); Group* group = bot->GetGroup(); if (!group) - { return false; - } Creature* brain = bot->FindNearestCreature(NPC_BRAIN, 200.0f, true); if (!brain) - { return false; - } group->SetTargetIcon(RtiTargetValue::squareIndex, bot->GetGUID(), brain->GetGUID()); @@ -3160,7 +2945,6 @@ bool YoggSaronMoveToExitPortalAction::Execute(Event /*event*/) if (botAI->HasCheat(BotCheatMask::raid)) bot->TeleportTo(bot->GetMapId(), portal->GetPositionX(), portal->GetPositionY(), portal->GetPositionZ(), bot->GetOrientation()); - else MoveTo(bot->GetMapId(), portal->GetPositionX(), portal->GetPositionY(), portal->GetPositionZ(), false, false, false, true, MovementPriority::MOVEMENT_FORCED, @@ -3189,9 +2973,7 @@ bool YoggSaronLunaticGazeAction::Execute(Event /*event*/) { if (AI_VALUE(std::string, "rti") != "cross") botAI->GetAiObjectContext()->GetValue("rti")->Set("cross"); - } - return true; } diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.h b/modules/mod-playerbots/src/Ai/Raid/Uld/UldActions.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.h rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldActions.h index c3d5c0d63..f28d6b634 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Action/RaidUlduarActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDULDUARACTIONS_H -#define _PLAYERBOT_RAIDULDUARACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ULDACTIONS_H +#define PLAYERBOTS_ULDACTIONS_H #include "Action.h" #include "AttackAction.h" @@ -8,8 +14,8 @@ #include "MovementActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "RaidUlduarBossHelper.h" -#include "RaidUlduarTriggers.h" +#include "UldBossHelper.h" +#include "UldTriggers.h" #include "Vehicle.h" // diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.cpp similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.cpp index 0a1b76a40..92ba7e76a 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.cpp @@ -1,4 +1,10 @@ -#include "RaidUlduarStrategy.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UldStrategy.h" void RaidUlduarStrategy::InitTriggers(std::vector& triggers) { diff --git a/modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.h new file mode 100644 index 000000000..38eebc6e1 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldStrategy.h @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ULDSTRATEGY_H +#define PLAYERBOTS_ULDSTRATEGY_H + +#include "Strategy.h" + +class RaidUlduarStrategy : public Strategy +{ +public: + RaidUlduarStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "ulduar"; } + virtual void InitTriggers(std::vector& triggers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggerContext.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggerContext.h index e4243fb10..d2dcd72f7 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/RaidUlduarTriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggerContext.h @@ -1,14 +1,14 @@ -// /* -// * 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. -// */ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#ifndef _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H +#ifndef PLAYERBOTS_ULDTRIGGERCONTEXT_H +#define PLAYERBOTS_ULDTRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "NamedObjectContext.h" -#include "RaidUlduarTriggers.h" +#include "UldTriggers.h" #include "BossAuraTriggers.h" class RaidUlduarTriggerContext : public NamedObjectContext diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggers.cpp similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggers.cpp index a99ea8dbc..c17725c51 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggers.cpp @@ -1,11 +1,17 @@ -#include "RaidUlduarTriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UldTriggers.h" #include "GameObject.h" #include "Object.h" #include "PlayerbotAI.h" #include "Playerbots.h" -#include "RaidUlduarBossHelper.h" -#include "RaidUlduarScripts.h" +#include "UldBossHelper.h" +#include "UldScripts.h" #include "ScriptedCreature.h" #include "SharedDefines.h" #include "Trigger.h" @@ -852,8 +858,6 @@ bool ThorimMarkDpsTargetTrigger::IsActive() Unit* runicColossus = AI_VALUE2(Unit*, "find target", "runic colossus"); Unit* ancientRuneGiant = AI_VALUE2(Unit*, "find target", "ancient rune giant"); - Unit* ironHonorGuard = AI_VALUE2(Unit*, "find target", "iron ring guard"); - Unit* ironRingGuard = AI_VALUE2(Unit*, "find target", "iron honor guard"); if (acolyte && acolyte->IsAlive() && (!currentCrossUnit || currentCrossUnit->GetEntry() != acolyte->GetEntry())) return true; @@ -1179,8 +1183,6 @@ bool MimironPhase1PositioningTrigger::IsActive() } Unit* leviathanMkII = nullptr; - Unit* vx001 = nullptr; - Unit* aerialCommandUnit = nullptr; GuidVector targets = AI_VALUE(GuidVector, "possible targets"); Unit* target = nullptr; @@ -1192,19 +1194,14 @@ bool MimironPhase1PositioningTrigger::IsActive() if (target->GetEntry() == NPC_LEVIATHAN_MKII) leviathanMkII = target; - else if (target->GetEntry() == NPC_VX001) return false; - else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) return false; - } if (!leviathanMkII || !leviathanMkII->IsAlive()) - { return false; - } return AI_VALUE(float, "disperse distance") != 6.0f; } @@ -1215,9 +1212,7 @@ bool MimironP3Wx2LaserBarrageTrigger::IsActive() // Check boss and it is alive if (!boss || !boss->IsAlive()) - { return false; - } bool isCasting = boss->HasUnitState(UNIT_STATE_CASTING); bool isP3WX2LaserBarrage = boss->FindCurrentSpellBySpellId(SPELL_SPINNING_UP) || @@ -1230,9 +1225,7 @@ bool MimironP3Wx2LaserBarrageTrigger::IsActive() boss->HasAura(SPELL_P3WX2_LASER_BARRAGE_AURA_1) || boss->HasAura(SPELL_P3WX2_LASER_BARRAGE_AURA_2); if ((!isCasting && !hasP3WX2LaserBarrageAura) || !isP3WX2LaserBarrage) - { return false; - } return true; } @@ -1252,51 +1245,33 @@ bool MimironRapidBurstTrigger::IsActive() continue; if (target->GetEntry() == NPC_LEVIATHAN_MKII) - { leviathanMkII = target; - } else if (target->GetEntry() == NPC_VX001) - { vx001 = target; - } else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) - { aerialCommandUnit = target; - } } if (!vx001 || !vx001->IsAlive()) - { return false; - } if (leviathanMkII && leviathanMkII->HasUnitState(UNIT_STATE_CASTING) && leviathanMkII->FindCurrentSpellBySpellId(SPELL_SHOCK_BLAST)) - { return false; - } if (botAI->IsMainTank(bot) && leviathanMkII && leviathanMkII->IsAlive() && leviathanMkII->GetVictim() != bot) - { return false; - } if (botAI->IsMelee(bot) && !botAI->IsMainTank(bot) && leviathanMkII && aerialCommandUnit) - { return false; - } MimironP3Wx2LaserBarrageTrigger mimironP3Wx2LaserBarrageTrigger(botAI); if (mimironP3Wx2LaserBarrageTrigger.IsActive()) - { return false; - } Group* group = bot->GetGroup(); if (!group) - { return false; - } uint32 memberSpotNumber = 0; Position memberPosition; @@ -1355,9 +1330,7 @@ bool MimironRapidBurstTrigger::IsActive() memberSpotNumber++; if (memberSpotNumber == 3) - { memberSpotNumber = 0; - } } GuidVector npcs = AI_VALUE(GuidVector, "nearest npcs"); @@ -1390,7 +1363,6 @@ bool MimironAerialCommandUnitTrigger::IsActive() Unit* leviathanMkII = nullptr; Unit* vx001 = nullptr; Unit* aerialCommandUnit = nullptr; - //Unit* bombBot = nullptr; Unit* assaultBot = nullptr; GuidVector targets = AI_VALUE(GuidVector, "possible targets"); @@ -1402,69 +1374,41 @@ bool MimironAerialCommandUnitTrigger::IsActive() continue; if (target->GetEntry() == NPC_LEVIATHAN_MKII) - { leviathanMkII = target; - } else if (target->GetEntry() == NPC_VX001) - { vx001 = target; - } else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) - { aerialCommandUnit = target; - } - //else if (target->GetEntry() == NPC_BOMB_BOT) - //{ - // bombBot = target; - //} else if (target->GetEntry() == NPC_ASSAULT_BOT) - { assaultBot = target; - } } if (!aerialCommandUnit || !aerialCommandUnit->IsAlive() || leviathanMkII || vx001) - { return false; - } if (!botAI->IsRanged(bot) && !botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0)) - { return false; - } if (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0)) { Group* group = bot->GetGroup(); if (!group) - { return false; - } ObjectGuid skullTarget = group->GetTargetIcon(RtiTargetValue::skullIndex); ObjectGuid crossTarget = group->GetTargetIcon(RtiTargetValue::crossIndex); - //if (bombBot && bombBot->GetGUID() != crossTarget) - //{ - // return true; - //} if (!crossTarget || aerialCommandUnit->GetGUID() != crossTarget) - { return true; - } else if (assaultBot && (!skullTarget || assaultBot->GetGUID() != skullTarget)) - { return true; - } return false; } std::string rtiMark = AI_VALUE(std::string, "rti"); if (rtiMark != "cross") - { return true; - } return false; } @@ -1475,16 +1419,12 @@ bool MimironRocketStrikeTrigger::IsActive() // Check boss and it is alive if (!boss || !boss->IsAlive()) - { return false; - } Creature* rocketStrikeN = bot->FindNearestCreature(NPC_ROCKET_STRIKE_N, 100.0f); if (!rocketStrikeN) - { return false; - } return bot->GetDistance2d(rocketStrikeN->GetPositionX(), rocketStrikeN->GetPositionY()) <= 10.0f; } @@ -1497,9 +1437,7 @@ bool MimironPhase4MarkDpsTrigger::IsActive() Group* group = bot->GetGroup(); if (!group) - { return false; - } GuidVector targets = AI_VALUE(GuidVector, "possible targets"); Unit* target = nullptr; @@ -1510,23 +1448,15 @@ bool MimironPhase4MarkDpsTrigger::IsActive() continue; if (target->GetEntry() == NPC_LEVIATHAN_MKII) - { leviathanMkII = target; - } else if (target->GetEntry() == NPC_VX001) - { vx001 = target; - } else if (target->GetEntry() == NPC_AERIAL_COMMAND_UNIT) - { aerialCommandUnit = target; - } } if (!leviathanMkII || !vx001 || !aerialCommandUnit) - { return false; - } if (botAI->IsMainTank(bot)) { @@ -1544,22 +1474,16 @@ bool MimironPhase4MarkDpsTrigger::IsActive() highestHealthUnit = vx001; } if (aerialCommandUnit && aerialCommandUnit->GetHealth() > highestHealth) - { highestHealthUnit = aerialCommandUnit; - } ObjectGuid skullTarget = group->GetTargetIcon(RtiTargetValue::skullIndex); if (!skullTarget) - { return true; - } return highestHealthUnit->GetGUID() != skullTarget; } else - { return AI_VALUE(std::string, "rti") != "skull"; - } return false; } @@ -1567,14 +1491,10 @@ bool MimironPhase4MarkDpsTrigger::IsActive() bool MimironCheatTrigger::IsActive() { if (!botAI->HasCheat(BotCheatMask::raid)) - { return false; - } if (!botAI->IsMainTank(bot)) - { return false; - } GuidVector targets = AI_VALUE(GuidVector, "nearest npcs"); for (const ObjectGuid& guid : targets) @@ -1584,13 +1504,9 @@ bool MimironCheatTrigger::IsActive() continue; if (unit->GetEntry() == NPC_PROXIMITY_MINE) - { return true; - } else if (unit->GetEntry() == NPC_BOMB_BOT) - { return true; - } } return false; @@ -1599,22 +1515,16 @@ bool MimironCheatTrigger::IsActive() bool VezaxCheatTrigger::IsActive() { if (!botAI->HasCheat(BotCheatMask::raid)) - { return false; - } Unit* boss = AI_VALUE2(Unit*, "find target", "general vezax"); // Check boss and it is alive if (!boss || !boss->IsAlive()) - { return false; - } if (!AI_VALUE2(bool, "has mana", "self target")) - { return false; - } return AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.lowMana; } @@ -1625,11 +1535,9 @@ bool VezaxShadowCrashTrigger::IsActive() // Check boss and it is alive if (!boss || !boss->IsAlive()) - { return false; - } - return botAI->HasAura(SPELL_VEZAX_SHADOW_CRASH, bot); + return bot->HasAura(SPELL_VEZAX_SHADOW_CRASH); } bool VezaxMarkOfTheFacelessTrigger::IsActive() @@ -1638,14 +1546,10 @@ bool VezaxMarkOfTheFacelessTrigger::IsActive() // Check boss and it is alive if (!boss || !boss->IsAlive()) - { return false; - } - if (!botAI->HasAura(SPELL_MARK_OF_THE_FACELESS, bot)) - { + if (!bot->HasAura(SPELL_MARK_OF_THE_FACELESS)) return false; - } float distance = bot->GetDistance2d(ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionX(), ULDUAR_VEZAX_MARK_OF_THE_FACELESS_SPOT.GetPositionY()); @@ -1657,9 +1561,8 @@ Unit* YoggSaronTrigger::GetSaraIfAlive() { Unit* sara = AI_VALUE2(Unit*, "find target", "sara"); if (!sara || !sara->IsAlive()) - { return nullptr; - } + return sara; } @@ -1689,9 +1592,7 @@ bool YoggSaronTrigger::IsYoggSaronFight() Unit* yoggsaron = AI_VALUE2(Unit*, "find target", "yogg-saron"); if ((sara && sara->IsAlive()) || (yoggsaron && yoggsaron->IsAlive())) - { return true; - } return false; } @@ -1699,24 +1600,16 @@ bool YoggSaronTrigger::IsYoggSaronFight() bool YoggSaronTrigger::IsInIllusionRoom() { if (!IsInBrainLevel()) - { return false; - } if (IsInStormwindKeeperIllusion()) - { return true; - } if (IsInIcecrownKeeperIllusion()) - { return true; - } if (IsInChamberOfTheAspectsIllusion()) - { return true; - } return false; } @@ -1753,9 +1646,7 @@ bool YoggSaronTrigger::IsMasterIsInBrainRoom() Player* master = botAI->GetMaster(); if (!master) - { return false; - } return master->GetDistance2d(ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE.GetPositionX(), ULDUAR_YOGG_SARON_BRAIN_ROOM_MIDDLE.GetPositionY()) < @@ -1766,43 +1657,29 @@ bool YoggSaronTrigger::IsMasterIsInBrainRoom() Position YoggSaronTrigger::GetIllusionRoomEntrancePosition() { if (IsInChamberOfTheAspectsIllusion()) - { return ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_ENTRANCE; - } else if (IsInIcecrownKeeperIllusion()) - { return ULDUAR_YOGG_SARON_ICECROWN_CITADEL_ENTRANCE; - } else if (IsInStormwindKeeperIllusion()) - { return ULDUAR_YOGG_SARON_STORMWIND_KEEPER_ENTRANCE; - } else - { return Position(); - } } Unit* YoggSaronTrigger::GetIllusionRoomRtiTarget() { Group* group = bot->GetGroup(); if (!group) - { return nullptr; - } int32_t rtiIndex = RtiTargetValue::GetRtiIndex(AI_VALUE(std::string, "rti")); if (rtiIndex == -1) - { return nullptr; // Invalid RTI mark - } ObjectGuid currentRtiTarget = group->GetTargetIcon(rtiIndex); Unit* currentRtiTargetUnit = botAI->GetUnit(currentRtiTarget); if (!currentRtiTargetUnit || !currentRtiTargetUnit->IsAlive()) - { currentRtiTargetUnit = nullptr; - } return currentRtiTargetUnit; } @@ -1812,13 +1689,10 @@ Unit* YoggSaronTrigger::GetNextIllusionRoomRtiTarget() float detectionRadius = 0.0f; if (IsInStormwindKeeperIllusion()) detectionRadius = ULDUAR_YOGG_SARON_STORMWIND_KEEPER_RADIUS; - else if (IsInIcecrownKeeperIllusion()) detectionRadius = ULDUAR_YOGG_SARON_ICECROWN_CITADEL_RADIUS; - else if (IsInChamberOfTheAspectsIllusion()) detectionRadius = ULDUAR_YOGG_SARON_CHAMBER_OF_ASPECTS_RADIUS; - else return nullptr; @@ -1855,9 +1729,7 @@ Unit* YoggSaronTrigger::GetNextIllusionRoomRtiTarget() } if (nextIllusionRoomRtiTarget) - { return nextIllusionRoomRtiTarget; - } if (IsInStormwindKeeperIllusion()) { @@ -1969,9 +1841,7 @@ bool YoggSaronMarkTargetTrigger::IsActive() ObjectGuid currentMoonTarget = group->GetTargetIcon(RtiTargetValue::moonIndex); Creature* yogg_saron = bot->FindNearestCreature(NPC_YOGG_SARON, 200.0f, true); if (!currentMoonTarget || currentMoonTarget != yogg_saron->GetGUID()) - { return true; - } ObjectGuid currentSkullTarget = group->GetTargetIcon(RtiTargetValue::skullIndex); @@ -2234,9 +2104,7 @@ bool YoggSaronPhase3PositioningTrigger::IsActive() if (botAI->IsRanged(bot) && bot->GetDistance2d(ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT.GetPositionX(), ULDUAR_YOGG_SARON_PHASE_3_RANGED_SPOT.GetPositionY()) > 15.0f) - { return true; - } if (botAI->IsMelee(bot) && !botAI->IsTank(bot) && bot->GetDistance2d(ULDUAR_YOGG_SARON_PHASE_3_MELEE_SPOT.GetPositionX(), diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h b/modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggers.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h rename to modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggers.h index 7f8cb51a8..a839d5001 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Trigger/RaidUlduarTriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/UldTriggers.h @@ -1,9 +1,15 @@ -#ifndef _PLAYERBOT_RAIDULDUARTRIGGERS_H -#define _PLAYERBOT_RAIDULDUARTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ULDTRIGGERS_H +#define PLAYERBOTS_ULDTRIGGERS_H #include "EventMap.h" #include "GenericTriggers.h" -#include "RaidUlduarBossHelper.h" +#include "UldBossHelper.h" #include "Trigger.h" // diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp b/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldBossHelper.cpp similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp rename to modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldBossHelper.cpp index fd6711cf0..7feefe814 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldBossHelper.cpp @@ -1,4 +1,10 @@ -#include "RaidUlduarBossHelper.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "UldBossHelper.h" #include "ObjectAccessor.h" #include "GameObject.h" #include "Group.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h b/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldBossHelper.h similarity index 97% rename from modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h rename to modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldBossHelper.h index b3d49ff58..4e67a6c5f 100644 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarBossHelper.h +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldBossHelper.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDULDUARBOSSHELPER_H -#define _PLAYERBOT_RAIDULDUARBOSSHELPER_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ULDBOSSHELPER_H +#define PLAYERBOTS_ULDBOSSHELPER_H #include #include diff --git a/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldScripts.h b/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldScripts.h new file mode 100644 index 000000000..36cbdac3b --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/Uld/Util/UldScripts.h @@ -0,0 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ULDSCRIPTS_H +#define PLAYERBOTS_ULDSCRIPTS_H + +#include "../../../../src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h" + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h b/modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h deleted file mode 100644 index bb2feefe4..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Strategy/RaidUlduarStrategy.h +++ /dev/null @@ -1,16 +0,0 @@ - -#ifndef _PLAYERBOT_RAIDULDUARSTRATEGY_H -#define _PLAYERBOT_RAIDULDUARSTRATEGY_H - -#include "AiObjectContext.h" -#include "Strategy.h" - -class RaidUlduarStrategy : public Strategy -{ -public: - RaidUlduarStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "ulduar"; } - virtual void InitTriggers(std::vector& triggers) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h b/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h deleted file mode 100644 index 14af97b96..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/Ulduar/Util/RaidUlduarScripts.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef _PLAYERBOT_RAIDULDUARSCRIPTS_H -#define _PLAYERBOT_RAIDULDUARSCRIPTS_H - -#include "../../../../src/server/scripts/Northrend/Ulduar/Ulduar/ulduar.h" - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h b/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h deleted file mode 100644 index 04ed2ac3a..000000000 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef _PLAYERBOT_RAIDVOASTRATEGY_H -#define _PLAYERBOT_RAIDVOASTRATEGY_H - -#include "Strategy.h" -#include "PlayerbotAI.h" -#include "string" -#include "Trigger.h" -#include "vector" - -class RaidVoAStrategy : public Strategy -{ -public: - RaidVoAStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "voa"; } - virtual void InitTriggers(std::vector& triggers) override; -}; - -#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAActionContext.h similarity index 81% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoAActionContext.h index 263d03d9b..b351e62ab 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAActionContext.h @@ -1,15 +1,16 @@ -// /* -// * 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. -// */ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#ifndef _PLAYERBOT_RAIDVOAACTIONCONTEXT_H -#define _PLAYERBOT_RAIDVOAACTIONCONTEXT_H +#ifndef PLAYERBOTS_VOAACTIONCONTEXT_H +#define PLAYERBOTS_VOAACTIONCONTEXT_H #include "Action.h" #include "BossAuraActions.h" #include "NamedObjectContext.h" -#include "RaidVoAActions.h" +#include "VoAActions.h" #include "PlayerbotAI.h" class RaidVoAActionContext : public NamedObjectContext diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Action/RaidVoAActions.cpp b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAActions.cpp similarity index 95% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Action/RaidVoAActions.cpp rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoAActions.cpp index 892ddf3eb..6cdcaca98 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Action/RaidVoAActions.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAActions.cpp @@ -1,5 +1,11 @@ -#include "RaidVoAActions.h" -#include "RaidVoATriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "VoAActions.h" +#include "VoATriggers.h" #include "Define.h" #include "Event.h" #include "Group.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Action/RaidVoAActions.h b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAActions.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Action/RaidVoAActions.h rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoAActions.h index 7aa72a234..f1503c6d7 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Action/RaidVoAActions.h +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAActions.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDVOAACTIONS_H -#define _PLAYERBOT_RAIDVOAACTIONS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VOAACTIONS_H +#define PLAYERBOTS_VOAACTIONS_H #include "Action.h" #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.cpp rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.cpp index db81477a8..a0c5ad5c7 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Strategy/RaidVoAStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.cpp @@ -1,4 +1,10 @@ -#include "RaidVoAStrategy.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "VoAStrategy.h" #include "Action.h" #include "Strategy.h" #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.h b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.h new file mode 100644 index 000000000..14f05e7f5 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoAStrategy.h @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VOASTRATEGY_H +#define PLAYERBOTS_VOASTRATEGY_H + +#include "Strategy.h" + +class RaidVoAStrategy : public Strategy +{ +public: + RaidVoAStrategy(PlayerbotAI* ai) : Strategy(ai) {} + virtual std::string const getName() override { return "voa"; } + virtual void InitTriggers(std::vector& triggers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggerContext.h similarity index 80% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggerContext.h index 6566793fd..c5a6bed3d 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggerContext.h @@ -1,15 +1,15 @@ -// /* -// * 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. -// */ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ -#ifndef _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H -#define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H +#ifndef PLAYERBOTS_VOATRIGGERCONTEXT_H +#define PLAYERBOTS_VOATRIGGERCONTEXT_H -#include "AiObjectContext.h" #include "BossAuraTriggers.h" #include "NamedObjectContext.h" -#include "RaidVoATriggers.h" +#include "VoATriggers.h" class RaidVoATriggerContext : public NamedObjectContext { diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Trigger/RaidVoATriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggers.cpp similarity index 92% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Trigger/RaidVoATriggers.cpp rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggers.cpp index 03a8b9b35..e92d78020 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Trigger/RaidVoATriggers.cpp +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggers.cpp @@ -1,4 +1,10 @@ -#include "RaidVoATriggers.h" +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "VoATriggers.h" #include "EventMap.h" #include "Object.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Trigger/RaidVoATriggers.h b/modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggers.h similarity index 77% rename from modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Trigger/RaidVoATriggers.h rename to modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggers.h index e952b2357..f26ee3321 100644 --- a/modules/mod-playerbots/src/Ai/Raid/VaultOfArchavon/Trigger/RaidVoATriggers.h +++ b/modules/mod-playerbots/src/Ai/Raid/VoA/VoATriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_RAIDVOATRIGGERS_H -#define _PLAYERBOT_RAIDVOATRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_VOATRIGGERS_H +#define PLAYERBOTS_VOATRIGGERS_H #include "EventMap.h" #include "GenericTriggers.h" diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActionContext.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActionContext.h new file mode 100644 index 000000000..c195e8b37 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActionContext.h @@ -0,0 +1,210 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZAACTIONCONTEXT_H +#define PLAYERBOTS_ZAACTIONCONTEXT_H + +#include "ZAActions.h" +#include "NamedObjectContext.h" + +class RaidZulAmanActionContext : public NamedObjectContext +{ +public: + RaidZulAmanActionContext() + { + // Trash + creators["amani'shi medicine man mark ward"] = + &RaidZulAmanActionContext::amanishi_medicine_man_mark_ward; + + // Akil'zon + creators["akil'zon misdirect boss to main tank"] = + &RaidZulAmanActionContext::akilzon_misdirect_boss_to_main_tank; + + creators["akil'zon tanks position boss"] = + &RaidZulAmanActionContext::akilzon_tanks_position_boss; + + creators["akil'zon spread ranged"] = + &RaidZulAmanActionContext::akilzon_spread_ranged; + + creators["akil'zon move to eye of the storm"] = + &RaidZulAmanActionContext::akilzon_move_to_eye_of_the_storm; + + creators["akil'zon manage electrical storm timer"] = + &RaidZulAmanActionContext::akilzon_manage_electrical_storm_timer; + + // Nalorakk + creators["nalorakk misdirect boss to main tank"] = + &RaidZulAmanActionContext::nalorakk_misdirect_boss_to_main_tank; + + creators["nalorakk tanks position boss"] = + &RaidZulAmanActionContext::nalorakk_tanks_position_boss; + + creators["nalorakk spread ranged"] = + &RaidZulAmanActionContext::nalorakk_spread_ranged; + + // Jan'alai + creators["jan'alai misdirect boss to main tank"] = + &RaidZulAmanActionContext::janalai_misdirect_boss_to_main_tank; + + creators["jan'alai tanks position boss"] = + &RaidZulAmanActionContext::janalai_tanks_position_boss; + + creators["jan'alai spread ranged in circle"] = + &RaidZulAmanActionContext::janalai_spread_ranged_in_circle; + + creators["jan'alai avoid fire bombs"] = + &RaidZulAmanActionContext::janalai_avoid_fire_bombs; + + creators["jan'alai mark amani'shi hatchers"] = + &RaidZulAmanActionContext::janalai_mark_amanishi_hatchers; + + // Halazzi + creators["halazzi misdirect boss to main tank"] = + &RaidZulAmanActionContext::halazzi_misdirect_boss_to_main_tank; + + creators["halazzi main tank position boss"] = + &RaidZulAmanActionContext::halazzi_main_tank_position_boss; + + creators["halazzi first assist tank attack spirit lynx"] = + &RaidZulAmanActionContext::halazzi_first_assist_tank_attack_spirit_lynx; + + creators["halazzi assign dps priority"] = + &RaidZulAmanActionContext::halazzi_assign_dps_priority; + + // Hex Lord Malacrass + creators["hex lord malacrass misdirect boss to main tank"] = + &RaidZulAmanActionContext::hex_lord_malacrass_misdirect_boss_to_main_tank; + + creators["hex lord malacrass assign dps priority"] = + &RaidZulAmanActionContext::hex_lord_malacrass_assign_dps_priority; + + creators["hex lord malacrass run away from whirlwind"] = + &RaidZulAmanActionContext::hex_lord_malacrass_run_away_from_whirlwind; + + creators["hex lord malacrass casters stop attacking"] = + &RaidZulAmanActionContext::hex_lord_malacrass_casters_stop_attacking; + + creators["hex lord malacrass move away from freezing trap"] = + &RaidZulAmanActionContext::hex_lord_malacrass_move_away_from_freezing_trap; + + // Zul'jin + creators["zul'jin misdirect boss to main tank"] = + &RaidZulAmanActionContext::zuljin_misdirect_boss_to_main_tank; + + creators["zul'jin tanks position boss"] = + &RaidZulAmanActionContext::zuljin_tanks_position_boss; + + creators["zul'jin run away from whirlwind"] = + &RaidZulAmanActionContext::zuljin_run_away_from_whirlwind; + + creators["zul'jin avoid cyclones"] = + &RaidZulAmanActionContext::zuljin_avoid_cyclones; + + creators["zul'jin spread ranged"] = + &RaidZulAmanActionContext::zuljin_spread_ranged; + } + +private: + // Trash + static Action* amanishi_medicine_man_mark_ward(PlayerbotAI* botAI) { + return new AmanishiMedicineManMarkWardAction(botAI); + } + + // Akil'zon + static Action* akilzon_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new AkilzonMisdirectBossToMainTankAction(botAI); + } + static Action* akilzon_tanks_position_boss(PlayerbotAI* botAI) { + return new AkilzonTanksPositionBossAction(botAI); + } + static Action* akilzon_spread_ranged(PlayerbotAI* botAI) { + return new AkilzonSpreadRangedAction(botAI); + } + static Action* akilzon_move_to_eye_of_the_storm(PlayerbotAI* botAI) { + return new AkilzonMoveToEyeOfTheStormAction(botAI); + } + static Action* akilzon_manage_electrical_storm_timer(PlayerbotAI* botAI) { + return new AkilzonManageElectricalStormTimerAction(botAI); + } + + // Nalorakk + static Action* nalorakk_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new NalorakkMisdirectBossToMainTankAction(botAI); + } + static Action* nalorakk_tanks_position_boss(PlayerbotAI* botAI) { + return new NalorakkTanksPositionBossAction(botAI); + } + static Action* nalorakk_spread_ranged(PlayerbotAI* botAI) { + return new NalorakkSpreadRangedAction(botAI); + } + + // Jan'alai + static Action* janalai_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new JanalaiMisdirectBossToMainTankAction(botAI); + } + static Action* janalai_tanks_position_boss(PlayerbotAI* botAI) { + return new JanalaiTanksPositionBossAction(botAI); + } + static Action* janalai_spread_ranged_in_circle(PlayerbotAI* botAI) { + return new JanalaiSpreadRangedInCircleAction(botAI); + } + static Action* janalai_avoid_fire_bombs(PlayerbotAI* botAI) { + return new JanalaiAvoidFireBombsAction(botAI); + } + static Action* janalai_mark_amanishi_hatchers(PlayerbotAI* botAI) { + return new JanalaiMarkAmanishiHatchersAction(botAI); + } + + // Halazzi + static Action* halazzi_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new HalazziMisdirectBossToMainTankAction(botAI); + } + static Action* halazzi_main_tank_position_boss(PlayerbotAI* botAI) { + return new HalazziMainTankPositionBossAction(botAI); + } + static Action* halazzi_first_assist_tank_attack_spirit_lynx(PlayerbotAI* botAI) { + return new HalazziFirstAssistTankAttackSpiritLynxAction(botAI); + } + static Action* halazzi_assign_dps_priority(PlayerbotAI* botAI) { + return new HalazziAssignDpsPriorityAction(botAI); + } + + // Hex Lord Malacrass + static Action* hex_lord_malacrass_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new HexLordMalacrassMisdirectBossToMainTankAction(botAI); + } + static Action* hex_lord_malacrass_assign_dps_priority(PlayerbotAI* botAI) { + return new HexLordMalacrassAssignDpsPriorityAction(botAI); + } + static Action* hex_lord_malacrass_run_away_from_whirlwind(PlayerbotAI* botAI) { + return new HexLordMalacrassRunAwayFromWhirlwindAction(botAI); + } + static Action* hex_lord_malacrass_casters_stop_attacking(PlayerbotAI* botAI) { + return new HexLordMalacrassCastersStopAttackingAction(botAI); + } + static Action* hex_lord_malacrass_move_away_from_freezing_trap(PlayerbotAI* botAI) { + return new HexLordMalacrassMoveAwayFromFreezingTrapAction(botAI); + } + + // Zul'jin + static Action* zuljin_misdirect_boss_to_main_tank(PlayerbotAI* botAI) { + return new ZuljinMisdirectBossToMainTankAction(botAI); + } + static Action* zuljin_tanks_position_boss(PlayerbotAI* botAI) { + return new ZuljinTanksPositionBossAction(botAI); + } + static Action* zuljin_run_away_from_whirlwind(PlayerbotAI* botAI) { + return new ZuljinRunAwayFromWhirlwindAction(botAI); + } + static Action* zuljin_avoid_cyclones(PlayerbotAI* botAI) { + return new ZuljinAvoidCyclonesAction(botAI); + } + static Action* zuljin_spread_ranged(PlayerbotAI* botAI) { + return new ZuljinSpreadRangedAction(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.cpp b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.cpp new file mode 100644 index 000000000..31d7b5444 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.cpp @@ -0,0 +1,754 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ZAActions.h" +#include "ZAHelpers.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace ZulAmanHelpers; + +// Trash + +bool AmanishiMedicineManMarkWardAction::Execute(Event /*event*/) +{ + if (Unit* protectiveWard = GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_AMANI_PROTECTIVE_WARD))) + { + return MarkTargetWithSkull(bot, protectiveWard); + } + + if (Unit* healingWard = GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_AMANI_HEALING_WARD))) + { + return MarkTargetWithSkull(bot, healingWard); + } + + return false; +} + +// Akil'zon + +bool AkilzonMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + if (!akilzon) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", akilzon)) + return botAI->CastSpell("steady shot", akilzon); + + return false; +} + +bool AkilzonTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + if (!akilzon) + return false; + + if (AI_VALUE(Unit*, "current target") != akilzon) + return Attack(akilzon); + + if (akilzon->GetVictim() == bot) + { + const Position& position = AKILZON_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool AkilzonSpreadRangedAction::Execute(Event /*event*/) +{ + constexpr float minDistance = 13.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance)) + return FleePosition(nearestPlayer->GetPosition(), minDistance, minInterval); + + return false; +} + +bool AkilzonMoveToEyeOfTheStormAction::Execute(Event /*event*/) +{ + Player* target = GetElectricalStormTarget(bot); + if (!target && !botAI->IsMainTank(bot)) + target = GetGroupMainTank(botAI, bot); + + if (target && bot->GetExactDist2d(target) > 2.0f) + { + botAI->Reset(); + return MoveTo(ZULAMAN_MAP_ID, target->GetPositionX(), target->GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); + } + + return false; +} + +bool AkilzonManageElectricalStormTimerAction::Execute(Event /*event*/) +{ + const time_t now = std::time(nullptr); + const uint32 instanceId = bot->GetMap()->GetInstanceId(); + + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + if (akilzon) + { + auto [it, inserted] = akilzonStormTimer.try_emplace(instanceId, now); + return inserted; + } + else if (!bot->IsInCombat() && !akilzon && akilzonStormTimer.erase(instanceId) > 0) + { + return true; + } + + return false; +} + +// Nalorakk + +bool NalorakkMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + if (!nalorakk) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", nalorakk)) + return botAI->CastSpell("steady shot", nalorakk); + + return false; +} + +bool NalorakkTanksPositionBossAction::Execute(Event /*event*/) +{ + if (!botAI->IsMainTank(bot) && !botAI->IsAssistTankOfIndex(bot, 0, true)) + return false; + + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + if (!nalorakk) + return false; + + if (botAI->IsMainTank(bot)) + return MainTankPositionTrollForm(nalorakk); + else + return FirstAssistTankPositionBearForm(nalorakk); +} + +bool NalorakkTanksPositionBossAction::MainTankPositionTrollForm(Unit* nalorakk) +{ + if (!nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + { + if (AI_VALUE(Unit*, "current target") != nalorakk) + return Attack(nalorakk); + + if (nalorakk->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + const Position& position = NALORAKK_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool NalorakkTanksPositionBossAction::FirstAssistTankPositionBearForm(Unit* nalorakk) +{ + if (nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + { + if (AI_VALUE(Unit*, "current target") != nalorakk) + return Attack(nalorakk); + + if (nalorakk->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + } + + const Position& position = NALORAKK_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool NalorakkSpreadRangedAction::Execute(Event /*event*/) +{ + constexpr float minDistance = 11.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance)) + return FleePosition(nearestPlayer->GetPosition(), minDistance, minInterval); + + return false; +} + +// Jan'alai + +bool JanalaiMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* janalai = AI_VALUE2(Unit*, "find target", "jan'alai"); + if (!janalai) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", janalai)) + return botAI->CastSpell("steady shot", janalai); + + return false; +} + +bool JanalaiTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* janalai = AI_VALUE2(Unit*, "find target", "jan'alai"); + if (!janalai) + return false; + + if (AI_VALUE(Unit*, "current target") != janalai) + return Attack(janalai); + + if (janalai->GetVictim() == bot) + { + const Position& position = JANALAI_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool JanalaiSpreadRangedInCircleAction::Execute(Event /*event*/) +{ + Group* group = bot->GetGroup(); + if (!group) + return false; + + std::vector rangedMembers; + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !botAI->IsRanged(member)) + continue; + + rangedMembers.push_back(member); + } + + if (rangedMembers.empty()) + return false; + + auto findIt = std::find(rangedMembers.begin(), rangedMembers.end(), bot); + size_t botIndex = + (findIt != rangedMembers.end()) ? std::distance(rangedMembers.begin(), findIt) : 0; + size_t count = rangedMembers.size(); + if (count == 0) + return false; + + constexpr float radius = 15.0f; + float angle = (count == 1) ? 0.0f : + (2.0f * M_PI * static_cast(botIndex) / static_cast(count)); + + float targetX = JANALAI_TANK_POSITION.GetPositionX() + radius * std::cos(angle); + float targetY = JANALAI_TANK_POSITION.GetPositionY() + radius * std::sin(angle); + + if (bot->GetExactDist2d(targetX, targetY) > 2.0f) + { + return MoveTo(ZULAMAN_MAP_ID, targetX, targetY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool JanalaiAvoidFireBombsAction::Execute(Event /*event*/) +{ + auto const& bombs = GetAllHazardTriggers( + bot, static_cast(ZulAmanNPCs::NPC_FIRE_BOMB), 50.0f); + + if (bombs.empty()) + return false; + + constexpr float hazardRadius = 5.0f; + bool inDanger = false; + for (Unit* bomb : bombs) + { + if (bot->GetDistance2d(bomb) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + const Position& janalaiCenter = JANALAI_TANK_POSITION; + constexpr float safeZoneRadius = 17.0f; + + Position safestPos = + FindSafestNearbyPosition(bot, bombs, janalaiCenter, safeZoneRadius, hazardRadius, false); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(ZULAMAN_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool JanalaiMarkAmanishiHatchersAction::Execute(Event /*event*/) +{ + auto [hatcherLow, hatcherHigh] = GetAmanishiHatcherPair(botAI); + + if (hatcherLow && hatcherHigh && hatcherHigh != hatcherLow) + { + return MarkTargetWithMoon(bot, hatcherHigh) || + MarkTargetWithSkull(bot, hatcherLow); + } + + return false; +} + +// Halazzi + +bool HalazziMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi"); + if (!halazzi) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", halazzi)) + return botAI->CastSpell("steady shot", halazzi); + + return false; +} + +bool HalazziMainTankPositionBossAction::Execute(Event /*event*/) +{ + Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi"); + if (!halazzi) + return false; + + if (MarkTargetWithStar(bot, halazzi)) + return true; + + SetRtiTarget(botAI, "star", halazzi); + + if (AI_VALUE(Unit*, "current target") != halazzi) + return Attack(halazzi); + + if (halazzi->GetVictim() == bot) + { + const Position& position = HALAZZI_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + } + + return false; +} + +bool HalazziFirstAssistTankAttackSpiritLynxAction::Execute(Event /*event*/) +{ + bool targetFound = false; + + if (Unit* lynx = AI_VALUE2(Unit*, "find target", "spirit of the lynx")) + { + if (MarkTargetWithCircle(bot, lynx)) + return true; + + SetRtiTarget(botAI, "circle", lynx); + + if (AI_VALUE(Unit*, "current target") != lynx) + return Attack(lynx); + + if (lynx->GetVictim() != bot) + return botAI->DoSpecificAction("taunt spell", Event(), true); + + targetFound = true; + } + else if (Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi")) + { + SetRtiTarget(botAI, "star", halazzi); + + if (AI_VALUE(Unit*, "current target") != halazzi) + return Attack(halazzi); + + targetFound = true; + } + + if (!targetFound) + return false; + + const Position& position = HALAZZI_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, false); + } + + return false; +} + +bool HalazziAssignDpsPriorityAction::Execute(Event /*event*/) +{ + // Target priority 1: Corrupted Lightning Totems + if (Unit* totem = GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_CORRUPTED_LIGHTNING_TOTEM))) + { + if (MarkTargetWithSkull(bot, totem)) + return true; + + SetRtiTarget(botAI, "skull", totem); + + if (AI_VALUE(Unit*, "current target") != totem) + return Attack(totem); + + return false; + } + + // Target priority 2: Halazzi + if (Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi")) + { + SetRtiTarget(botAI, "star", halazzi); + + if (AI_VALUE(Unit*, "current target") != halazzi) + return Attack(halazzi); + } + + // Don't attack the Lynx + return false; +} + +// Hex Lord Malacrass + +bool HexLordMalacrassMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", malacrass)) + return botAI->CastSpell("steady shot", malacrass); + + return false; +} + +bool HexLordMalacrassAssignDpsPriorityAction::Execute(Event /*event*/) +{ + static constexpr uint32 priorityEntries[] = + { + static_cast(ZulAmanNPCs::NPC_LORD_RAADAN), + static_cast(ZulAmanNPCs::NPC_ALYSON_ANTILLE), + static_cast(ZulAmanNPCs::NPC_KORAGG), + static_cast(ZulAmanNPCs::NPC_DARKHEART), + static_cast(ZulAmanNPCs::NPC_FENSTALKER), + static_cast(ZulAmanNPCs::NPC_GAZAKROTH), + static_cast(ZulAmanNPCs::NPC_THURG), + static_cast(ZulAmanNPCs::NPC_SLITHER), + static_cast(ZulAmanNPCs::NPC_HEX_LORD_MALACRASS) + }; + + auto const& targets = + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get(); + + Unit* priorityTarget = nullptr; + + for (uint32 entry : priorityEntries) + { + for (auto const& guid : targets) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && unit->IsAlive() && unit->GetEntry() == entry) + { + priorityTarget = unit; + break; + } + } + + if (priorityTarget) + break; + } + + if (priorityTarget) + { + if (MarkTargetWithSkull(bot, priorityTarget)) + return true; + + SetRtiTarget(botAI, "skull", priorityTarget); + } + + return false; +} + +bool HexLordMalacrassRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + if (Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass")) + { + float currentDistance = bot->GetDistance2d(malacrass); + constexpr float safeDistance = 9.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(malacrass, safeDistance - currentDistance); + } + } + + return false; +} + +bool HexLordMalacrassCastersStopAttackingAction::Execute(Event /*event*/) +{ + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_SPELL_REFLECTION))) + return false; + + if (AI_VALUE(Unit*, "current target") == malacrass) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return true; + } + + return false; +} + +bool HexLordMalacrassMoveAwayFromFreezingTrapAction::Execute(Event /*event*/) +{ + GameObject* trapGo = bot->FindNearestGameObject( + static_cast(ZulAmanObjects::GO_FREEZING_TRAP), 20.0f, true); + + if (!trapGo) + return false; + + float currentDistance = bot->GetDistance2d(trapGo); + constexpr float safeDistance = 6.0f; + constexpr uint32 minInterval = 0; + if (currentDistance < safeDistance) + return FleePosition(trapGo->GetPosition(), safeDistance, minInterval); + + return false; +} + +// Zul'jin + +bool ZuljinMisdirectBossToMainTankAction::Execute(Event /*event*/) +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin) + return false; + + Player* mainTank = GetGroupMainTank(botAI, bot); + if (!mainTank) + return false; + + if (botAI->CanCastSpell("misdirection", mainTank)) + return botAI->CastSpell("misdirection", mainTank); + + if (bot->HasAura(static_cast(ZulAmanSpells::SPELL_MISDIRECTION)) && + botAI->CanCastSpell("steady shot", zuljin)) + return botAI->CastSpell("steady shot", zuljin); + + return false; +} + +bool ZuljinTanksPositionBossAction::Execute(Event /*event*/) +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin) + return false; + + if (AI_VALUE(Unit*, "current target") != zuljin) + return Attack(zuljin); + + if (zuljin->GetVictim() == bot) + { + const Position& position = ZULJIN_TANK_POSITION; + float distToPosition = + bot->GetExactDist2d(position.GetPositionX(), position.GetPositionY()); + + if (distToPosition > 2.0f) + { + float dX = position.GetPositionX() - bot->GetPositionX(); + float dY = position.GetPositionY() - bot->GetPositionY(); + float moveDist = std::min(10.0f, distToPosition); + float moveX = bot->GetPositionX() + (dX / distToPosition) * moveDist; + float moveY = bot->GetPositionY() + (dY / distToPosition) * moveDist; + + return MoveTo(ZULAMAN_MAP_ID, moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, true); + } + } + + return false; +} + +bool ZuljinRunAwayFromWhirlwindAction::Execute(Event /*event*/) +{ + if (Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin")) + { + float currentDistance = bot->GetExactDist2d(zuljin); + constexpr float safeDistance = 10.0f; + if (currentDistance < safeDistance) + { + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveAway(zuljin, safeDistance - currentDistance); + } + } + + return false; +} + +bool ZuljinAvoidCyclonesAction::Execute(Event /*event*/) +{ + auto const& cyclones = GetAllHazardTriggers( + bot, static_cast(ZulAmanNPCs::NPC_FEATHER_VORTEX), 50.0f); + + if (cyclones.empty()) + return false; + + constexpr float hazardRadius = 6.0f; + bool inDanger = false; + for (Unit* cyclone : cyclones) + { + if (bot->GetDistance2d(cyclone) < hazardRadius) + { + inDanger = true; + break; + } + } + + if (!inDanger) + return false; + + const Position& zuljinCenter = ZULJIN_TANK_POSITION; + constexpr float safeZoneRadius = 30.0f; + + Position safestPos = + FindSafestNearbyPosition(bot, cyclones, zuljinCenter, safeZoneRadius, hazardRadius, true); + + bot->AttackStop(); + bot->InterruptNonMeleeSpells(true); + return MoveTo(ZULAMAN_MAP_ID, safestPos.GetPositionX(), safestPos.GetPositionY(), + bot->GetPositionZ(), false, false, false, false, + MovementPriority::MOVEMENT_FORCED, true, false); +} + +bool ZuljinSpreadRangedAction::Execute(Event /*event*/) +{ + constexpr float minDistance = 6.0f; + constexpr uint32 minInterval = 1000; + if (Player* nearestPlayer = GetNearestPlayerInRadius(bot, minDistance)) + return FleePosition(nearestPlayer->GetPosition(), minDistance, minInterval); + + return false; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.h new file mode 100644 index 000000000..e51eb4706 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAActions.h @@ -0,0 +1,254 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZAACTIONS_H +#define PLAYERBOTS_ZAACTIONS_H + +#include "Action.h" +#include "AttackAction.h" +#include "MovementActions.h" + +// Trash + +class AmanishiMedicineManMarkWardAction : public Action +{ +public: + AmanishiMedicineManMarkWardAction( + PlayerbotAI* botAI, std::string const name = "amani'shi medicine man mark ward") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Akil'zon + +class AkilzonMisdirectBossToMainTankAction : public AttackAction +{ +public: + AkilzonMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "akil'zon misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonTanksPositionBossAction : public AttackAction +{ +public: + AkilzonTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "akil'zon tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonSpreadRangedAction : public MovementAction +{ +public: + AkilzonSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "akil'zon spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonMoveToEyeOfTheStormAction : public MovementAction +{ +public: + AkilzonMoveToEyeOfTheStormAction( + PlayerbotAI* botAI, std::string const name = "akil'zon move to eye of the storm") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class AkilzonManageElectricalStormTimerAction : public Action +{ +public: + AkilzonManageElectricalStormTimerAction( + PlayerbotAI* botAI, std::string const name = "akil'zon manage electrical storm timer") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Nalorakk + +class NalorakkMisdirectBossToMainTankAction : public AttackAction +{ +public: + NalorakkMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "nalorakk misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class NalorakkTanksPositionBossAction : public AttackAction +{ +public: + NalorakkTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "nalorakk tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; + +private: + bool MainTankPositionTrollForm(Unit* nalorakk); + bool FirstAssistTankPositionBearForm(Unit* nalorakk); +}; + +class NalorakkSpreadRangedAction : public MovementAction +{ +public: + NalorakkSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "nalorakk spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Jan'alai + +class JanalaiMisdirectBossToMainTankAction : public AttackAction +{ +public: + JanalaiMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "jan'alai misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiTanksPositionBossAction : public AttackAction +{ +public: + JanalaiTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "jan'alai tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiSpreadRangedInCircleAction : public MovementAction +{ +public: + JanalaiSpreadRangedInCircleAction( + PlayerbotAI* botAI, std::string const name = "jan'alai spread ranged in circle") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiAvoidFireBombsAction : public MovementAction +{ +public: + JanalaiAvoidFireBombsAction(PlayerbotAI* botAI, std::string const name = "jan'alai avoid fire bombs") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class JanalaiMarkAmanishiHatchersAction : public Action +{ +public: + JanalaiMarkAmanishiHatchersAction( + PlayerbotAI* botAI, std::string const name = "jan'alai mark amani'shi hatchers") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +// Halazzi + +class HalazziMisdirectBossToMainTankAction : public AttackAction +{ +public: + HalazziMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "halazzi misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HalazziMainTankPositionBossAction : public AttackAction +{ +public: + HalazziMainTankPositionBossAction( + PlayerbotAI* botAI, std::string const name = "halazzi main tank position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HalazziFirstAssistTankAttackSpiritLynxAction : public AttackAction +{ +public: + HalazziFirstAssistTankAttackSpiritLynxAction( + PlayerbotAI* botAI, std::string const name = "halazzi first assist tank attack spirit lynx") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HalazziAssignDpsPriorityAction : public AttackAction +{ +public: + HalazziAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "halazzi assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Hex Lord Malacrass + +class HexLordMalacrassMisdirectBossToMainTankAction : public AttackAction +{ +public: + HexLordMalacrassMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassAssignDpsPriorityAction : public AttackAction +{ +public: + HexLordMalacrassAssignDpsPriorityAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass assign dps priority") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassRunAwayFromWhirlwindAction : public MovementAction +{ +public: + HexLordMalacrassRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass run away from whirlwind") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassCastersStopAttackingAction : public Action +{ +public: + HexLordMalacrassCastersStopAttackingAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass casters stop attacking") : Action(botAI, name) {} + bool Execute(Event event) override; +}; + +class HexLordMalacrassMoveAwayFromFreezingTrapAction : public MovementAction +{ +public: + HexLordMalacrassMoveAwayFromFreezingTrapAction( + PlayerbotAI* botAI, std::string const name = "hex lord malacrass move away from freezing trap") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +// Zul'jin + +class ZuljinMisdirectBossToMainTankAction : public AttackAction +{ +public: + ZuljinMisdirectBossToMainTankAction( + PlayerbotAI* botAI, std::string const name = "zul'jin misdirect boss to main tank") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinTanksPositionBossAction : public AttackAction +{ +public: + ZuljinTanksPositionBossAction( + PlayerbotAI* botAI, std::string const name = "zul'jin tanks position boss") : AttackAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinRunAwayFromWhirlwindAction : public MovementAction +{ +public: + ZuljinRunAwayFromWhirlwindAction( + PlayerbotAI* botAI, std::string const name = "zul'jin run away from whirlwind") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinAvoidCyclonesAction : public MovementAction +{ +public: + ZuljinAvoidCyclonesAction(PlayerbotAI* botAI, std::string const name = "zul'jin avoid cyclones") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class ZuljinSpreadRangedAction : public MovementAction +{ +public: + ZuljinSpreadRangedAction( + PlayerbotAI* botAI, std::string const name = "zul'jin spread ranged") : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.cpp b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.cpp new file mode 100644 index 000000000..5f089b5ad --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.cpp @@ -0,0 +1,191 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ZAHelpers.h" +#include "Group.h" +#include "Playerbots.h" + +namespace ZulAmanHelpers +{ + // General + Position FindSafestNearbyPosition(Player* bot, + const std::vector& hazards, const Position& safeZoneCenter, + float safeZoneRadius, float hazardRadius, bool requireSafePath) + { + constexpr float searchStep = M_PI / 8.0f; + constexpr float distanceStep = 1.0f; + + Position bestPos; + float minMoveDistance = std::numeric_limits::max(); + bool foundSafe = false; + + for (float distance = 0.0f; + distance <= safeZoneRadius; distance += distanceStep) + { + for (float angle = 0.0f; angle < 2 * M_PI; angle += searchStep) + { + float x = bot->GetPositionX() + distance * std::cos(angle); + float y = bot->GetPositionY() + distance * std::sin(angle); + + if (safeZoneCenter.GetExactDist2d(x, y) > safeZoneRadius) + continue; + + if (!IsPositionSafeFromHazards(x, y, hazards, hazardRadius)) + continue; + + Position testPos(x, y, bot->GetPositionZ()); + + bool pathSafe = true; + if (requireSafePath) + { + pathSafe = + IsPathSafeFromHazards(bot->GetPosition(), testPos, hazards, hazardRadius); + if (!pathSafe) + continue; + } + + float moveDistance = bot->GetExactDist2d(x, y); + if (!foundSafe || moveDistance < minMoveDistance) + { + bestPos = testPos; + minMoveDistance = moveDistance; + foundSafe = pathSafe; + } + } + + if (foundSafe) + break; + } + + return bestPos; + } + + bool IsPathSafeFromHazards(const Position& start, const Position& end, + const std::vector& hazards, float hazardRadius) + { + constexpr uint8 numChecks = 10; + float dx = end.GetPositionX() - start.GetPositionX(); + float dy = end.GetPositionY() - start.GetPositionY(); + + for (uint8 i = 1; i <= numChecks; ++i) + { + float ratio = static_cast(i) / numChecks; + float checkX = start.GetPositionX() + dx * ratio; + float checkY = start.GetPositionY() + dy * ratio; + + if (!IsPositionSafeFromHazards(checkX, checkY, hazards, hazardRadius)) + return false; + } + + return true; + } + + bool IsPositionSafeFromHazards( + float x, float y, const std::vector& hazards, float hazardRadius) + { + for (Unit* hazard : hazards) + { + if (hazard->GetDistance2d(x, y) < hazardRadius) + return false; + } + + return true; + } + + std::vector GetAllHazardTriggers(Player* bot, uint32 entry, float searchRadius) + { + std::vector triggers; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid(creatureList, entry, searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + triggers.push_back(creature); + } + + return triggers; + } + + // Akil'zon + const Position AKILZON_TANK_POSITION = { 378.369f, 1407.718f, 74.797f }; + std::unordered_map akilzonStormTimer; + + bool IsInStormWindow(time_t start, time_t now) + { + time_t elapsed = now - start; + uint32 seconds = elapsed % 60; + return elapsed >= 55 && (seconds >= 55 || seconds < 10); + } + + Player* GetElectricalStormTarget(Player* bot) + { + Group* group = bot->GetGroup(); + if (!group) + return nullptr; + + for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (member && + member->HasAura(static_cast(ZulAmanSpells::SPELL_ELECTRICAL_STORM))) + return member; + } + + return nullptr; + } + + // Nalorakk + const Position NALORAKK_TANK_POSITION = { -80.208f, 1324.530f, 40.942f }; + + // Jan'alai + const Position JANALAI_TANK_POSITION = { -33.873f, 1149.571f, 19.146f }; + + bool HasFireBombNearby(Player* bot) + { + constexpr float searchRadius = 30.0f; + std::list creatureList; + bot->GetCreatureListWithEntryInGrid( + creatureList, static_cast(ZulAmanNPCs::NPC_FIRE_BOMB), searchRadius); + + for (Creature* creature : creatureList) + { + if (creature && creature->IsAlive()) + return true; + } + + return false; + } + + std::pair GetAmanishiHatcherPair(PlayerbotAI* botAI) + { + Unit* lowest = nullptr; + Unit* highest = nullptr; + + for (auto const& guid : + botAI->GetAiObjectContext()->GetValue("possible targets no los")->Get()) + { + Unit* unit = botAI->GetUnit(guid); + if (unit && + unit->GetEntry() == static_cast(ZulAmanNPCs::NPC_AMANISHI_HATCHER)) + { + if (!lowest || unit->GetGUID().GetCounter() < lowest->GetGUID().GetCounter()) + lowest = unit; + + if (!highest || unit->GetGUID().GetCounter() > highest->GetGUID().GetCounter()) + highest = unit; + } + } + + return {lowest, highest}; + } + + // Halazzi + const Position HALAZZI_TANK_POSITION = { 370.733f, 1131.202f, 6.516f }; + + // Zul'jin + const Position ZULJIN_TANK_POSITION = { 120.210f, 705.564f, 45.111f }; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.h new file mode 100644 index 000000000..a037c2a67 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAHelpers.h @@ -0,0 +1,111 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZAHELPERS_H +#define PLAYERBOTS_ZAHELPERS_H + +#include + +#include "AiObject.h" +#include "Position.h" +#include "Unit.h" + +namespace ZulAmanHelpers +{ + enum class ZulAmanSpells : uint32 + { + // Akil'zon + SPELL_ELECTRICAL_STORM = 43648, + + // Nalorakk + SPELL_BEARFORM = 42377, + + // Hex Lord Malacrass + SPELL_HEX_LORD_WHIRLWIND = 43442, + SPELL_HEX_LORD_SPELL_REFLECTION = 43443, + SPELL_UNSTABLE_AFFLICTION = 43522, + + // Zul'jin + SPELL_ZULJIN_WHIRLWIND = 17207, + SPELL_SHAPE_OF_THE_BEAR = 42594, + SPELL_SHAPE_OF_THE_EAGLE = 42606, + SPELL_SHAPE_OF_THE_LYNX = 42607, + SPELL_SHAPE_OF_THE_DRAGONHAWK = 42608, + // SPELL_CLAW_RAGE = 43149, // Would require getting Zul'jin's bossai + + // Hunter + SPELL_MISDIRECTION = 35079, + }; + + enum class ZulAmanNPCs : uint32 + { + // Trash + NPC_AMANI_HEALING_WARD = 23757, + NPC_AMANI_PROTECTIVE_WARD = 23822, + + // Jan'alai + NPC_AMANI_DRAGONHAWK_HATCHLING = 23598, + NPC_AMANISHI_HATCHER = 23818, + NPC_FIRE_BOMB = 23920, + + // Halazzi + NPC_CORRUPTED_LIGHTNING_TOTEM = 24224, + + // Hex Lord Malacrass + NPC_HEX_LORD_MALACRASS = 24239, + NPC_ALYSON_ANTILLE = 24240, + NPC_THURG = 24241, + NPC_SLITHER = 24242, + NPC_LORD_RAADAN = 24243, + NPC_GAZAKROTH = 24244, + NPC_FENSTALKER = 24245, + NPC_DARKHEART = 24246, + NPC_KORAGG = 24247, + + // Zul'jin + NPC_FEATHER_VORTEX = 24136, + }; + + enum class ZulAmanObjects : uint32 + { + GO_FREEZING_TRAP = 186669, + }; + + // General + constexpr uint32 ZULAMAN_MAP_ID = 568; + Position FindSafestNearbyPosition( + Player* bot, const std::vector& hazards, const Position& center, + float safeZoneRadius, float hazardRadius, bool requireSafePath); + bool IsPathSafeFromHazards( + const Position& start, const Position& end, + const std::vector& hazards, float hazardRadius); + bool IsPositionSafeFromHazards( + float x, float y, const std::vector& hazards, float hazardRadius); + std::vector GetAllHazardTriggers( + Player* bot, uint32 entry, float searchRadius); + + // Akil'zon + extern const Position AKILZON_TANK_POSITION; + extern std::unordered_map akilzonStormTimer; + bool IsInStormWindow(time_t start, time_t now); + Player* GetElectricalStormTarget(Player* bot); + + // Nalorakk + extern const Position NALORAKK_TANK_POSITION; + + // Jan'alai + extern const Position JANALAI_TANK_POSITION; + bool HasFireBombNearby(Player* bot); + std::pair GetAmanishiHatcherPair(PlayerbotAI* botAI); + + // Halazzi + extern const Position HALAZZI_TANK_POSITION; + + // Zul'jin + extern const Position ZULJIN_TANK_POSITION; +} + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.cpp b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.cpp new file mode 100644 index 000000000..306eb9630 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.cpp @@ -0,0 +1,383 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ZAMultipliers.h" +#include "ZAActions.h" +#include "ZAHelpers.h" +#include "ChooseTargetActions.h" +#include "DKActions.h" +#include "DruidBearActions.h" +#include "FollowActions.h" +#include "GenericSpellActions.h" +#include "HunterActions.h" +#include "MageActions.h" +#include "PaladinActions.h" +#include "Playerbots.h" +#include "PriestActions.h" +#include "RaidBossHelpers.h" +#include "ReachTargetActions.h" +#include "RogueActions.h" +#include "ShamanActions.h" +#include "WarlockActions.h" +#include "WarriorActions.h" + +using namespace ZulAmanHelpers; + +// Akil'zon + +float AkilzonDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "akil'zon")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float AkilzonStayInEyeOfTheStormMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "akil'zon") /* || + !GetElectricalStormTarget(bot)*/) + return 1.0f; + + auto it = akilzonStormTimer.find(bot->GetMap()->GetInstanceId()); + if (it == akilzonStormTimer.end() || + !IsInStormWindow(it->second, std::time(nullptr))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Nalorakk + +float NalorakkDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + if (!nalorakk) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + bool shouldTankBoss = false; + + if (botAI->IsMainTank(bot) && + !nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + shouldTankBoss = true; + + if (botAI->IsAssistTankOfIndex(bot, 0, true) && + nalorakk->HasAura(static_cast(ZulAmanSpells::SPELL_BEARFORM))) + shouldTankBoss = true; + + if (!shouldTankBoss && + (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action))) + return 0.0f; + + return 1.0f; +} + +float NalorakkControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "nalorakk")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Jan'alai + +float JanalaiDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() == nullptr) + return 1.0f; + + if (botAI->IsMainTank(bot) && + dynamic_cast(action)) + return 0.0f; + + if (botAI->IsAssistTank(bot) && + !GetFirstAliveUnitByEntry( + botAI, static_cast(ZulAmanNPCs::NPC_AMANI_DRAGONHAWK_HATCHLING)) && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiDisableCombatFormationMoveMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (dynamic_cast(action) && + !dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiStayAwayFromFireBombsMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (!HasFireBombNearby(bot)) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiDoNotCrowdControlHatchersMultiplier::GetValue(Action* action) +{ + if (!AI_VALUE2(Unit*, "find target", "amani'shi hatcher")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float JanalaiDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "jan'alai")) + return 1.0f; + + if (AI_VALUE2(Unit*, "find target", "amani dragonhawk hatchling")) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Halazzi + +float HalazziDisableTankActionsMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "halazzi")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + if (bot->GetVictim() != nullptr && + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float HalazziControlMisdirectionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_HUNTER || + !AI_VALUE2(Unit*, "find target", "halazzi")) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Hex Lord Malacrass + +float HexLordMalacrassAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot)) + return 1.0f; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_WHIRLWIND))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier::GetValue(Action* action) +{ + if (!botAI->IsCaster(bot)) + return 1.0f; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_SPELL_REFLECTION))) + return 1.0f; + + auto castSpellAction = dynamic_cast(action); + if (!castSpellAction) + return 1.0f; + + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe || + (AI_VALUE(Unit*, "current target") == malacrass && + castSpellAction->getThreatType() == Action::ActionThreatType::Single)) + return 0.0f; + + return 1.0f; +} + +float HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_PRIEST && + bot->getClass() != CLASS_PALADIN && + bot->getClass() != CLASS_WARLOCK) + return 1.0f; + + if (!AI_VALUE2(Unit*, "find target", "hex lord malacrass")) + return 1.0f; + + Group* group = bot->GetGroup(); + if (!group) + return 1.0f; + + bool hasUnstableAffliction = false; + for (GroupReference* ref = bot->GetGroup()->GetFirstMember(); ref != nullptr; ref = ref->next()) + { + Player* member = ref->GetSource(); + if (!member || !member->IsAlive()) + continue; + + if (member->HasAura(static_cast(ZulAmanSpells::SPELL_UNSTABLE_AFFLICTION))) + { + hasUnstableAffliction = true; + break; + } + } + + if (!hasUnstableAffliction) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +// Zul'jin + +float ZuljinDisableTankFaceMultiplier::GetValue(Action* action) +{ + if (!botAI->IsTank(bot)) + return 1.0f; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK))) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float ZuljinAvoidWhirlwindMultiplier::GetValue(Action* action) +{ + if (botAI->IsMainTank(bot)) + return 1.0f; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_ZULJIN_WHIRLWIND))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float ZuljinDisableAvoidAoeMultiplier::GetValue(Action* action) +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE))) + return 1.0f; + + if (dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} + +float ZuljinDelayBloodlustAndHeroismMultiplier::GetValue(Action* action) +{ + if (bot->getClass() != CLASS_SHAMAN) + return 1.0f; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE))) + return 1.0f; + + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.h new file mode 100644 index 000000000..e66773962 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAMultipliers.h @@ -0,0 +1,168 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZAMULTIPLIERS_H +#define PLAYERBOTS_ZAMULTIPLIERS_H + +#include "Multiplier.h" + +// Akil'zon + +class AkilzonDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + AkilzonDisableCombatFormationMoveMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "akil'zon disable combat formation move") {} + virtual float GetValue(Action* action); +}; + +class AkilzonStayInEyeOfTheStormMultiplier : public Multiplier +{ +public: + AkilzonStayInEyeOfTheStormMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "akil'zon stay in eye of the storm") {} + virtual float GetValue(Action* action); +}; + +// Nalorakk + +class NalorakkDisableTankActionsMultiplier : public Multiplier +{ +public: + NalorakkDisableTankActionsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "nalorakk disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class NalorakkControlMisdirectionMultiplier : public Multiplier +{ +public: + NalorakkControlMisdirectionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "nalorakk control misdirection") {} + virtual float GetValue(Action* action); +}; + +// Jan'alai + +class JanalaiDisableTankActionsMultiplier : public Multiplier +{ +public: + JanalaiDisableTankActionsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class JanalaiDisableCombatFormationMoveMultiplier : public Multiplier +{ +public: + JanalaiDisableCombatFormationMoveMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai disable combat formation move") {} + virtual float GetValue(Action* action); +}; + +class JanalaiStayAwayFromFireBombsMultiplier : public Multiplier +{ +public: + JanalaiStayAwayFromFireBombsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai stay away from fire bombs") {} + virtual float GetValue(Action* action); +}; + +class JanalaiDoNotCrowdControlHatchersMultiplier : public Multiplier +{ +public: + JanalaiDoNotCrowdControlHatchersMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai do not crowd control hatchers") {} + virtual float GetValue(Action* action); +}; + +class JanalaiDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + JanalaiDelayBloodlustAndHeroismMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "jan'alai delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +// Halazzi + +class HalazziDisableTankActionsMultiplier : public Multiplier +{ +public: + HalazziDisableTankActionsMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "halazzi disable tank actions") {} + virtual float GetValue(Action* action); +}; + +class HalazziControlMisdirectionMultiplier : public Multiplier +{ +public: + HalazziControlMisdirectionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "halazzi control misdirection") {} + virtual float GetValue(Action* action); +}; + +// Hex Lord Malacrass + +class HexLordMalacrassAvoidWhirlwindMultiplier : public Multiplier +{ +public: + HexLordMalacrassAvoidWhirlwindMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "hex lord malacrass avoid whirlwind") {} + virtual float GetValue(Action* action); +}; + +class HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier : public Multiplier +{ +public: + HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "hex lord malacrass do not dispel unstable affliction") {} + virtual float GetValue(Action* action); +}; + +class HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier : public Multiplier +{ +public: + HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "hex lord malacrass stop attacking during spell reflection") {} + virtual float GetValue(Action* action); +}; + +// Zul'jin + +class ZuljinDisableTankFaceMultiplier : public Multiplier +{ +public: + ZuljinDisableTankFaceMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin disable tank face") {} + virtual float GetValue(Action* action); +}; + +class ZuljinAvoidWhirlwindMultiplier : public Multiplier +{ +public: + ZuljinAvoidWhirlwindMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin avoid whirlwind") {} + virtual float GetValue(Action* action); +}; + +class ZuljinDisableAvoidAoeMultiplier : public Multiplier +{ +public: + ZuljinDisableAvoidAoeMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin disable avoid aoe") {} + virtual float GetValue(Action* action); +}; + +class ZuljinDelayBloodlustAndHeroismMultiplier : public Multiplier +{ +public: + ZuljinDelayBloodlustAndHeroismMultiplier(PlayerbotAI* botAI) : Multiplier( + botAI, "zul'jin delay bloodlust and heroism") {} + virtual float GetValue(Action* action); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.cpp b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.cpp new file mode 100644 index 000000000..0823d6ad0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.cpp @@ -0,0 +1,135 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ZAStrategy.h" +#include "ZAMultipliers.h" + +void RaidZulAmanStrategy::InitTriggers(std::vector& triggers) +{ + // Trash + triggers.push_back(new TriggerNode("amani'shi medicine man summoned ward", { + NextAction("amani'shi medicine man mark ward", ACTION_RAID + 1) })); + + // Akil'zon + triggers.push_back(new TriggerNode("akil'zon pulling boss", { + NextAction("akil'zon misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("akil'zon boss engaged by main tank", { + NextAction("akil'zon main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("akil'zon boss casts static disruption", { + NextAction("akil'zon spread ranged", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("akil'zon electrical storm incoming", { + NextAction("akil'zon move to eye of the storm", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("akil'zon bots need to prepare for electrical storm", { + NextAction("akil'zon manage electrical storm timer", ACTION_EMERGENCY + 10) })); + + // Nalorakk + triggers.push_back(new TriggerNode("nalorakk pulling boss", { + NextAction("nalorakk misdirect boss to main tank", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("nalorakk boss switches forms", { + NextAction("nalorakk tanks position boss", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("nalorakk boss casts surge", { + NextAction("nalorakk spread ranged", ACTION_RAID + 1) })); + + // Jan'alai + triggers.push_back(new TriggerNode("jan'alai pulling boss", { + NextAction("jan'alai misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("jan'alai boss engaged by main tank", { + NextAction("jan'alai main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("jan'alai boss casts flame breath", { + NextAction("jan'alai spread ranged in circle", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("jan'alai boss summoning fire bombs", { + NextAction("jan'alai avoid fire bombs", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("jan'alai amani'shi hatchers spawned", { + NextAction("jan'alai mark amani'shi hatchers", ACTION_RAID + 2) })); + + // Halazzi + triggers.push_back(new TriggerNode("halazzi pulling boss", { + NextAction("halazzi misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("halazzi boss engaged by main tank", { + NextAction("halazzi main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("halazzi boss summons spirit lynx", { + NextAction("halazzi first assist tank attack spirit lynx", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("halazzi determining dps target", { + NextAction("halazzi assign dps priority", ACTION_RAID + 1) })); + + // Hex Lord Malacrass + triggers.push_back(new TriggerNode("hex lord malacrass pulling boss", { + NextAction("hex lord malacrass misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("hex lord malacrass determining kill order", { + NextAction("hex lord malacrass assign dps priority", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("hex lord malacrass boss is channeling whirlwind", { + NextAction("hex lord malacrass run away from whirlwind", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("hex lord malacrass boss has spell reflection", { + NextAction("hex lord malacrass casters stop attacking", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("hex lord malacrass boss placed freezing trap", { + NextAction("hex lord malacrass move away from freezing trap", ACTION_EMERGENCY + 1) })); + + // Zul'jin + triggers.push_back(new TriggerNode("zul'jin main tank needs aggro upon pull or phase change", { + NextAction("zul'jin misdirect boss to main tank", ACTION_RAID + 2) })); + + triggers.push_back(new TriggerNode("zul'jin boss engaged by main tank", { + NextAction("zul'jin main tank position boss", ACTION_RAID + 1) })); + + triggers.push_back(new TriggerNode("zul'jin boss is channeling whirlwind in troll form", { + NextAction("zul'jin run away from whirlwind", ACTION_EMERGENCY + 6) })); + + triggers.push_back(new TriggerNode("zul'jin boss is summoning cyclones in eagle form", { + NextAction("zul'jin avoid cyclones", ACTION_EMERGENCY + 1) })); + + triggers.push_back(new TriggerNode("zul'jin boss casts aoe abilities in dragonhawk form", { + NextAction("zul'jin spread ranged", ACTION_RAID + 1) })); +} + +void RaidZulAmanStrategy::InitMultipliers(std::vector& multipliers) +{ + // Akil'zon + multipliers.push_back(new AkilzonDisableCombatFormationMoveMultiplier(botAI)); + multipliers.push_back(new AkilzonStayInEyeOfTheStormMultiplier(botAI)); + + // Nalorakk + multipliers.push_back(new NalorakkDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new NalorakkControlMisdirectionMultiplier(botAI)); + + // Jan'alai + multipliers.push_back(new JanalaiDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new JanalaiDisableCombatFormationMoveMultiplier(botAI)); + multipliers.push_back(new JanalaiStayAwayFromFireBombsMultiplier(botAI)); + multipliers.push_back(new JanalaiDoNotCrowdControlHatchersMultiplier(botAI)); + multipliers.push_back(new JanalaiDelayBloodlustAndHeroismMultiplier(botAI)); + + // Halazzi + multipliers.push_back(new HalazziDisableTankActionsMultiplier(botAI)); + multipliers.push_back(new HalazziControlMisdirectionMultiplier(botAI)); + + // Hex Lord Malacrass + multipliers.push_back(new HexLordMalacrassAvoidWhirlwindMultiplier(botAI)); + multipliers.push_back(new HexLordMalacrassStopAttackingDuringSpellReflectionMultiplier(botAI)); + multipliers.push_back(new HexLordMalacrassDoNotDispelUnstableAfflictionMultiplier(botAI)); + + // Zul'jin + multipliers.push_back(new ZuljinDisableTankFaceMultiplier(botAI)); + multipliers.push_back(new ZuljinAvoidWhirlwindMultiplier(botAI)); + multipliers.push_back(new ZuljinDisableAvoidAoeMultiplier(botAI)); + multipliers.push_back(new ZuljinDelayBloodlustAndHeroismMultiplier(botAI)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.h new file mode 100644 index 000000000..94b3f9ea9 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZAStrategy.h @@ -0,0 +1,23 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZASTRATEGY_H +#define PLAYERBOTS_ZASTRATEGY_H + +#include "Strategy.h" + +class RaidZulAmanStrategy : public Strategy +{ +public: + RaidZulAmanStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + + std::string const getName() override { return "zulaman"; } + + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector& multipliers) override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggerContext.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggerContext.h new file mode 100644 index 000000000..4044667f0 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggerContext.h @@ -0,0 +1,212 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZATRIGGERCONTEXT_H +#define PLAYERBOTS_ZATRIGGERCONTEXT_H + +#include "ZATriggers.h" +#include "NamedObjectContext.h" + +class RaidZulAmanTriggerContext : public NamedObjectContext +{ +public: + RaidZulAmanTriggerContext() + { + // Trash + creators["amani'shi medicine man summoned ward"] = + &RaidZulAmanTriggerContext::amanishi_medicine_man_summoned_ward; + + // Akil'zon + creators["akil'zon pulling boss"] = + &RaidZulAmanTriggerContext::akilzon_pulling_boss; + + creators["akil'zon boss engaged by tanks"] = + &RaidZulAmanTriggerContext::akilzon_boss_engaged_by_tanks; + + creators["akil'zon boss casts static disruption"] = + &RaidZulAmanTriggerContext::akilzon_boss_casts_static_disruption; + + creators["akil'zon electrical storm incoming"] = + &RaidZulAmanTriggerContext::akilzon_electrical_storm_incoming; + + creators["akil'zon bots need to prepare for electrical storm"] = + &RaidZulAmanTriggerContext::akilzon_bots_need_to_prepare_for_electrical_storm; + + // Nalorakk + creators["nalorakk pulling boss"] = + &RaidZulAmanTriggerContext::nalorakk_pulling_boss; + + creators["nalorakk boss casts surge"] = + &RaidZulAmanTriggerContext::nalorakk_boss_casts_surge; + + creators["nalorakk boss switches forms"] = + &RaidZulAmanTriggerContext::nalorakk_boss_switches_forms; + + // Jan'alai + creators["jan'alai pulling boss"] = + &RaidZulAmanTriggerContext::janalai_pulling_boss; + + creators["jan'alai boss engaged by tanks"] = + &RaidZulAmanTriggerContext::janalai_boss_engaged_by_tanks; + + creators["jan'alai boss casts flame breath"] = + &RaidZulAmanTriggerContext::janalai_boss_casts_flame_breath; + + creators["jan'alai boss summoning fire bombs"] = + &RaidZulAmanTriggerContext::janalai_boss_summoning_fire_bombs; + + creators["jan'alai amani'shi hatchers spawned"] = + &RaidZulAmanTriggerContext::janalai_amanishi_hatchers_spawned; + + // Halazzi + creators["halazzi pulling boss"] = + &RaidZulAmanTriggerContext::halazzi_pulling_boss; + + creators["halazzi boss engaged by main tank"] = + &RaidZulAmanTriggerContext::halazzi_boss_engaged_by_main_tank; + + creators["halazzi boss summons spirit lynx"] = + &RaidZulAmanTriggerContext::halazzi_boss_summons_spirit_lynx; + + creators["halazzi determining dps target"] = + &RaidZulAmanTriggerContext::halazzi_determining_dps_target; + + // Hex Lord Malacrass + + creators["hex lord malacrass pulling boss"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_pulling_boss; + + creators["hex lord malacrass determining kill order"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_determining_kill_order; + + creators["hex lord malacrass boss is channeling whirlwind"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_boss_is_channeling_whirlwind; + + creators["hex lord malacrass boss has spell reflection"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_boss_has_spell_reflection; + + creators["hex lord malacrass boss placed freezing trap"] = + &RaidZulAmanTriggerContext::hex_lord_malacrass_boss_placed_freezing_trap; + + // Zul'jin + + creators["zul'jin main tank needs aggro upon pull or phase change"] = + &RaidZulAmanTriggerContext::zuljin_main_tank_needs_aggro_upon_pull_or_phase_change; + + creators["zul'jin boss engaged by tanks"] = + &RaidZulAmanTriggerContext::zuljin_boss_engaged_by_tanks; + + creators["zul'jin boss is channeling whirlwind in troll form"] = + &RaidZulAmanTriggerContext::zuljin_boss_is_channeling_whirlwind_in_troll_form; + + creators["zul'jin boss is summoning cyclones in eagle form"] = + &RaidZulAmanTriggerContext::zuljin_boss_is_summoning_cyclones_in_eagle_form; + + creators["zul'jin boss casts aoe abilities in dragonhawk form"] = + &RaidZulAmanTriggerContext::zuljin_boss_casts_aoe_abilities_in_dragonhawk_form; + } + +private: + // Trash + static Trigger* amanishi_medicine_man_summoned_ward(PlayerbotAI* botAI) { + return new AmanishiMedicineManSummonedWardTrigger(botAI); + } + + // Akil'zon + static Trigger* akilzon_pulling_boss(PlayerbotAI* botAI) { + return new AkilzonPullingBossTrigger(botAI); + } + static Trigger* akilzon_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new AkilzonBossEngagedByTanksTrigger(botAI); + } + static Trigger* akilzon_boss_casts_static_disruption(PlayerbotAI* botAI) { + return new AkilzonBossCastsStaticDisruptionTrigger(botAI); + } + static Trigger* akilzon_electrical_storm_incoming(PlayerbotAI* botAI) { + return new AkilzonElectricalStormIncomingTrigger(botAI); + } + static Trigger* akilzon_bots_need_to_prepare_for_electrical_storm(PlayerbotAI* botAI) { + return new AkilzonBotsNeedToPrepareForElectricalStormTrigger(botAI); + } + + // Nalorakk + static Trigger* nalorakk_pulling_boss(PlayerbotAI* botAI) { + return new NalorakkPullingBossTrigger(botAI); + } + static Trigger* nalorakk_boss_casts_surge(PlayerbotAI* botAI) { + return new NalorakkBossCastsSurgeTrigger(botAI); + } + static Trigger* nalorakk_boss_switches_forms(PlayerbotAI* botAI) { + return new NalorakkBossSwitchesFormsTrigger(botAI); + } + + // Jan'alai + static Trigger* janalai_pulling_boss(PlayerbotAI* botAI) { + return new JanalaiPullingBossTrigger(botAI); + } + static Trigger* janalai_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new JanalaiBossEngagedByTanksTrigger(botAI); + } + static Trigger* janalai_boss_casts_flame_breath(PlayerbotAI* botAI) { + return new JanalaiBossCastsFlameBreathTrigger(botAI); + } + static Trigger* janalai_boss_summoning_fire_bombs(PlayerbotAI* botAI) { + return new JanalaiBossSummoningFireBombsTrigger(botAI); + } + static Trigger* janalai_amanishi_hatchers_spawned(PlayerbotAI* botAI) { + return new JanalaiAmanishiHatchersSpawnedTrigger(botAI); + } + + // Halazzi + static Trigger* halazzi_pulling_boss(PlayerbotAI* botAI) { + return new HalazziPullingBossTrigger(botAI); + } + static Trigger* halazzi_boss_engaged_by_main_tank(PlayerbotAI* botAI) { + return new HalazziBossEngagedByMainTankTrigger(botAI); + } + static Trigger* halazzi_boss_summons_spirit_lynx(PlayerbotAI* botAI) { + return new HalazziBossSummonsSpiritLynxTrigger(botAI); + } + static Trigger* halazzi_determining_dps_target(PlayerbotAI* botAI) { + return new HalazziDeterminingDpsTargetTrigger(botAI); + } + + // Hex Lord Malacrass + static Trigger* hex_lord_malacrass_pulling_boss(PlayerbotAI* botAI) { + return new HexLordMalacrassPullingBossTrigger(botAI); + } + static Trigger* hex_lord_malacrass_determining_kill_order(PlayerbotAI* botAI) { + return new HexLordMalacrassDeterminingKillOrderTrigger(botAI); + } + static Trigger* hex_lord_malacrass_boss_is_channeling_whirlwind(PlayerbotAI* botAI) { + return new HexLordMalacrassBossIsChannelingWhirlwindTrigger(botAI); + } + static Trigger* hex_lord_malacrass_boss_has_spell_reflection(PlayerbotAI* botAI) { + return new HexLordMalacrassBossHasSpellReflectionTrigger(botAI); + } + static Trigger* hex_lord_malacrass_boss_placed_freezing_trap(PlayerbotAI* botAI) { + return new HexLordMalacrassBossPlacedFreezingTrapTrigger(botAI); + } + + // Zul'jin + static Trigger* zuljin_boss_engaged_by_tanks(PlayerbotAI* botAI) { + return new ZuljinBossEngagedByTanksTrigger(botAI); + } + static Trigger* zuljin_main_tank_needs_aggro_upon_pull_or_phase_change(PlayerbotAI* botAI) { + return new ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger(botAI); + } + static Trigger* zuljin_boss_is_channeling_whirlwind_in_troll_form(PlayerbotAI* botAI) { + return new ZuljinBossIsChannelingWhirlwindInTrollFormTrigger(botAI); + } + static Trigger* zuljin_boss_is_summoning_cyclones_in_eagle_form(PlayerbotAI* botAI) { + return new ZuljinBossIsSummoningCyclonesInEagleFormTrigger(botAI); + } + static Trigger* zuljin_boss_casts_aoe_abilities_in_dragonhawk_form(PlayerbotAI* botAI) { + return new ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger(botAI); + } +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.cpp b/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.cpp new file mode 100644 index 000000000..f2a57c3cd --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.cpp @@ -0,0 +1,272 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "ZATriggers.h" +#include "ZAHelpers.h" +#include "ZAActions.h" +#include "Playerbots.h" +#include "RaidBossHelpers.h" + +using namespace ZulAmanHelpers; + +// Trash + +bool AmanishiMedicineManSummonedWardTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "amani'shi medicine man"); +} + +// Akil'zon + +bool AkilzonPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* akilzon = AI_VALUE2(Unit*, "find target", "akil'zon"); + return akilzon && akilzon->GetHealthPct() > 95.0f; +} + +bool AkilzonBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "akil'zon")) + return false; + + return !GetElectricalStormTarget(bot); +} + +bool AkilzonBossCastsStaticDisruptionTrigger::IsActive() +{ + if (!botAI->IsRanged(bot) || + !AI_VALUE2(Unit*, "find target", "akil'zon")) + return false; + + auto it = akilzonStormTimer.find(bot->GetMap()->GetInstanceId()); + if (it == akilzonStormTimer.end()) + return true; + + return !IsInStormWindow(it->second, std::time(nullptr)); +} + +bool AkilzonElectricalStormIncomingTrigger::IsActive() +{ + if (!AI_VALUE2(Unit*, "find target", "akil'zon")) + return false; + + auto it = akilzonStormTimer.find(bot->GetMap()->GetInstanceId()); + if (it == akilzonStormTimer.end()) + return false; + + return IsInStormWindow(it->second, std::time(nullptr)); +} + +bool AkilzonBotsNeedToPrepareForElectricalStormTrigger::IsActive() +{ + return IsMechanicTrackerBot(bot, ZULAMAN_MAP_ID); +} + +// Nalorakk + +bool NalorakkPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* nalorakk = AI_VALUE2(Unit*, "find target", "nalorakk"); + return nalorakk && nalorakk->GetHealthPct() > 95.0f; +} + +bool NalorakkBossSwitchesFormsTrigger::IsActive() +{ + return (botAI->IsMainTank(bot) || botAI->IsAssistTankOfIndex(bot, 0, true)) && + AI_VALUE2(Unit*, "find target", "nalorakk"); +} + +bool NalorakkBossCastsSurgeTrigger::IsActive() +{ + return botAI->IsRanged(bot) && + AI_VALUE2(Unit*, "find target", "nalorakk"); +} + +// Jan'alai + +bool JanalaiPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* janalai = AI_VALUE2(Unit*, "find target", "jan'alai"); + return janalai && janalai->GetHealthPct() > 95.0f; +} + +bool JanalaiBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai")) + return false; + + return !HasFireBombNearby(bot); +} + +bool JanalaiBossCastsFlameBreathTrigger::IsActive() +{ + if (!botAI->IsRanged(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai") || + AI_VALUE2(Unit*, "find target", "amani dragonhawk hatchling")) + return false; + + return !HasFireBombNearby(bot); +} + +bool JanalaiBossSummoningFireBombsTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "jan'alai") && + HasFireBombNearby(bot); +} + +bool JanalaiAmanishiHatchersSpawnedTrigger::IsActive() +{ + if (!botAI->IsRangedDps(bot) || + !AI_VALUE2(Unit*, "find target", "jan'alai")) + return false; + + return bot->FindNearestCreature( + static_cast(ZulAmanNPCs::NPC_AMANISHI_HATCHER), 40.0f); +} + +// Halazzi + +bool HalazziPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* halazzi = AI_VALUE2(Unit*, "find target", "halazzi"); + return halazzi && halazzi->GetHealthPct() > 95.0f; +} + +bool HalazziBossEngagedByMainTankTrigger::IsActive() +{ + return botAI->IsMainTank(bot) && + AI_VALUE2(Unit*, "find target", "halazzi"); +} + +bool HalazziBossSummonsSpiritLynxTrigger::IsActive() +{ + return botAI->IsAssistTankOfIndex(bot, 0, true) && + AI_VALUE2(Unit*, "find target", "halazzi"); +} + +bool HalazziDeterminingDpsTargetTrigger::IsActive() +{ + return botAI->IsDps(bot) && + AI_VALUE2(Unit*, "find target", "halazzi"); +} + +// Hex Lord Malacrass + +bool HexLordMalacrassPullingBossTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + return malacrass && malacrass->GetHealthPct() > 95.0f; +} + +bool HexLordMalacrassDeterminingKillOrderTrigger::IsActive() +{ + return botAI->IsDps(bot) && + AI_VALUE2(Unit*, "find target", "hex lord malacrass"); +} + +bool HexLordMalacrassBossIsChannelingWhirlwindTrigger::IsActive() +{ + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + if (!malacrass || + !malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_WHIRLWIND))) + return false; + + return !(botAI->IsTank(bot) && malacrass->GetVictim() == bot); +} + +bool HexLordMalacrassBossHasSpellReflectionTrigger::IsActive() +{ + if (!botAI->IsCaster(bot)) + return false; + + Unit* malacrass = AI_VALUE2(Unit*, "find target", "hex lord malacrass"); + return malacrass && + malacrass->HasAura(static_cast(ZulAmanSpells::SPELL_HEX_LORD_SPELL_REFLECTION)); +} + +bool HexLordMalacrassBossPlacedFreezingTrapTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "hex lord malacrass") && + bot->FindNearestGameObject( + static_cast(ZulAmanObjects::GO_FREEZING_TRAP), 20.0f, true); +} + +// Zul'jin + +bool ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger::IsActive() +{ + if (bot->getClass() != CLASS_HUNTER) + return false; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin) + return false; + + float hp = zuljin->GetHealthPct(); + + return (hp <= 100.0f && hp > 95.0f) || + (hp <= 80.0f && hp > 75.0f && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_BEAR))) || + (hp <= 40.0f && hp > 35.0f && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_LYNX))) || + (hp <= 20.0f && hp > 15.0f && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK))); +} + +bool ZuljinBossEngagedByTanksTrigger::IsActive() +{ + if (!botAI->IsTank(bot)) + return false; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + return zuljin && + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE)) && + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK)); +} + +bool ZuljinBossIsChannelingWhirlwindInTrollFormTrigger::IsActive() +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + if (!zuljin || + !zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_ZULJIN_WHIRLWIND))) + return false; + + return !(botAI->IsTank(bot) && zuljin->GetVictim() == bot); +} + +bool ZuljinBossIsSummoningCyclonesInEagleFormTrigger::IsActive() +{ + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + return zuljin && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_EAGLE)); +} + +bool ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger::IsActive() +{ + if (!botAI->IsRanged(bot)) + return false; + + Unit* zuljin = AI_VALUE2(Unit*, "find target", "zul'jin"); + return zuljin && + zuljin->HasAura(static_cast(ZulAmanSpells::SPELL_SHAPE_OF_THE_DRAGONHAWK)); +} diff --git a/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.h b/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.h new file mode 100644 index 000000000..675834b65 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/Raid/ZA/ZATriggers.h @@ -0,0 +1,250 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_ZATRIGGERS_H +#define PLAYERBOTS_ZATRIGGERS_H + +#include "Trigger.h" + +// Trash + +class AmanishiMedicineManSummonedWardTrigger : public Trigger +{ +public: + AmanishiMedicineManSummonedWardTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "amani'shi medicine man summoned ward") {} + bool IsActive() override; +}; + +// Akil'zon + +class AkilzonPullingBossTrigger : public Trigger +{ +public: + AkilzonPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon pulling boss") {} + bool IsActive() override; +}; + +class AkilzonBossEngagedByTanksTrigger : public Trigger +{ +public: + AkilzonBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon boss engaged by tanks") {} + bool IsActive() override; +}; + +class AkilzonBossCastsStaticDisruptionTrigger : public Trigger +{ +public: + AkilzonBossCastsStaticDisruptionTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon boss casts static disruption") {} + bool IsActive() override; +}; + +class AkilzonElectricalStormIncomingTrigger : public Trigger +{ +public: + AkilzonElectricalStormIncomingTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon electrical storm incoming") {} + bool IsActive() override; +}; + +class AkilzonBotsNeedToPrepareForElectricalStormTrigger : public Trigger +{ +public: + AkilzonBotsNeedToPrepareForElectricalStormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "akil'zon bots need to prepare for electrical storm") {} + bool IsActive() override; +}; + +// Nalorakk + +class NalorakkPullingBossTrigger : public Trigger +{ +public: + NalorakkPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nalorakk pulling boss") {} + bool IsActive() override; +}; + +class NalorakkBossSwitchesFormsTrigger : public Trigger +{ +public: + NalorakkBossSwitchesFormsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nalorakk boss switches forms") {} + bool IsActive() override; +}; + +class NalorakkBossCastsSurgeTrigger : public Trigger +{ +public: + NalorakkBossCastsSurgeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "nalorakk boss casts surge") {} + bool IsActive() override; +}; + +// Jan'alai + +class JanalaiPullingBossTrigger : public Trigger +{ +public: + JanalaiPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai pulling boss") {} + bool IsActive() override; +}; + +class JanalaiBossEngagedByTanksTrigger : public Trigger +{ +public: + JanalaiBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai boss engaged by tanks") {} + bool IsActive() override; +}; + +class JanalaiBossCastsFlameBreathTrigger : public Trigger +{ +public: + JanalaiBossCastsFlameBreathTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai boss casts flame breath") {} + bool IsActive() override; +}; + +class JanalaiBossSummoningFireBombsTrigger : public Trigger +{ +public: + JanalaiBossSummoningFireBombsTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai boss summoning fire bombs") {} + bool IsActive() override; +}; + +class JanalaiAmanishiHatchersSpawnedTrigger : public Trigger +{ +public: + JanalaiAmanishiHatchersSpawnedTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "jan'alai amani'shi hatchers spawned") {} + bool IsActive() override; +}; + +// Halazzi + +class HalazziPullingBossTrigger : public Trigger +{ +public: + HalazziPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi pulling boss") {} + bool IsActive() override; +}; + +class HalazziBossEngagedByMainTankTrigger : public Trigger +{ +public: + HalazziBossEngagedByMainTankTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi boss engaged by main tank") {} + bool IsActive() override; +}; + +class HalazziBossSummonsSpiritLynxTrigger : public Trigger +{ +public: + HalazziBossSummonsSpiritLynxTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi boss summons spirit lynx") {} + bool IsActive() override; +}; + +class HalazziDeterminingDpsTargetTrigger : public Trigger +{ +public: + HalazziDeterminingDpsTargetTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "halazzi determining dps target") {} + bool IsActive() override; +}; + +// Hex Lord Malacrass + +class HexLordMalacrassPullingBossTrigger : public Trigger +{ +public: + HexLordMalacrassPullingBossTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass pulling boss") {} + bool IsActive() override; +}; + +class HexLordMalacrassDeterminingKillOrderTrigger : public Trigger +{ +public: + HexLordMalacrassDeterminingKillOrderTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass determining kill order") {} + bool IsActive() override; +}; + +class HexLordMalacrassBossIsChannelingWhirlwindTrigger : public Trigger +{ +public: + HexLordMalacrassBossIsChannelingWhirlwindTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass boss is channeling whirlwind") {} + bool IsActive() override; +}; + +class HexLordMalacrassBossHasSpellReflectionTrigger : public Trigger +{ +public: + HexLordMalacrassBossHasSpellReflectionTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass boss has spell reflection") {} + bool IsActive() override; +}; + +class HexLordMalacrassBossPlacedFreezingTrapTrigger : public Trigger +{ +public: + HexLordMalacrassBossPlacedFreezingTrapTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "hex lord malacrass boss placed freezing trap") {} + bool IsActive() override; +}; + +// Zul'jin + +class ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger : public Trigger +{ +public: + ZuljinMainTankNeedsAggroUponPullOrPhaseChangeTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin main tank needs aggro upon pull or phase change") {} + bool IsActive() override; +}; + +class ZuljinBossEngagedByTanksTrigger : public Trigger +{ +public: + ZuljinBossEngagedByTanksTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss engaged by tanks") {} + bool IsActive() override; +}; + +class ZuljinBossIsChannelingWhirlwindInTrollFormTrigger : public Trigger +{ +public: + ZuljinBossIsChannelingWhirlwindInTrollFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss is channeling whirlwind in troll form") {} + bool IsActive() override; +}; + +class ZuljinBossIsSummoningCyclonesInEagleFormTrigger : public Trigger +{ +public: + ZuljinBossIsSummoningCyclonesInEagleFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss is summoning cyclones in eagle form") {} + bool IsActive() override; +}; + +class ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger : public Trigger +{ +public: +ZuljinBossCastsAoeAbilitiesInDragonhawkFormTrigger( + PlayerbotAI* botAI) : Trigger(botAI, "zul'jin boss casts aoe abilities in dragonhawk form") {} + bool IsActive() override; +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 6102797a9..f3d8d491e 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -1,8 +1,15 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "NewRpgAction.h" #include #include +#include "AreaDefines.h" #include "BroadcastHelper.h" #include "ChatHelper.h" #include "G3D/Vector2.h" @@ -62,7 +69,7 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) { case RPG_IDLE: return RandomChangeStatus({RPG_GO_CAMP, RPG_GO_GRIND, RPG_WANDER_RANDOM, RPG_WANDER_NPC, RPG_DO_QUEST, - RPG_TRAVEL_FLIGHT, RPG_REST}); + RPG_TRAVEL_FLIGHT, RPG_REST, RPG_OUTDOOR_PVP}); case RPG_GO_GRIND: { @@ -140,6 +147,15 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/) } break; } + case RPG_OUTDOOR_PVP: + { + if (info.HasStatusPersisted(statusOutDoorPvPDuration)) + { + info.ChangeToIdle(); + return true; + } + break; + } default: break; } @@ -151,7 +167,14 @@ bool NewRpgGoGrindAction::Execute(Event /*event*/) if (SearchQuestGiverAndAcceptOrReward()) return true; if (auto* data = std::get_if(&botAI->rpgInfo.data)) - return MoveFarTo(data->pos); + { + if (MoveFarTo(data->pos)) + return true; + // Small nudge so the next tick's MoveFarTo starts from a + // slightly different position. Kept small so it doesn't look + // like the bot is abandoning its destination. + return MoveRandomNear(10.0f); + } return false; } @@ -162,7 +185,11 @@ bool NewRpgGoCampAction::Execute(Event /*event*/) return true; if (auto* data = std::get_if(&botAI->rpgInfo.data)) - return MoveFarTo(data->pos); + { + if (MoveFarTo(data->pos)) + return true; + return MoveRandomNear(10.0f); + } return false; } @@ -215,7 +242,14 @@ bool NewRpgWanderNpcAction::Execute(Event /*event*/) data.lastReach = 0; } else - return MoveWorldObjectTo(data.npcOrGo); + { + if (MoveWorldObjectTo(data.npcOrGo)) + return true; + // NPC pathing failed (random offset in a wall, mmap hiccup, etc). + // Take a small random step so the next tick retries from a + // different spot instead of staring at the NPC from afar. + return MoveRandomNear(15.0f); + } return true; } @@ -231,7 +265,6 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/) return false; auto& data = *dataPtr; uint32 questId = data.questId; - const Quest* quest = data.quest; uint8 questStatus = bot->GetQuestStatus(questId); switch (questStatus) { @@ -306,7 +339,12 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) { - return MoveFarTo(data.pos); + if (MoveFarTo(data.pos)) + return true; + // Long-range sampler couldn't land a candidate — nudge the + // bot a short distance so the next tick retries from a + // different position instead of sitting idle. + return MoveRandomNear(10.0f); } // Now we are near the quest objective // kill mobs and looting quest should be done automatically by grind strategy @@ -353,7 +391,11 @@ bool NewRpgDoQuestAction::DoIncompleteQuest(NewRpgInfo::DoQuest& data) return true; } - return MoveRandomNear(20.0f); + // At the POI: keep the bot actively placed but avoid large + // random 20yd hops that look like pacing back and forth. A small + // ~8yd wander reads as the bot looking around while grind/loot + // strategies do their work. + return MoveRandomNear(8.0f); } bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) @@ -393,7 +435,11 @@ bool NewRpgDoQuestAction::DoCompletedQuest(NewRpgInfo::DoQuest& data) return false; if (bot->GetDistance(data.pos) > 10.0f && !data.lastReachPOI) - return MoveFarTo(data.pos); + { + if (MoveFarTo(data.pos)) + return true; + return MoveRandomNear(10.0f); + } // Now we are near the qoi of reward // the quest should be rewarded by SearchQuestGiverAndAcceptOrReward @@ -429,27 +475,33 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) data.inFlight = true; return false; } - Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster); + + if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE) + return MoveFarTo(data.flightMasterPos); + + Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3); if (!flightMaster || !flightMaster->IsAlive()) { - botAI->rpgInfo.ChangeToIdle(); + info.ChangeToIdle(); return true; } - const TaxiNodesEntry* entry = sTaxiNodesStore.LookupEntry(data.toNode); if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) return MoveFarTo(flightMaster); - std::vector nodes = {data.fromNode, data.toNode}; + std::vector nodes = data.path; botAI->RemoveShapeshift(); if (bot->IsMounted()) bot->Dismount(); + bot->GetSession()->SendLearnNewTaxiNode(flightMaster); + if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0)) { LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), - flightMaster->GetEntry(), nodes[0], nodes[1]); - botAI->rpgInfo.ChangeToIdle(); + flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]); + info.ChangeToIdle(); + return true; } return true; } diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.h b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.h index a8cb7a2bc..6f70747f1 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_NEWRPGACTION_H -#define _PLAYERBOT_NEWRPGACTION_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEWRPGACTION_H +#define PLAYERBOTS_NEWRPGACTION_H #include "Duration.h" #include "MovementActions.h" @@ -47,10 +53,11 @@ public: protected: // static NewRpgStatusTransitionProb transitionMat; - const int32 statusWanderNpcDuration = 5 * 60 * 1000; - const int32 statusWanderRandomDuration = 5 * 60 * 1000; - const int32 statusRestDuration = 30 * 1000; - const int32 statusDoQuestDuration = 30 * 60 * 1000; + const int32 statusWanderNpcDuration = 5 * MINUTE * IN_MILLISECONDS ; + const int32 statusWanderRandomDuration = 5 * MINUTE * IN_MILLISECONDS ; + const int32 statusRestDuration = 30 * IN_MILLISECONDS ; + const int32 statusDoQuestDuration = 30 * MINUTE * IN_MILLISECONDS ; + const int32 statusOutDoorPvPDuration = HOUR * IN_MILLISECONDS ; }; class NewRpgGoGrindAction : public NewRpgBaseAction diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 3e1cea014..1038825a9 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -1,9 +1,14 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "NewRpgBaseAction.h" #include "BroadcastHelper.h" #include "ChatHelper.h" #include "Creature.h" -#include "FlightMasterCache.h" #include "G3D/Vector2.h" #include "GameObject.h" #include "GossipDef.h" @@ -13,6 +18,7 @@ #include "NewRpgStrategy.h" #include "Object.h" #include "ObjectAccessor.h" +#include "OutdoorPvPMgr.h" #include "ObjectDefines.h" #include "ObjectGuid.h" #include "ObjectMgr.h" @@ -20,9 +26,11 @@ #include "Player.h" #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "Position.h" #include "QuestDef.h" +#include "QuestPackets.h" #include "Random.h" #include "RandomPlayerbotMgr.h" #include "SharedDefines.h" @@ -47,17 +55,51 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) return false; } + // Let previously committed movement finish before recomputing. + // + // MoveTo internally caps its stored delay at maxWaitForMove + // (default 5s), but a long path (200+ yd routed around a + // mountain) takes 30+ seconds to walk. After 5s + // IsWaitingForLastMove returns false and MoveFarTo re-enters. + // Without this gate, DoMovePoint would call mm->Clear() and + // reissue MovePoint from the new bot position — and from a new + // position mmap's partial-path endpoint often differs, so the + // bot gets clobbered mid-walk and ends up oscillating (e.g. + // cave entrance -> inside cave -> cave entrance -> mountain + // base -> cave entrance...) around an unreachable destination. + // + // If the bot is still actively walking toward its last + // committed point on the same map, just let the current spline + // finish. The stuck counter below continues to track real + // progress toward dest and triggers teleport recovery if the + // committed paths genuinely aren't closing the gap. + { + LastMovement& lastMove = AI_VALUE(LastMovement&, "last movement"); + if (bot->isMoving() && lastMove.lastMoveToMapId == bot->GetMapId()) + { + float remaining = bot->GetExactDist(lastMove.lastMoveToX, lastMove.lastMoveToY, lastMove.lastMoveToZ); + if (remaining > 10.0f) + return true; + } + } + // stuck check float disToDest = bot->GetDistance(dest); - if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis) + // Require a meaningful improvement (5yd) to reset the stuck counter. + // The old 1yd threshold was small enough that bots oscillating back + // and forth around an obstacle would keep "making progress" forever + // and never trigger the teleport recovery below. + if (disToDest + 5.0f < botAI->rpgInfo.nearestMoveFarDis) { botAI->rpgInfo.nearestMoveFarDis = disToDest; botAI->rpgInfo.stuckTs = getMSTime(); botAI->rpgInfo.stuckAttempts = 0; } - else if (++botAI->rpgInfo.stuckAttempts >= 10 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) + else if (++botAI->rpgInfo.stuckAttempts >= 5 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime) { - // Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination + // No meaningful progress toward dest for `stuckTime`: fall + // back to teleporting directly so the bot can get on with + // its RPG objective instead of oscillating indefinitely. botAI->rpgInfo.stuckTs = getMSTime(); botAI->rpgInfo.stuckAttempts = 0; const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId()); @@ -79,26 +121,62 @@ bool NewRpgBaseAction::MoveFarTo(WorldPosition dest) false, true); } + const uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; + + // Primary strategy: ask mmap for a route to the TRUE destination. + // If mmap can reach it directly (PATHFIND_NORMAL) or partially + // (PATHFIND_INCOMPLETE — destinations beyond the smooth-path cap + // of ~296 yards, or where local geometry blocks the final step), + // walk to the furthest reachable waypoint mmap computed. This + // lets bots follow the real route around obstacles (mountains, + // cave walls, cliffs) instead of trying to cut straight through. + // The spline system walks the whole returned path smoothly, so + // subsequent ticks early-out via IsWaitingForLastMove and no + // further PathGenerator calls fire until the bot arrives. + { + PathGenerator path(bot); + path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ()); + PathType type = path.GetPathType(); + bool canReach = !(type & (~typeOk)); + if (canReach) + { + const G3D::Vector3& endPos = path.GetActualEndPosition(); + // Only commit if the mmap endpoint actually makes progress + // toward the destination. For pathological INCOMPLETE + // results (e.g. disconnected polys that still report + // INCOMPLETE) the endpoint can land right under the bot; + // fall through to cone sampling in that case. + float endDistToDest = dest.GetExactDist(endPos.x, endPos.y, endPos.z); + if (endDistToDest + 5.0f < disToDest) + { + return MoveTo(bot->GetMapId(), endPos.x, endPos.y, endPos.z, false, false, false, true); + } + } + } + + // Fallback: mmap couldn't route to the destination. Sample the + // forward cone for a reachable stepping stone so the bot keeps + // moving and can try again from a new vantage point. Cap at 2 + // samples — we already spent one PathGenerator call above and at + // 3000 bots every extra CalculatePath matters. float minDelta = M_PI; const float x = bot->GetPositionX(); const float y = bot->GetPositionY(); const float z = bot->GetPositionZ(); + const float baseAngle = bot->GetAngle(&dest); float rx, ry, rz; bool found = false; - int attempt = 3; - while (attempt--) + for (int attempt = 0; attempt < 2; ++attempt) { - float angle = bot->GetAngle(&dest); - float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2; - angle += delta; - float dis = rand_norm() * pathFinderDis; - float dx = x + cos(angle) * dis; - float dy = y + sin(angle) * dis; + float delta = (rand_norm() - 0.5f) * static_cast(M_PI); // ±π/2, forward cone + float sampleDis = (0.5f + rand_norm() * 0.5f) * pathFinderDis; + float angle = baseAngle + delta; + float dx = x + cos(angle) * sampleDis; + float dy = y + sin(angle) * sampleDis; float dz = z + 0.5f; PathGenerator path(bot); path.CalculatePath(dx, dy, dz); PathType type = path.GetPathType(); - uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY; bool canReach = !(type & (~typeOk)); if (canReach && fabs(delta) <= minDelta) @@ -153,21 +231,23 @@ bool NewRpgBaseAction::MoveWorldObjectTo(ObjectGuid guid, float distance) return MoveTo(mapId, x, y, z, false, false, false, true); } -bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority) +bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority, WorldObject*) { if (IsWaitingForLastMove(priority)) - { return false; - } - float distance = rand_norm() * moveStep; Map* map = bot->GetMap(); const float x = bot->GetPositionX(); const float y = bot->GetPositionY(); const float z = bot->GetPositionZ(); - int attempts = 1; - while (attempts--) + // Previously: attempts = 1. A single random sample often landed in + // water / blocked geometry / unreachable poly, the function returned + // false, and the caller had no fallback — bot stood still. Retry a + // handful of times with a fresh distance each loop so a bad roll + // doesn't lock the bot in place. + for (int attempt = 0; attempt < 8; ++attempt) { + float distance = (0.4f + rand_norm() * 0.6f) * moveStep; float angle = (float)rand_norm() * 2 * static_cast(M_PI); float dx = x + distance * cos(angle); float dy = y + distance * sin(angle); @@ -238,7 +318,10 @@ bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) { AcceptQuest(quest, guid); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Задание принято: " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "new_rpg_quest_accepted", + "Quest accepted %quest", + {{"%quest", ChatHelper::FormatQuest(quest)}})); BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest); botAI->rpgStatistic.questAccepted++; LOG_DEBUG("playerbots", "[New RPG] {} accept quest {}", bot->GetName(), quest->GetQuestId()); @@ -247,7 +330,10 @@ bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) { TurnInQuest(quest, guid); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Награда за задание получена: " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "new_rpg_quest_rewarded", + "Quest rewarded %quest", + {{"%quest", ChatHelper::FormatQuest(quest)}})); BroadcastHelper::BroadcastQuestTurnedIn(botAI, bot, quest); botAI->rpgStatistic.questRewarded++; LOG_DEBUG("playerbots", "[New RPG] {} turned in quest {}", bot->GetName(), quest->GetQuestId()); @@ -262,10 +348,10 @@ bool NewRpgBaseAction::CanInteractWithQuestGiver(Object* questGiver) // that removes the distance check and keeps all other checks switch (questGiver->GetTypeId()) { - case TYPEID_UNIT: + case TYPEID_UNIT: // Player::GetNPCIfCanInteractWith { ObjectGuid guid = questGiver->GetGUID(); - uint32 npcflagmask = UNIT_NPC_FLAG_QUESTGIVER; + // unit checks if (!guid) return false; @@ -292,7 +378,7 @@ bool NewRpgBaseAction::CanInteractWithQuestGiver(Object* questGiver) return false; // appropriate npc type - if (npcflagmask && !creature->HasNpcFlag(NPCFlags(npcflagmask))) + if (!creature->HasNpcFlag(UNIT_NPC_FLAG_QUESTGIVER)) return false; // not allow interaction under control, but allow with own pets @@ -303,35 +389,24 @@ bool NewRpgBaseAction::CanInteractWithQuestGiver(Object* questGiver) if (creature->GetReactionTo(bot) <= REP_UNFRIENDLY) return false; - Trainer::Trainer* trainer = sObjectMgr->GetTrainer(creature->GetEntry()); - - // pussywizard: many npcs have missing conditions for class training and rogue trainer can for eg. train - // dual wield to a shaman :/ too many to change in sql and watch in the future pussywizard: this function is - // not used when talking, but when already taking action (buy spell, reset talents, show spell list) - if (npcflagmask & (UNIT_NPC_FLAG_TRAINER | UNIT_NPC_FLAG_TRAINER_CLASS) && - trainer->GetTrainerType() == Trainer::Type::Class && - !trainer->IsTrainerValidForPlayer(bot)) - return false; - return true; } - case TYPEID_GAMEOBJECT: + case TYPEID_GAMEOBJECT: // Player::GetGameObjectIfCanInteractWith { ObjectGuid guid = questGiver->GetGUID(); - GameobjectTypes type = GAMEOBJECT_TYPE_QUESTGIVER; + if (GameObject* go = bot->GetMap()->GetGameObject(guid)) { - if (go->GetGoType() == type) + if (go->GetGoType() == GAMEOBJECT_TYPE_QUESTGIVER) { // Players cannot interact with gameobjects that use the "Point" icon if (go->GetGOInfo()->IconName == "Point") - { return false; - } return true; } } + return false; } // unused for now @@ -371,7 +446,6 @@ bool NewRpgBaseAction::IsWithinInteractionDist(Object* questGiver) case TYPEID_GAMEOBJECT: { ObjectGuid guid = questGiver->GetGUID(); - GameobjectTypes type = GAMEOBJECT_TYPE_QUESTGIVER; if (GameObject* go = bot->GetMap()->GetGameObject(guid)) { if (go->IsWithinDistInMap(bot)) @@ -537,9 +611,14 @@ bool NewRpgBaseAction::OrganizeQuestLog() LOG_DEBUG("playerbots", "[New RPG] {} drop quest {}", bot->GetName(), questId); WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST); packet << (uint8)i; - bot->GetSession()->HandleQuestLogRemoveQuest(packet); + WorldPackets::Quest::QuestLogRemoveQuest removeQuest(std::move(packet)); + removeQuest.Read(); + bot->GetSession()->HandleQuestLogRemoveQuest(removeQuest); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Задание отменено: " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "new_rpg_quest_dropped", + "Quest dropped %quest", + {{"%quest", ChatHelper::FormatQuest(quest)}})); botAI->rpgStatistic.questDropped++; dropped++; } @@ -557,14 +636,21 @@ bool NewRpgBaseAction::OrganizeQuestLog() continue; const Quest* quest = sObjectMgr->GetQuestTemplate(questId); - if (quest->GetZoneOrSort() < 0 || (quest->GetZoneOrSort() > 0 && quest->GetZoneOrSort() != bot->GetZoneId())) + const int64_t botZoneId = this->bot->GetZoneId(); + + if (quest->GetZoneOrSort() < 0 || (quest->GetZoneOrSort() > 0 && quest->GetZoneOrSort() != botZoneId)) { LOG_DEBUG("playerbots", "[New RPG] {} drop quest {}", bot->GetName(), questId); WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST); packet << (uint8)i; - bot->GetSession()->HandleQuestLogRemoveQuest(packet); + WorldPackets::Quest::QuestLogRemoveQuest removeQuest(std::move(packet)); + removeQuest.Read(); + bot->GetSession()->HandleQuestLogRemoveQuest(removeQuest); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Задание отменено: " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "new_rpg_quest_dropped", + "Quest dropped %quest", + {{"%quest", ChatHelper::FormatQuest(quest)}})); botAI->rpgStatistic.questDropped++; dropped++; } @@ -584,9 +670,14 @@ bool NewRpgBaseAction::OrganizeQuestLog() LOG_DEBUG("playerbots", "[New RPG] {} drop quest {}", bot->GetName(), questId); WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST); packet << (uint8)i; - bot->GetSession()->HandleQuestLogRemoveQuest(packet); + WorldPackets::Quest::QuestLogRemoveQuest removeQuest(std::move(packet)); + removeQuest.Read(); + bot->GetSession()->HandleQuestLogRemoveQuest(removeQuest); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Задание отменено: " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "new_rpg_quest_dropped", + "Quest dropped %quest", + {{"%quest", ChatHelper::FormatQuest(quest)}})); botAI->rpgStatistic.questDropped++; } @@ -822,7 +913,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector bool inComplete = false; for (uint32 objective : incompleteObjectiveIdx) { - if (qPoi.ObjectiveIndex == objective) + if (qPoi.ObjectiveIndex == static_cast(objective)) { inComplete = true; break; @@ -866,7 +957,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) { - const std::vector& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()]; + const std::vector& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel()); float hiRange = 500.0f; float loRange = 2500.0f; if (bot->GetLevel() < 5) @@ -924,9 +1015,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) { - const std::vector& locs = IsAlliance(bot->getRace()) - ? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()] - : sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()]; + const std::vector locs = sTravelMgr.GetTravelHubs(bot); bool inCity = false; @@ -967,70 +1056,21 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) return dest; } -bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode) +bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path) { - Creature* nearestFlightMaster = FlightMasterCache::Instance().GetNearestFlightMaster(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot); + if (!info) return false; - fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), - nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), - bot->GetTeamId()); - - if (!fromNode) + std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot); + if (availablePaths.empty()) return false; - std::vector availableToNodes; - for (uint32 i = 1; i < sTaxiNodesStore.GetNumRows(); ++i) - { - if (fromNode == i) - continue; - - TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(i); - - // check map - if (!node || node->map_id != bot->GetMapId() || - (!node->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0])) // dk flight - continue; - - // check taxi node known - if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(i)) - continue; - - // check distance by level - if (!botAI->CheckLocationDistanceByLevel(bot, WorldLocation(node->map_id, node->x, node->y, node->z), false)) - continue; - - // check path - uint32 path, cost; - sObjectMgr->GetTaxiPath(fromNode, i, path, cost); - if (!path) - continue; - - // check area level - uint32 nodeZoneId = bot->GetMap()->GetZoneId(bot->GetPhaseMask(), node->x, node->y, node->z); - bool capital = false; - if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(nodeZoneId)) - { - capital = zone->flags & AREA_FLAG_CAPITAL; - } - - auto itr = sRandomPlayerbotMgr.zone2LevelBracket.find(nodeZoneId); - if (!capital && itr == sRandomPlayerbotMgr.zone2LevelBracket.end()) - continue; - - if (!capital && (bot->GetLevel() < itr->second.low || bot->GetLevel() > itr->second.high)) - continue; - - availableToNodes.push_back(i); - } - if (availableToNodes.empty()) - return false; - - flightMaster = nearestFlightMaster->GetGUID(); - toNode = availableToNodes[urand(0, availableToNodes.size() - 1)]; + flightMasterEntry = info->templateEntry; + flightMasterPos = info->pos; + path = availablePaths[urand(0, availablePaths.size() - 1)]; LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", - bot->GetName(), flightMaster.GetEntry(), fromNode, toNode, availableToNodes.size()); + bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size()); return true; } @@ -1130,11 +1170,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMaster; - uint32 fromNode, toNode; - if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode)) + uint32 flightMasterEntry = 0; + WorldPosition flightMasterPos; + std::vector path; + if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode); + botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path); return true; } return false; @@ -1150,6 +1191,11 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta bot->SetStandState(UNIT_STAND_STATE_SIT); return true; } + case RPG_OUTDOOR_PVP: + { + botAI->rpgInfo.ChangeToOutdoorPvp(); + return true; + } default: { botAI->rpgInfo.ChangeToRest(); @@ -1206,9 +1252,21 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) } case RPG_TRAVEL_FLIGHT: { - ObjectGuid flightMaster; - uint32 fromNode, toNode; - return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode); + uint32 flightMasterEntry = 0; + WorldPosition flightMasterPos; + std::vector path; + return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path); + } + case RPG_OUTDOOR_PVP: + { + if (!bot->IsPvP()) + return false; + uint32 zoneId = bot->GetZoneId(); + if (zoneId == AREA_NAGRAND) + return false; + + OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId); + return outdoorPvP != nullptr; } default: return false; diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 910e5f494..f7a26d677 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_NEWRPGBASEACTION_H -#define _PLAYERBOT_NEWRPGBASEACTION_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEWRPGBASEACTION_H +#define PLAYERBOTS_NEWRPGBASEACTION_H #include "Duration.h" #include "LastMovementValue.h" @@ -31,7 +37,7 @@ protected: /* MOVEMENT RELATED */ bool MoveFarTo(WorldPosition dest); bool MoveWorldObjectTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE); - bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); + bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL, WorldObject* center = nullptr); bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL); /* QUEST RELATED CHECK */ @@ -54,14 +60,21 @@ protected: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); - bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode); + bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector& path); bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); protected: /* FOR MOVE FAR */ const float pathFinderDis = 70.0f; - const uint32 stuckTime = 5 * 60 * 1000; + // Time without real progress toward dest before MoveFarTo + // falls back to teleport recovery. Kept short enough that a + // bot truly oscillating around an unreachable destination + // (mmap returning non-progressing partial paths, or NOPATH + + // cone fallback wandering) doesn't spin for 5 minutes before + // the teleport fires, but long enough that a genuine long + // walk that is slowly making progress never triggers it. + const uint32 stuckTime = 90 * 1000; }; #endif diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp new file mode 100644 index 000000000..3c223d252 --- /dev/null +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.cpp @@ -0,0 +1,134 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "NewRpgOutdoorPvP.h" +#include "OutdoorPvP.h" +#include "OutdoorPvPMgr.h" + +bool NewRpgOutdoorPvpAction::Execute(Event) +{ + if (!bot->IsPvP()) + { + botAI->rpgInfo.ChangeToIdle(); + return false; + } + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL) || !bot->IsOutdoorPvPActive()) + return false; + + uint32 zoneId = bot->GetZoneId(); + OutdoorPvP* outdoorPvP = sOutdoorPvPMgr->GetOutdoorPvPToZoneId(zoneId); + if (!outdoorPvP || zoneId == AREA_NAGRAND) + { + botAI->rpgInfo.ChangeToIdle(); + return false; + } + + OutdoorPvP::OPvPCapturePointMap const& capturePointMap = outdoorPvP->GetCapturePoints(); + + NewRpgInfo& info = botAI->rpgInfo; + auto* dataPtr = std::get_if(&info.data); + if (!dataPtr) + return false; + auto& data = *dataPtr; + // Re-resolve stored spawn ID from the capture point map each tick (avoids dangling pointers) + OPvPCapturePoint* objective = nullptr; + if (data.capturePointSpawnId && !capturePointMap.empty()) + { + auto it = capturePointMap.find(data.capturePointSpawnId); + if (it != capturePointMap.end()) + { + OPvPCapturePoint* capturePoint = it->second; + if (capturePoint && capturePoint->_capturePoint) + { + float threshold = capturePoint->GetMinValue(); + float slider = capturePoint->GetSlider(); + uint8 faction = bot->GetTeamId(); + LOG_DEBUG("playerbots", "[NEW RPG] Bot {} with faction {} is evaluating existing RPG objective {} with threshold {} and slider value {}", bot->GetName(), faction, capturePoint->_capturePoint->GetName(), threshold, slider); + if ((faction == TEAM_HORDE && slider >= -threshold) || + (faction == TEAM_ALLIANCE && slider <= threshold)) + objective = capturePoint; + } + } + if (!objective) + data.capturePointSpawnId = 0; + } + + if (!objective) + { + objective = SelectNewObjective(capturePointMap); + if (!objective) + { + botAI->rpgInfo.ChangeToIdle(); + return true; + } + data.capturePointSpawnId = objective->m_capturePointSpawnId; + LOG_DEBUG("playerbots","[NEW RPG] Bot {} selected OutDoorPvP target capturePointSpawnId {}", bot->GetName(), data.capturePointSpawnId); + } + + GameObject* objectiveGO = objective->_capturePoint; + if (!objectiveGO) + return false; + + if (objectiveGO->GetGoType() != GAMEOBJECT_TYPE_CAPTURE_POINT) + return false; + + float radius = objectiveGO->GetGOInfo()->capturePoint.radius / 2.0f; + if (!objectiveGO->IsWithinDistInMap(bot, radius)) + return MoveFarTo(WorldPosition(objectiveGO)); + + return PatrolCapturePoint(objectiveGO, radius); +} + +OPvPCapturePoint* NewRpgOutdoorPvpAction::SelectNewObjective(OutdoorPvP::OPvPCapturePointMap const& capturePointMap) +{ + OPvPCapturePoint* objective = nullptr; + uint8 faction = bot->GetTeamId(); + std::vector candidateObjectives; + + if (capturePointMap.empty()) + { + botAI->rpgInfo.ChangeToIdle(); + return objective; + } + for (auto const& [guid, point] : capturePointMap) + { + GameObject* capturePointObject = point->_capturePoint; + if (!capturePointObject) + continue; + + float threshold = point->GetMinValue(); + float slider = point->GetSlider(); + if (faction == TEAM_HORDE && slider > -threshold) + candidateObjectives.push_back(point); + else if (faction == TEAM_ALLIANCE && slider < threshold) + candidateObjectives.push_back(point); + } + if (candidateObjectives.empty()) + { + LOG_DEBUG("playerbots", "[New RPG] Bot {} found no valid outdoor PVP objectives to capture", bot->GetName()); + botAI->rpgInfo.ChangeToIdle(); + return objective; + } + int randomIndex = urand(0, candidateObjectives.size() - 1); + objective = candidateObjectives[randomIndex]; + return objective; +} + +bool NewRpgOutdoorPvpAction::PatrolCapturePoint(GameObject* objectiveGO, float radius) +{ + if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL)) + return false; + + // Randomly pause at the current spot before picking a new patrol point + if (urand(0, 2) == 0) + return ForceToWait(urand(3000, 6000)); + + float patrolRadius = radius * 0.8f; + if (MoveRandomNear(patrolRadius, MovementPriority::MOVEMENT_NORMAL, objectiveGO)) + return true; + + return ForceToWait(urand(3000, 6000)); +} diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h new file mode 100644 index 000000000..f22f3c5ee --- /dev/null +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgOutdoorPvP.h @@ -0,0 +1,25 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEWRPGOUTDOORPVP_H +#define PLAYERBOTS_NEWRPGOUTDOORPVP_H + +#include "NewRpgBaseAction.h" +#include "OutdoorPvP.h" + +class NewRpgOutdoorPvpAction : public NewRpgBaseAction +{ +public: + NewRpgOutdoorPvpAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg outdoor pvp") {} + + virtual bool Execute(Event event) override; + OPvPCapturePoint* SelectNewObjective(OutdoorPvP::OPvPCapturePointMap const& capturePointMap); + +private: + bool PatrolCapturePoint(GameObject* objectiveGO, float radius); +}; + +#endif diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RpgAction.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgAction.cpp similarity index 94% rename from modules/mod-playerbots/src/Ai/Base/Actions/RpgAction.cpp rename to modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgAction.cpp index 73068a0b1..9b44ebb56 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RpgAction.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgAction.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RpgAction.h" @@ -107,7 +108,7 @@ bool RpgAction::SetNextRpgAction() { std::vector> sortedActions; - for (int i = 0; i < actions.size(); i++) + for (uint32 i = 0; i < actions.size(); i++) sortedActions.push_back(std::make_pair(actions[i], relevances[i])); std::sort(sortedActions.begin(), sortedActions.end(), [](std::pairi, std::pair j) {return i.second > j.second; }); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RpgAction.h b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgAction.h similarity index 69% rename from modules/mod-playerbots/src/Ai/Base/Actions/RpgAction.h rename to modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgAction.h index c0485a8e7..cd1355b76 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RpgAction.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgAction.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RPGACTION_H -#define _PLAYERBOT_RPGACTION_H +#ifndef PLAYERBOTS_RPGACTION_H +#define PLAYERBOTS_RPGACTION_H #include "MovementActions.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgSubActions.cpp similarity index 80% rename from modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp rename to modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgSubActions.cpp index e067702d6..b7ac20065 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgSubActions.cpp @@ -1,10 +1,12 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RpgSubActions.h" +#include "BudgetValues.h" #include "ChooseRpgTargetAction.h" #include "EmoteAction.h" #include "Formations.h" @@ -12,6 +14,7 @@ #include "GuildCreateActions.h" #include "LastMovementValue.h" #include "MovementActions.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "PossibleRpgTargetsValue.h" #include "SocialMgr.h" @@ -51,10 +54,15 @@ GuidPosition RpgHelper::guidP() { return AI_VALUE(GuidPosition, "rpg target"); } ObjectGuid RpgHelper::guid() { return (ObjectGuid)guidP(); } -bool RpgHelper::InRange() -{ - return guidP() ? (guidP().sqDistance2d(bot) < INTERACTION_DISTANCE * INTERACTION_DISTANCE) : false; -} + bool RpgHelper::InRange() + { + GuidPosition targetGuid = guidP(); + if (!targetGuid) + return false; + + return bot->GetExactDist2dSq(targetGuid.GetPositionX(), targetGuid.GetPositionY()) < + INTERACTION_DISTANCE * INTERACTION_DISTANCE; + } void RpgHelper::setFacingTo(GuidPosition guidPosition) { @@ -250,6 +258,60 @@ Event RpgSellAction::ActionEvent(Event /*event*/) { return Event("rpg action", " std::string const RpgRepairAction::ActionName() { return "repair"; } +bool RpgTrainAction::isUseful() +{ + if (!rpg->InRange()) + return false; + + Creature* creature = rpg->guidP().GetCreature(); + if (!creature) + return false; + + if (!creature->IsInWorld() || creature->IsDuringRemoveFromWorld() || !creature->IsAlive()) + return false; + + return true; +} + +bool RpgTrainAction::isPossible() +{ + GuidPosition gp = rpg->guidP(); + + CreatureTemplate const* cinfo = gp.GetCreatureTemplate(); + if (!cinfo) + return false; + + Trainer::Trainer* trainer = sObjectMgr->GetTrainer(cinfo->Entry); + if (!trainer) + return false; + + if (!trainer->IsTrainerValidForPlayer(bot)) + return false; + + FactionTemplateEntry const* factionTemplate = sFactionTemplateStore.LookupEntry(cinfo->faction); + float reputationDiscount = bot->GetReputationPriceDiscount(factionTemplate); + uint32 currentGold = AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::spells); + + for (auto& spell : trainer->GetSpells()) + { + Trainer::Spell const* trainerSpell = trainer->GetSpell(spell.SpellId); + if (!trainerSpell) + continue; + + if (!trainer->CanTeachSpell(bot, trainerSpell)) + continue; + + if (currentGold < static_cast(floor(trainerSpell->MoneyCost * reputationDiscount))) + continue; + + // we only check if at least one spell can be learned from the trainer; + // otherwise, the train action should not be allowed + return true; + } + + return false; +} + std::string const RpgTrainAction::ActionName() { return "trainer"; } bool RpgHealAction::Execute(Event /*event*/) @@ -370,12 +432,15 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/) if (bot->GetTradeData() && bot->GetTradeData()->HasItem(item->GetGUID())) { if (bot->GetGroup() && bot->GetGroup()->IsMember(guidP) && botAI->HasRealPlayerMaster()) - botAI->TellMasterNoFacing( - "You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " + - guidP.GetPlayer()->GetName() /*chat->FormatWorldobject(guidP.GetPlayer())*/ + "."); + botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "rpg_item_better_for_player", + "You can use this %item better than me, %player.", + {{"%item", chat->FormatItem(item->GetTemplate())}, {"%player", guidP.GetPlayer()->GetName()}})); else - bot->Say("Можно использовать: " + chat->FormatItem(item->GetTemplate()) + " better than me, " + - player->GetName() /*chat->FormatWorldobject(player)*/ + ".", + bot->Say(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "rpg_item_better_for_player", + "You can use this %item better than me, %player.", + {{"%item", chat->FormatItem(item->GetTemplate())}, {"%player", player->GetName()}}), (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); if (!urand(0, 4) || items.size() < 2) @@ -389,7 +454,10 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/) } } else - bot->Say("Начинаю обмен с" + chat->FormatWorldobject(player), + bot->Say(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "rpg_start_trade_with_player", + "Start trade with %player", + {{"%player", chat->FormatWorldobject(player)}}), (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); botAI->SetNextCheckDelay(sPlayerbotAIConfig.rpgDelay); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.h b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgSubActions.h similarity index 94% rename from modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.h rename to modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgSubActions.h index 8d5fbda4c..31dea19e7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/RpgSubActions.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RPGSUBACTIONS_H -#define _PLAYERBOT_RPGSUBACTIONS_H +#ifndef PLAYERBOTS_RPGSUBACTIONS_H +#define PLAYERBOTS_RPGSUBACTIONS_H #include "Action.h" #include "AiObject.h" @@ -165,6 +166,9 @@ class RpgTrainAction : public RpgSubAction public: RpgTrainAction(PlayerbotAI* botAI, std::string const name = "rpg train") : RpgSubAction(botAI, name) {} + bool isPossible() override; + bool isUseful() override; + private: std::string const ActionName() override; }; diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp index 323ff4b80..b6d5ae69f 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "NewRpgInfo.h" #include @@ -37,17 +43,25 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) data = do_quest; } -void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode) +void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path) { startT = getMSTime(); TravelFlight flight; - flight.fromFlightMaster = fromFlightMaster; - flight.fromNode = fromNode; - flight.toNode = toNode; + flight.flightMasterEntry = flightMasterEntry; + flight.flightMasterPos = flightMasterPos; + flight.path = std::move(path); flight.inFlight = false; data = flight; } +void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId) +{ + startT = getMSTime(); + OutdoorPvP pvp; + pvp.capturePointSpawnId = capturePointSpawnId; + data = pvp; +} + void NewRpgInfo::ChangeToRest() { startT = getMSTime(); @@ -60,7 +74,10 @@ void NewRpgInfo::ChangeToIdle() data = Idle{}; } -bool NewRpgInfo::CanChangeTo(NewRpgStatus status) { return true; } +bool NewRpgInfo::CanChangeTo(NewRpgStatus) +{ + return true; +} void NewRpgInfo::Reset() { @@ -88,6 +105,7 @@ NewRpgStatus NewRpgInfo::GetStatus() if constexpr (std::is_same_v) return RPG_REST; if constexpr (std::is_same_v) return RPG_DO_QUEST; if constexpr (std::is_same_v) return RPG_TRAVEL_FLIGHT; + if constexpr (std::is_same_v) return RPG_OUTDOOR_PVP; return RPG_IDLE; }, data); } @@ -146,11 +164,19 @@ std::string NewRpgInfo::ToString() else if constexpr (std::is_same_v) { out << "TRAVEL_FLIGHT"; - out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); - out << "\nfromNode: " << arg.fromNode; - out << "\ntoNode: " << arg.toNode; + out << "\nflightMasterEntry: " << arg.flightMasterEntry; + out << "\nfromNode: " << arg.path[0]; + out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; } + else if constexpr (std::is_same_v) + { + out << "OUTDOOR_PVP"; + if (!arg.capturePointSpawnId) + out << "\nNo capture point assigned."; + else + out << "\ncapturePointSpawnId: " << arg.capturePointSpawnId; + } else out << "UNKNOWN"; }, data); diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.h b/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.h index 5b6ae3cb9..acfc3bdc6 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_NEWRPGINFO_H -#define _PLAYERBOT_NEWRPGINFO_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEWRPGINFO_H +#define PLAYERBOTS_NEWRPGINFO_H #include "Define.h" #include "ObjectGuid.h" @@ -49,9 +55,9 @@ struct NewRpgInfo // RPG_TRAVEL_FLIGHT struct TravelFlight { - ObjectGuid fromFlightMaster{}; - uint32 fromNode{0}; - uint32 toNode{0}; + uint32 flightMasterEntry{0}; + WorldPosition flightMasterPos{}; + std::vector path; bool inFlight{false}; }; // RPG_REST @@ -59,6 +65,11 @@ struct NewRpgInfo { Rest() = default; }; + // RPG_OUTDOOR_PVP + struct OutdoorPvP + { + ObjectGuid::LowType capturePointSpawnId{0}; + }; struct Idle { }; @@ -80,7 +91,8 @@ struct NewRpgInfo WanderRandom, DoQuest, Rest, - TravelFlight + TravelFlight, + OutdoorPvP >; RpgData data; @@ -91,7 +103,8 @@ struct NewRpgInfo void ChangeToWanderNpc(); void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); - void ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode); + void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector path); + void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0); void ChangeToRest(); void ChangeToIdle(); bool CanChangeTo(NewRpgStatus status); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RpgValues.h b/modules/mod-playerbots/src/Ai/World/Rpg/RpgValues.h similarity index 52% rename from modules/mod-playerbots/src/Ai/Base/Actions/RpgValues.h rename to modules/mod-playerbots/src/Ai/World/Rpg/RpgValues.h index bff78cc84..4c1866e63 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RpgValues.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/RpgValues.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RPGVALUES_H -#define _PLAYERBOT_RPGVALUES_H +#ifndef PLAYERBOTS_RPGVALUES_H +#define PLAYERBOTS_RPGVALUES_H #include "Value.h" diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp index f0d0ce5a9..3658aa899 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NewRpgStrategy.h" @@ -65,6 +66,14 @@ void NewRpgStrategy::InitTriggers(std::vector& triggers) } ) ); + triggers.push_back( + new TriggerNode( + "outdoor pvp status", + { + NextAction("new rpg outdoor pvp", 3.0f) + } + ) + ); } void NewRpgStrategy::InitMultipliers(std::vector&) diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.h b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.h index 47b2a550d..ba88a9c38 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/NewRpgStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NEWRPGSTRATEGY_H -#define _PLAYERBOT_NEWRPGSTRATEGY_H +#ifndef PLAYERBOTS_NEWRPGSTRATEGY_H +#define PLAYERBOTS_NEWRPGSTRATEGY_H #include "Strategy.h" #include "TravelMgr.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RpgStrategy.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp similarity index 93% rename from modules/mod-playerbots/src/Ai/Base/Strategy/RpgStrategy.cpp rename to modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp index 2c0b3544e..39aadbe5c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RpgStrategy.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RpgStrategy.h" diff --git a/modules/mod-playerbots/src/Ai/Base/Strategy/RpgStrategy.h b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.h similarity index 65% rename from modules/mod-playerbots/src/Ai/Base/Strategy/RpgStrategy.h rename to modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.h index cb1db7042..e4acccb5e 100644 --- a/modules/mod-playerbots/src/Ai/Base/Strategy/RpgStrategy.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Strategy/RpgStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RPGSTRATEGY_H -#define _PLAYERBOT_RPGSTRATEGY_H +#ifndef PLAYERBOTS_RPGSTRATEGY_H +#define PLAYERBOTS_RPGSTRATEGY_H #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTrigger.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTrigger.cpp index 65e78eb4c..c88f24da7 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTrigger.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTrigger.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "NewRpgTriggers.h" #include "PlayerbotAI.h" diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTriggers.h b/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTriggers.h index fe2efe3e2..9449a3cb0 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTriggers.h +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Trigger/NewRpgTriggers.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_NEWRPGTRIGGERS_H -#define _PLAYERBOT_NEWRPGTRIGGERS_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_NEWRPGTRIGGERS_H +#define PLAYERBOTS_NEWRPGTRIGGERS_H #include "NewRpgStrategy.h" #include "Trigger.h" diff --git a/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.cpp b/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.cpp index 6025a5627..dd673e23d 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.cpp +++ b/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChatFilter.h" diff --git a/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.h b/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.h index ebb1d9cff..73155a794 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.h +++ b/modules/mod-playerbots/src/Bot/Cmd/ChatFilter.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATFILTER_H -#define _PLAYERBOT_CHATFILTER_H +#ifndef PLAYERBOTS_CHATFILTER_H +#define PLAYERBOTS_CHATFILTER_H #include diff --git a/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp b/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp index 68ac1923a..f2500562a 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp +++ b/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ChatHelper.h" @@ -598,9 +599,9 @@ uint32 ChatHelper::parseSlot(std::string const text) return EQUIPMENT_SLOT_END; } -bool ChatHelper::parseable(std::string const text) +bool ChatHelper::parseableItem(std::string const text) { - return text.find("|H") != std::string::npos || text == "questitem" || text == "ammo" || + return text.find("|Hitem:") != std::string::npos || text == "questitem" || text == "ammo" || substrContainsInMap(text, consumableSubClasses) || substrContainsInMap(text, tradeSubClasses) || substrContainsInMap(text, itemQualities) || substrContainsInMap(text, slots) || substrContainsInMap(text, chats) || diff --git a/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.h b/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.h index 562f23078..7f6501b0d 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.h +++ b/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CHATHELPER_H -#define _PLAYERBOT_CHATHELPER_H +#ifndef PLAYERBOTS_CHATHELPER_H +#define PLAYERBOTS_CHATHELPER_H #include @@ -66,7 +67,7 @@ public: static uint32 parseSlot(std::string const text); uint32 parseSkill(std::string const text); - static bool parseable(std::string const text); + static bool parseableItem(std::string const text); void eraseAllSubStr(std::string& mainStr, std::string const toErase); diff --git a/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.cpp b/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.cpp index 7f3de21a2..40f4fb0c4 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.cpp +++ b/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlayerbotCommandServer.h" @@ -26,7 +27,7 @@ bool ReadLine(socket_ptr sock, std::string* buffer, std::string* line) char buf[1025]; boost::system::error_code error; size_t n = sock->read_some(boost::asio::buffer(buf), error); - if (n == -1 || error == boost::asio::error::eof) + if (n == static_cast(-1) || error == boost::asio::error::eof) return false; else if (error) throw boost::system::system_error(error); // Some other error. diff --git a/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.h b/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.h index eb410c644..5da1ab2f0 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.h +++ b/modules/mod-playerbots/src/Bot/Cmd/PlayerbotCommandServer.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTCOMMANDSERVER_H -#define _PLAYERBOT_PLAYERBOTCOMMANDSERVER_H +#ifndef PLAYERBOTS_PLAYERBOTCOMMANDSERVER_H +#define PLAYERBOTS_PLAYERBOTCOMMANDSERVER_H class PlayerbotCommandServer { diff --git a/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.cpp b/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.cpp index 82eece916..986bfcc7a 100644 --- a/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.cpp +++ b/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.cpp @@ -1,6 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * ike3 - original author + * Sebastiaan Keek (mostlikely4r) */ #include "PerfMonitor.h" diff --git a/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.h b/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.h index 156392ee2..590dc936a 100644 --- a/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.h +++ b/modules/mod-playerbots/src/Bot/Debug/PerfMonitor.h @@ -1,10 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PERFORMANCEMONITOR_H -#define _PLAYERBOT_PERFORMANCEMONITOR_H +/* + * Ported from the CMaNGOS playerbots project (https://github.com/cmangos/playerbots), GPL v2, + * with modifications for AzerothCore. + * Original authors: + * ike3 - original author + * Sebastiaan Keek (mostlikely4r) + */ + +#ifndef PLAYERBOTS_PERFMONITOR_H +#define PLAYERBOTS_PERFMONITOR_H #include #include diff --git a/modules/mod-playerbots/src/Bot/Engine/Action/Action.cpp b/modules/mod-playerbots/src/Bot/Engine/Action/Action.cpp index 464f9dcfd..af3ccf6fa 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Action/Action.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/Action/Action.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Action.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/Action/Action.h b/modules/mod-playerbots/src/Bot/Engine/Action/Action.h index 084a40a55..43495b551 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Action/Action.h +++ b/modules/mod-playerbots/src/Bot/Engine/Action/Action.h @@ -1,9 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#pragma once +#ifndef PLAYERBOTS_ACTION_H +#define PLAYERBOTS_ACTION_H #include "AiObject.h" #include "Event.h" @@ -16,8 +18,7 @@ class NextAction { public: NextAction(std::string const name, float relevance = 0.0f) - : relevance(relevance), name(name) {} // name after relevance - whipowill - NextAction(NextAction const& o) : relevance(o.relevance), name(o.name) {} // name after relevance - whipowill + : relevance(relevance), name(name) {} // name after relevance - whipowill std::string const getName() { return name; } float getRelevance() { return relevance; } @@ -163,3 +164,5 @@ private: Event event; uint32_t created; }; + +#endif diff --git a/modules/mod-playerbots/src/Bot/Engine/AiObject.cpp b/modules/mod-playerbots/src/Bot/Engine/AiObject.cpp index fd57a5d6e..08fc50eeb 100644 --- a/modules/mod-playerbots/src/Bot/Engine/AiObject.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/AiObject.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AiObject.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/AiObject.h b/modules/mod-playerbots/src/Bot/Engine/AiObject.h index c161d4edb..dcf64aa7a 100644 --- a/modules/mod-playerbots/src/Bot/Engine/AiObject.h +++ b/modules/mod-playerbots/src/Bot/Engine/AiObject.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AIOBJECT_H -#define _PLAYERBOT_AIOBJECT_H +#ifndef PLAYERBOTS_AIOBJECT_H +#define PLAYERBOTS_AIOBJECT_H #include "Common.h" #include "PlayerbotAIAware.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.cpp b/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.cpp index 3a2037a08..169ebab7e 100644 --- a/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.cpp @@ -1,60 +1,21 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AiObjectContext.h" - -#include "ActionContext.h" -#include "ChatActionContext.h" -#include "ChatTriggerContext.h" +#include "Helpers.h" #include "DKAiObjectContext.h" #include "DruidAiObjectContext.h" #include "HunterAiObjectContext.h" #include "MageAiObjectContext.h" #include "PaladinAiObjectContext.h" -#include "Playerbots.h" #include "PriestAiObjectContext.h" #include "RogueAiObjectContext.h" #include "ShamanAiObjectContext.h" -#include "SharedValueContext.h" -#include "StrategyContext.h" -#include "TriggerContext.h" -#include "ValueContext.h" #include "WarlockAiObjectContext.h" #include "WarriorAiObjectContext.h" -#include "WorldPacketActionContext.h" -#include "WorldPacketTriggerContext.h" -#include "Ai/Dungeon/DungeonStrategyContext.h" -#include "Ai/Dungeon/WotlkDungeonActionContext.h" -#include "Ai/Dungeon/WotlkDungeonTriggerContext.h" -#include "Ai/Raid/RaidStrategyContext.h" -#include "Ai/Raid/Aq20/RaidAq20ActionContext.h" -#include "Ai/Raid/Aq20/RaidAq20TriggerContext.h" -#include "Ai/Raid/MoltenCore/RaidMcActionContext.h" -#include "Ai/Raid/MoltenCore/RaidMcTriggerContext.h" -#include "Ai/Raid/BlackwingLair/RaidBwlActionContext.h" -#include "Ai/Raid/BlackwingLair/RaidBwlTriggerContext.h" -#include "Ai/Raid/Karazhan/RaidKarazhanActionContext.h" -#include "Ai/Raid/Karazhan/RaidKarazhanTriggerContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonActionContext.h" -#include "Ai/Raid/Magtheridon/RaidMagtheridonTriggerContext.h" -#include "Ai/Raid/GruulsLair/RaidGruulsLairActionContext.h" -#include "Ai/Raid/GruulsLair/RaidGruulsLairTriggerContext.h" -#include "Ai/Raid/SerpentshrineCavern/RaidSSCActionContext.h" -#include "Ai/Raid/SerpentshrineCavern/RaidSSCTriggerContext.h" -#include "Ai/Raid/EyeOfEternity/RaidEoEActionContext.h" -#include "Ai/Raid/EyeOfEternity/RaidEoETriggerContext.h" -#include "Ai/Raid/VaultOfArchavon/RaidVoAActionContext.h" -#include "Ai/Raid/VaultOfArchavon/RaidVoATriggerContext.h" -#include "Ai/Raid/ObsidianSanctum/RaidOsActionContext.h" -#include "Ai/Raid/ObsidianSanctum/RaidOsTriggerContext.h" -#include "Ai/Raid/Ulduar/RaidUlduarActionContext.h" -#include "Ai/Raid/Ulduar/RaidUlduarTriggerContext.h" -#include "Ai/Raid/Onyxia/RaidOnyxiaActionContext.h" -#include "Ai/Raid/Onyxia/RaidOnyxiaTriggerContext.h" -#include "Ai/Raid/Icecrown/RaidIccActionContext.h" -#include "Ai/Raid/Icecrown/RaidIccTriggerContext.h" SharedNamedObjectContextList AiObjectContext::sharedStrategyContexts; SharedNamedObjectContextList AiObjectContext::sharedActionContexts; @@ -96,91 +57,6 @@ void AiObjectContext::BuildSharedContexts() BuildSharedValueContexts(sharedValueContexts); } -void AiObjectContext::BuildSharedStrategyContexts(SharedNamedObjectContextList& strategyContexts) -{ - strategyContexts.Add(new StrategyContext()); - strategyContexts.Add(new MovementStrategyContext()); - strategyContexts.Add(new AssistStrategyContext()); - strategyContexts.Add(new QuestStrategyContext()); - strategyContexts.Add(new DungeonStrategyContext()); - strategyContexts.Add(new RaidStrategyContext()); -} - -void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) -{ - actionContexts.Add(new ActionContext()); - actionContexts.Add(new ChatActionContext()); - actionContexts.Add(new WorldPacketActionContext()); - actionContexts.Add(new RaidAq20ActionContext()); - actionContexts.Add(new RaidMcActionContext()); - actionContexts.Add(new RaidBwlActionContext()); - actionContexts.Add(new RaidKarazhanActionContext()); - actionContexts.Add(new RaidMagtheridonActionContext()); - actionContexts.Add(new RaidGruulsLairActionContext()); - actionContexts.Add(new RaidSSCActionContext()); - actionContexts.Add(new RaidOsActionContext()); - actionContexts.Add(new RaidEoEActionContext()); - actionContexts.Add(new RaidVoAActionContext()); - actionContexts.Add(new RaidUlduarActionContext()); - actionContexts.Add(new RaidOnyxiaActionContext()); - actionContexts.Add(new RaidIccActionContext()); - actionContexts.Add(new WotlkDungeonUKActionContext()); - actionContexts.Add(new WotlkDungeonNexActionContext()); - actionContexts.Add(new WotlkDungeonANActionContext()); - actionContexts.Add(new WotlkDungeonOKActionContext()); - actionContexts.Add(new WotlkDungeonDTKActionContext()); - actionContexts.Add(new WotlkDungeonVHActionContext()); - actionContexts.Add(new WotlkDungeonGDActionContext()); - actionContexts.Add(new WotlkDungeonHoSActionContext()); - actionContexts.Add(new WotlkDungeonHoLActionContext()); - actionContexts.Add(new WotlkDungeonOccActionContext()); - actionContexts.Add(new WotlkDungeonUPActionContext()); - actionContexts.Add(new WotlkDungeonCoSActionContext()); - actionContexts.Add(new WotlkDungeonFoSActionContext()); - actionContexts.Add(new WotlkDungeonPoSActionContext()); - actionContexts.Add(new WotlkDungeonToCActionContext()); -} - -void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList& triggerContexts) -{ - triggerContexts.Add(new TriggerContext()); - triggerContexts.Add(new ChatTriggerContext()); - triggerContexts.Add(new WorldPacketTriggerContext()); - triggerContexts.Add(new RaidAq20TriggerContext()); - triggerContexts.Add(new RaidMcTriggerContext()); - triggerContexts.Add(new RaidBwlTriggerContext()); - triggerContexts.Add(new RaidKarazhanTriggerContext()); - triggerContexts.Add(new RaidMagtheridonTriggerContext()); - triggerContexts.Add(new RaidGruulsLairTriggerContext()); - triggerContexts.Add(new RaidSSCTriggerContext()); - triggerContexts.Add(new RaidOsTriggerContext()); - triggerContexts.Add(new RaidEoETriggerContext()); - triggerContexts.Add(new RaidVoATriggerContext()); - triggerContexts.Add(new RaidUlduarTriggerContext()); - triggerContexts.Add(new RaidOnyxiaTriggerContext()); - triggerContexts.Add(new RaidIccTriggerContext()); - triggerContexts.Add(new WotlkDungeonUKTriggerContext()); - triggerContexts.Add(new WotlkDungeonNexTriggerContext()); - triggerContexts.Add(new WotlkDungeonANTriggerContext()); - triggerContexts.Add(new WotlkDungeonOKTriggerContext()); - triggerContexts.Add(new WotlkDungeonDTKTriggerContext()); - triggerContexts.Add(new WotlkDungeonVHTriggerContext()); - triggerContexts.Add(new WotlkDungeonGDTriggerContext()); - triggerContexts.Add(new WotlkDungeonHoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonHoLTriggerContext()); - triggerContexts.Add(new WotlkDungeonOccTriggerContext()); - triggerContexts.Add(new WotlkDungeonUPTriggerContext()); - triggerContexts.Add(new WotlkDungeonCoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonFoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonPoSTriggerContext()); - triggerContexts.Add(new WotlkDungeonToCTriggerContext()); -} - -void AiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) -{ - valueContexts.Add(new ValueContext()); -} - std::vector AiObjectContext::Save() { std::vector result; diff --git a/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.h b/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.h index 3ffd1850f..15510f3c8 100644 --- a/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.h +++ b/modules/mod-playerbots/src/Bot/Engine/AiObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AIOBJECTCONTEXT_H -#define _PLAYERBOT_AIOBJECTCONTEXT_H +#ifndef PLAYERBOTS_AIOBJECTCONTEXT_H +#define PLAYERBOTS_AIOBJECTCONTEXT_H #include #include diff --git a/modules/mod-playerbots/src/Bot/Engine/BuildSharedActionContexts.cpp b/modules/mod-playerbots/src/Bot/Engine/BuildSharedActionContexts.cpp new file mode 100644 index 000000000..7cf47b369 --- /dev/null +++ b/modules/mod-playerbots/src/Bot/Engine/BuildSharedActionContexts.cpp @@ -0,0 +1,73 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AiObjectContext.h" +#include "ActionContext.h" +#include "ChatActionContext.h" +#include "WorldPacketActionContext.h" +#include "Aq20ActionContext.h" +#include "MCActionContext.h" +#include "BWLActionContext.h" +#include "KaraActionContext.h" +#include "GruulActionContext.h" +#include "NaxxActionContext.h" +#include "MagActionContext.h" +#include "SSCActionContext.h" +#include "TKActionContext.h" +#include "HyjalActionContext.h" +#include "BTActionContext.h" +#include "ZAActionContext.h" +#include "OSActionContext.h" +#include "EoEActionContext.h" +#include "VoAActionContext.h" +#include "UldActionContext.h" +#include "OnyActionContext.h" +#include "ICCActionContext.h" +#include "RSActionContext.h" +#include "Ai/Dungeon/TbcDungeonActionContext.h" +#include "Ai/Dungeon/WotlkDungeonActionContext.h" + +void AiObjectContext::BuildSharedActionContexts(SharedNamedObjectContextList& actionContexts) +{ + actionContexts.Add(new ActionContext()); + actionContexts.Add(new ChatActionContext()); + actionContexts.Add(new WorldPacketActionContext()); + actionContexts.Add(new RaidAq20ActionContext()); + actionContexts.Add(new RaidMcActionContext()); + actionContexts.Add(new RaidBwlActionContext()); + actionContexts.Add(new RaidKarazhanActionContext()); + actionContexts.Add(new RaidGruulsLairActionContext()); + actionContexts.Add(new RaidMagtheridonActionContext()); + actionContexts.Add(new RaidSSCActionContext()); + actionContexts.Add(new RaidTempestKeepActionContext()); + actionContexts.Add(new RaidHyjalSummitActionContext()); + actionContexts.Add(new RaidBlackTempleActionContext()); + actionContexts.Add(new RaidZulAmanActionContext()); + actionContexts.Add(new RaidNaxxActionContext()); + actionContexts.Add(new RaidOsActionContext()); + actionContexts.Add(new RaidEoEActionContext()); + actionContexts.Add(new RaidVoAActionContext()); + actionContexts.Add(new RaidUlduarActionContext()); + actionContexts.Add(new RaidOnyxiaActionContext()); + actionContexts.Add(new RaidIccActionContext()); + actionContexts.Add(new RaidRsActionContext()); + actionContexts.Add(new TbcDungeonAuchenaiCryptsActionContext()); + actionContexts.Add(new WotlkDungeonUKActionContext()); + actionContexts.Add(new WotlkDungeonNexActionContext()); + actionContexts.Add(new WotlkDungeonANActionContext()); + actionContexts.Add(new WotlkDungeonOKActionContext()); + actionContexts.Add(new WotlkDungeonDTKActionContext()); + actionContexts.Add(new WotlkDungeonVHActionContext()); + actionContexts.Add(new WotlkDungeonGDActionContext()); + actionContexts.Add(new WotlkDungeonHoSActionContext()); + actionContexts.Add(new WotlkDungeonHoLActionContext()); + actionContexts.Add(new WotlkDungeonOccActionContext()); + actionContexts.Add(new WotlkDungeonUPActionContext()); + actionContexts.Add(new WotlkDungeonCoSActionContext()); + actionContexts.Add(new WotlkDungeonFoSActionContext()); + actionContexts.Add(new WotlkDungeonPoSActionContext()); + actionContexts.Add(new WotlkDungeonToCActionContext()); +} diff --git a/modules/mod-playerbots/src/Bot/Engine/BuildSharedStrategyContexts.cpp b/modules/mod-playerbots/src/Bot/Engine/BuildSharedStrategyContexts.cpp new file mode 100644 index 000000000..f89877360 --- /dev/null +++ b/modules/mod-playerbots/src/Bot/Engine/BuildSharedStrategyContexts.cpp @@ -0,0 +1,20 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AiObjectContext.h" +#include "StrategyContext.h" +#include "Ai/Dungeon/DungeonStrategyContext.h" +#include "Ai/Raid/RaidStrategyContext.h" + +void AiObjectContext::BuildSharedStrategyContexts(SharedNamedObjectContextList& strategyContexts) +{ + strategyContexts.Add(new StrategyContext()); + strategyContexts.Add(new MovementStrategyContext()); + strategyContexts.Add(new AssistStrategyContext()); + strategyContexts.Add(new QuestStrategyContext()); + strategyContexts.Add(new DungeonStrategyContext()); + strategyContexts.Add(new RaidStrategyContext()); +} diff --git a/modules/mod-playerbots/src/Bot/Engine/BuildSharedTriggerContexts.cpp b/modules/mod-playerbots/src/Bot/Engine/BuildSharedTriggerContexts.cpp new file mode 100644 index 000000000..a89ce02c9 --- /dev/null +++ b/modules/mod-playerbots/src/Bot/Engine/BuildSharedTriggerContexts.cpp @@ -0,0 +1,73 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AiObjectContext.h" +#include "TriggerContext.h" +#include "ChatTriggerContext.h" +#include "WorldPacketTriggerContext.h" +#include "Aq20TriggerContext.h" +#include "MCTriggerContext.h" +#include "BWLTriggerContext.h" +#include "KaraTriggerContext.h" +#include "GruulTriggerContext.h" +#include "MagTriggerContext.h" +#include "NaxxTriggerContext.h" +#include "SSCTriggerContext.h" +#include "TKTriggerContext.h" +#include "HyjalTriggerContext.h" +#include "BTTriggerContext.h" +#include "ZATriggerContext.h" +#include "OSTriggerContext.h" +#include "EoETriggerContext.h" +#include "VoATriggerContext.h" +#include "UldTriggerContext.h" +#include "OnyTriggerContext.h" +#include "ICCTriggerContext.h" +#include "RSTriggerContext.h" +#include "Ai/Dungeon/TbcDungeonTriggerContext.h" +#include "Ai/Dungeon/WotlkDungeonTriggerContext.h" + +void AiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextList& triggerContexts) +{ + triggerContexts.Add(new TriggerContext()); + triggerContexts.Add(new ChatTriggerContext()); + triggerContexts.Add(new WorldPacketTriggerContext()); + triggerContexts.Add(new RaidAq20TriggerContext()); + triggerContexts.Add(new RaidMcTriggerContext()); + triggerContexts.Add(new RaidBwlTriggerContext()); + triggerContexts.Add(new RaidKarazhanTriggerContext()); + triggerContexts.Add(new RaidGruulsLairTriggerContext()); + triggerContexts.Add(new RaidMagtheridonTriggerContext()); + triggerContexts.Add(new RaidNaxxTriggerContext()); + triggerContexts.Add(new RaidSSCTriggerContext()); + triggerContexts.Add(new RaidTempestKeepTriggerContext()); + triggerContexts.Add(new RaidHyjalSummitTriggerContext()); + triggerContexts.Add(new RaidBlackTempleTriggerContext()); + triggerContexts.Add(new RaidZulAmanTriggerContext()); + triggerContexts.Add(new RaidOsTriggerContext()); + triggerContexts.Add(new RaidEoETriggerContext()); + triggerContexts.Add(new RaidVoATriggerContext()); + triggerContexts.Add(new RaidUlduarTriggerContext()); + triggerContexts.Add(new RaidOnyxiaTriggerContext()); + triggerContexts.Add(new RaidIccTriggerContext()); + triggerContexts.Add(new RaidRsTriggerContext()); + triggerContexts.Add(new TbcDungeonAuchenaiCryptsTriggerContext()); + triggerContexts.Add(new WotlkDungeonUKTriggerContext()); + triggerContexts.Add(new WotlkDungeonNexTriggerContext()); + triggerContexts.Add(new WotlkDungeonANTriggerContext()); + triggerContexts.Add(new WotlkDungeonOKTriggerContext()); + triggerContexts.Add(new WotlkDungeonDTKTriggerContext()); + triggerContexts.Add(new WotlkDungeonVHTriggerContext()); + triggerContexts.Add(new WotlkDungeonGDTriggerContext()); + triggerContexts.Add(new WotlkDungeonHoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonHoLTriggerContext()); + triggerContexts.Add(new WotlkDungeonOccTriggerContext()); + triggerContexts.Add(new WotlkDungeonUPTriggerContext()); + triggerContexts.Add(new WotlkDungeonCoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonFoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonPoSTriggerContext()); + triggerContexts.Add(new WotlkDungeonToCTriggerContext()); +} diff --git a/modules/mod-playerbots/src/Bot/Engine/BuildSharedValueContexts.cpp b/modules/mod-playerbots/src/Bot/Engine/BuildSharedValueContexts.cpp new file mode 100644 index 000000000..1dae1e93f --- /dev/null +++ b/modules/mod-playerbots/src/Bot/Engine/BuildSharedValueContexts.cpp @@ -0,0 +1,13 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "AiObjectContext.h" +#include "ValueContext.h" + +void AiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList& valueContexts) +{ + valueContexts.Add(new ValueContext()); +} diff --git a/modules/mod-playerbots/src/Bot/Engine/Engine.cpp b/modules/mod-playerbots/src/Bot/Engine/Engine.cpp index bb4f2eb35..07be365fe 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Engine.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/Engine.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Engine.h" @@ -118,10 +119,12 @@ void Engine::Init() { Reset(); + hasTargetExclusions = false; for (std::map::iterator i = strategies.begin(); i != strategies.end(); i++) { Strategy* strategy = i->second; strategyTypeMask |= strategy->GetType(); + hasTargetExclusions |= strategy->HasTargetExclusions(); strategy->InitMultipliers(multipliers); strategy->InitTriggers(triggers); for (auto &iter : strategy->actionNodeFactories.creators) @@ -138,7 +141,7 @@ void Engine::Init() } } -bool Engine::DoNextAction(Unit* unit, uint32 depth, bool minimal) +bool Engine::DoNextAction(Unit* /*unit*/, uint32 /*depth*/, bool minimal) { LogAction("--- AI Tick ---"); @@ -428,6 +431,12 @@ void Engine::toggleStrategy(std::string const name) bool Engine::HasStrategy(std::string const name) { return strategies.find(name) != strategies.end(); } +Strategy* Engine::GetStrategy(std::string const name) +{ + std::map::iterator i = strategies.find(name); + return i != strategies.end() ? i->second : nullptr; +} + void Engine::ProcessTriggers(bool minimal) { std::unordered_map fires; diff --git a/modules/mod-playerbots/src/Bot/Engine/Engine.h b/modules/mod-playerbots/src/Bot/Engine/Engine.h index 8a7c34189..b0b6a76b1 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Engine.h +++ b/modules/mod-playerbots/src/Bot/Engine/Engine.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_ENGINE_H -#define _PLAYERBOT_ENGINE_H +#ifndef PLAYERBOTS_ENGINE_H +#define PLAYERBOTS_ENGINE_H #include @@ -70,6 +71,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(); @@ -85,6 +87,7 @@ public: void removeActionExecutionListener(ActionExecutionListener* listener) { actionExecutionListeners.Remove(listener); } bool HasStrategyType(StrategyType type) { return strategyTypeMask & type; } + bool HasTargetExclusions() const { return hasTargetExclusions; } virtual ~Engine(void); bool testMode; @@ -114,6 +117,7 @@ protected: float lastRelevance; std::string lastAction; uint32 strategyTypeMask; + bool hasTargetExclusions = false; NamedObjectFactoryList actionNodeFactories; }; diff --git a/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.cpp b/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.cpp index 2f42eee92..496885471 100644 --- a/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ExternalEventHelper.h" @@ -30,11 +31,14 @@ bool ExternalEventHelper::ParseChatCommand(std::string const command, Player* ow return true; } - if (!ChatHelper::parseable(command)) + if (!ChatHelper::parseableItem(command)) return false; - HandleCommand("c", command, owner); - HandleCommand("t", command, owner); + if (sPlayerbotAIConfig.enableAutoTradeOnItemMention) + { + HandleCommand("c", command, owner); + HandleCommand("t", command, owner); + } return true; } diff --git a/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.h b/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.h index 7e95c556d..8b4d6bb30 100644 --- a/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.h +++ b/modules/mod-playerbots/src/Bot/Engine/ExternalEventHelper.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EXTERNALEVENTHELPER_H -#define _PLAYERBOT_EXTERNALEVENTHELPER_H +#ifndef PLAYERBOTS_EXTERNALEVENTHELPER_H +#define PLAYERBOTS_EXTERNALEVENTHELPER_H #include diff --git a/modules/mod-playerbots/src/Bot/Engine/Multiplier.h b/modules/mod-playerbots/src/Bot/Engine/Multiplier.h index 5a8bba68b..5643ecb95 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Multiplier.h +++ b/modules/mod-playerbots/src/Bot/Engine/Multiplier.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_MULTIPLIER_H -#define _PLAYERBOT_MULTIPLIER_H +#ifndef PLAYERBOTS_MULTIPLIER_H +#define PLAYERBOTS_MULTIPLIER_H #include "AiObject.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.cpp b/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.cpp index fcb85261d..83a9a82f6 100644 --- a/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "NamedObjectContext.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.h b/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.h index b60232a5c..94d821052 100644 --- a/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.h +++ b/modules/mod-playerbots/src/Bot/Engine/NamedObjectContext.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_NAMEDOBJECTCONEXT_H -#define _PLAYERBOT_NAMEDOBJECTCONEXT_H +#ifndef PLAYERBOTS_NAMEDOBJECTCONTEXT_H +#define PLAYERBOTS_NAMEDOBJECTCONTEXT_H #include #include diff --git a/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.cpp b/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.cpp index 0b95e01a5..e154a7fb8 100644 --- a/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PassiveMultiplier.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.h b/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.h index b407f9337..ff31a22f9 100644 --- a/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.h +++ b/modules/mod-playerbots/src/Bot/Engine/PassiveMultiplier.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PASSIVEMULTIPLIER_H -#define _PLAYERBOT_PASSIVEMULTIPLIER_H +#ifndef PLAYERBOTS_PASSIVEMULTIPLIER_H +#define PLAYERBOTS_PASSIVEMULTIPLIER_H #include diff --git a/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIAware.h b/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIAware.h index 8e1fda1b4..648859508 100644 --- a/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIAware.h +++ b/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIAware.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERbotAIAWARE_H -#define _PLAYERBOT_PLAYERbotAIAWARE_H +#ifndef PLAYERBOTS_PLAYERBOTAIAWARE_H +#define PLAYERBOTS_PLAYERBOTAIAWARE_H class PlayerbotAI; diff --git a/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.cpp b/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.cpp index cf4ad172c..7f9167b1a 100644 --- a/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlayerbotAIBase.h" @@ -25,7 +26,7 @@ void PlayerbotAIBase::UpdateAI(uint32 elapsed, bool minimal) return; UpdateAIInternal(elapsed, minimal); - YieldThread(); + YieldThread(nullptr); } void PlayerbotAIBase::SetNextCheckDelay(uint32 const delay) @@ -49,10 +50,14 @@ void PlayerbotAIBase::IncreaseNextCheckDelay(uint32 delay) bool PlayerbotAIBase::CanUpdateAI() { return nextAICheckDelay == 0; } -void PlayerbotAIBase::YieldThread(uint32 delay) +void PlayerbotAIBase::YieldThread(Player* bot, uint32 delay) { if (nextAICheckDelay < delay) - nextAICheckDelay = delay; + { + // Adding a deterministic per-bot slight offset (0–200 ms) to stagger updates and prevent cpu spikes. + uint32 offset = bot ? (bot->GetGUID().GetCounter() % 201) : 0; + nextAICheckDelay = delay + offset; + } } bool PlayerbotAIBase::IsActive() { return nextAICheckDelay < sPlayerbotAIConfig.maxWaitForMove; } diff --git a/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.h b/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.h index d0e0b775b..6d72a7c2d 100644 --- a/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.h +++ b/modules/mod-playerbots/src/Bot/Engine/PlayerbotAIBase.h @@ -1,13 +1,15 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTAIBASE_H -#define _PLAYERBOT_PLAYERBOTAIBASE_H +#ifndef PLAYERBOTS_PLAYERBOTAIBASE_H +#define PLAYERBOTS_PLAYERBOTAIBASE_H #include "Define.h" #include "PlayerbotAIConfig.h" +#include "Player.h" class PlayerbotAIBase { @@ -17,7 +19,7 @@ public: bool CanUpdateAI(); void SetNextCheckDelay(uint32 const delay); void IncreaseNextCheckDelay(uint32 delay); - void YieldThread(uint32 delay = sPlayerbotAIConfig.reactDelay); + void YieldThread(Player* bot, uint32 delay = sPlayerbotAIConfig.reactDelay); virtual void UpdateAI(uint32 elapsed, bool minimal = false); virtual void UpdateAIInternal(uint32 elapsed, bool minimal = false) = 0; bool IsActive(); diff --git a/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.cpp b/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.cpp index 65bf411d9..2589067d1 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "CustomStrategy.h" @@ -35,7 +36,7 @@ std::vector toNextActionArray(const std::string actions) const std::vector tokens = split(actions, ','); std::vector res = {}; - for (const std::string token : tokens) + for (const std::string& token : tokens) res.push_back(toNextAction(token)); return res; diff --git a/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.h b/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.h index 9a4007952..5721f561c 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.h +++ b/modules/mod-playerbots/src/Bot/Engine/Strategy/CustomStrategy.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_CUSTOMSTRATEGY_H -#define _PLAYERBOT_CUSTOMSTRATEGY_H +#ifndef PLAYERBOTS_CUSTOMSTRATEGY_H +#define PLAYERBOTS_CUSTOMSTRATEGY_H #include diff --git a/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.cpp b/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.cpp index d6193e11e..bbd20d6a2 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Strategy.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.h b/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.h index 8f57bc078..a488deae9 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.h +++ b/modules/mod-playerbots/src/Bot/Engine/Strategy/Strategy.h @@ -1,17 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STRATEGY_H -#define _PLAYERBOT_STRATEGY_H +#ifndef PLAYERBOTS_STRATEGY_H +#define PLAYERBOTS_STRATEGY_H #include "Action.h" #include "Multiplier.h" #include "NamedObjectContext.h" +#include "ObjectGuid.h" #include "PlayerbotAIAware.h" #include "Trigger.h" +enum class TargetValueExclusionType : uint8 +{ + None = 0, + Tank, + Dps, + Attacker +}; + enum StrategyType : uint32 { STRATEGY_TYPE_GENERIC = 0, @@ -63,6 +73,9 @@ public: virtual std::vector getDefaultActions() { return {}; } virtual void InitTriggers([[maybe_unused]] std::vector& triggers) {} virtual void InitMultipliers([[maybe_unused]] std::vector& multipliers) {} + virtual void AppendTargetExclusions([[maybe_unused]] GuidSet& exclusions, + [[maybe_unused]] TargetValueExclusionType type) {} + virtual bool HasTargetExclusions() const { return false; } virtual std::string const getName() = 0; virtual uint32 GetType() const { return STRATEGY_TYPE_GENERIC; } virtual ActionNode* GetAction(std::string const name); diff --git a/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.cpp b/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.cpp index bfd8eeaed..c6e5e5f20 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Trigger.h" @@ -36,7 +37,7 @@ bool Trigger::needCheck(uint32 now) if (checkInterval < 2) return true; - if (!lastCheckTime || now - lastCheckTime >= checkInterval) + if (!lastCheckTime || now - lastCheckTime >= uint32(checkInterval)) { lastCheckTime = now; return true; diff --git a/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.h b/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.h index 8878530f8..43fa8e502 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.h +++ b/modules/mod-playerbots/src/Bot/Engine/Trigger/Trigger.h @@ -1,9 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#pragma once +#ifndef PLAYERBOTS_TRIGGER_H +#define PLAYERBOTS_TRIGGER_H #include "Action.h" @@ -81,3 +83,5 @@ private: std::vector handlers; const std::string name; }; + +#endif diff --git a/modules/mod-playerbots/src/Bot/Engine/Value/Value.cpp b/modules/mod-playerbots/src/Bot/Engine/Value/Value.cpp index a9235a8e9..fbfc9614a 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Value/Value.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/Value/Value.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Value.h" @@ -58,7 +59,7 @@ std::string const CDPairCalculatedValue::Format() CreatureData const* creatureData = Calculate(); if (creatureData) { - CreatureTemplate const* bmTemplate = sObjectMgr->GetCreatureTemplate(creatureData->id1); + CreatureTemplate const* bmTemplate = sObjectMgr->GetCreatureTemplate(creatureData->id); return bmTemplate ? bmTemplate->Name : ""; } @@ -78,7 +79,7 @@ std::string const CDPairListCalculatedValue::Format() std::vector cdPairs = Calculate(); for (CreatureData const* cdPair : cdPairs) { - out << cdPair->id1 << ","; + out << cdPair->id << ","; } out << "}"; diff --git a/modules/mod-playerbots/src/Bot/Engine/Value/Value.h b/modules/mod-playerbots/src/Bot/Engine/Value/Value.h index fe84bb4d7..e49d4fe98 100644 --- a/modules/mod-playerbots/src/Bot/Engine/Value/Value.h +++ b/modules/mod-playerbots/src/Bot/Engine/Value/Value.h @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_VALUE_H -#define _PLAYERBOT_VALUE_H +#ifndef PLAYERBOTS_VALUE_H +#define PLAYERBOTS_VALUE_H #include +#include #include "AiObject.h" #include "ObjectGuid.h" @@ -416,4 +418,17 @@ private: std::list data = {}; }; +class MissingBuffReagentNoticeValue : public ManualSetValue&> +{ +public: + MissingBuffReagentNoticeValue( + PlayerbotAI* botAI, std::string const name = "missing buff reagent notice") + : ManualSetValue&>(botAI, noticeTimes, name) + { + } + +private: + std::unordered_map noticeTimes; +}; + #endif diff --git a/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.cpp b/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.cpp index 5bee3a322..486a1eb02 100644 --- a/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.cpp +++ b/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Event.h" diff --git a/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.h b/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.h index 6caf4e92f..313901f0c 100644 --- a/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.h +++ b/modules/mod-playerbots/src/Bot/Engine/WorldPacket/Event.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_EVENT_H -#define _PLAYERBOT_EVENT_H +#ifndef PLAYERBOTS_EVENT_H +#define PLAYERBOTS_EVENT_H #include "WorldPacket.h" diff --git a/modules/mod-playerbots/src/Bot/Factory/AiFactory.cpp b/modules/mod-playerbots/src/Bot/Factory/AiFactory.cpp index 84b3a7dc5..c895385c3 100644 --- a/modules/mod-playerbots/src/Bot/Factory/AiFactory.cpp +++ b/modules/mod-playerbots/src/Bot/Factory/AiFactory.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "AiFactory.h" @@ -26,6 +27,15 @@ #include "WarlockAiObjectContext.h" #include "WarriorAiObjectContext.h" +namespace +{ +constexpr uint32 SPELL_FROSTFIRE_BOLT = 44614; +constexpr uint32 SPELL_ICE_SHARDS = 15047; +constexpr uint32 SPELL_WHIRLWIND = 1680; +constexpr uint32 SPELL_CAT_FORM = 768; +constexpr uint32 SPELL_DRUID_THICK_HIDE = 16931; +} + AiObjectContext* AiFactory::createAiObjectContext(Player* player, PlayerbotAI* botAI) { switch (player->getClass()) @@ -91,9 +101,6 @@ uint8 AiFactory::GetPlayerSpecTab(Player* bot) case CLASS_WARLOCK: tab = WARLOCK_TAB_DEMONOLOGY; break; - case CLASS_SHAMAN: - tab = SHAMAN_TAB_ELEMENTAL; - break; } return tab; @@ -292,33 +299,33 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa if (tab == PRIEST_TAB_SHADOW) engine->addStrategiesNoInit("dps", "shadow debuff", "shadow aoe", nullptr); else if (tab == PRIEST_TAB_DISCIPLINE) - engine->addStrategiesNoInit("heal", nullptr); - else - engine->addStrategiesNoInit("holy heal", nullptr); + engine->addStrategy("heal", false); + else // if (tab == PRIEST_TAB_HOLY) + engine->addStrategy("holy heal", false); engine->addStrategiesNoInit("dps assist", "cure", nullptr); break; case CLASS_MAGE: if (tab == MAGE_TAB_ARCANE) - engine->addStrategiesNoInit("arcane", nullptr); + engine->addStrategiesNoInit("arcane", "bdps", nullptr); else if (tab == MAGE_TAB_FIRE) { - if (player->HasSpell(44614) /*Frostfire Bolt*/ && player->HasAura(15047) /*Ice Shards*/) - engine->addStrategiesNoInit("frostfire", nullptr); + if (player->HasSpell(SPELL_FROSTFIRE_BOLT) && player->HasAura(SPELL_ICE_SHARDS)) + engine->addStrategiesNoInit("frostfire", "bdps", nullptr); else - engine->addStrategiesNoInit("fire", nullptr); + engine->addStrategiesNoInit("fire", "bdps", nullptr); } - else - engine->addStrategiesNoInit("frost", nullptr); + else // if (tab == MAGE_TAB_FROST) + engine->addStrategiesNoInit("frost", "bmana", nullptr); - engine->addStrategiesNoInit("dps", "dps assist", "cure", "aoe", nullptr); + engine->addStrategiesNoInit("dps", "dps assist", "cure", "cc", "aoe", nullptr); break; case CLASS_WARRIOR: if (tab == WARRIOR_TAB_PROTECTION) - engine->addStrategiesNoInit("tank", "tank assist", "aoe", nullptr); - else if (tab == WARRIOR_TAB_ARMS || !player->HasSpell(1680)) // Whirlwind + engine->addStrategiesNoInit("tank", "tank assist", "pull", "pull back", "aoe", nullptr); + else if (tab == WARRIOR_TAB_ARMS || !player->HasSpell(SPELL_WHIRLWIND)) engine->addStrategiesNoInit("arms", "aoe", "dps assist", nullptr); - else + else // if (tab == WARRIOR_TAB_FURY) engine->addStrategiesNoInit("fury", "aoe", "dps assist", nullptr); break; case CLASS_SHAMAN: @@ -326,49 +333,48 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa engine->addStrategiesNoInit("ele", "stoneskin", "wrath", "mana spring", "wrath of air", nullptr); else if (tab == SHAMAN_TAB_RESTORATION) engine->addStrategiesNoInit("resto", "stoneskin", "flametongue", "mana spring", "wrath of air", nullptr); - else + else // if (tab == SHAMAN_TAB_ENHANCEMENT) engine->addStrategiesNoInit("enh", "strength of earth", "magma", "healing stream", "windfury", nullptr); engine->addStrategiesNoInit("dps assist", "cure", "aoe", nullptr); 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 + else // if (tab == PALADIN_TAB_RETRIBUTION) engine->addStrategiesNoInit("dps", "dps assist", "cure", "baoe", nullptr); break; case CLASS_DRUID: if (tab == DRUID_TAB_BALANCE) { - engine->addStrategiesNoInit("caster", "cure", "caster aoe", "dps assist", nullptr); - engine->addStrategy("caster debuff", false); + engine->addStrategiesNoInit("balance", "cure", "aoe", "cc", "dps assist", nullptr); } else if (tab == DRUID_TAB_RESTORATION) - engine->addStrategiesNoInit("heal", "cure", "dps assist", nullptr); + engine->addStrategiesNoInit("resto", "cure", "dps assist", "tranquility", nullptr); else { - if (player->HasSpell(768) /*cat form*/ && !player->HasAura(16931) /*thick hide*/) - engine->addStrategiesNoInit("cat", "dps assist", nullptr); + if (player->HasSpell(SPELL_CAT_FORM) && !player->HasAura(SPELL_DRUID_THICK_HIDE)) + engine->addStrategiesNoInit("cat", "aoe", "cc", "dps assist", "feral charge", nullptr); else - engine->addStrategiesNoInit("bear", "tank assist", nullptr); + engine->addStrategiesNoInit("bear", "tank assist", "pull", "pull back", "feral charge", nullptr); } break; case CLASS_HUNTER: if (tab == HUNTER_TAB_BEAST_MASTERY) - engine->addStrategiesNoInit("bm", nullptr); + engine->addStrategy("bm", false); else if (tab == HUNTER_TAB_MARKSMANSHIP) - engine->addStrategiesNoInit("mm", nullptr); - else - engine->addStrategiesNoInit("surv", nullptr); + engine->addStrategy("mm", false); + else // if (tab == HUNTER_TAB_SURVIVAL) + engine->addStrategy("surv", false); - engine->addStrategiesNoInit("cc", "dps assist", "aoe", nullptr); + engine->addStrategiesNoInit("cc", "dps assist", "aoe", "bdps", nullptr); break; case CLASS_ROGUE: if (tab == ROGUE_TAB_ASSASSINATION || tab == ROGUE_TAB_SUBTLETY) engine->addStrategiesNoInit("melee", "dps assist", "aoe", nullptr); - else + else // if (tab == ROGUE_TAB_COMBAT) engine->addStrategiesNoInit("dps", "dps assist", "aoe", nullptr); break; case CLASS_WARLOCK: @@ -376,17 +382,17 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa engine->addStrategiesNoInit("affli", "curse of agony", nullptr); else if (tab == WARLOCK_TAB_DEMONOLOGY) engine->addStrategiesNoInit("demo", "curse of agony", "meta melee", nullptr); - else + else // if (tab == WARLOCK_TAB_DESTRUCTION) engine->addStrategiesNoInit("destro", "curse of elements", nullptr); engine->addStrategiesNoInit("cc", "dps assist", "aoe", nullptr); 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 + else // if (tab == DEATH_KNIGHT_TAB_UNHOLY) engine->addStrategiesNoInit("unholy", "unholy aoe", "dps assist", nullptr); break; } @@ -426,15 +432,14 @@ void AiFactory::AddDefaultCombatStrategies(Player* player, PlayerbotAI* const fa { if (tab == DRUID_TAB_RESTORATION) { - engine->addStrategiesNoInit("caster", "caster aoe", nullptr); - engine->addStrategy("caster debuff", false); + engine->addStrategiesNoInit("aoe", nullptr); } break; } case CLASS_SHAMAN: { if (tab == SHAMAN_TAB_RESTORATION) - engine->addStrategiesNoInit("caster", "caster aoe", "bmana", nullptr); + engine->addStrategiesNoInit("caster", "caster aoe", nullptr); break; } case CLASS_PALADIN: @@ -505,21 +510,21 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const switch (player->getClass()) { case CLASS_PRIEST: - nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr); + nonCombatEngine->addStrategiesNoInit("dps assist", "cure", "rshadow", nullptr); break; 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); + nonCombatEngine->addStrategy("bsanc", false); else - nonCombatEngine->addStrategy("bdps", false); + nonCombatEngine->addStrategy("bmight", false); } else if (tab == PALADIN_TAB_HOLY) - nonCombatEngine->addStrategiesNoInit("dps assist", "bmana", "bcast", nullptr); + nonCombatEngine->addStrategiesNoInit("dps assist", "bwisdom", "bcast", nullptr); else - nonCombatEngine->addStrategiesNoInit("dps assist", "bdps", "baoe", nullptr); + nonCombatEngine->addStrategiesNoInit("dps assist", "bmight", "baoe", nullptr); nonCombatEngine->addStrategiesNoInit("cure", nullptr); break; @@ -527,11 +532,6 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const nonCombatEngine->addStrategiesNoInit("bdps", "dps assist", "pet", nullptr); break; case CLASS_SHAMAN: - if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION) - nonCombatEngine->addStrategy("bmana", false); - else - nonCombatEngine->addStrategy("bdps", false); - nonCombatEngine->addStrategiesNoInit("dps assist", "cure", nullptr); break; case CLASS_MAGE: @@ -545,17 +545,17 @@ void AiFactory::AddDefaultNonCombatStrategies(Player* player, PlayerbotAI* const case CLASS_DRUID: if (tab == DRUID_TAB_FERAL) { - if (player->GetLevel() >= 20 && !player->HasAura(16931) /*thick hide*/) + if (player->GetLevel() >= 20 && !player->HasAura(SPELL_DRUID_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/modules/mod-playerbots/src/Bot/Factory/AiFactory.h b/modules/mod-playerbots/src/Bot/Factory/AiFactory.h index b853bf670..1d6461b9d 100644 --- a/modules/mod-playerbots/src/Bot/Factory/AiFactory.h +++ b/modules/mod-playerbots/src/Bot/Factory/AiFactory.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_AIFACTORY_H -#define _PLAYERBOT_AIFACTORY_H +#ifndef PLAYERBOTS_AIFACTORY_H +#define PLAYERBOTS_AIFACTORY_H #include #include "Common.h" diff --git a/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.cpp b/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.cpp index f50260577..2bccbccc4 100644 --- a/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.cpp +++ b/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.cpp @@ -5,6 +5,7 @@ #include "PlayerbotFactory.h" +#include #include #include "AccountMgr.h" @@ -47,16 +48,326 @@ 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; std::vector PlayerbotFactory::enchantSpellIdCache; std::vector PlayerbotFactory::enchantGemIdCache; std::unordered_map> PlayerbotFactory::trainerIdCache; +std::vector PlayerbotFactory::ccBreakTrinketCache; + +namespace +{ +constexpr uint32 SPELL_DRUID_THICK_HIDE = 16931; +constexpr uint32 SPELL_OWLKIN_FRENZY = 48393; +constexpr uint32 SPELL_PRIMAL_TENACITY = 33957; +constexpr uint32 SPELL_IMPROVED_BARKSKIN = 63411; + +constexpr uint32 SPELL_SECOND_WIND = 29838; +constexpr uint32 SPELL_BLOOD_CRAZE = 16492; +constexpr uint32 SPELL_GAG_ORDER = 12958; + +constexpr uint32 SPELL_SACRED_CLEANSING = 53553; +constexpr uint32 SPELL_RECKONING = 20179; +constexpr uint32 SPELL_DIVINE_PURPOSE = 31872; + +constexpr uint32 SPELL_HUNTER_THICK_HIDE = 19612; +constexpr uint32 SPELL_CONCUSSIVE_BARRAGE = 35102; +constexpr uint32 SPELL_ENTRAPMENT = 19388; + +constexpr uint32 SPELL_DEADLY_BREW = 51626; +constexpr uint32 SPELL_THROWING_SPECIALIZATION = 51679; +constexpr uint32 SPELL_WAYLAY = 51696; + +constexpr uint32 SPELL_IMPROVED_MANA_BURN = 14772; +constexpr uint32 SPELL_BODY_AND_SOUL = 64129; +constexpr uint32 SPELL_IMPROVED_VAMPIRIC_EMBRACE = 27840; + +constexpr uint32 SPELL_ABOMINATIONS_MIGHT = 53138; +constexpr uint32 SPELL_IMPROVED_ICY_TALONS = 55610; +constexpr uint32 SPELL_SUDDEN_DOOM = 49529; +constexpr uint32 SPELL_ACCLIMATION = 50152; +constexpr uint32 SPELL_MAGIC_SUPPRESSION = 49611; + +constexpr uint32 SPELL_SHAMAN_DUAL_WIELD = 30798; +constexpr uint32 SPELL_ASTRAL_SHIFT = 51479; +constexpr uint32 SPELL_EARTHEN_POWER = 51524; +constexpr uint32 SPELL_FOCUSED_MIND = 30866; + +constexpr uint32 SPELL_BURNOUT = 44472; +constexpr uint32 SPELL_ICE_SHARDS = 15047; +constexpr uint32 SPELL_IMPROVED_BLINK = 31570; +constexpr uint32 SPELL_FIERY_PAYBACK = 64357; +constexpr uint32 SPELL_SHATTERED_BARRIER = 54787; + +constexpr uint32 SPELL_IMPROVED_HOWL_OF_TERROR = 30057; +constexpr uint32 SPELL_NEMESIS = 63123; +constexpr uint32 SPELL_INTENSITY = 18136; +constexpr uint32 SPELL_NETHER_PROTECTION = 30302; +} + +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) @@ -186,7 +497,7 @@ void PlayerbotFactory::Init() continue; } - if (sRandomItemMgr.IsTestItem(gemId)) + if (sRandomItemMgr.IsInternalItem(proto)) { continue; } @@ -200,6 +511,69 @@ void PlayerbotFactory::Init() enchantGemIdCache.push_back(gemId); } LOG_INFO("playerbots", "Loading {} enchantment gems", enchantGemIdCache.size()); + + BuildCcBreakTrinketCache(); +} + +void PlayerbotFactory::BuildCcBreakTrinketCache() +{ + ccBreakTrinketCache.clear(); + // Spell 42292: removes all movement-impairing and loss-of-control effects — the PvP trinket spell. + QueryResult result = WorldDatabase.Query( + "SELECT entry, ItemLevel FROM item_template " + "WHERE Quality >= 2 AND InventoryType = 12 " + "AND (FlagsExtra & 8192) = 0 " + "AND (spellid_1 = 42292 OR spellid_2 = 42292 OR spellid_3 = 42292 " + " OR spellid_4 = 42292 OR spellid_5 = 42292)"); + + if (!result) + { + LOG_INFO("playerbots", "CC-break trinket cache: no items found."); + return; + } + + struct CcItem { uint32 itemId; uint16 itemLevel; }; + std::vector tmp; + do + { + Field* f = result->Fetch(); + tmp.push_back({f[0].Get(), f[1].Get()}); + } while (result->NextRow()); + + std::sort(tmp.begin(), tmp.end(), [](const CcItem& a, const CcItem& b) { + return a.itemLevel > b.itemLevel; + }); + for (auto& c : tmp) + ccBreakTrinketCache.push_back(c.itemId); + + LOG_INFO("playerbots", "CC-break trinket cache: {} items.", ccBreakTrinketCache.size()); +} + +uint8 PlayerbotFactory::GetPreferredArmorType(uint8 cls) +{ + switch (cls) + { + case CLASS_WARRIOR: + case CLASS_PALADIN: + case CLASS_DEATH_KNIGHT: + return ITEM_SUBCLASS_ARMOR_PLATE; + + case CLASS_HUNTER: + case CLASS_SHAMAN: + return ITEM_SUBCLASS_ARMOR_MAIL; + + case CLASS_ROGUE: + case CLASS_DRUID: + return ITEM_SUBCLASS_ARMOR_LEATHER; + + case CLASS_PRIEST: + case CLASS_MAGE: + case CLASS_WARLOCK: + return ITEM_SUBCLASS_ARMOR_CLOTH; + + default: + return 0; + } } void PlayerbotFactory::Prepare() @@ -214,12 +588,23 @@ void PlayerbotFactory::Prepare() { bot->SetUInt32Value(PLAYER_XP, 0); } - if (!sPlayerbotAIConfig.randomBotShowHelmet || !urand(0, 4)) + + if (sPlayerbotAIConfig.randomBotShowHelmet == ShowHideCosmetic::ALWAYS_SHOW || + (sPlayerbotAIConfig.randomBotShowHelmet == ShowHideCosmetic::RANDOMIZE && urand(0, 4))) + { + bot->RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_HIDE_HELM); + } + else { bot->SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_HIDE_HELM); } - if (!sPlayerbotAIConfig.randomBotShowCloak || !urand(0, 4)) + if (sPlayerbotAIConfig.randomBotShowCloak == ShowHideCosmetic::ALWAYS_SHOW || + (sPlayerbotAIConfig.randomBotShowCloak == ShowHideCosmetic::RANDOMIZE && urand(0, 4))) + { + bot->RemoveFlag(PLAYER_FLAGS, PLAYER_FLAGS_HIDE_CLOAK); + } + else { bot->SetFlag(PLAYER_FLAGS, PLAYER_FLAGS_HIDE_CLOAK); } @@ -228,9 +613,8 @@ void PlayerbotFactory::Prepare() void PlayerbotFactory::Randomize(bool incremental) { // if (sPlayerbotAIConfig.disableRandomLevels) - // { // return; - // } + LOG_DEBUG("playerbots", "{} randomizing {} (level {} class = {})...", (incremental ? "Incremental" : "Full"), bot->GetName().c_str(), level, bot->getClass()); // LOG_DEBUG("playerbots", "Preparing to {} randomize...", (incremental ? "incremental" : "full")); @@ -238,16 +622,22 @@ void PlayerbotFactory::Randomize(bool incremental) LOG_DEBUG("playerbots", "Resetting player..."); PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "PlayerbotFactory_Reset"); - if (!PlayerbotAIConfig::instance().equipmentPersistence || level < PlayerbotAIConfig::instance().equipmentPersistenceLevel) + if (!sPlayerbotAIConfig.equipAndSpecPersistence || + level < uint32(sPlayerbotAIConfig.equipAndSpecPersistenceLevel)) + { bot->resetTalents(true); + } if (!incremental) { ClearSkills(); ClearSpells(); ResetQuests(); - if (!PlayerbotAIConfig::instance().equipmentPersistence || level < PlayerbotAIConfig::instance().equipmentPersistenceLevel) + if (!sPlayerbotAIConfig.equipAndSpecPersistence || + level < uint32(sPlayerbotAIConfig.equipAndSpecPersistenceLevel)) + { ClearAllItems(); + } } ClearInventory(); bot->RemoveAllSpellCooldown(); @@ -298,12 +688,12 @@ void PlayerbotFactory::Randomize(bool incremental) pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "PlayerbotFactory_Talents"); LOG_DEBUG("playerbots", "Initializing talents..."); - if (!incremental || !sPlayerbotAIConfig.equipmentPersistence || - bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) + if (!incremental || !sPlayerbotAIConfig.equipAndSpecPersistence || + bot->GetLevel() < sPlayerbotAIConfig.equipAndSpecPersistenceLevel) { - InitTalentsTree(); + uint32 specIndex = InitTalentsTree(); + sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", specIndex + 1); } - sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", 0); if (botAI) { PlayerbotRepository::instance().Reset(botAI); @@ -346,11 +736,10 @@ void PlayerbotFactory::Randomize(bool incremental) pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "PlayerbotFactory_Equip"); LOG_DEBUG("playerbots", "Initializing equipmemt..."); - if (!incremental || !sPlayerbotAIConfig.equipmentPersistence || - bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) + if (!incremental || !sPlayerbotAIConfig.equipAndSpecPersistence || + bot->GetLevel() < sPlayerbotAIConfig.equipAndSpecPersistenceLevel) { - if (sPlayerbotAIConfig.incrementalGearInit || !incremental) - InitEquipment(incremental, incremental ? false : sPlayerbotAIConfig.twoRoundsGearInit); + InitEquipment(incremental, incremental ? false : sPlayerbotAIConfig.twoRoundsGearInit); } // bot->SaveToDB(false, false); if (pmo) @@ -487,7 +876,8 @@ void PlayerbotFactory::Randomize(bool incremental) void PlayerbotFactory::Refresh() { // Prepare(); - // if (!sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) + // if (!sPlayerbotAIConfig.equipAndSpecPersistence || + // bot->GetLevel() < sPlayerbotAIConfig.equipAndSpecPersistenceLevel) // { // InitEquipment(true); // } @@ -507,14 +897,13 @@ void PlayerbotFactory::Refresh() InitSpecialSpells(); InitMounts(); InitKeyring(); - if (!sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) + if (!sPlayerbotAIConfig.equipAndSpecPersistence || + bot->GetLevel() < sPlayerbotAIConfig.equipAndSpecPersistenceLevel) { InitTalentsTree(true, true, true); } if (bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel) - { ApplyEnchantAndGemsNew(); - } bot->DurabilityRepairAll(false, 1.0f, false); if (bot->isDead()) bot->ResurrectPlayer(1.0f, false); @@ -526,36 +915,36 @@ void PlayerbotFactory::Refresh() void PlayerbotFactory::InitConsumables() { - int specTab = AiFactory::GetPlayerSpecTab(bot); + uint8 specTab = AiFactory::GetPlayerSpecTab(bot); std::vector> items; switch (bot->getClass()) { case CLASS_PRIEST: { - // Discipline or Holy: Mana Oil - if (specTab == 0 || specTab == 1) + if (specTab == PRIEST_TAB_SHADOW) { - std::vector mana_oils = {BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL}; - for (uint32 itemId : mana_oils) - { - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (proto->RequiredLevel > level || level > 75) - continue; - items.push_back({itemId, 4}); - break; - } - } - // Shadow: Wizard Oil - if (specTab == 2) - { - std::vector wizard_oils = {BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL}; + std::vector wizard_oils = { + BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL }; for (uint32 itemId : wizard_oils) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (proto->RequiredLevel > level || level > 75) continue; - items.push_back({itemId, 4}); + items.push_back({itemId, 2}); + break; + } + } + else + { + std::vector mana_oils = { + BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL }; + for (uint32 itemId : mana_oils) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (proto->RequiredLevel > level || level > 75) + continue; + items.push_back({itemId, 2}); break; } } @@ -563,38 +952,41 @@ void PlayerbotFactory::InitConsumables() } case CLASS_MAGE: { - // Always Wizard Oil - std::vector wizard_oils = {BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL}; + std::vector wizard_oils = { + BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL }; for (uint32 itemId : wizard_oils) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (proto->RequiredLevel > level || level > 75) continue; - items.push_back({itemId, 4}); + items.push_back({itemId, 2}); break; } break; } case CLASS_DRUID: { - // Balance: Wizard Oil - if (specTab == 0) + if (specTab == DRUID_TAB_BALANCE) { - std::vector wizard_oils = {BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL}; + std::vector wizard_oils = { + BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL }; for (uint32 itemId : wizard_oils) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (proto->RequiredLevel > level || level > 75) continue; - items.push_back({itemId, 4}); + items.push_back({itemId, 2}); break; } } - // Feral: Sharpening Stones & Weightstones - else if (specTab == 1) + else if (specTab == DRUID_TAB_FERAL) { - std::vector sharpening_stones = {ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE}; - std::vector weightstones = {ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE}; + std::vector sharpening_stones = { + ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, + HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE }; + std::vector weightstones = { + ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, + HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE }; for (uint32 itemId : sharpening_stones) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); @@ -612,16 +1004,16 @@ void PlayerbotFactory::InitConsumables() break; } } - // Restoration: Mana Oil - else if (specTab == 2) + else { - std::vector mana_oils = {BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL}; + std::vector mana_oils = { + BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL }; for (uint32 itemId : mana_oils) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (proto->RequiredLevel > level || level > 75) continue; - items.push_back({itemId, 4}); + items.push_back({itemId, 2}); break; } } @@ -629,37 +1021,27 @@ void PlayerbotFactory::InitConsumables() } case CLASS_PALADIN: { - // Holy: Mana Oil - if (specTab == 0) + if (specTab == PALADIN_TAB_HOLY) { - std::vector mana_oils = {BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL}; + std::vector mana_oils = { + BRILLIANT_MANA_OIL, SUPERIOR_MANA_OIL, LESSER_MANA_OIL, MINOR_MANA_OIL }; for (uint32 itemId : mana_oils) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (proto->RequiredLevel > level || level > 75) continue; - items.push_back({itemId, 4}); + items.push_back({itemId, 2}); break; } } - // Protection: Wizard Oil (Protection prioritizes Superior over Brilliant) - else if (specTab == 1) + else { - std::vector wizard_oils = {BRILLIANT_WIZARD_OIL, SUPERIOR_WIZARD_OIL, WIZARD_OIL, LESSER_WIZARD_OIL, MINOR_WIZARD_OIL}; - for (uint32 itemId : wizard_oils) - { - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (proto->RequiredLevel > level || level > 75) - continue; - items.push_back({itemId, 4}); - break; - } - } - // Retribution: Sharpening Stones & Weightstones - else if (specTab == 2) - { - std::vector sharpening_stones = {ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE}; - std::vector weightstones = {ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE}; + std::vector sharpening_stones = { + ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, + HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE }; + std::vector weightstones = { + ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, + HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE }; for (uint32 itemId : sharpening_stones) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); @@ -681,10 +1063,14 @@ void PlayerbotFactory::InitConsumables() } case CLASS_WARRIOR: case CLASS_HUNTER: + case CLASS_DEATH_KNIGHT: { - // Sharpening Stones & Weightstones - std::vector sharpening_stones = {ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE}; - std::vector weightstones = {ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE}; + std::vector sharpening_stones = { + ADAMANTITE_SHARPENING_STONE, FEL_SHARPENING_STONE, DENSE_SHARPENING_STONE, SOLID_SHARPENING_STONE, + HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE }; + std::vector weightstones = { + ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, + HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE }; for (uint32 itemId : sharpening_stones) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); @@ -705,9 +1091,12 @@ void PlayerbotFactory::InitConsumables() } case CLASS_ROGUE: { - // Poisons - std::vector instant_poisons = {INSTANT_POISON_IX, INSTANT_POISON_VIII, INSTANT_POISON_VII, INSTANT_POISON_VI, INSTANT_POISON_V, INSTANT_POISON_IV, INSTANT_POISON_III, INSTANT_POISON_II, INSTANT_POISON}; - std::vector deadly_poisons = {DEADLY_POISON_IX, DEADLY_POISON_VIII, DEADLY_POISON_VII, DEADLY_POISON_VI, DEADLY_POISON_V, DEADLY_POISON_IV, DEADLY_POISON_III, DEADLY_POISON_II, DEADLY_POISON}; + std::vector instant_poisons = { + INSTANT_POISON_IX, INSTANT_POISON_VIII, INSTANT_POISON_VII, INSTANT_POISON_VI, INSTANT_POISON_V, + INSTANT_POISON_IV, INSTANT_POISON_III, INSTANT_POISON_II, INSTANT_POISON }; + std::vector deadly_poisons = { + DEADLY_POISON_IX, DEADLY_POISON_VIII, DEADLY_POISON_VII, DEADLY_POISON_VI, DEADLY_POISON_V, + DEADLY_POISON_IV, DEADLY_POISON_III, DEADLY_POISON_II, DEADLY_POISON }; for (uint32 itemId : deadly_poisons) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); @@ -762,7 +1151,7 @@ void PlayerbotFactory::InitPetTalents() // pet_family->petTalentType); return; } - std::unordered_map> spells; + std::map> spells; bool diveTypePet = (1LL << ci->family) & diveMask; for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) @@ -811,7 +1200,7 @@ void PlayerbotFactory::InitPetTalents() int index = urand(0, spells_row.size() - 1); TalentEntry const* talentInfo = spells_row[index]; int maxRank = 0; - for (int rank = 0; rank < std::min((uint32)MAX_TALENT_RANK, (uint32)pet->GetFreeTalentPoints()); ++rank) + for (uint32 rank = 0; rank < std::min((uint32)MAX_TALENT_RANK, (uint32)pet->GetFreeTalentPoints()); ++rank) { uint32 spellId = talentInfo->RankID[rank]; if (!spellId) @@ -948,8 +1337,6 @@ void PlayerbotFactory::InitPet() continue; if (co->Name.size() > 21) continue; - uint32 guid = map->GenerateLowGuid(); - uint32 pet_number = sObjectMgr->GeneratePetNumber(); if (bot->GetPetStable() && bot->GetPetStable()->CurrentPet) { auto petGuid = bot->GetPetStable()->CurrentPet.value(); // To correct the build warnin in VS @@ -1101,7 +1488,7 @@ void PlayerbotFactory::ResetQuests() } } -void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/) +uint32 PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/) { uint32 specTab; uint8 cls = bot->getClass(); @@ -1114,7 +1501,7 @@ void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_templa /// @todo: fix cat druid hardcode if (bot->getClass() == CLASS_DRUID && specTab == DRUID_TAB_FERAL && bot->GetLevel() >= 20) { - bool isCat = !bot->HasAura(16931); + bool isCat = !bot->HasAura(SPELL_DRUID_THICK_HIDE); if (!isCat && bot->GetLevel() == 20) { uint32 bearP = sPlayerbotAIConfig.randomClassSpecProb[cls][1]; @@ -1169,7 +1556,14 @@ void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_templa if (bot->GetFreeTalentPoints()) InitTalents((specTab + 2) % 3); + if (bot->getClass() == CLASS_SHAMAN && bot->HasSpell(SPELL_SHAMAN_DUAL_WIELD)) + { + bot->SetSkill(SKILL_DUAL_WIELD, 0, 1, 1); + bot->SetCanDualWield(true); + } + bot->SendTalentsInfoData(false); + return sPlayerbotAIConfig.randomClassSpecIndex[cls][specTab]; } void PlayerbotFactory::InitTalentsBySpecNo(Player* bot, int specNo, bool reset) @@ -1247,7 +1641,15 @@ void PlayerbotFactory::InitTalentsBySpecNo(Player* bot, int specNo, bool reset) break; } } + + if (bot->getClass() == CLASS_SHAMAN && bot->HasSpell(SPELL_SHAMAN_DUAL_WIELD)) + { + bot->SetSkill(SKILL_DUAL_WIELD, 0, 1, 1); + bot->SetCanDualWield(true); + } + bot->SendTalentsInfoData(false); + sRandomPlayerbotMgr.SetValue(bot->GetGUID().GetCounter(), "specNo", (uint32)specNo + 1); } void PlayerbotFactory::InitTalentsByParsedSpecLink(Player* bot, std::vector> parsedSpecLink, @@ -1700,9 +2102,6 @@ void Shuffle(std::vector& items) void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) { - if (incremental && !sPlayerbotAIConfig.incrementalGearInit) - return; - if (level < 5) { // original items @@ -1744,13 +2143,41 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) return; } - std::unordered_map> items; + std::unordered_map>> items; // int tab = AiFactory::GetPlayerSpecTab(bot); uint32 blevel = bot->GetLevel(); int32 delta = std::min(blevel, 10u); + bool isPvp = sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass()); + StatsWeightCalculator calculator(bot); + if (isPvp) + calculator.SetPvpSpec(true); + + // Pre-select CC-break trinket for PvP specs: best available by item level + // that the bot meets the level requirement for. + // Humans (Every Man for Himself) and Undead (Will of the Forsaken) have a + // racial that shares the PvP trinket cooldown, so they don't need one. + bool racialHasCcBreak = (bot->getRace() == RACE_HUMAN || bot->getRace() == RACE_UNDEAD_PLAYER); + uint32 pvpTrinket1 = 0; + if (isPvp && level >= 50 && !racialHasCcBreak) + { + for (uint32 itemId : ccBreakTrinketCache) + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) continue; + // Respect gear quality limit: trinket must not exceed itemQuality setting + if (static_cast(proto->Quality) > static_cast(itemQuality)) continue; + if (proto->RequiredLevel > level) continue; + if (!CanEquipItem(proto)) continue; + uint16 dest; + if (!CanEquipUnseenItem(EQUIPMENT_SLOT_TRINKET1, dest, itemId)) continue; + pvpTrinket1 = itemId; + break; + } + } + for (int32 slot : initSlotsOrder) { if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY) @@ -1770,6 +2197,10 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) (slot != EQUIPMENT_SLOT_RANGED)) continue; + // Exclude resilience weighting for trinkets + bool isTrinketSlot = (slot == EQUIPMENT_SLOT_TRINKET1 || slot == EQUIPMENT_SLOT_TRINKET2); + calculator.SetExcludeResilience(isTrinketSlot); + Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); if (second_chance && oldItem) @@ -1779,23 +2210,46 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + // PvP specs: force TRINKET1 to the best available CC-break trinket. + if (slot == EQUIPMENT_SLOT_TRINKET1 && pvpTrinket1 != 0) + { + if (oldItem) + { + uint8 bagIndex = oldItem->GetBagSlot(); + uint8 oldSlot = oldItem->GetSlot(); + uint8 dstBag = NULL_BAG; + WorldPacket packet(CMSG_AUTOSTORE_BAG_ITEM, 3); + packet << bagIndex << oldSlot << dstBag; + WorldPackets::Item::AutoStoreBagItem nicePacket(std::move(packet)); + nicePacket.Read(); + bot->GetSession()->HandleAutoStoreBagItemOpcode(nicePacket); + oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot); + if (oldItem) continue; + } + uint16 dest; + if (CanEquipUnseenItem(slot, dest, pvpTrinket1)) + bot->EquipNewItem(dest, pvpTrinket1, true); + continue; + } + int32 desiredQuality = itemQuality; if (urand(0, 100) < 100 * sPlayerbotAIConfig.randomGearLoweringChance && desiredQuality > ITEM_QUALITY_NORMAL) desiredQuality--; do { - for (uint32 requiredLevel = bot->GetLevel(); requiredLevel > std::max((int32)bot->GetLevel() - delta, 0); + for (uint32 requiredLevel = bot->GetLevel(); requiredLevel > uint32(std::max((int32)bot->GetLevel() - delta, 0)); requiredLevel--) { for (InventoryType inventoryType : GetPossibleInventoryTypeListBySlot((EquipmentSlots)slot)) { - for (uint32 itemId : sRandomItemMgr.GetCachedEquipments(requiredLevel, inventoryType)) + for (uint32 itemId : sRandomItemMgr.GetEquipmentNew(requiredLevel, inventoryType)) { uint32 skipProb = 25; if (urand(1, 100) <= skipProb) continue; + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); // disable next expansion gear if (sPlayerbotAIConfig.limitGearExpansion && bot->GetLevel() <= 60 && itemId >= 23728) continue; @@ -1806,7 +2260,6 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) // wearable TBC items above 35570 but nothing of significance continue; - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (!proto) continue; @@ -1820,7 +2273,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR) continue; - if (proto->Quality != desiredQuality) + if (proto->Quality != uint32(desiredQuality)) continue; if (proto->Class == ITEM_CLASS_ARMOR && @@ -1837,13 +2290,17 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (slot == EQUIPMENT_SLOT_OFFHAND && bot->getClass() == CLASS_ROGUE && proto->Class != ITEM_CLASS_WEAPON) continue; - items[slot].push_back(itemId); + + int32 bestRandomProp = 0; + if (proto->RandomProperty || proto->RandomSuffix) + bestRandomProp = calculator.PickBestRandomPropertyId(itemId); + items[slot].push_back({itemId, bestRandomProp}); } } } } while (items[slot].size() < 25 && desiredQuality-- > ITEM_QUALITY_POOR); - std::vector& ids = items[slot]; + std::vector>& ids = items[slot]; if (ids.empty()) { continue; @@ -1851,13 +2308,23 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) float bestScoreForSlot = -1; uint32 bestItemForSlot = 0; - for (int index = 0; index < ids.size(); index++) + int32 bestRandomPropForSlot = 0; + for (size_t index = 0; index < ids.size(); index++) { - uint32 newItemId = ids[index]; + uint32 newItemId = ids[index].first; + int32 newItemProp = ids[index].second; ItemTemplate const* proto = sObjectMgr->GetItemTemplate(newItemId); - float cur_score = calculator.CalculateItem(newItemId); + float cur_score = calculator.CalculateItem(newItemId, newItemProp, slot); + + if (cur_score > 0.0f && proto && proto->Class == ITEM_CLASS_ARMOR && sPlayerbotAIConfig.preferClassArmorType) + { + uint8 preferredArmorType = GetPreferredArmorType(bot->getClass()); + if (preferredArmorType != 0 && proto->SubClass == preferredArmorType) + cur_score *= 3.0f; // 3x multiplier for preferred armor type + } + if (cur_score > bestScoreForSlot) { // delay heavy check to here @@ -1868,6 +2335,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) continue; bestScoreForSlot = cur_score; bestItemForSlot = newItemId; + bestRandomPropForSlot = newItemProp; } } @@ -1883,7 +2351,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (incremental && oldItem) { - float old_score = calculator.CalculateItem(oldItem->GetEntry(), oldItem->GetItemRandomPropertyId()); + float old_score = calculator.CalculateItem(oldItem->GetEntry(), oldItem->GetItemRandomPropertyId(), slot); if (bestScoreForSlot < 1.2f * old_score) continue; } @@ -1905,7 +2373,16 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (oldItem) continue; - Item* newItem = bot->EquipNewItem(dest, bestItemForSlot, true); + if (Item* equipped = bot->EquipNewItem(dest, bestItemForSlot, true)) + { + if (bestRandomPropForSlot != 0) + { + uint8 equipSlot = equipped->GetSlot(); + bot->_ApplyItemMods(equipped, equipSlot, false); + equipped->SetItemRandomProperties(bestRandomPropForSlot); + bot->_ApplyItemMods(equipped, equipSlot, true); + } + } bot->AutoUnequipOffhandIfNeed(); // if (newItem) // { @@ -1936,22 +2413,39 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) (slot != EQUIPMENT_SLOT_RANGED)) continue; + // CC-break trinket was force-equipped in the main pass; leave it alone. + if (slot == EQUIPMENT_SLOT_TRINKET1 && pvpTrinket1 != 0) + continue; + + bool isTrinketSlot = (slot == EQUIPMENT_SLOT_TRINKET1 || slot == EQUIPMENT_SLOT_TRINKET2); + calculator.SetExcludeResilience(isTrinketSlot); + if (Item* oldItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true); - std::vector& ids = items[slot]; + std::vector>& ids = items[slot]; if (ids.empty()) continue; float bestScoreForSlot = -1; uint32 bestItemForSlot = 0; - for (int index = 0; index < ids.size(); index++) + int32 bestRandomPropForSlot = 0; + for (size_t index = 0; index < ids.size(); index++) { - uint32 newItemId = ids[index]; + uint32 newItemId = ids[index].first; + int32 newItemProp = ids[index].second; ItemTemplate const* proto = sObjectMgr->GetItemTemplate(newItemId); - float cur_score = calculator.CalculateItem(newItemId); + float cur_score = calculator.CalculateItem(newItemId, newItemProp, slot); + + if (cur_score > 0.0f && proto && proto->Class == ITEM_CLASS_ARMOR && sPlayerbotAIConfig.preferClassArmorType) + { + uint8 preferredArmorType = GetPreferredArmorType(bot->getClass()); + if (preferredArmorType != 0 && proto->SubClass == preferredArmorType) + cur_score *= 3.0f; // 3x multiplier for preferred armor type + } + if (cur_score > bestScoreForSlot) { // delay heavy check to here @@ -1962,6 +2456,7 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) continue; bestScoreForSlot = cur_score; bestItemForSlot = newItemId; + bestRandomPropForSlot = newItemProp; } } @@ -1972,13 +2467,17 @@ void PlayerbotFactory::InitEquipment(bool incremental, bool second_chance) if (!CanEquipUnseenItem(slot, dest, bestItemForSlot)) continue; - Item* newItem = bot->EquipNewItem(dest, bestItemForSlot, true); + if (Item* equipped = bot->EquipNewItem(dest, bestItemForSlot, true)) + { + if (bestRandomPropForSlot != 0) + { + uint8 equipSlot = equipped->GetSlot(); + bot->_ApplyItemMods(equipped, equipSlot, false); + equipped->SetItemRandomProperties(bestRandomPropForSlot); + bot->_ApplyItemMods(equipped, equipSlot, true); + } + } bot->AutoUnequipOffhandIfNeed(); - // if (newItem) - // { - // newItem->AddToWorld(); - // newItem->AddToUpdateQueueOf(bot); - // } } } } @@ -1991,16 +2490,10 @@ bool PlayerbotFactory::IsDesiredReplacement(Item* item) ItemTemplate const* proto = item->GetTemplate(); uint32 requiredLevel = proto->RequiredLevel; if (!requiredLevel) - { return true; - } - // if (!requiredLevel) - // { - // requiredLevel = sRandomItemMgr.GetMinLevelFromCache(proto->ItemId); - // } uint32 delta = 1 + (80 - bot->GetLevel()) / 10; - return proto->Quality < ITEM_QUALITY_RARE || int32(bot->GetLevel() - requiredLevel) > delta; + return proto->Quality < ITEM_QUALITY_RARE || (bot->GetLevel() - requiredLevel) > delta; } inline Item* StoreNewItemInInventorySlot(Player* player, uint32 newItemId, uint32 count) @@ -2010,9 +2503,7 @@ inline Item* StoreNewItemInInventorySlot(Player* player, uint32 newItemId, uint3 if (msg == EQUIP_ERR_OK) { if (Item* newItem = player->StoreNewItem(vDest, newItemId, true, Item::GenerateItemRandomPropertyId(newItemId))) - { return newItem; - } } return nullptr; @@ -2146,7 +2637,7 @@ void PlayerbotFactory::InitBags(bool destroyOld) if (old_bag) continue; - Item* newItem = bot->EquipNewItem(dest, newItemId, true); + bot->EquipNewItem(dest, newItemId, true); // if (newItem) // { // newItem->AddToWorld(); @@ -2263,69 +2754,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() @@ -2469,6 +3169,9 @@ void PlayerbotFactory::InitSkills() break; } + InitTradeSkills(); + InitInventorySkill(); + // switch (bot->getClass()) // { // case CLASS_WARRIOR: @@ -2566,17 +3269,15 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(7386, false); // Sunder Armor } if (level >= 30) - { bot->learnSpell(2458, false); // Berserker Stance - } break; case CLASS_PALADIN: bot->learnSpell(21084, true); bot->learnSpell(635, true); if (level >= 12) - { bot->learnSpell(7328, false); // Redemption - } + if (level >= 20) + bot->learnSpell(5502, false); // Sense Undead break; case CLASS_ROGUE: bot->learnSpell(1752, true); @@ -2618,17 +3319,11 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(686, true); bot->learnSpell(688, false); // summon imp if (level >= 10) - { bot->learnSpell(697, false); // summon voidwalker - } if (level >= 20) - { bot->learnSpell(712, false); // summon succubus - } if (level >= 30) - { bot->learnSpell(691, false); // summon felhunter - } break; case CLASS_DRUID: bot->learnSpell(5176, true); @@ -2645,17 +3340,11 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(331, true); // bot->learnSpell(66747, true); // Totem of the Earthen Ring if (level >= 4) - { bot->learnSpell(8071, false); // stoneskin totem - } if (level >= 10) - { bot->learnSpell(3599, false); // searing totem - } if (level >= 20) - { bot->learnSpell(5394, false); // healing stream totem - } break; default: break; @@ -2680,7 +3369,7 @@ void PlayerbotFactory::InitSpecialSpells() void PlayerbotFactory::InitTalents(uint32 specNo) { uint32 classMask = bot->getClassMask(); - std::unordered_map> spells; + std::map> spells; for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) { TalentEntry const* talentInfo = sTalentStore.LookupEntry(i); @@ -2713,7 +3402,7 @@ void PlayerbotFactory::InitTalents(uint32 specNo) int index = urand(0, spells_row.size() - 1); TalentEntry const* talentInfo = spells_row[index]; int maxRank = 0; - for (int rank = 0; rank < std::min((uint32)MAX_TALENT_RANK, bot->GetFreeTalentPoints()); ++rank) + for (uint32 rank = 0; rank < std::min((uint32)MAX_TALENT_RANK, bot->GetFreeTalentPoints()); ++rank) { uint32 spellId = talentInfo->RankID[rank]; if (!spellId) @@ -2875,7 +3564,6 @@ void PlayerbotFactory::AddPrevQuests(uint32 questId, std::list& questIds void PlayerbotFactory::InitQuests(std::list& questMap, bool withRewardItem) { - uint32 count = 0; for (std::list::iterator i = questMap.begin(); i != questMap.end(); ++i) { uint32 questId = *i; @@ -2941,15 +3629,20 @@ void PlayerbotFactory::ClearAllItems() void PlayerbotFactory::InitAmmo() { - if (bot->getClass() != CLASS_HUNTER && bot->getClass() != CLASS_ROGUE && bot->getClass() != CLASS_WARRIOR) + uint8 const botClass = bot->getClass(); + if (botClass != CLASS_HUNTER && botClass != CLASS_ROGUE && botClass != CLASS_WARRIOR) return; - Item* const pItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); - if (!pItem) + Item const* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_RANGED); + if (!item) + return; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) return; uint32 subClass = 0; - switch (pItem->GetTemplate()->SubClass) + switch (proto->SubClass) { case ITEM_SUBCLASS_WEAPON_GUN: subClass = ITEM_SUBCLASS_BULLET; @@ -2958,45 +3651,23 @@ void PlayerbotFactory::InitAmmo() case ITEM_SUBCLASS_WEAPON_CROSSBOW: subClass = ITEM_SUBCLASS_ARROW; break; - default: - break; - } - - if (!subClass) - return; - - std::vector ammoEntryList = sRandomItemMgr.GetAmmo(level, subClass); - uint32 entry = 0; - for (uint32 tEntry : ammoEntryList) - { - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(tEntry); - if (!proto) - continue; - - // disable next expansion ammo - if (sPlayerbotAIConfig.limitGearExpansion && bot->GetLevel() <= 60 && tEntry >= 23728) - continue; - - if (sPlayerbotAIConfig.limitGearExpansion && bot->GetLevel() <= 70 && tEntry >= 35570) - continue; - - entry = tEntry; - break; + default: // invalid weapon type, nothing to do + return; } + uint32 entry = sRandomItemMgr.GetAmmo(level, subClass); if (!entry) return; uint32 count = bot->GetItemCount(entry); - uint32 maxCount = bot->getClass() == CLASS_HUNTER ? 6000 : 1000; + uint32 maxCount = botClass == CLASS_HUNTER ? 6000 : 1000; if (count < maxCount) { if (Item* newItem = StoreNewItemInInventorySlot(bot, entry, maxCount - count)) - { newItem->AddToUpdateQueueOf(bot); - } } + bot->SetAmmo(entry); } @@ -3060,6 +3731,17 @@ void PlayerbotFactory::InitMounts() slow = {33660, 35020, 35022, 35018}; fast = {35025, 35025, 35027}; break; + default: + if (bot->GetTeamId() == TEAM_HORDE) + { // Orc mounts + slow = {470, 6648, 458, 472}; + fast = {23228, 23227, 23229}; + } + else // Human mounts + { + slow = {6654, 6653, 580}; + fast = {23250, 23252, 23251}; + } } switch (bot->GetTeamId()) @@ -3247,7 +3929,7 @@ void PlayerbotFactory::InitFood() } uint32 categories[] = {11, 59}; - for (int i = 0; i < sizeof(categories) / sizeof(uint32); ++i) + for (size_t i = 0; i < sizeof(categories) / sizeof(uint32); ++i) { uint32 category = categories[i]; std::vector& ids = items[category]; @@ -3280,7 +3962,6 @@ void PlayerbotFactory::InitFood() void PlayerbotFactory::InitReagents() { - int specTab = AiFactory::GetPlayerSpecTab(bot); std::vector> items; switch (bot->getClass()) { @@ -3336,7 +4017,7 @@ void PlayerbotFactory::InitReagents() break; case CLASS_PALADIN: if (level >= 52) - items.push_back({21177, 80}); // Symbol of Kings + items.push_back({21177, 100}); // Symbol of Kings break; case CLASS_PRIEST: if (level >= 48 && level < 56) @@ -3357,18 +4038,36 @@ void PlayerbotFactory::InitReagents() items.push_back({44615, 40}); // Devout Candle break; case CLASS_SHAMAN: - if (level >= 4) - items.push_back({5175, 1}); // Earth Totem - if (level >= 10) - items.push_back({5176, 1}); // Flame Totem - if (level >= 20) - items.push_back({5177, 1}); // Water Totem + { + HasRelicBySubclassVisitor relicVisitor(ITEM_SUBCLASS_ARMOR_TOTEM); + IterateItems(&relicVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP)); + bool hasRelic = relicVisitor.found; + + if (!hasRelic) + { + if (level >= 4) + items.push_back({5175, 1}); // Earth Totem + if (level >= 10) + items.push_back({5176, 1}); // Flame Totem + if (level >= 20) + items.push_back({5177, 1}); // Water Totem + } + else + { + ItemIds totemIds = {5175, 5176, 5177, 5178}; + FindItemByIdsVisitor totemVisitor(totemIds); + IterateItems(&totemVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP | ITERATE_ITEMS_IN_BANK)); + for (Item* item : totemVisitor.GetResult()) + bot->DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + } if (level >= 30) { - items.push_back({5178, 1}); // Air Totem + if (!hasRelic) + items.push_back({5178, 1}); // Air Totem items.push_back({17030, 20}); // Ankh } break; + } case CLASS_WARLOCK: items.push_back({6265, 5}); // Soul Shard break; @@ -3515,13 +4214,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_WARRIOR) { // Arms PvP (spec index 3): If the bot has the Second Wind talent - if (bot->HasAura(29838)) + if (bot->HasAura(SPELL_SECOND_WIND)) tab = 3; // Fury PvP (spec index 4): If the bot has the Blood Craze talent - else if (bot->HasAura(16492)) + else if (bot->HasAura(SPELL_BLOOD_CRAZE)) tab = 4; // Protection PvP (spec index 5): If the bot has the Gag Order talent - else if (bot->HasAura(12958)) + else if (bot->HasAura(SPELL_GAG_ORDER)) tab = 5; } @@ -3529,13 +4228,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_PALADIN) { // Holy PvP (spec index 3): If the bot has the Sacred Cleansing talent - if (bot->HasAura(53553)) + if (bot->HasAura(SPELL_SACRED_CLEANSING)) tab = 3; // Protection PvP (spec index 4): If the bot has the Reckoning talent - else if (bot->HasAura(20179)) + else if (bot->HasAura(SPELL_RECKONING)) tab = 4; // Retribution PvP (spec index 5): If the bot has the Divine Purpose talent - else if (bot->HasAura(31872)) + else if (bot->HasAura(SPELL_DIVINE_PURPOSE)) tab = 5; } @@ -3543,13 +4242,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_HUNTER) { // Beast Mastery PvP (spec index 3): If the bot has the Thick Hide talent - if (bot->HasAura(19612)) + if (bot->HasAura(SPELL_HUNTER_THICK_HIDE)) tab = 3; // Marksmanship PvP (spec index 4): If the bot has the Concussive Barrage talent - else if (bot->HasAura(35102)) + else if (bot->HasAura(SPELL_CONCUSSIVE_BARRAGE)) tab = 4; // Survival PvP (spec index 5): If the bot has the Entrapment talent and does NOT have the Concussive Barrage talent - else if (bot->HasAura(19388) && !bot->HasAura(35102)) + else if (bot->HasAura(SPELL_ENTRAPMENT) && !bot->HasAura(SPELL_CONCUSSIVE_BARRAGE)) tab = 5; } @@ -3557,13 +4256,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_ROGUE) { // Assassination PvP (spec index 3): If the bot has the Deadly Brew talent - if (bot->HasAura(51626)) + if (bot->HasAura(SPELL_DEADLY_BREW)) tab = 3; // Combat PvP (spec index 4): If the bot has the Throwing Specialization talent - else if (bot->HasAura(51679)) + else if (bot->HasAura(SPELL_THROWING_SPECIALIZATION)) tab = 4; // Subtlety PvP (spec index 5): If the bot has the Waylay talent - else if (bot->HasAura(51696)) + else if (bot->HasAura(SPELL_WAYLAY)) tab = 5; } @@ -3571,13 +4270,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_PRIEST) { // Discipline PvP (spec index 3): If the bot has the Improved Mana Burn talent - if (bot->HasAura(14772)) + if (bot->HasAura(SPELL_IMPROVED_MANA_BURN)) tab = 3; // Holy PvP (spec index 4): If the bot has the Body and Soul talent - else if (bot->HasAura(64129)) + else if (bot->HasAura(SPELL_BODY_AND_SOUL)) tab = 4; // Shadow PvP (spec index 5): If the bot has the Improved Vampiric Embrace talent - else if (bot->HasAura(27840)) + else if (bot->HasAura(SPELL_IMPROVED_VAMPIRIC_EMBRACE)) tab = 5; } @@ -3586,16 +4285,16 @@ void PlayerbotFactory::InitGlyphs(bool increment) { // Double Aura Blood PvE (spec index 3): If the bot has both the Abomination's Might and Improved Icy Talons // talents - if (bot->HasAura(53138) && bot->HasAura(55610)) + if (bot->HasAura(SPELL_ABOMINATIONS_MIGHT) && bot->HasAura(SPELL_IMPROVED_ICY_TALONS)) tab = 3; // Blood PvP (spec index 4): If the bot has the Sudden Doom talent - else if (bot->HasAura(49529)) + else if (bot->HasAura(SPELL_SUDDEN_DOOM)) tab = 4; // Frost PvP (spec index 5): If the bot has the Acclimation talent - else if (bot->HasAura(50152)) + else if (bot->HasAura(SPELL_ACCLIMATION)) tab = 5; // Unholy PvP (spec index 6): If the bot has the Magic Suppression talent - else if (bot->HasAura(49611)) + else if (bot->HasAura(SPELL_MAGIC_SUPPRESSION)) tab = 6; } @@ -3603,13 +4302,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_SHAMAN) { // Elemental PvP (spec index 3): If the bot has the Astral Shift talent - if (bot->HasAura(51479)) + if (bot->HasAura(SPELL_ASTRAL_SHIFT)) tab = 3; // Enhancement PvP (spec index 4): If the bot has the Earthen Power talent - else if (bot->HasAura(51524)) + else if (bot->HasAura(SPELL_EARTHEN_POWER)) tab = 4; // Restoration PvP (spec index 5): If the bot has the Focused Mind talent - else if (bot->HasAura(30866)) + else if (bot->HasAura(SPELL_FOCUSED_MIND)) tab = 5; } @@ -3617,16 +4316,16 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_MAGE) { // Frostfire PvE (spec index 3): If the bot has both the Burnout talent and the Ice Shards talent - if (bot->HasAura(44472) && bot->HasAura(15047)) + if (bot->HasAura(SPELL_BURNOUT) && bot->HasAura(SPELL_ICE_SHARDS)) tab = 3; // Arcane PvP (spec index 4): If the bot has the Improved Blink talent - else if (bot->HasAura(31570)) + else if (bot->HasAura(SPELL_IMPROVED_BLINK)) tab = 4; // Fire PvP (spec index 5): If the bot has the Fiery Payback talent - else if (bot->HasAura(64357)) + else if (bot->HasAura(SPELL_FIERY_PAYBACK)) tab = 5; // Frost PvP (spec index 6): If the bot has the Shattered Barrier talent - else if (bot->HasAura(54787)) + else if (bot->HasAura(SPELL_SHATTERED_BARRIER)) tab = 6; } @@ -3634,13 +4333,13 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_WARLOCK) { // Affliction PvP (spec index 3): If the bot has the Improved Howl of Terror talent - if (bot->HasAura(30057)) + if (bot->HasAura(SPELL_IMPROVED_HOWL_OF_TERROR)) tab = 3; // Demonology PvP (spec index 4): If the bot has both the Nemesis talent and the Intensity talent - else if (bot->HasAura(63123) && bot->HasAura(18136)) + else if (bot->HasAura(SPELL_NEMESIS) && bot->HasAura(SPELL_INTENSITY)) tab = 4; // Destruction PvP (spec index 5): If the bot has the Nether Protection talent - else if (bot->HasAura(30302)) + else if (bot->HasAura(SPELL_NETHER_PROTECTION)) tab = 5; } @@ -3648,16 +4347,16 @@ void PlayerbotFactory::InitGlyphs(bool increment) if (bot->getClass() == CLASS_DRUID) { // Cat PvE (spec index 3): If the bot is Feral spec, level 20 or higher, and does NOT have the Thick Hide talent - if (tab == DRUID_TAB_FERAL && bot->GetLevel() >= 20 && !bot->HasAura(16931)) + if (tab == DRUID_TAB_FERAL && bot->GetLevel() >= 20 && !bot->HasAura(SPELL_DRUID_THICK_HIDE)) tab = 3; // Balance PvP (spec index 4): If the bot has the Owlkin Frenzy talent - else if (bot->HasAura(48393)) + else if (bot->HasAura(SPELL_OWLKIN_FRENZY)) tab = 4; // Feral PvP (spec index 5): If the bot has the Primal Tenacity talent - else if (bot->HasAura(33957)) + else if (bot->HasAura(SPELL_PRIMAL_TENACITY)) tab = 5; // Resto PvP (spec index 6): If the bot has the Improved Barkskin talent - else if (bot->HasAura(63411)) + else if (bot->HasAura(SPELL_IMPROVED_BARKSKIN)) tab = 6; } @@ -3804,30 +4503,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) @@ -4266,7 +4956,7 @@ void PlayerbotFactory::ApplyEnchantTemplate(uint8 spec) // const SpellItemEnchantmentEntry* a = sSpellItemEnchantmentStore.LookupEntry(1); } -void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) +void PlayerbotFactory::ApplyEnchantAndGemsNew(bool /*destroyOld*/) { //int32 bestGemEnchantId[4] = {-1, -1, -1, -1}; // 1, 2, 4, 8 color //not used, line marked for removal. //float bestGemScore[4] = {0, 0, 0, 0}; //not used, line marked for removal. @@ -4336,15 +5026,11 @@ void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) continue; if (!item->IsFitToSpellRequirements(spellInfo)) - { continue; - } uint32 requiredLevel = spellInfo->BaseLevel; if (requiredLevel > bot->GetLevel()) - { continue; - } // disable next expansion enchantments if (sPlayerbotAIConfig.limitEnchantExpansion && bot->GetLevel() <= 60 && enchantSpell >= 27899) @@ -4364,9 +5050,8 @@ void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) SpellItemEnchantmentEntry const* enchant = sSpellItemEnchantmentStore.LookupEntry(enchant_id); if (!enchant || (enchant->slot != PERM_ENCHANTMENT_SLOT && enchant->slot != TEMP_ENCHANTMENT_SLOT)) - { continue; - } + if (enchant->requiredSkill && (!bot->HasSkill(enchant->requiredSkill) || (bot->GetSkillValue(enchant->requiredSkill) < enchant->requiredSkillValue))) @@ -4374,9 +5059,8 @@ void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) continue; } if (enchant->requiredLevel > bot->GetLevel()) - { continue; - } + float score = calculator.CalculateEnchant(enchant_id); if (score >= bestScore) { @@ -4399,11 +5083,9 @@ void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) { uint8 socketColor = item->GetTemplate()->Socket[enchant_slot - SOCK_ENCHANTMENT_SLOT].Color; if (!socketColor) - { continue; - } + int32 enchantIdChosen = -1; - int32 colorChosen; bool jewelersGemChosen; float bestGemScore = -1; for (uint32& enchantGem : availableGems) @@ -4434,9 +5116,9 @@ void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) if (curCount[0] != 0) { // Ensure meta gem activation - for (int i = 1; i < curCount.size(); i++) + for (size_t i = 1; i < curCount.size(); i++) { - if (curCount[i] < requiredActive && (gemProperties->color & (1 << i))) + if (curCount[i] < (uint32)requiredActive && (gemProperties->color & (1 << i))) { score *= 2; break; @@ -4448,7 +5130,6 @@ void PlayerbotFactory::ApplyEnchantAndGemsNew(bool destroyOld) if (score > bestGemScore) { enchantIdChosen = enchant_id; - colorChosen = gemProperties->color; bestGemScore = score; jewelersGemChosen = isJewelersGem; } @@ -4739,38 +5420,13 @@ void PlayerbotFactory::InitAttunementQuests() uint32 currentXP = bot->GetUInt32Value(PLAYER_XP); - // List of attunement quest IDs - std::list attunementQuestsTBC = { - // Caverns of Time - Part 1 - 10279, // To The Master's Lair - 10277, // The Caverns of Time - - // Caverns of Time - Part 2 (Escape from Durnholde Keep) - 10282, // Old Hillsbrad - 10283, // Taretha's Diversion - 10284, // Escape from Durnholde - 10285, // Return to Andormu - - // Caverns of Time - Part 2 (The Black Morass) - 10296, // The Black Morass - 10297, // The Opening of the Dark Portal - 10298, // Hero of the Brood - - // Magister's Terrace Attunement - 11481, // Crisis at the Sunwell - 11482, // Duty Calls - 11488, // Magisters' Terrace - 11490, // The Scryer's Scryer - 11492 // Hard to Kill - }; - // Complete all level-appropriate attunement quests for the bot if (level >= 60) { std::list questsToComplete; // Check each quest status before adding to the completion list - for (uint32 questId : attunementQuestsTBC) + for (uint32 questId : sPlayerbotAIConfig.attunementQuests) { QuestStatus questStatus = bot->GetQuestStatus(questId); diff --git a/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.h b/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.h index d943463ea..50b7d6baa 100644 --- a/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.h +++ b/modules/mod-playerbots/src/Bot/Factory/PlayerbotFactory.h @@ -1,10 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTFACTORY_H -#define _PLAYERBOT_PLAYERBOTFACTORY_H +#ifndef PLAYERBOTS_PLAYERBOTFACTORY_H +#define PLAYERBOTS_PLAYERBOTFACTORY_H + +#include +#include #include "InventoryAction.h" #include "Player.h" @@ -60,7 +64,7 @@ public: static uint32 tradeSkills[]; static float CalculateEnchantScore(uint32 enchant_id, Player* bot); - void InitTalentsTree(bool incremental = false, bool use_template = true, bool reset = false); + uint32 InitTalentsTree(bool incremental = false, bool use_template = true, bool reset = false); static void InitTalentsBySpecNo(Player* bot, int specNo, bool reset); static void InitTalentsByParsedSpecLink(Player* bot, std::vector> parsedSpecLink, bool reset); void InitAvailableSpells(); @@ -85,14 +89,94 @@ public: void InitKeyring(); void InitReputation(); void InitAttunementQuests(); + void InitGuild(); 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(); @@ -107,6 +191,8 @@ private: std::vector GetCurrentGemsCount(); bool CanEquipArmor(ItemTemplate const* proto); bool CanEquipWeapon(ItemTemplate const* proto); + static void BuildCcBreakTrinketCache(); + uint8 GetPreferredArmorType(uint8 cls); void EnchantItem(Item* item); void AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank); bool CheckItemStats(uint8 sp, uint8 ap, uint8 tank); @@ -117,7 +203,6 @@ private: void InitInventoryEquip(); void InitInventorySkill(); Item* StoreItem(uint32 itemId, uint32 count); - void InitGuild(); void InitArenaTeam(); void InitImmersive(); static void AddPrevQuests(uint32 questId, std::list& questIds); @@ -138,6 +223,7 @@ private: static std::unordered_map> trainerIdCache; static std::vector enchantSpellIdCache; static std::vector enchantGemIdCache; + static std::vector ccBreakTrinketCache; protected: EnchantContainer m_EnchantContainer; diff --git a/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.cpp b/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.cpp index 8c802126f..c57cd715a 100644 --- a/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.cpp +++ b/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.cpp @@ -1,7 +1,8 @@ /* -* 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. -*/ + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ #include "RandomPlayerbotFactory.h" @@ -9,6 +10,7 @@ #include "ArenaTeamMgr.h" #include "DatabaseEnv.h" #include "PlayerbotAI.h" +#include "RaceMgr.h" #include "ScriptMgr.h" #include "SharedDefines.h" #include "SocialMgr.h" @@ -60,7 +62,7 @@ Player* RandomPlayerbotFactory::CreateRandomBot(WorldSession* session, uint8 cls const bool alliance = static_cast(urand(0, 1)); std::vector raceOptions; - for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race) { // skip disabled with config races if ((1 << (race - 1)) & sWorld->getIntConfig(CONFIG_CHARACTER_CREATING_DISABLED_RACEMASK)) @@ -240,7 +242,7 @@ std::string const RandomPlayerbotFactory::CreateRandomBotName(NameRaceAndGender botName += (botName.size() < 2) ? groupFormEnd[gender][rand() % 4] : ""; // Replace Catagory value with random Letter from that Catagory's Letter string for a given bot gender - for (int i = 0; i < botName.size(); i++) + for (size_t i = 0; i < botName.size(); i++) { botName[i] = groupLetter[gender][groupCategory.find(botName[i])] [rand() % groupLetter[gender][groupCategory.find(botName[i])].size()]; @@ -334,18 +336,12 @@ uint32 RandomPlayerbotFactory::CalculateTotalAccountCount() sPlayerbotAIConfig.addClassAccountPoolSize == 0 ? 2 : -1); if (!res || res->Fetch()[0].Get() == 0) - { break; - } std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Extra 50ms fixed delay for safety. } } - // Checks if randomBotAccountCount is set, otherwise calculate it dynamically. - if (sPlayerbotAIConfig.randomBotAccountCount > 0) - return sPlayerbotAIConfig.randomBotAccountCount; - // Check existing account types uint32 existingRndBotAccounts = 0; uint32 existingAddClassAccounts = 0; @@ -366,16 +362,14 @@ uint32 RandomPlayerbotFactory::CalculateTotalAccountCount() } while (typeCheck->NextRow()); } - // Determine divisor based on Death Knight login eligibility and requested A&H faction ratio + // Determine divisor based on Death Knight availability and requested A&H faction ratio int divisor = CalculateAvailableCharsPerAccount(); // Calculate max bots int maxBots = sPlayerbotAIConfig.maxRandomBots; - // Take periodic online - offline into account + // Take periodic online/offline into account if (sPlayerbotAIConfig.enablePeriodicOnlineOffline) - { maxBots *= sPlayerbotAIConfig.periodicOnlineOfflineRatio; - } // Calculate number of accounts needed for RNDbots // Result is rounded up for maxBots not cleanly divisible by the divisor @@ -416,11 +410,22 @@ uint32 RandomPlayerbotFactory::CalculateTotalAccountCount() } // Return existing total plus any additional accounts needed - return existingTotal + additionalAccountsNeeded; + uint32 calculatedTotal = existingTotal + additionalAccountsNeeded; + + // Manually set randomBotAccountCount meets the requirements + if (sPlayerbotAIConfig.randomBotAccountCount >= calculatedTotal) + return sPlayerbotAIConfig.randomBotAccountCount; + // Manually set randomBotAccountCount doesn't meet the requirements. Using calculated value + if (sPlayerbotAIConfig.randomBotAccountCount > 0) + LOG_WARN("playerbots", "RandomBotAccountCount ({}) is lower than the required calculated value ({}). Using the calculated value instead.", + sPlayerbotAIConfig.randomBotAccountCount, calculatedTotal); + + return calculatedTotal; } uint32 RandomPlayerbotFactory::CalculateAvailableCharsPerAccount() { + // Death Knight availability according to their login eligibility, and if WotLK is enabled at all. bool noDK = sPlayerbotAIConfig.disableDeathKnightLogin || sWorld->getIntConfig(CONFIG_EXPANSION) != EXPANSION_WRATH_OF_THE_LICH_KING; uint32 availableChars = noDK ? 9 : 10; @@ -434,11 +439,9 @@ uint32 RandomPlayerbotFactory::CalculateAvailableCharsPerAccount() float unavailableRatio = static_cast((std::max(hordeRatio, allianceRatio) - std::min(hordeRatio, allianceRatio))) / (std::max(hordeRatio, allianceRatio) * 2); + // Conservative floor to ensure enough characters (may result in more accounts than needed). if (unavailableRatio != 0) - { - // conservative floor to ensure enough chars (may result in more accounts than needed) availableChars = availableChars - availableChars * unavailableRatio; - } return availableChars; } @@ -619,7 +622,7 @@ void RandomPlayerbotFactory::CreateRandomBots() else password = accountName; - AccountMgr::CreateAccount(accountName, password); + sAccountMgr->CreateAccount(accountName, password); LOG_DEBUG("playerbots", "Account {} created for random bots", accountName.c_str()); } diff --git a/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.h b/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.h index 92f2c9f33..5620e991f 100644 --- a/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.h +++ b/modules/mod-playerbots/src/Bot/Factory/RandomPlayerbotFactory.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANDOMPLAYERBOTFACTORY_H -#define _PLAYERBOT_RANDOMPLAYERBOTFACTORY_H +#ifndef PLAYERBOTS_RANDOMPLAYERBOTFACTORY_H +#define PLAYERBOTS_RANDOMPLAYERBOTFACTORY_H #include #include diff --git a/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp b/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp index af4c1f2c4..cd947c82b 100644 --- a/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp +++ b/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp @@ -15,6 +15,7 @@ #include "ChannelMgr.h" #include "CharacterPackets.h" #include "ChatHelper.h" +#include "CheckMountStateAction.h" #include "Common.h" #include "CreatureData.h" #include "EmoteAction.h" @@ -37,11 +38,13 @@ #include "ObjectMgr.h" #include "PerfMonitor.h" #include "Player.h" +#include "PlayerbotTextMgr.h" #include "PlayerbotAIConfig.h" #include "PlayerbotMgr.h" #include "PlayerbotGuildMgr.h" #include "Playerbots.h" #include "PositionValue.h" +#include "RBAC.h" #include "RandomPlayerbotMgr.h" #include "SayAction.h" #include "ScriptMgr.h" @@ -55,7 +58,8 @@ #include "UpdateTime.h" #include "Vehicle.h" -const int SPELL_TITAN_GRIP = 49152; +constexpr uint32 SPELL_TITAN_GRIP = 49152; +constexpr uint32 SPELL_DK_FROST_PRESENCE = 48263; std::vector PlayerbotAI::dispel_whitelist = { "mutating injection", @@ -119,7 +123,7 @@ PlayerbotAI::PlayerbotAI() for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { - allowActiveCheckTimer[i] = time(nullptr); + allowActiveCheckTimer[i] = 0; allowActive[i] = false; } } @@ -137,19 +141,20 @@ PlayerbotAI::PlayerbotAI(Player* bot) for (uint8 i = 0; i < MAX_ACTIVITY_TYPE; i++) { - allowActiveCheckTimer[i] = time(nullptr); + allowActiveCheckTimer[i] = 0; allowActive[i] = false; } accountId = bot->GetSession()->GetAccountId(); - aiObjectContext = AiFactory::createAiObjectContext(bot, this); engines[BOT_STATE_COMBAT] = AiFactory::createCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_NON_COMBAT] = AiFactory::createNonCombatEngine(bot, this, aiObjectContext); engines[BOT_STATE_DEAD] = AiFactory::createDeadEngine(bot, this, aiObjectContext); + if (sPlayerbotAIConfig.applyInstanceStrategies) ApplyInstanceStrategies(bot->GetMapId()); + currentEngine = engines[BOT_STATE_NON_COMBAT]; currentState = BOT_STATE_NON_COMBAT; @@ -264,77 +269,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(GetReactDelay()); + YieldThread(bot, GetReactDelay()); return; } - GameObject* goSpellTarget = currentSpell->m_targets.GetGOTarget(); - - if (goSpellTarget && !goSpellTarget->isSpawned()) + const SpellInfo* spellInfo = currentSpell->GetSpellInfo(); + if (spellInfo) { - InterruptSpell(); - YieldThread(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(); - YieldThread(GetReactDelay()); + 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 - YieldThread(GetReactDelay()); + 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 @@ -368,7 +400,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) // Update internal AI UpdateAIInternal(elapsed, minimal); - YieldThread(GetReactDelay()); + YieldThread(bot, GetReactDelay()); } // Helper function for UpdateAI to check group membership and handle removal if necessary @@ -383,12 +415,10 @@ void PlayerbotAI::UpdateAIGroupMaster() Group* group = bot->GetGroup(); - bool IsRandomBot = sRandomPlayerbotMgr.IsRandomBot(bot); - // If bot is not in group verify that for is RandomBot before clearing master and resetting. if (!group) { - if (master && IsRandomBot) + if (master && sRandomPlayerbotMgr.IsRandomBot(bot)) { SetMaster(nullptr); Reset(true); @@ -399,7 +429,7 @@ void PlayerbotAI::UpdateAIGroupMaster() // Bot in BG, but master no longer part of a group: release master // Exclude alt and addclass bots as they rely on current (real player) master, security-wise. - if (bot->InBattleground() && IsRandomBot && master && !master->GetGroup()) + if (bot->InBattleground() && sRandomPlayerbotMgr.IsRandomBot(bot) && master && !master->GetGroup()) SetMaster(nullptr); PlayerbotAI* masterBotAI = nullptr; @@ -420,9 +450,11 @@ void PlayerbotAI::UpdateAIGroupMaster() botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); if (botAI->GetMaster() == botAI->GetGroupLeader()) - botAI->TellMaster("Привет! Я иду за тобой."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "hello_follow", "Hello, I follow you!", {})); else - botAI->TellMaster(!urand(0, 2) ? "Hello!" : "Hi!"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "hello", "Hello!", {})); } else { @@ -445,9 +477,11 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal if (!bot->GetMap()) return; // instances are created and destroyed on demand + // kinda expensive call to make on every single updateAI, do we really need this information? std::string const mapString = WorldPosition(bot).isOverworld() ? std::to_string(bot->GetMapId()) : "I"; PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_TOTAL, "PlayerbotAI::UpdateAIInternal " + mapString); + ExternalEventHelper helper(aiObjectContext); // chat replies @@ -460,7 +494,7 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal continue; } - ChatReplyAction::ChatReplyDo(bot, it->m_type, it->m_guid1, it->m_guid2, it->m_msg, it->m_chanName, it->m_name); + ChatReplyAction::ChatReplyDo(bot, it->m_type, it->m_guid1, it->m_msg, it->m_chanName, it->m_name); it = chatReplies.erase(it); } @@ -475,7 +509,7 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal logout = true; if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || bot->HasUnitState(UNIT_STATE_IN_FLIGHT) || - botWorldSessionPtr->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)) + botWorldSessionPtr->HasPermission(rbac::RBAC_PERM_INSTANT_LOGOUT)) { logout = true; } @@ -483,7 +517,7 @@ void PlayerbotAI::UpdateAIInternal([[maybe_unused]] uint32 elapsed, bool minimal if (master && (master->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING) || master->HasUnitState(UNIT_STATE_IN_FLIGHT) || (master->GetSession() && - master->GetSession()->GetSecurity() >= (AccountTypes)sWorld->getIntConfig(CONFIG_INSTANT_LOGOUT)))) + master->GetSession()->HasPermission(rbac::RBAC_PERM_INSTANT_LOGOUT)))) { logout = true; } @@ -824,7 +858,8 @@ void PlayerbotAI::Reset(bool full) { WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); - TellMaster("Выход из игры отменён!"); + TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "logout_cancel", "Logout cancelled!", {})); } currentEngine = engines[BOT_STATE_NON_COMBAT]; @@ -836,6 +871,7 @@ void PlayerbotAI::Reset(bool full) aiObjectContext->GetValue("current target")->Set(nullptr); aiObjectContext->GetValue("prioritized targets")->Reset(); aiObjectContext->GetValue("pull target")->Set(ObjectGuid::Empty); + aiObjectContext->GetValue("pull strategy target")->Set(ObjectGuid::Empty); aiObjectContext->GetValue("rpg target")->Set(GuidPosition()); aiObjectContext->GetValue("loot target")->Set(LootObject()); aiObjectContext->GetValue("lfg proposal")->Set(0); @@ -1015,28 +1051,58 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro } else if (filtered == "logout") { - if (!bot->GetSession()->isLogingOut()) + if (bot->GetSession()->isLogingOut()) + return; + + // Verify the command came from this bot's master. Also handles nullptr + if (fromPlayer != master) { if (type == CHAT_MSG_WHISPER) - TellMaster("Я выхожу из игры!"); + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bot_not_your_master", "You are not my master!", {}); + bot->Whisper(message, LANG_UNIVERSAL, fromPlayer); + } + return; + } - PlayerbotMgr* masterBotMgr = nullptr; - if (master) - masterBotMgr = GET_PLAYERBOT_MGR(master); - if (masterBotMgr) - masterBotMgr->LogoutPlayerBot(bot->GetGUID()); + PlayerbotMgr* masterBotMgr = GET_PLAYERBOT_MGR(master); + if (!masterBotMgr) + return; + + // Only respond if this bot is in master's collection (alt/addclass) + if (masterBotMgr->GetPlayerBot(bot->GetGUID())) + { + if (type == CHAT_MSG_WHISPER) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "logout_start", "I'm logging out!", {}); + TellMaster(message); + } + + masterBotMgr->LogoutPlayerBot(bot->GetGUID()); + } + else if (type == CHAT_MSG_WHISPER) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "bot_rndbot_no_logout", "You can't command me to logout!", {}); + TellMaster(message); } } else if (filtered == "logout cancel") { - if (bot->GetSession()->isLogingOut()) - { - if (type == CHAT_MSG_WHISPER) - TellMaster("Выход из игры отменён!"); + if (!bot->GetSession()->isLogingOut()) + return; - WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); - bot->GetSession()->HandleLogoutCancelOpcode(data); + if (type == CHAT_MSG_WHISPER) + { + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "logout_cancel", "Logout cancelled!", {}); + TellMaster(message); } + + WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); + bot->GetSession()->HandleLogoutCancelOpcode(data); } else { @@ -1119,6 +1185,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE) return; + if (lang == LANG_ADDON) + return; + if (p.GetOpcode() == SMSG_GM_MESSAGECHAT) { p >> textLen; @@ -1168,26 +1237,19 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) return; - if (lang == LANG_ADDON) - return; + auto itemIds = GetChatHelper()->ExtractAllItemIds(message); if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && - (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || - GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && + (itemIds.size() > 0 || GetChatHelper()->ExtractAllQuestIds(message).size() > 0) && sPlayerbotAIConfig.toxicLinksRepliesChance) { if (urand(0, 50) > 0 || urand(1, 100) > sPlayerbotAIConfig.toxicLinksRepliesChance) - { return; - } } - else if ((GetChatHelper()->ExtractAllItemIds(message).count(19019) && - sPlayerbotAIConfig.thunderfuryRepliesChance)) + else if (itemIds.count(19019) && sPlayerbotAIConfig.thunderfuryRepliesChance) { if (urand(0, 60) > 0 || urand(1, 100) > sPlayerbotAIConfig.thunderfuryRepliesChance) - { return; - } } else { @@ -1249,17 +1311,10 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed; if (horizontalSpeed <= 0.1f) - { horizontalSpeed = 0.11f; - } verticalSpeed = -verticalSpeed; - // high vertical may result in stuck as bot can not handle gravity - if (verticalSpeed > 35.0f) - break; - // stop casting - InterruptSpell(); - // stop movement + InterruptSpell(); bot->StopMoving(); bot->GetMotionMaster()->Clear(); @@ -1314,6 +1369,17 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) // */ return; } + case SMSG_DISMOUNT: + { + WorldPacket p(packet); + p.rpos(0); + ObjectGuid guid; + p >> guid.ReadAsPacked(); + if (guid != bot->GetGUID()) + return; + CheckMountStateAction::CompleteDismount(bot); + return; + } default: botOutgoingPacketHandlers.AddPacket(packet); } @@ -1426,6 +1492,7 @@ void PlayerbotAI::DoNextAction(bool min) aiObjectContext->GetValue("current target")->Set(nullptr); aiObjectContext->GetValue("enemy player target")->Set(nullptr); aiObjectContext->GetValue("pull target")->Set(ObjectGuid::Empty); + aiObjectContext->GetValue("pull strategy target")->Set(ObjectGuid::Empty); aiObjectContext->GetValue("loot target")->Set(LootObject()); ChangeEngine(BOT_STATE_DEAD); @@ -1521,6 +1588,27 @@ void PlayerbotAI::ClearStrategies(BotState type) e->removeAllStrategies(); } +// Resets only the combat or non-combat engine: wipe strategies, repopulate with class/spec defaults, +// re-apply current map's instance strategy (if any), and call Init() to rebuild trigger/action lists. +void PlayerbotAI::SelectiveResetStrategies(BotState type) +{ + Engine* e = engines[type]; + if (!e) + return; + + e->removeAllStrategies(); + + if (type == BOT_STATE_COMBAT) + AiFactory::AddDefaultCombatStrategies(bot, this, e); + else if (type == BOT_STATE_NON_COMBAT) + AiFactory::AddDefaultNonCombatStrategies(bot, this, e); + + if (sPlayerbotAIConfig.applyInstanceStrategies) + ApplyInstanceStrategies(bot->GetMapId()); + + e->Init(); +} + std::vector PlayerbotAI::GetStrategies(BotState type) { Engine* e = engines[type]; @@ -1532,6 +1620,22 @@ std::vector PlayerbotAI::GetStrategies(BotState type) void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) { + static const std::vector allInstanceStrategies = + { + "aq20", "blacktemple", "bwl", "gruulslair", "hyjal", "icc", "karazhan", + "magtheridon", "moltencore", "naxx", "onyxia", "rs", "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" + }; + + for (const std::string& strat : allInstanceStrategies) + { + engines[BOT_STATE_COMBAT]->removeStrategy(strat); + engines[BOT_STATE_NON_COMBAT]->removeStrategy(strat); + } + std::string strategyName; switch (mapId) { @@ -1550,15 +1654,33 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) case 532: strategyName = "karazhan"; // Karazhan break; + case 533: + strategyName = "naxx"; // Naxxramas + break; + case 534: + strategyName = "hyjal"; // The Battle for Mount Hyjal (Hyjal Summit) + break; case 544: strategyName = "magtheridon"; // Magtheridon's Lair break; case 548: strategyName = "ssc"; // Serpentshrine Cavern break; + case 550: + strategyName = "tempestkeep"; // Tempest Keep: The Eye + break; + case 558: + strategyName = "tbc-ac"; // Auchindoun: Auchenai Crypts + break; + case 564: + strategyName = "blacktemple"; // Black Temple + break; case 565: strategyName = "gruulslair"; // Gruul's Lair break; + case 568: + strategyName = "zulaman"; // Zul'Aman + break; case 574: strategyName = "wotlk-uk"; // Utgarde Keep break; @@ -1622,13 +1744,19 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) case 668: strategyName = "wotlk-hor"; // Halls of Reflection break; + case 724: + strategyName = "rs"; // Ruby Sanctum + break; default: break; } + if (strategyName.empty()) return; + engines[BOT_STATE_COMBAT]->addStrategy(strategyName); engines[BOT_STATE_NON_COMBAT]->addStrategy(strategyName); + if (tellMaster && !strategyName.empty()) { std::ostringstream out; @@ -1637,6 +1765,11 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) } } +bool PlayerbotAI::HasTargetExclusions() const +{ + return engines[BOT_STATE_COMBAT] && engines[BOT_STATE_COMBAT]->HasTargetExclusions(); +} + bool PlayerbotAI::DoSpecificAction(std::string const name, Event event, bool silent, std::string const qualifier) { std::ostringstream out; @@ -1726,7 +1859,12 @@ bool PlayerbotAI::ContainsStrategy(StrategyType type) bool PlayerbotAI::HasStrategy(std::string const name, BotState type) { return engines[type]->HasStrategy(name); } -void PlayerbotAI::ResetStrategies(bool load) +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++) engines[i]->removeAllStrategies(); @@ -1797,98 +1935,104 @@ bool PlayerbotAI::IsCombo(Player* player) bool PlayerbotAI::IsRangedDps(Player* player, bool bySpec) { return IsRanged(player, bySpec) && IsDps(player, bySpec); } -bool PlayerbotAI::IsAssistHealOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) { + if (!IsHeal(player)) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsHeal(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsHeal(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsHeal(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - - return false; + return playerIndex == index; } -bool PlayerbotAI::IsAssistRangedDpsOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) { + if (!IsRangedDps(player)) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsRangedDps(member)) continue; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + bool isAssistant = group->IsAssistant(member->GetGUID()); - if (group->IsAssistant(member->GetGUID()) && IsRangedDps(member)) + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) { - if (index == counter) - return player == member; - counter++; + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsRangedDps(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - - return false; + return playerIndex == index; } bool PlayerbotAI::HasAggro(Unit* unit) @@ -1896,7 +2040,7 @@ bool PlayerbotAI::HasAggro(Unit* unit) if (!IsValidUnit(unit)) return false; - bool isMT = IsMainTank(bot); + bool isMT = IsExplicitMainTank(bot); Unit* victim = unit->GetVictim(); if (victim && (victim->GetGUID() == bot->GetGUID() || (!isMT && victim->ToPlayer() && IsTank(victim->ToPlayer())))) { @@ -1905,6 +2049,11 @@ bool PlayerbotAI::HasAggro(Unit* unit) return false; } +bool PlayerbotAI::IsMovementImpaired(Unit* unit) +{ + return unit && (unit->HasAuraType(SPELL_AURA_MOD_ROOT) || unit->IsRooted() || unit->GetSpeedRate(MOVE_RUN) < 1.0f); +} + int32 PlayerbotAI::GetAssistTankIndex(Player* player) { Group* group = player->GetGroup(); @@ -2103,7 +2252,7 @@ bool PlayerbotAI::IsTank(Player* player, bool bySpec) switch (player->getClass()) { case CLASS_DEATH_KNIGHT: - if (tab == DEATH_KNIGHT_TAB_BLOOD) + if (tab == DEATH_KNIGHT_TAB_BLOOD || player->HasAura(SPELL_DK_FROST_PRESENCE)) { return true; } @@ -2226,45 +2375,49 @@ bool PlayerbotAI::IsDps(Player* player, bool bySpec) return false; } -bool PlayerbotAI::IsMainTank(Player* player) +ObjectGuid PlayerbotAI::GetMainTankGuid(Group* group) { - Group* group = player->GetGroup(); if (!group) - { - return IsTank(player); - } + return ObjectGuid::Empty; - ObjectGuid mainTank = ObjectGuid(); Group::MemberSlotList const& slots = group->GetMemberSlots(); - for (Group::member_citerator itr = slots.begin(); itr != slots.end(); ++itr) { if (itr->flags & MEMBER_FLAG_MAINTANK) - { - mainTank = itr->guid; - break; - } - } - - if (mainTank != ObjectGuid::Empty) - { - return player->GetGUID() == mainTank; + return itr->guid; } for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) - { - continue; - } - - if (IsTank(member) && member->IsAlive()) - { - return player->GetGUID() == member->GetGUID(); - } + if (member && IsTank(member) && member->IsAlive()) + return member->GetGUID(); } + return ObjectGuid::Empty; +} + +bool PlayerbotAI::IsMainTank(Player* player) +{ + Group* group = player->GetGroup(); + if (!group) + return IsTank(player); + + ObjectGuid const mainTankGuid = GetMainTankGuid(group); + return !mainTankGuid.IsEmpty() && player->GetGUID() == mainTankGuid; +} + +bool PlayerbotAI::IsExplicitMainTank(Player* player) +{ + Group* group = player->GetGroup(); + if (!group) + return false; + + for (Group::member_citerator itr = group->GetMemberSlots().begin(); itr != group->GetMemberSlots().end(); ++itr) + { + if (itr->flags & MEMBER_FLAG_MAINTANK) + return player->GetGUID() == itr->guid; + } return false; } @@ -2281,47 +2434,31 @@ bool PlayerbotAI::IsBotMainTank(Player* player) return false; if (IsMainTank(player)) - { return true; - } Group* group = player->GetGroup(); if (!group) - { - return true; // If no group, consider the bot as main tank - } + return true; int32 botAssistTankIndex = GetAssistTankIndex(player); if (botAssistTankIndex == -1) - { return false; - } for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) { Player* member = gref->GetSource(); if (!member) - { continue; - } int32 memberAssistTankIndex = GetAssistTankIndex(member); if (memberAssistTankIndex == -1) - { continue; - } if (memberAssistTankIndex == botAssistTankIndex && player == member) - { return true; - } if (memberAssistTankIndex < botAssistTankIndex && member->GetSession()->IsBot()) - { return false; - } - - return false; } return false; @@ -2331,73 +2468,91 @@ uint32 PlayerbotAI::GetGroupTankNum(Player* player) { Group* group = player->GetGroup(); if (!group) - { return 0; - } + uint32 result = 0; for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); if (!member) - { continue; - } if (IsTank(member) && member->IsAlive()) - { result++; - } } + return result; } -bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } - -bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers) +bool PlayerbotAI::IsAssistTank(Player* player) { + if (!IsTank(player)) + return false; + Group* group = player->GetGroup(); if (!group) return false; - int counter = 0; + return player->GetGUID() != GetMainTankGuid(group); +} + +bool PlayerbotAI::IsAssistTankOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers) +{ + if (!IsTank(player)) + return false; + + Group* group = player->GetGroup(); + if (!group) + return false; + + ObjectGuid const mainTankGuid = GetMainTankGuid(group); + + if (player->GetGUID() == mainTankGuid) + return false; + + if (ignoreDeadPlayers && !player->IsAlive()) + return false; + + uint8 totalAssistants = 0; + uint8 assistantsBeforePlayer = 0; + uint8 nonAssistantsBeforePlayer = 0; + bool playerFound = false; - // First, assistants for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { Player* member = ref->GetSource(); - if (!member) - continue; - - if (ignoreDeadPlayers && !member->IsAlive()) - continue; - - if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) + if (!member || (ignoreDeadPlayers && !member->IsAlive()) || !IsTank(member) || + member->GetGUID() == mainTankGuid) { - if (index == counter) - return player == member; - counter++; + continue; + } + + bool isAssistant = group->IsAssistant(member->GetGUID()); + + if (isAssistant) + totalAssistants++; + + if (member == player) + playerFound = true; + else if (!playerFound) + { + if (isAssistant) + assistantsBeforePlayer++; + else + nonAssistantsBeforePlayer++; } } - // If not enough assistants, get non-assistants - for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) - { - Player* member = ref->GetSource(); - if (!member) - continue; + if (!playerFound) + return false; - if (ignoreDeadPlayers && !member->IsAlive()) - continue; + // If the player is an assistant, their index is just the number of assistants before them. + // If they are a non-assistant, their index is shifted by the total number of assistants. + uint8 playerIndex = group->IsAssistant(player->GetGUID()) + ? assistantsBeforePlayer : (totalAssistants + nonAssistantsBeforePlayer); - if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) - { - if (index == counter) - return player == member; - counter++; - } - } - return false; + return playerIndex == index; } namespace acore @@ -2458,7 +2613,7 @@ Player* PlayerbotAI::GetPlayer(ObjectGuid guid) uint32 GetCreatureIdForCreatureTemplateId(uint32 creatureTemplateId) { QueryResult results = - WorldDatabase.Query("SELECT guid FROM `creature` WHERE id1 = {} LIMIT 1;", creatureTemplateId); + WorldDatabase.Query("SELECT guid FROM `creature` WHERE id = {} LIMIT 1;", creatureTemplateId); if (results) { Field* fields = results->Fetch(); @@ -2479,7 +2634,7 @@ Unit* PlayerbotAI::GetUnit(CreatureData const* creatureData) uint32 spawnId = creatureData->spawnId; if (!spawnId) // workaround for CreatureData with missing spawnId (this just uses first matching creatureId in DB, // but thats ok this method is only used for battlemasters and theres only 1 of each type) - spawnId = GetCreatureIdForCreatureTemplateId(creatureData->id1); + spawnId = GetCreatureIdForCreatureTemplateId(creatureData->id); auto creatureBounds = map->GetCreatureBySpawnIdStore().equal_range(spawnId); if (creatureBounds.first == creatureBounds.second) return nullptr; @@ -2751,7 +2906,7 @@ bool PlayerbotAI::SayToParty(const std::string& msg) bool PlayerbotAI::SayToRaid(const std::string& msg) { - if (!bot->GetGroup() || bot->GetGroup()->isRaidGroup()) + if (!bot->GetGroup() || !bot->GetGroup()->isRaidGroup()) return false; WorldPacket data; @@ -2877,7 +3032,7 @@ bool PlayerbotAI::IsTellAllowed(PlayerbotSecurityLevel securityLevel) return false; if (sPlayerbotAIConfig.whisperDistance && !bot->GetGroup() && sRandomPlayerbotMgr.IsRandomBot(bot) && - master->GetSession()->GetSecurity() < SEC_GAMEMASTER && + !master->CanBeGameMaster() && (bot->GetMapId() != master->GetMapId() || ServerFacade::instance().GetDistance2d(bot, master) > sPlayerbotAIConfig.whisperDistance)) return false; @@ -2896,6 +3051,8 @@ bool PlayerbotAI::TellMaster(std::string const text, PlayerbotSecurityLevel secu { if (sPlayerbotAIConfig.randomBotSayWithoutMaster) return TellMasterNoFacing(text, securityLevel); + + return false; } if (!TellMasterNoFacing(text, securityLevel)) return false; @@ -3019,20 +3176,10 @@ bool PlayerbotAI::HasAura(std::string const name, Unit* unit, bool maxStack, boo return false; } -bool PlayerbotAI::HasAura(uint32 spellId, Unit const* unit) +bool PlayerbotAI::HasSpell(std::string const spellName) const { - if (!spellId || !unit) - return false; - - return unit->HasAura(spellId); - // for (uint8 effect = EFFECT_0; effect <= EFFECT_2; effect++) - // { - // AuraEffect const* aurEff = unit->GetAuraEffect(spellId, effect); - // if (IsRealAura(bot, aurEff, unit)) - // return true; - // } - - // return false; + uint32 const spellId = aiObjectContext->GetValue("spell id", spellName)->Get(); + return spellId && bot->HasSpell(spellId); } Aura* PlayerbotAI::GetAura(std::string const name, Unit* unit, bool checkIsOwner, bool checkDuration, int checkStack) @@ -4136,10 +4283,23 @@ void PlayerbotAI::InterruptSpell() void PlayerbotAI::RemoveAura(std::string const name) { uint32 spellid = aiObjectContext->GetValue("spell id", name)->Get(); - if (spellid && HasAura(spellid, bot)) + if (spellid && bot->HasAura(spellid)) 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)) @@ -4315,7 +4475,7 @@ bool PlayerbotAI::HasRealPlayerMaster() bool PlayerbotAI::HasActivePlayerMaster() { return master && !GET_PLAYERBOT_AI(master); } -bool PlayerbotAI::IsAlt() { return HasRealPlayerMaster() && !sRandomPlayerbotMgr.IsRandomBot(bot); } +bool PlayerbotAI::IsAltBot() { return HasRealPlayerMaster() && !sRandomPlayerbotMgr.IsRandomBot(bot); } Player* PlayerbotAI::GetGroupLeader() { @@ -4327,21 +4487,27 @@ Player* PlayerbotAI::GetGroupLeader() return master; } -uint32 PlayerbotAI::GetFixedBotNumer(uint32 maxNum, float cyclePerMin) +uint32 PlayerbotAI::GetFixedBotNumber(uint32 maxNum) { - uint32 randseed = rand32(); // Seed random number - uint32 randnum = bot->GetGUID().GetCounter() + randseed; // Semi-random but fixed number for each bot. + if (maxNum == 0) + return 0; - if (cyclePerMin > 0) - { - uint32 cycle = floor(getMSTime() / (1000)); // Semi-random number adds 1 each second. - cycle = cycle * cyclePerMin / 60; // Cycles cyclePerMin per minute. - randnum += cycle; // Make the random number cylce. - } + // Deterministic pseudo-random hash based on the bot GUID evenly distributed across active slots + uint32 id = bot->GetGUID().GetCounter(); + uint32 h = id; + h ^= h >> 16; + h *= 0x7feb352d; + h ^= h >> 15; + h *= 0x846ca68b; + h ^= h >> 16; - randnum = - (randnum % (maxNum + 1)); // Loops the randomnumber at maxNum. Bassically removes all the numbers above 99. - return randnum; // Now we have a number unique for each bot between 0 and maxNum that increases by cyclePerMin. + // Current time slot + uint32 timeSlot = (getMSTime() / 1000) / sPlayerbotAIConfig.BotActiveAloneDurationSeconds; + + // Mix timeSlot into the hash to reshuffle every rotation window + uint32 mixed = h ^ (timeSlot * 0x9e3779b9); // with multiplicative constant + + return mixed % maxNum; } /* @@ -4358,7 +4524,7 @@ enum GrouperType GrouperType PlayerbotAI::GetGrouperType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GrouperType::SOLO; @@ -4380,7 +4546,7 @@ GrouperType PlayerbotAI::GetGrouperType() GuilderType PlayerbotAI::GetGuilderType() { - uint32 grouperNumber = GetFixedBotNumer(100, 0); + uint32 grouperNumber = GetFixedBotNumber(100); if (grouperNumber < 20 && !HasRealPlayerMaster()) return GuilderType::SOLO; @@ -4403,7 +4569,6 @@ GuilderType PlayerbotAI::GetGuilderType() bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) { float sqRange = range * range; - bool nearPlayer = false; for (auto& player : sRandomPlayerbotMgr.GetPlayers()) { if (!player->IsGameMaster() || player->isGMVisible()) @@ -4412,19 +4577,18 @@ bool PlayerbotAI::HasPlayerNearby(WorldPosition* pos, float range) continue; if (pos->sqDistance(WorldPosition(player)) < sqRange) - nearPlayer = true; + return true; - // if player is far check farsight/cinematic camera WorldObject* viewObj = player->GetViewpoint(); if (viewObj && viewObj != player) { if (pos->sqDistance(WorldPosition(viewObj)) < sqRange) - nearPlayer = true; + return true; } } } - return nearPlayer; + return false; } bool PlayerbotAI::HasPlayerNearby(float range) @@ -4433,173 +4597,97 @@ bool PlayerbotAI::HasPlayerNearby(float range) return HasPlayerNearby(&botPos, range); }; -bool PlayerbotAI::HasManyPlayersNearby(uint32 trigerrValue, float range) -{ - float sqRange = range * range; - uint32 found = 0; - - for (auto& player : sRandomPlayerbotMgr.GetPlayers()) - { - if ((!player->IsGameMaster() || player->isGMVisible()) && ServerFacade::instance().GetDistance2d(player, bot) < sqRange) - { - found++; - - if (found >= trigerrValue) - return true; - } - } - - return false; -} - -inline bool HasRealPlayers(Map* map) -{ - Map::PlayerList const& players = map->GetPlayers(); - if (players.IsEmpty()) - { - return false; - } - - for (auto const& itr : players) - { - Player* player = itr.GetSource(); - if (!player || !player->IsVisible()) - { - continue; - } - - PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); - if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) - { - return true; - } - } - - return false; -} - -inline bool ZoneHasRealPlayers(Player* bot) -{ - Map* map = bot->GetMap(); - if (!bot || !map) - { - return false; - } - - for (Player* player : sRandomPlayerbotMgr.GetPlayers()) - { - if (player->GetMapId() != bot->GetMapId()) - continue; - - if (player->IsGameMaster() && !player->IsVisible()) - { - continue; - } - - if (player->GetZoneId() == bot->GetZoneId()) - { - PlayerbotAI* botAI = GET_PLAYERBOT_AI(player); - if (!botAI || botAI->IsRealPlayer() || botAI->HasRealPlayerMaster()) - { - return true; - } - } - } - - return false; -} - bool PlayerbotAI::AllowActive(ActivityType activityType) { - // Early return if bot is in invalid state + // bot is in an invalid state, not safe to process if (!bot || !bot->GetSession() || !bot->IsInWorld() || bot->IsBeingTeleported() || bot->GetSession()->isLogingOut() || bot->IsDuringRemoveFromWorld()) return false; - // when botActiveAlone is 100% and smartScale disabled - if (sPlayerbotAIConfig.botActiveAlone >= 100 && !sPlayerbotAIConfig.botActiveAloneSmartScale) - { + // always allow packet handling (e.g. group invites, trade, loot, friend requests etc) + if (activityType == PACKET_ACTIVITY) return true; - } - // Is in combat. Always defend yourself. + // all bots forced active, no rotation or scaling needed + if (sPlayerbotAIConfig.botActiveAlone >= 100 && !sPlayerbotAIConfig.botActiveAloneSmartScale) + return true; + + // bot is in combat, always defend yourself if (activityType != OUT_OF_PARTY_ACTIVITY && activityType != PACKET_ACTIVITY) { if (bot->IsInCombat()) - { return true; - } } - // only keep updating till initializing time has completed, - // which prevents unneeded expensive GameTime calls. - if (_isBotInitializing) - { - _isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig.maxRandomBots * 0.11; - - // no activity allowed during bot initialization - if (_isBotInitializing) - { - return false; - } - } - - // General exceptions - if (activityType == PACKET_ACTIVITY) - { - return true; - } - - // bg, raid, dungeon + // bot is inside a BG, dungeon, or raid — always active if (!WorldPosition(bot).isOverworld()) - { return true; - } - // bot map has active players. - if (sPlayerbotAIConfig.BotActiveAloneForceWhenInMap) - { - if (HasRealPlayers(bot->GetMap())) - { - return true; - } - } + // bot is waiting in a BG queue — stay active to speed up join + if (bot->InBattlegroundQueue()) + return true; - // bot zone has active players. - if (sPlayerbotAIConfig.BotActiveAloneForceWhenInZone) - { - if (ZoneHasRealPlayers(bot)) - { - return true; - } - } - - // when in real guild + // bot is in a guild that contains a real player if (sPlayerbotAIConfig.BotActiveAloneForceWhenInGuild) { - if (IsInRealGuild()) - { + if (IsInRealGuild()) // checks cache list return true; + } + + // a real player is in the same zone (e.g. Elwynn Forest), same continent or within configured yard radius + // combined into a single loop to multiple iterations since this function is called so often + bool checkMap = sPlayerbotAIConfig.BotActiveAloneForceWhenInMap; + bool checkZone = sPlayerbotAIConfig.BotActiveAloneForceWhenInZone; + bool checkRadius = sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius > 0; + if (checkMap || checkZone || checkRadius) + { + uint32 botMapId = bot->GetMapId(); + uint32 botZoneId = checkZone ? bot->GetZoneId() : 0; + float sqRange = 0.0f; + WorldPosition botPos(bot); + if (checkRadius) + { + float range = static_cast(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius); + sqRange = range * range; + } + + for (auto& player : sRandomPlayerbotMgr.GetPlayers()) + { + if (!player || player->GetMapId() != botMapId) + continue; + + bool isGM = player->IsGameMaster(); + + // map check + if (checkMap && !(isGM && !player->IsVisible())) + return true; + + // zone check + if (checkZone && !(isGM && !player->IsVisible()) && player->GetZoneId() == botZoneId) + return true; + + // radius check + if (checkRadius && (!isGM || player->isGMVisible())) + { + if (botPos.sqDistance(WorldPosition(player)) < sqRange) + return true; + + WorldObject* viewObj = player->GetViewpoint(); + if (viewObj && viewObj != player && botPos.sqDistance(WorldPosition(viewObj)) < sqRange) + return true; + } } } - // Player is near. Always active. - if (HasPlayerNearby(sPlayerbotAIConfig.BotActiveAloneForceWhenInRadius)) - { - return true; - } - - // Has player master. Always active. + // bot has a real player master (not another bot) if (GetMaster()) { PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(GetMaster()); if (!masterBotAI || masterBotAI->IsRealPlayer()) - { return true; - } } - // if grouped up + // bot is grouped with a real player (or a bot owned by one) Group* group = bot->GetGroup(); if (group) { @@ -4610,52 +4698,37 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) continue; if (member == bot) - { continue; - } PlayerbotAI* memberBotAI = GET_PLAYERBOT_AI(member); - { - if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) - { - return true; - } - } + // group member is a real player or owned by one — stay active + if (!memberBotAI || memberBotAI->HasRealPlayerMaster()) + return true; + + // if group leader (bot) is inactive, follow suit if (group->IsLeader(member->GetGUID())) { if (!memberBotAI->AllowActivity(PARTY_ACTIVITY)) - { return false; - } } } } - // In bg queue. Speed up bg queue/join. - if (bot->InBattlegroundQueue()) - { - return true; - } - + // bot is in LFG queue — stay active bool isLFG = false; if (group) { if (sLFGMgr->GetState(group->GetGUID()) != lfg::LFG_STATE_NONE) - { isLFG = true; - } } if (sLFGMgr->GetState(bot->GetGUID()) != lfg::LFG_STATE_NONE) - { isLFG = true; - } - if (isLFG) - { - return true; - } - // HasFriend + if (isLFG) + return true; + + // a real player has this bot on their friends list if (sPlayerbotAIConfig.BotActiveAloneForceWhenIsFriend) { // shouldnt be needed analyse in future @@ -4672,41 +4745,27 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) if (!playerAI || !playerAI->IsRealPlayer()) continue; - // if a real player has the bot as a friend PlayerSocial* social = player->GetSocial(); if (social && social->HasFriend(bot->GetGUID())) return true; } } - // Force the bots to spread - if (activityType == OUT_OF_PARTY_ACTIVITY || activityType == GRIND_ACTIVITY) - { - if (HasManyPlayersNearby(10, 40)) - { - return true; - } - } - - // Bots don't need react to PathGenerator activities + // pathfinding only runs for bots forced active by the rules above — + // skip it for bots that would only be active via random rotation if (activityType == DETAILED_MOVE_ACTIVITY) - { return false; - } + // ####################################################################################### + // Acitivity throttling logic + // ####################################################################################### if (sPlayerbotAIConfig.botActiveAlone <= 0) - { return false; - } - // ####################################################################################### - // All mandatory conditations are checked to be active or not, from here the remaining - // situations are usable for scaling when enabled. - // ####################################################################################### - - // Below is code to have a specified % of bots active at all times. - // The default is 100%. With 1% of all bots going active or inactive each minute. + // base threshold capped at 100 uint32 mod = sPlayerbotAIConfig.botActiveAlone > 100 ? 100 : sPlayerbotAIConfig.botActiveAlone; + + // reduce threshold based on server tick time when SmartScale is enabled if (sPlayerbotAIConfig.botActiveAloneSmartScale && bot->GetLevel() >= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMinLevel && bot->GetLevel() <= sPlayerbotAIConfig.botActiveAloneSmartScaleWhenMaxLevel) @@ -4714,34 +4773,27 @@ bool PlayerbotAI::AllowActive(ActivityType activityType) mod = AutoScaleActivity(mod); } - uint32 ActivityNumber = - GetFixedBotNumer(100, sPlayerbotAIConfig.botActiveAlone * static_cast(mod) / 100 * 0.01f); - - return ActivityNumber <= - (sPlayerbotAIConfig.botActiveAlone * mod) / - 100; // The given percentage of bots should be active and rotate 1% of those active bots each minute. + // deterministic rotation — bot is active if its hash falls below the threshold + uint32 ActivityNumber = GetFixedBotNumber(100); + return ActivityNumber < mod; } bool PlayerbotAI::AllowActivity(ActivityType activityType, bool checkNow) { const int activityIndex = static_cast(activityType); - // Unknown/out-of-range avoid blocking, added logging for further analysing should not happen in the first place. - if (activityIndex <= 0 || activityIndex >= MAX_ACTIVITY_TYPE) - { - LOG_ERROR("playerbots", "AllowActivity received invalid activity type value: {}", activityIndex); - return true; - } - if (!allowActiveCheckTimer[activityIndex]) - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); - if (!checkNow && time(nullptr) < (allowActiveCheckTimer[activityIndex] + 5)) + // 4500ms base + 0–499ms per-bot offset = 4500–4999ms, capping at just under 5 seconds + uint32 offset = bot->GetGUID().GetCounter() % 500; + + if (!checkNow && getMSTime() < (allowActiveCheckTimer[activityIndex] + 4500 + offset)) return allowActive[activityIndex]; const bool allowed = AllowActive(activityType); allowActive[activityIndex] = allowed; - allowActiveCheckTimer[activityIndex] = time(nullptr); + allowActiveCheckTimer[activityIndex] = getMSTime(); return allowed; } @@ -5535,7 +5587,7 @@ Item* PlayerbotAI::FindStoneFor(Item* weapon) const SOLID_SHARPENING_STONE, HEAVY_SHARPENING_STONE, COARSE_SHARPENING_STONE, ROUGH_SHARPENING_STONE}; static const std::vector uPrioritizedWeightStoneIds = { - ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, + ADAMANTITE_WEIGHTSTONE, FEL_WEIGHTSTONE, ELEMENTAL_SHARPENING_STONE, DENSE_WEIGHTSTONE, SOLID_WEIGHTSTONE, HEAVY_WEIGHTSTONE, COARSE_WEIGHTSTONE, ROUGH_WEIGHTSTONE}; Item* stone = nullptr; @@ -5938,29 +5990,6 @@ void PlayerbotAI::EnchantItemT(uint32 spellid, uint8 slot) LOG_INFO("playerbots", "{}: items was enchanted successfully!", bot->GetName().c_str()); } -uint32 PlayerbotAI::GetBuffedCount(Player* player, std::string const spellname) -{ - uint32 bcount = 0; - - if (Group* group = bot->GetGroup()) - { - for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next()) - { - Player* member = gref->GetSource(); - if (!member || !member->IsInWorld()) - continue; - - if (!member->IsInSameRaidWith(player)) - continue; - - if (HasAura(spellname, member, true)) - bcount++; - } - } - - return bcount; -} - int32 PlayerbotAI::GetNearGroupMemberCount(float dis) { int count = 1; // yourself @@ -6471,7 +6500,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st return ChatChannelSource::SRC_UNDEFINED; } -bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp) +bool PlayerbotAI::StarterLevelDistanceCheck(Player* player, const WorldLocation& loc, bool fromStartUp) { if (player->GetLevel() > 16) return true; @@ -6692,7 +6721,7 @@ float PlayerbotAI::GetItemScoreMultiplier(ItemQualities quality) return 1.0f; } -bool PlayerbotAI::IsHealingSpell(uint32 spellFamilyName, flag96 spellFalimyFlags) +bool PlayerbotAI::IsHealingSpell(uint32 spellFamilyName, flag96 spellFamilyFlags) { if (!spellFamilyName) return false; @@ -6737,7 +6766,7 @@ bool PlayerbotAI::IsHealingSpell(uint32 spellFamilyName, flag96 spellFalimyFlags default: break; } - return spellFalimyFlags & healingFlags; + return spellFamilyFlags & healingFlags; } SpellFamilyNames PlayerbotAI::Class2SpellFamilyName(uint8 cls) diff --git a/modules/mod-playerbots/src/Bot/PlayerbotAI.h b/modules/mod-playerbots/src/Bot/PlayerbotAI.h index 05710548d..b4186ce0f 100644 --- a/modules/mod-playerbots/src/Bot/PlayerbotAI.h +++ b/modules/mod-playerbots/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 PLAYERBOTS_PLAYERBOTAI_H +#define PLAYERBOTS_PLAYERBOTAI_H #include @@ -28,6 +28,7 @@ class AiObjectContext; class Creature; class Engine; class ExternalEventHelper; +class Group; class Gameobject; class Item; class ObjectGuid; @@ -404,8 +405,11 @@ public: std::string const qualifier = ""); void ChangeStrategy(std::string const name, BotState type); void ClearStrategies(BotState type); + void SelectiveResetStrategies(BotState type); std::vector GetStrategies(BotState type); + Strategy* GetStrategy(std::string const name, BotState type); void ApplyInstanceStrategies(uint32 mapId, bool tellMaster = false); + bool HasTargetExclusions() const; void EvaluateHealerDpsStrategy(); bool ContainsStrategy(StrategyType type); bool HasStrategy(std::string const name, BotState type); @@ -422,14 +426,17 @@ public: static bool IsCaster(Player* player, bool bySpec = false); static bool IsRangedDps(Player* player, bool bySpec = false); static bool IsCombo(Player* player); - static bool IsBotMainTank(Player* player); + static ObjectGuid GetMainTankGuid(Group* group); static bool IsMainTank(Player* player); + static bool IsExplicitMainTank(Player* player); + static bool IsBotMainTank(Player* player); static uint32 GetGroupTankNum(Player* player); static bool IsAssistTank(Player* player); - static bool IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); - static bool IsAssistHealOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); - static bool IsAssistRangedDpsOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); + static bool IsAssistTankOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); + static bool IsAssistHealOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); + static bool IsAssistRangedDpsOfIndex(Player* player, uint8 index, bool ignoreDeadPlayers = false); bool HasAggro(Unit* unit); + bool IsMovementImpaired(Unit* unit); static int32 GetAssistTankIndex(Player* player); int32 GetGroupSlotIndex(Player* player); int32 GetRangedIndex(Player* player); @@ -470,6 +477,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); @@ -489,11 +497,11 @@ public: void ImbueItem(Item* item, Unit* target); void ImbueItem(Item* item); void EnchantItemT(uint32 spellid, uint8 slot); - uint32 GetBuffedCount(Player* player, std::string const spellname); int32 GetNearGroupMemberCount(float dis = sPlayerbotAIConfig.sightDistance); virtual bool CanCastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr); virtual bool CastSpell(std::string const name, Unit* target, Item* itemTarget = nullptr); + virtual bool HasSpell(std::string const spellName) const; virtual bool HasAura(std::string const spellName, Unit* player, bool maxStack = false, bool checkIsOwner = false, int maxAmount = -1, bool checkDuration = false); virtual bool HasAnyAuraOf(Unit* player, ...); @@ -506,7 +514,6 @@ public: bool CanCastSpell(uint32 spellid, float x, float y, float z, bool checkHasSpell = true, Item* itemTarget = nullptr); - bool HasAura(uint32 spellId, Unit const* player); Aura* GetAura(std::string const spellName, Unit* unit, bool checkIsOwner = false, bool checkDuration = false, int checkStack = -1); bool CastSpell(uint32 spellId, Unit* target, Item* itemTarget = nullptr); @@ -538,15 +545,13 @@ public: bool HasActivePlayerMaster(); // Get the group leader or the master of the bot. // Checks if the bot is summoned as alt of a player - bool IsAlt(); + bool IsAltBot(); Player* GetGroupLeader(); - // Returns a semi-random (cycling) number that is fixed for each bot. - uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); + uint32 GetFixedBotNumber(uint32 maxNum = 100); GrouperType GetGrouperType(); GuilderType GetGuilderType(); bool HasPlayerNearby(WorldPosition* pos, float range = sPlayerbotAIConfig.reactDistance); bool HasPlayerNearby(float range = sPlayerbotAIConfig.reactDistance); - bool HasManyPlayersNearby(uint32 trigerrValue = 20, float range = sPlayerbotAIConfig.sightDistance); bool AllowActive(ActivityType activityType); bool AllowActivity(ActivityType activityType = ALL_ACTIVITY, bool checkNow = false); uint32 AutoScaleActivity(uint32 mod); @@ -556,7 +561,7 @@ public: bool IsSafe(WorldObject* obj); ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName); - bool CheckLocationDistanceByLevel(Player* player, const WorldLocation &loc, bool fromStartUp = false); + bool StarterLevelDistanceCheck(Player* player, const WorldLocation &loc, bool fromStartUp = false); bool HasCheat(BotCheatMask mask) { @@ -596,7 +601,7 @@ public: std::set GetCurrentIncompleteQuestIds(); void PetFollow(); static float GetItemScoreMultiplier(ItemQualities quality); - static bool IsHealingSpell(uint32 spellFamilyName, flag96 spelFalimyFlags); + static bool IsHealingSpell(uint32 spellFamilyName, flag96 spellFamilyFlags); static SpellFamilyNames Class2SpellFamilyName(uint8 cls); NewRpgInfo rpgInfo; NewRpgStatistic rpgStatistic; @@ -614,7 +619,6 @@ private: Item* FindItemInInventory(std::function checkItem) const; void HandleCommands(); void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL); - bool _isBotInitializing = false; inline bool IsValidUnit(const Unit* unit) const { return unit && unit->IsInWorld() && !unit->IsDuringRemoveFromWorld(); @@ -649,6 +653,7 @@ protected: BotCheatMask cheatMask = BotCheatMask::none; Position jumpDestination = Position(); uint32 nextTransportCheck = 0; + bool spellInterruptRequested = false; }; #endif diff --git a/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp b/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp index 75272eb9b..495aac737 100644 --- a/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp +++ b/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp @@ -22,11 +22,13 @@ #include "GuildMgr.h" #include "ObjectAccessor.h" #include "ObjectGuid.h" +#include "ObjectMgr.h" #include "PlayerbotAIConfig.h" #include "PlayerbotRepository.h" #include "PlayerbotFactory.h" #include "PlayerbotOperations.h" #include "PlayerbotSecurity.h" +#include "PlayerbotTextMgr.h" #include "PlayerbotWorldThreadProcessor.h" #include "Playerbots.h" #include "PlayerbotGuildMgr.h" @@ -64,14 +66,13 @@ private: }; std::unordered_set BotInitGuard::botsBeingInitialized; -std::unordered_set PlayerbotHolder::botLoading; +std::unordered_map PlayerbotHolder::botLoading; PlayerbotHolder::PlayerbotHolder() : PlayerbotAIBase(false) {} class PlayerbotLoginQueryHolder : public LoginQueryHolder { private: uint32 masterAccountId; - PlayerbotHolder* playerbotHolder; public: PlayerbotLoginQueryHolder(uint32 masterAccount, uint32 accountId, ObjectGuid guid) : LoginQueryHolder(accountId, guid), masterAccountId(masterAccount) @@ -122,8 +123,14 @@ 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(); - if (count >= PlayerbotAIConfig::instance().maxAddedBots) + uint32 loadingForMaster = 0; + for (auto const& [guid, acctId] : botLoading) + { + if (acctId == masterAccountId) + ++loadingForMaster; + } + uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster; + if (count >= uint32(PlayerbotAIConfig::instance().maxAddedBots)) { allowed = false; out << "Ошибка: добавлено слишком много ботов (больше " << sPlayerbotAIConfig.maxAddedBots << ")"; @@ -145,7 +152,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)) @@ -315,7 +322,8 @@ void PlayerbotMgr::CancelLogout() { WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); - botAI->TellMaster("Выход из игры отменён!"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "logout_cancel", "Logout cancelled!", {})); } } @@ -351,10 +359,15 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(cleanupOp)); LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str()); + + // Remove taxi cheat flag on alts. + if (!sRandomPlayerbotMgr.IsRandomBot(bot) && bot->isTaxiCheater()) + bot->SetTaxiCheater(false); + bot->SaveToDB(false, false); WorldSession* botWorldSessionPtr = bot->GetSession(); - WorldSession* masterWorldSessionPtr = nullptr; + [[maybe_unused]] WorldSession* masterWorldSessionPtr = nullptr; // Remove [[maybe_unused]] tag if timed logout implemented. if (botWorldSessionPtr->isLogingOut()) return; @@ -363,6 +376,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) 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)); @@ -374,61 +390,22 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) 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; - } - - TravelTarget* target = nullptr; - if (botAI->GetAiObjectContext()) // Maybe some day re-write to delate all pointer values. +*/ + // Instant logout (the only option right now) { - target = botAI->GetAiObjectContext()->GetValue("travel target")->Get(); - } - - // Peiru: Allow bots to always instant logout to see if this resolves logout crashes - logout = true; - - // if no instant logout, request normal logout - if (!logout) - { - if (bot->GetSession()->isLogingOut()) - return; - else if (bot) - { - botAI->TellMaster("Я выхожу из игры!"); - WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); - botWorldSessionPtr->HandleLogoutRequestOpcode(data); - if (!bot) - { - RemoveFromPlayerbotsMap(guid); - delete botWorldSessionPtr; - if (target) - delete target; - } - return; - } - else - { - RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap - delete botWorldSessionPtr; // finally delete the bot's WorldSession - if (target) - delete target; - } - return; - } // if instant logout possible, do it - else if (bot && (logout || !botWorldSessionPtr->isLogingOut())) - { - botAI->TellMaster("До свидания!"); - RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap - botWorldSessionPtr->LogoutPlayer(true); // this will delete the bot Player object and PlayerbotAI object - delete botWorldSessionPtr; // finally delete the bot's WorldSession + std::string message = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "goodbye", "Goodbye!", {}); + botAI->TellMaster(message); + RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap + botWorldSessionPtr->LogoutPlayer(true); // this will delete the bot Player object and PlayerbotAI object + delete botWorldSessionPtr; // finally delete the bot's WorldSession } } } @@ -442,7 +419,8 @@ void PlayerbotHolder::DisablePlayerBot(ObjectGuid guid) { return; } - botAI->TellMaster("До свидания!"); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "goodbye", "Goodbye!", {})); bot->StopMoving(); bot->GetMotionMaster()->Clear(); @@ -509,12 +487,6 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) } Player* master = botAI->GetMaster(); - if (master) - { - ObjectGuid masterGuid = master->GetGUID(); - if (master->GetGroup() && !master->GetGroup()->IsLeader(masterGuid)) - master->GetGroup()->ChangeLeader(masterGuid); - } Group* group = bot->GetGroup(); if (group) @@ -575,7 +547,8 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) // set delay on login botAI->SetNextCheckDelay(urand(2000, 4000)); - botAI->TellMaster("Здравствуйте!", PLAYERBOT_SECURITY_TALK); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "hello", "Hello!", {}), PLAYERBOT_SECURITY_TALK); // Queue group operations for world thread if (master && master->GetGroup() && !group) @@ -583,15 +556,14 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) Group* mgroup = master->GetGroup(); if (mgroup->GetMembersCount() >= 5) { - if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup()) + // A 5-member party is full, so only a raid can take a 6th member. GroupInviteOperation:: + // Execute() self-converts a non-raid group with >= 5 members to a raid before adding, so a + // separate ConvertToRaid is unnecessary here. Queue the invite for a raid OR a plain + // (non-LFG/BG/battlefield) party; we deliberately skip LFG/BG/BFG groups so a bot login + // never force-converts a live dungeon-finder or battleground group into a raid. + if (mgroup->isRaidGroup() || (!mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup())) { - // Queue ConvertToRaid operation - auto convertOp = std::make_unique(master->GetGUID()); - PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(convertOp)); - } - if (mgroup->isRaidGroup()) - { - // Queue AddMember operation + // Queue AddMember operation; Execute() converts the party to a raid before adding. auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); PlayerbotWorldThreadProcessor::instance().QueueOperation(std::move(addOp)); } @@ -764,7 +736,10 @@ std::string const PlayerbotHolder::ProcessBotCommand(std::string const cmd, Obje bool addClassBot = sRandomPlayerbotMgr.IsAddclassBot(guid.GetCounter()); if (!addClassBot) - return "ERROR: You can not use this command on non-addclass bot."; + { + if (!(cmd == "refresh=raid" && sPlayerbotAIConfig.resetInstanceIdForAltBots)) + return "ERROR: You can only use this command on addclass bots."; + } if (!admin) { @@ -950,7 +925,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (!strcmp(cmd, "initself")) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { // OnBotLogin(master); PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_EPIC); @@ -969,7 +944,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg { if (!strcmp(cmd, "initself=uncommon")) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { // OnBotLogin(master); PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_UNCOMMON); @@ -985,7 +960,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg } if (!strcmp(cmd, "initself=rare")) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { // OnBotLogin(master); PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_RARE); @@ -1001,7 +976,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg } if (!strcmp(cmd, "initself=epic")) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { // OnBotLogin(master); PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_EPIC); @@ -1017,7 +992,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg } if (!strcmp(cmd, "initself=legendary")) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { // OnBotLogin(master); PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_LEGENDARY); @@ -1034,7 +1009,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg int32 gs; if (sscanf(cmd, "initself=%d", &gs) != -1) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { // OnBotLogin(master); PlayerbotFactory factory(master, master->GetLevel(), ITEM_QUALITY_LEGENDARY, gs); @@ -1058,7 +1033,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (!strcmp(cmd, "reload")) { - if (master->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (master->CanBeGameMaster()) { sPlayerbotAIConfig.Initialize(); messages.push_back("Config reloaded."); @@ -1090,13 +1065,14 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg } else if (sPlayerbotAIConfig.selfBotLevel == 0) messages.push_back("Self-bot is disabled"); - else if (sPlayerbotAIConfig.selfBotLevel == 1 && master->GetSession()->GetSecurity() < SEC_GAMEMASTER) + else if (sPlayerbotAIConfig.selfBotLevel == 1 && !master->CanBeGameMaster()) messages.push_back("You do not have permission to enable player botAI"); else { messages.push_back("Enable player botAI"); PlayerbotsMgr::instance().AddPlayerbotData(master, true); GET_PLAYERBOT_AI(master)->SetMaster(master); + PlayerbotRepository::instance().Load(GET_PLAYERBOT_AI(master)); } return messages; @@ -1110,7 +1086,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (!strcmp(cmd, "addclass")) { - if (sPlayerbotAIConfig.addClassCommand == 0 && master->GetSession()->GetSecurity() < SEC_GAMEMASTER) + if (sPlayerbotAIConfig.addClassCommand == 0 && !master->CanBeGameMaster()) { messages.push_back("You do not have permission to create bot by addclass command"); return messages; @@ -1273,7 +1249,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg std::vector chars = split(charnameStr, ','); for (std::vector::iterator i = chars.begin(); i != chars.end(); i++) { - std::string const s = *i; + std::string s = *i; if (!strcmp(cmd, "addaccount")) { @@ -1282,7 +1258,13 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg if (!accountId) { // If not found, try to get account ID from character name - ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(s); + std::string charName = s; + if (!normalizePlayerName(charName)) + { + messages.push_back("Neither account nor character '" + s + "' found"); + continue; + } + ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(charName); if (!charGuid) { messages.push_back("Neither account nor character '" + s + "' found"); @@ -1310,6 +1292,11 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg else { // For regular add command, only add the specific character + if (!normalizePlayerName(s)) + { + messages.push_back("Character '" + *i + "' not found"); + continue; + } ObjectGuid charGuid = sCharacterCache->GetCharacterGuidByName(s); if (!charGuid) { @@ -1335,7 +1322,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg else if (master && member != master->GetGUID()) { out << ProcessBotCommand(cmdStr, member, master->GetGUID(), - master->GetSession()->GetSecurity() >= SEC_GAMEMASTER, + master->CanBeGameMaster(), master->GetSession()->GetAccountId(), master->GetGuildId()); } else if (!master) @@ -1573,6 +1560,24 @@ void PlayerbotMgr::HandleMasterIncomingPacket(WorldPacket const& packet) // if master is logging out, log out all bots case CMSG_LOGOUT_REQUEST: { + Player* master = GetMaster(); + if (master) + { + // Replicate the AFK logout prevention checks from WorldSession::HandleLogoutRequestOpcode + // so bots are not logged out when the master's own logout is going to be prevented. + AreaTableEntry const* areaEntry = sAreaTableStore.LookupEntry(master->GetAreaId()); + bool preventAfkSanctuaryLogout = sWorld->getIntConfig(CONFIG_AFK_PREVENT_LOGOUT) == 1 + && master->isAFK() && areaEntry && areaEntry->IsSanctuary(); + + bool preventAfkLogout = sWorld->getIntConfig(CONFIG_AFK_PREVENT_LOGOUT) == 2 + && master->isAFK(); + + if (preventAfkSanctuaryLogout || preventAfkLogout) + { + break; + } + } + LogoutAllBots(); break; } @@ -1698,7 +1703,7 @@ void PlayerbotMgr::TellError(std::string const botName, std::string const text) errors[text] = names; } -void PlayerbotMgr::CheckTellErrors(uint32 elapsed) +void PlayerbotMgr::CheckTellErrors(uint32 /*elapsed*/) { time_t now = time(nullptr); if ((now - lastErrorTell) < sPlayerbotAIConfig.errorDelay / 1000) @@ -1797,7 +1802,7 @@ PlayerbotAI* PlayerbotsMgr::GetPlayerbotAI(Player* player) if (itr != _playerbotsAIMap.end()) { if (itr->second->IsBotAI()) - return reinterpret_cast(itr->second); + return dynamic_cast(itr->second); } return nullptr; @@ -1813,7 +1818,7 @@ PlayerbotMgr* PlayerbotsMgr::GetPlayerbotMgr(Player* player) if (itr != _playerbotsMgrMap.end()) { if (!itr->second->IsBotAI()) - return reinterpret_cast(itr->second); + return dynamic_cast(itr->second); } return nullptr; diff --git a/modules/mod-playerbots/src/Bot/PlayerbotMgr.h b/modules/mod-playerbots/src/Bot/PlayerbotMgr.h index b80f6f236..005b82862 100644 --- a/modules/mod-playerbots/src/Bot/PlayerbotMgr.h +++ b/modules/mod-playerbots/src/Bot/PlayerbotMgr.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTMGR_H -#define _PLAYERBOT_PLAYERBOTMGR_H +#ifndef PLAYERBOTS_PLAYERBOTMGR_H +#define PLAYERBOTS_PLAYERBOTMGR_H #include "ObjectGuid.h" #include "Player.h" @@ -57,7 +58,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/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp b/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp index e40d53d0c..b8ef98033 100644 --- a/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp +++ b/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RandomPlayerbotMgr.h" @@ -23,7 +24,6 @@ #include "DatabaseEnv.h" #include "Define.h" #include "FleeManager.h" -#include "FlightMasterCache.h" #include "GridNotifiers.h" #include "LFGMgr.h" #include "MapMgr.h" @@ -35,8 +35,10 @@ #include "PlayerbotAI.h" #include "PlayerbotAIConfig.h" #include "PlayerbotFactory.h" +#include "PlayerbotTextMgr.h" #include "Playerbots.h" #include "Position.h" +#include "RaceMgr.h" #include "Random.h" #include "RandomPlayerbotFactory.h" #include "ServerFacade.h" @@ -46,9 +48,7 @@ #include "World.h" #include "Cell.h" #include "GridNotifiers.h" -// Required for Cell because of poor AC implementation #include "CellImpl.h" -// Required for GridNotifiers because of poor AC implementation #include "GridNotifiersImpl.h" struct GuidClassRaceInfo @@ -58,48 +58,6 @@ struct GuidClassRaceInfo uint32 rRace; }; -enum class CityId : uint8 { - STORMWIND, IRONFORGE, DARNASSUS, EXODAR, - ORGRIMMAR, UNDERCITY, THUNDER_BLUFF, SILVERMOON_CITY, - SHATTRATH_CITY, DALARAN -}; - -enum class FactionId : uint8 { ALLIANCE, HORDE, NEUTRAL }; - -// Map of banker entry → city + faction -static const std::unordered_map> bankerToCity = { - {2455, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2456, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2457, {CityId::STORMWIND, FactionId::ALLIANCE}}, - {2460, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {2461, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {5099, {CityId::IRONFORGE, FactionId::ALLIANCE}}, - {4155, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4208, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4209, {CityId::DARNASSUS, FactionId::ALLIANCE}}, - {17773, {CityId::EXODAR, FactionId::ALLIANCE}}, {18350, {CityId::EXODAR, FactionId::ALLIANCE}}, {16710, {CityId::EXODAR, FactionId::ALLIANCE}}, - {3320, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3309, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3318, {CityId::ORGRIMMAR, FactionId::HORDE}}, - {4549, {CityId::UNDERCITY, FactionId::HORDE}}, {2459, {CityId::UNDERCITY, FactionId::HORDE}}, {2458, {CityId::UNDERCITY, FactionId::HORDE}}, {4550, {CityId::UNDERCITY, FactionId::HORDE}}, - {2996, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8356, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8357, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, - {17631, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17632, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17633, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {16615, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16616, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16617, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {19246, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {19034, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {30604, {CityId::DALARAN, FactionId::NEUTRAL}}, {30605, {CityId::DALARAN, FactionId::NEUTRAL}}, {30607, {CityId::DALARAN, FactionId::NEUTRAL}}, - {28675, {CityId::DALARAN, FactionId::NEUTRAL}}, {28676, {CityId::DALARAN, FactionId::NEUTRAL}}, {28677, {CityId::DALARAN, FactionId::NEUTRAL}} -}; - -// Map of city → available banker entries -static const std::unordered_map> cityToBankers = { - {CityId::STORMWIND, {2455, 2456, 2457}}, - {CityId::IRONFORGE, {2460, 2461, 5099}}, - {CityId::DARNASSUS, {4155, 4208, 4209}}, - {CityId::EXODAR, {17773, 18350, 16710}}, - {CityId::ORGRIMMAR, {3320, 3309, 3318}}, - {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, - {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, - {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, - {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, - {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} -}; - -// Quick lookup map: banker entry → location -static std::unordered_map bankerEntryToLocation; - void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); } void activatePrintStatsThread() @@ -322,7 +280,7 @@ void RandomPlayerbotMgr::LogPlayerLocation() } } -void RandomPlayerbotMgr::UpdateAIInternal(uint32 elapsed, bool /*minimal*/) +void RandomPlayerbotMgr::UpdateAIInternal(uint32 /*elapsed*/, bool /*minimal*/) { if (totalPmo) totalPmo->finish(); @@ -994,7 +952,7 @@ void RandomPlayerbotMgr::CheckBgQueue() // Arena logic bool isRated = false; - if (uint8 arenaType = BattlegroundMgr::BGArenaType(queueTypeId)) + if (BattlegroundMgr::BGArenaType(queueTypeId)) { BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(queueTypeId); GroupQueueInfo ginfo; @@ -1081,7 +1039,7 @@ void RandomPlayerbotMgr::CheckBgQueue() BattlegroundData[queueTypeId][bracketId].minLevel = pvpDiff->minLevel; BattlegroundData[queueTypeId][bracketId].maxLevel = pvpDiff->maxLevel; - if (uint8 arenaType = BattlegroundMgr::BGArenaType(queueTypeId)) + if (BattlegroundMgr::BGArenaType(queueTypeId)) { bool isRated = false; BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(queueTypeId); @@ -1662,7 +1620,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& tlocs.push_back(WorldPosition(loc)); // Do not teleport to maps disabled in config tlocs.erase(std::remove_if(tlocs.begin(), tlocs.end(), - [bot](WorldPosition l) + [](WorldPosition l) { std::vector::iterator i = find(sPlayerbotAIConfig.randomBotMaps.begin(), @@ -1717,7 +1675,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& z = 0.05f + ground; - if (!botAI->CheckLocationDistanceByLevel(bot, loc, true)) + if (!botAI->StarterLevelDistanceCheck(bot, loc, true)) continue; const LocaleConstant& locale = sWorld->GetDefaultDbcLocale(); @@ -1761,333 +1719,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& // tlocs.size()); } -void RandomPlayerbotMgr::PrepareZone2LevelBracket() -{ - // Classic WoW - Low - level zones - zone2LevelBracket[1] = {5, 12}; // Dun Morogh - zone2LevelBracket[12] = {5, 12}; // Elwynn Forest - zone2LevelBracket[14] = {5, 12}; // Durotar - zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades - zone2LevelBracket[141] = {5, 12}; // Teldrassil - zone2LevelBracket[215] = {5, 12}; // Mulgore - zone2LevelBracket[3430] = {5, 12}; // Eversong Woods - zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle - - // Classic WoW - Mid - level zones - zone2LevelBracket[17] = {10, 25}; // Barrens - zone2LevelBracket[38] = {10, 20}; // Loch Modan - zone2LevelBracket[40] = {10, 21}; // Westfall - zone2LevelBracket[130] = {10, 23}; // Silverpine Forest - zone2LevelBracket[148] = {10, 21}; // Darkshore - zone2LevelBracket[3433] = {10, 22}; // Ghostlands - zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle - - // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Duskwood - zone2LevelBracket[11] = {21, 30}; // Wetlands - zone2LevelBracket[44] = {16, 28}; // Redridge Mountains - zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills - zone2LevelBracket[331] = {18, 33}; // Ashenvale - zone2LevelBracket[400] = {24, 36}; // Thousand Needles - zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains - - // Classic WoW - Higher - level zones - zone2LevelBracket[3] = {36, 46}; // Badlands - zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows - zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh - zone2LevelBracket[16] = {45, 52}; // Azshara - zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale - zone2LevelBracket[45] = {30, 42}; // Arathi Highlands - zone2LevelBracket[47] = {42, 51}; // Hinterlands - zone2LevelBracket[51] = {45, 51}; // Searing Gorge - zone2LevelBracket[357] = {40, 52}; // Feralas - zone2LevelBracket[405] = {30, 41}; // Desolace - zone2LevelBracket[440] = {41, 52}; // Tanaris - - // Classic WoW - Top - level zones - zone2LevelBracket[4] = {52, 57}; // Blasted Lands - zone2LevelBracket[28] = {50, 60}; // Western Plaguelands - zone2LevelBracket[46] = {51, 60}; // Burning Steppes - zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands - zone2LevelBracket[361] = {47, 57}; // Felwood - zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater - zone2LevelBracket[618] = {54, 61}; // Winterspring - zone2LevelBracket[1377] = {54, 63}; // Silithus - - // The Burning Crusade - Zones - zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula - zone2LevelBracket[3518] = {64, 70}; // Nagrand - zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest - zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley - zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh - zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains - zone2LevelBracket[3523] = {67, 73}; // Netherstorm - zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas - - // Wrath of the Lich King - Zones - zone2LevelBracket[65] = {71, 77}; // Dragonblight - zone2LevelBracket[66] = {74, 80}; // Zul'Drak - zone2LevelBracket[67] = {77, 80}; // Storm Peaks - zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier - zone2LevelBracket[394] = {72, 78}; // Grizzly Hills - zone2LevelBracket[495] = {68, 74}; // Howling Fjord - zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest - zone2LevelBracket[3537] = {68, 75}; // Borean Tundra - zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin - zone2LevelBracket[4197] = {79, 80}; // Wintergrasp - - // Override with values from config - for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) - { - zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; - } -} - -void RandomPlayerbotMgr::PrepareTeleportCache() -{ - uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("playerbots", "Preparing random teleport caches for {} levels...", maxLevel); - - QueryResult results = WorldDatabase.Query( - "SELECT " - "g.map, " - "position_x, " - "position_y, " - "position_z, " - "t.minlevel, " - "t.maxlevel " - "FROM " - "(SELECT " - "map, " - "MIN( c.guid ) guid " - "FROM " - "creature c " - "INNER JOIN creature_template t ON c.id1 = t.entry " - "WHERE " - "t.npcflag = 0 " - "AND t.lootid != 0 " - "AND t.maxlevel - t.minlevel < 3 " - "AND map IN ({}) " - "AND t.entry not in (32820, 24196, 30627, 30617) " - "AND c.spawntimesecs < 1000 " - "AND t.faction not in (11, 71, 79, 85, 188, 1575) " - "AND (t.unit_flags & 256) = 0 " - "AND (t.unit_flags & 4096) = 0 " - "AND t.rank = 0 " - // "AND (t.flags_extra & 32768) = 0 " - "GROUP BY " - "map, " - "ROUND(position_x / 50), " - "ROUND(position_y / 50), " - "ROUND(position_z / 50) " - "HAVING " - "count(*) >= 2) " - "AS g " - "INNER JOIN creature c ON g.guid = c.guid " - "INNER JOIN creature_template t on c.id1 = t.entry " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - uint32 collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - uint32 min_level = fields[4].Get(); - uint32 max_level = fields[5].Get(); - uint32 level = (min_level + max_level + 1) / 2; - WorldLocation loc(mapId, x, y, z, 0); - collected_locs++; - for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; - l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) - { - if (l < 1 || l > maxLevel) - { - continue; - } - locsPerLevelCache[(uint8)l].push_back(loc); - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} locations for level collected.", collected_locs); - - if (sPlayerbotAIConfig.enableNewRpgStrategy) - { - PrepareZone2LevelBracket(); - LOG_INFO("playerbots", "Preparing innkeepers / flightmasters locations for level..."); - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.faction, " - "t.entry, " - "t.npcflag, " - "c.guid " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 73728 " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 faction = fields[5].Get(); - uint32 tEntry = fields[6].Get(); - uint32 tNpcflag = fields[7].Get(); - uint32 guid = fields[8].Get(); - - if (tEntry == 3838 || tEntry == 29480) - continue; - - const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction); - - WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); - collected_locs++; - Map* map = sMapMgr->FindMap(loc.GetMapId(), 0); - if (!map) - continue; - bool forHorde = !(entry->hostileMask & 4); - bool forAlliance = !(entry->hostileMask & 2); - if (tNpcflag & UNIT_NPC_FLAG_FLIGHTMASTER) - { - WorldPosition pos(mapId, x, y, z, orient); - if (forHorde) - FlightMasterCache::Instance().AddHordeFlightMaster(guid, pos); - - if (forAlliance) - FlightMasterCache::Instance().AddAllianceFlightMaster(guid, pos); - } - const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); - uint32 zoneId = area->zone ? area->zone : area->ID; - if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end()) - continue; - LevelBracket bracket = zone2LevelBracket[zoneId]; - for (int i = bracket.low; i <= bracket.high; i++) - { - if (forHorde) - { - hordeStarterPerLevelCache[i].push_back(loc); - } - if (forAlliance) - { - allianceStarterPerLevelCache[i].push_back(loc); - } - } - - } while (results->NextRow()); - } - - // add all initial position - for (uint32 i = 1; i < MAX_RACES; i++) - { - for (uint32 j = 1; j < MAX_CLASSES; j++) - { - PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); - - if (!info) - continue; - - WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); - - for (int32 l = 1; l <= 5; l++) - { - if ((1 << (i - 1)) & RACEMASK_ALLIANCE) - allianceStarterPerLevelCache[(uint8)l].push_back(pos); - else - hordeStarterPerLevelCache[(uint8)l].push_back(pos); - } - break; - } - } - LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs); - } - - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.minlevel, " - "t.entry " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 131072 " - "AND t.npcflag != 135298 " - "AND t.minlevel != 55 " - "AND t.minlevel != 65 " - "AND t.faction not in (35, 474, 69, 57) " - "AND t.entry not in (30606, 30608, 29282) " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 level = fields[5].Get(); - uint32 entry = fields[6].Get(); - BankerLocation bLoc; - bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); - bLoc.entry = entry; - collected_locs++; - for (int32 l = 1; l <= maxLevel; l++) - { - // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) - if (l <=60 && level > 45) - { - continue; - } - // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) - if ((l >=61 && l <=70) && (level < 60 || level > 70)) - { - continue; - } - // Bots 71+ go to Dalaran bankers (all have minlevel 75) - if ((l >=71) && level != 75) - { - continue; - } - bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); - bankerEntryToLocation[bLoc.entry] = bLoc.loc; - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} banker locations for level collected.", collected_locs); -} - void RandomPlayerbotMgr::PrepareAddclassCache() { // Using accounts marked as type 2 (AddClass) @@ -2128,11 +1759,6 @@ void RandomPlayerbotMgr::Init() if (sPlayerbotAIConfig.addClassCommand) sRandomPlayerbotMgr.PrepareAddclassCache(); - if (sPlayerbotAIConfig.enabled) - { - sRandomPlayerbotMgr.PrepareTeleportCache(); - } - if (sPlayerbotAIConfig.randomBotJoinBG) sRandomPlayerbotMgr.LoadBattleMastersCache(); @@ -2144,103 +1770,20 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; - if (level >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) { - std::vector fallbackLocs; - for (auto& bLoc : bankerLocsPerLevelCache[level]) - fallbackLocs.push_back(bLoc.loc); - - if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) + std::vector locs = sTravelMgr.GetCityLocations(bot); + if (!locs.empty()) { - RandomTeleport(bot, fallbackLocs, true); + RandomTeleport(bot, locs, true); return; } - - // Collect valid cities based on bot faction. - std::unordered_set validBankerCities; - for (auto& loc : bankerLocsPerLevelCache[level]) - { - auto cityIt = bankerToCity.find(loc.entry); - if (cityIt == bankerToCity.end()) continue; - - CityId cityId = cityIt->second.first; - FactionId cityFactionId = cityIt->second.second; - - if ((IsAlliance(bot->getRace()) && cityFactionId == FactionId::ALLIANCE) || - (!IsAlliance(bot->getRace()) && cityFactionId == FactionId::HORDE) || - (cityFactionId == FactionId::NEUTRAL)) - { - validBankerCities.insert(cityId); - } - } - - // Fallback if no valid cities - if (validBankerCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Apply weights to valid cities - std::vector weightedCities; - for (CityId city : validBankerCities) - { - int weight = 0; - switch (city) - { - case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; - case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; - case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; - case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; - case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; - case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; - case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; - case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; - case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; - case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; - default: weight = 0; break; - } - if (weight <= 0) continue; - - for (int i = 0; i < weight; ++i) - { - weightedCities.push_back(city); - } - } - - // Fallback if no valid cities - if (weightedCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Pick a weighted city randomly, then a random banker in that city - // then teleport to that banker - CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; - auto const& bankers = cityToBankers.at(selectedCity); - uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; - auto locIt = bankerEntryToLocation.find(selectedBankerEntry); - if (locIt != bankerEntryToLocation.end()) - { - std::vector teleportTarget = { locIt->second }; - RandomTeleport(bot, teleportTarget, true); - return; - } - - // Fallback if something went wrong - RandomTeleport(bot, *locs); } - else + std::vector locs = sTravelMgr.GetTeleportLocations(bot); + if (!locs.empty()) { - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, false); + return; } } @@ -2249,17 +1792,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; + std::vector locs = sTravelMgr.GetTeleportLocations(bot); LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(), - bot->GetLevel(), locs->size()); + bot->GetLevel(), locs.size()); - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs); } void RandomPlayerbotMgr::RandomTeleport(Player* bot) @@ -2486,14 +2023,14 @@ void RandomPlayerbotMgr::Clear(Player* bot) factory.ClearEverything(); } -uint32 RandomPlayerbotMgr::GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ) +uint32 RandomPlayerbotMgr::GetZoneLevel(uint16 mapId, float teleX, float teleY, float /*teleZ*/) { uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); uint32 level = 0; QueryResult results = WorldDatabase.Query( "SELECT AVG(t.minlevel) minlevel, AVG(t.maxlevel) maxlevel FROM creature c " - "INNER JOIN creature_template t ON c.id1 = t.entry WHERE map = {} AND minlevel > 1 AND ABS(position_x - {}) < " + "INNER JOIN creature_template t ON c.id = t.entry WHERE map = {} AND minlevel > 1 AND ABS(position_x - {}) < " "{} AND ABS(position_y - {}) < {}", mapId, teleX, sPlayerbotAIConfig.randomBotTeleportDistance / 2, teleY, sPlayerbotAIConfig.randomBotTeleportDistance / 2); @@ -2542,7 +2079,7 @@ void RandomPlayerbotMgr::Refresh(Player* bot) bot->DurabilityRepairAll(false, 1.0f, false); bot->SetFullHealth(); - bot->SetPvP(true); + bot->SetPvP(sWorld->IsPvPRealm()); PlayerbotFactory factory(bot, bot->GetLevel()); factory.Refresh(); @@ -2731,6 +2268,16 @@ CachedEvent* RandomPlayerbotMgr::FindEvent(uint32 bot, std::string const& event) return &e; } +bool RandomPlayerbotMgr::IsSpecPvp(uint32 bot, uint8 cls) +{ + uint32 stored = GetValue(bot, "specNo"); + if (!stored) + return false; + uint32 specIndex = stored - 1; + std::string const& name = sPlayerbotAIConfig.premadeSpecName[cls][specIndex]; + return !name.empty() && name.find("pvp") != std::string::npos; +} + uint32 RandomPlayerbotMgr::GetEventValue(uint32 bot, std::string const& event) { if (CachedEvent* e = FindEvent(bot, event)) @@ -2817,7 +2364,7 @@ void RandomPlayerbotMgr::SetValue(Player* bot, std::string const& type, uint32 v SetValue(bot->GetGUID().GetCounter(), type, value, data); } -bool RandomPlayerbotMgr::HandlePlayerbotConsoleCommand(ChatHandler* handler, char const* args) +bool RandomPlayerbotMgr::HandlePlayerbotConsoleCommand(ChatHandler* /*handler*/, char const* args) { if (!sPlayerbotAIConfig.enabled) { @@ -2997,6 +2544,13 @@ void RandomPlayerbotMgr::OnBotLoginInternal(Player* const bot) } } + // Run guild recovery/assignment at login to handle empty guild tables after restart. + if (sPlayerbotAIConfig.randomBotGuildCount > 0) + { + PlayerbotFactory factory(bot, bot->GetLevel()); + factory.InitGuild(); + } + if (sPlayerbotAIConfig.randomBotFixedLevel) { bot->SetPlayerFlag(PLAYER_FLAGS_NO_XP_GAIN); @@ -3037,7 +2591,8 @@ void RandomPlayerbotMgr::OnPlayerLogin(Player* player) { botAI->SetMaster(player); botAI->ResetStrategies(); - botAI->TellMaster("Здравствуйте."); + botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault( + "hello", "Hello", {})); } break; @@ -3090,6 +2645,7 @@ void RandomPlayerbotMgr::OnPlayerLogin(Player* player) { // ObjectGuid::LowType guid = player->GetGUID().GetCounter(); //not used, conditional could be rewritten for // simplicity. line marked for removal. + player->SetPvP(sWorld->IsPvPRealm()); } else { @@ -3130,7 +2686,7 @@ void RandomPlayerbotMgr::PrintStats() std::map lvlPerRace; std::map lvlPerClass; - for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race) { perRace[race] = 0; lvlPerRace[race] = 0; @@ -3146,10 +2702,10 @@ void RandomPlayerbotMgr::PrintStats() uint32 heal = 0; uint32 tank = 0; uint32 active = 0; - uint32 update = 0; +/* uint32 update = 0; uint32 randomize = 0; uint32 teleport = 0; - uint32 changeStrategy = 0; + uint32 changeStrategy = 0;*/ uint32 dead = 0; uint32 combat = 0; // uint32 revive = 0; //not used, line marked for removal. @@ -3189,7 +2745,7 @@ void RandomPlayerbotMgr::PrintStats() if (botAI->AllowActivity()) ++active; - + /* TODO: Review statistics on rpg merge if (botAI->GetAiObjectContext()->GetValue("random bot update")->Get()) ++update; @@ -3202,7 +2758,7 @@ void RandomPlayerbotMgr::PrintStats() if (!GetEventValue(botId, "change_strategy")) ++changeStrategy; - + */ if (bot->isDead()) { ++dead; @@ -3277,7 +2833,7 @@ void RandomPlayerbotMgr::PrintStats() } LOG_INFO("playerbots", "Bots race:"); - for (uint8 race = RACE_HUMAN; race < MAX_RACES; ++race) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race) { if (perRace[race]) { @@ -3326,10 +2882,10 @@ void RandomPlayerbotMgr::PrintStats() LOG_INFO("playerbots", "Bots rpg status:"); LOG_INFO("playerbots", " Idle: {}, Rest: {}, GoGrind: {}, GoCamp: {}, MoveRandom: {}, MoveNpc: {}, DoQuest: {}, " - "TravelFlight: {}", + "TravelFlight: {}, OutdoorPvP: {}", rpgStatusCount[RPG_IDLE], rpgStatusCount[RPG_REST], rpgStatusCount[RPG_GO_GRIND], rpgStatusCount[RPG_GO_CAMP], rpgStatusCount[RPG_WANDER_RANDOM], rpgStatusCount[RPG_WANDER_NPC], - rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT]); + rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT], rpgStatusCount[RPG_OUTDOOR_PVP]); LOG_INFO("playerbots", "Bots total quests:"); LOG_INFO("playerbots", " Accepted: {}, Rewarded: {}, Dropped: {}", rpgStasticTotal.questAccepted, @@ -3498,7 +3054,7 @@ CreatureData const* RandomPlayerbotMgr::GetCreatureDataByEntry(uint32 entry) if (entry != 0) { for (auto const& itr : sObjectMgr->GetAllCreatureData()) - if (itr.second.id1 == entry) + if (itr.second.id == entry) return &itr.second; } diff --git a/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.h b/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.h index 94c0a0151..4e167dd2a 100644 --- a/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.h +++ b/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANDOMPLAYERBOTMGR_H -#define _PLAYERBOT_RANDOMPLAYERBOTMGR_H +#ifndef PLAYERBOTS_RANDOMPLAYERBOTMGR_H +#define PLAYERBOTS_RANDOMPLAYERBOTMGR_H #include "NewRpgInfo.h" #include "ObjectGuid.h" @@ -140,6 +141,7 @@ public: std::string GetData(uint32 bot, std::string const& type); void SetValue(uint32 bot, std::string const& type, uint32 value, std::string const& data = ""); void SetValue(Player* bot, std::string const& type, uint32 value, std::string const& data = ""); + bool IsSpecPvp(uint32 bot, uint8 cls); void Remove(Player* bot); ObjectGuid GetBattleMasterGUID(Player* bot, BattlegroundTypeId bgTypeId); CreatureData const* GetCreatureDataByEntry(uint32 entry); @@ -164,25 +166,8 @@ public: static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } void PrepareAddclassCache(); - void PrepareZone2LevelBracket(); - void PrepareTeleportCache(); void Init(); std::map> addclassCache; - std::map> locsPerLevelCache; - std::map> allianceStarterPerLevelCache; - std::map> hordeStarterPerLevelCache; - - struct LevelBracket { - uint32 low; - uint32 high; - bool InsideBracket(uint32 val) { return val >= low && val <= high; } - }; - std::map zone2LevelBracket; - struct BankerLocation { - WorldLocation loc; - uint32 entry; - }; - std::map> bankerLocsPerLevelCache; // Account type management void AssignAccountTypes(); diff --git a/modules/mod-playerbots/src/Db/FlightMasterCache.cpp b/modules/mod-playerbots/src/Db/FlightMasterCache.cpp deleted file mode 100644 index effe24993..000000000 --- a/modules/mod-playerbots/src/Db/FlightMasterCache.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "FlightMasterCache.h" - -void FlightMasterCache::AddHordeFlightMaster(uint32 entry, WorldPosition pos) -{ - hordeFlightMasterCache[entry] = pos; -} - -void FlightMasterCache::AddAllianceFlightMaster(uint32 entry, WorldPosition pos) -{ - allianceFlightMasterCache[entry] = pos; -} - -Creature* FlightMasterCache::GetNearestFlightMaster(Player* bot) -{ - std::map& flightMasterCache = - (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; - - Creature* nearestFlightMaster = nullptr; - float nearestDistance = std::numeric_limits::max(); - - for (auto const& [entry, pos] : flightMasterCache) - { - if (pos.GetMapId() == bot->GetMapId()) - { - float distance = bot->GetExactDist2dSq(pos); - if (distance < nearestDistance) - { - Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); - if (flightMaster) - { - nearestDistance = distance; - nearestFlightMaster = flightMaster; - } - } - } - } - - return nearestFlightMaster; -} diff --git a/modules/mod-playerbots/src/Db/FlightMasterCache.h b/modules/mod-playerbots/src/Db/FlightMasterCache.h deleted file mode 100644 index 7f8b95310..000000000 --- a/modules/mod-playerbots/src/Db/FlightMasterCache.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef _PLAYERBOT_FLIGHTMASTER_H -#define _PLAYERBOT_FLIGHTMASTER_H - -#include "Creature.h" -#include "Player.h" -#include "TravelMgr.h" - -class FlightMasterCache -{ -public: - static FlightMasterCache& Instance() - { - static FlightMasterCache instance; - - return instance; - } - - Creature* GetNearestFlightMaster(Player* bot); - void AddHordeFlightMaster(uint32 entry, WorldPosition pos); - void AddAllianceFlightMaster(uint32 entry, WorldPosition pos); - -private: - FlightMasterCache() = default; - ~FlightMasterCache() = default; - - FlightMasterCache(const FlightMasterCache&) = delete; - FlightMasterCache& operator=(const FlightMasterCache&) = delete; - - FlightMasterCache(FlightMasterCache&&) = delete; - FlightMasterCache& operator=(FlightMasterCache&&) = delete; - - std::map allianceFlightMasterCache; - std::map hordeFlightMasterCache; -}; - -#endif diff --git a/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.cpp b/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.cpp index 5e1f370b6..ea23cbbd3 100644 --- a/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.cpp +++ b/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Log.h" diff --git a/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.h b/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.h index 160a7622c..f45f82796 100644 --- a/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.h +++ b/modules/mod-playerbots/src/Db/PlayerbotDungeonRepository.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTDUNGEONREPOSITORY_H -#define _PLAYERBOT_PLAYERBOTDUNGEONREPOSITORY_H +#ifndef PLAYERBOTS_PLAYERBOTDUNGEONREPOSITORY_H +#define PLAYERBOTS_PLAYERBOTDUNGEONREPOSITORY_H #include #include diff --git a/modules/mod-playerbots/src/Db/PlayerbotRepository.cpp b/modules/mod-playerbots/src/Db/PlayerbotRepository.cpp index 731534edd..b516a8e46 100644 --- a/modules/mod-playerbots/src/Db/PlayerbotRepository.cpp +++ b/modules/mod-playerbots/src/Db/PlayerbotRepository.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlayerbotRepository.h" @@ -39,6 +40,8 @@ void PlayerbotRepository::Load(PlayerbotAI* botAI) botAI->ChangeStrategy(value, BOT_STATE_DEAD); } while (result->NextRow()); + botAI->GetAiObjectContext()->GetUntypedValue("outfit list"); + botAI->GetAiObjectContext()->Load(values); } } @@ -65,7 +68,7 @@ void PlayerbotRepository::Save(PlayerbotAI* botAI) SaveValue(guid, "dead", FormatStrategies("dead", botAI->GetStrategies(BOT_STATE_DEAD))); } -std::string const PlayerbotRepository::FormatStrategies(std::string const type, std::vector strategies) +std::string const PlayerbotRepository::FormatStrategies(std::string const /*type*/, std::vector strategies) { std::ostringstream out; for (std::vector::iterator i = strategies.begin(); i != strategies.end(); ++i) diff --git a/modules/mod-playerbots/src/Db/PlayerbotRepository.h b/modules/mod-playerbots/src/Db/PlayerbotRepository.h index b7b9b4b3c..211e0f3df 100644 --- a/modules/mod-playerbots/src/Db/PlayerbotRepository.h +++ b/modules/mod-playerbots/src/Db/PlayerbotRepository.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTREPOSITORY_H -#define _PLAYERBOT_PLAYERBOTREPOSITORY_H +#ifndef PLAYERBOTS_PLAYERBOTREPOSITORY_H +#define PLAYERBOTS_PLAYERBOTREPOSITORY_H #include #include diff --git a/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.cpp b/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.cpp index d745ba307..f03a445d0 100644 --- a/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.cpp +++ b/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "Log.h" #include "DBCStores.h" #include "DatabaseEnv.h" diff --git a/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.h b/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.h index 7b026dd29..5e44a2314 100644 --- a/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.h +++ b/modules/mod-playerbots/src/Db/PlayerbotSpellRepository.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTSPELLREPOSITORY_H -#define _PLAYERBOT_PLAYERBOTSPELLREPOSITORY_H +#ifndef PLAYERBOTS_PLAYERBOTSPELLREPOSITORY_H +#define PLAYERBOTS_PLAYERBOTSPELLREPOSITORY_H #include diff --git a/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.cpp b/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.cpp index 96162c044..1a0c9e518 100644 --- a/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.cpp +++ b/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "GuildTaskMgr.h" @@ -223,7 +224,7 @@ bool GuildTaskMgr::CreateKillTask(Player* player, uint32 guildId) uint32 level = player->GetLevel(); QueryResult results = WorldDatabase.Query( "SELECT ct.Entry, c.map, c.position_x, c.position_y, ct.Name FROM creature_template ct " - "JOIN creature c ON ct.Entry = c.id1 WHERE ct.MaxLevel < {} AND ct.MinLevel > {} AND ct.Rank = {} ", + "JOIN creature c ON ct.Entry = c.id WHERE ct.MaxLevel < {} AND ct.MinLevel > {} AND ct.Rank = {} ", level + 4, level - 3, rank); if (results) { @@ -387,7 +388,7 @@ bool GuildTaskMgr::SendKillAdvertisement(CharacterDatabaseTransaction& trans, ui return false; QueryResult result = - WorldDatabase.Query("SELECT map, position_x, position_y, position_z FROM creature WHERE id1 = {}", creatureId); + WorldDatabase.Query("SELECT map, position_x, position_y, position_z FROM creature WHERE id = {}", creatureId); if (!result) return false; @@ -1070,7 +1071,7 @@ void GuildTaskMgr::CheckKillTaskInternal(Player* player, Unit* victim) if (!victim->IsCreature()) return; - Creature* creature = reinterpret_cast(victim); + Creature* creature = dynamic_cast(victim); if (!creature) return; diff --git a/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.h b/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.h index 6251f2c1e..eb4616296 100644 --- a/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.h +++ b/modules/mod-playerbots/src/Mgr/Guild/GuildTaskMgr.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GUILDTASKMGR_H -#define _PLAYERBOT_GUILDTASKMGR_H +#ifndef PLAYERBOTS_GUILDTASKMGR_H +#define PLAYERBOTS_GUILDTASKMGR_H #include #include diff --git a/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.cpp b/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.cpp index bd05aca85..33b1600cd 100644 --- a/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.cpp +++ b/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "PlayerbotGuildMgr.h" #include "Player.h" #include "PlayerbotAIConfig.h" @@ -150,13 +156,10 @@ void PlayerbotGuildMgr::OnGuildUpdate(Guild* guild) void PlayerbotGuildMgr::ResetGuildCache() { - for (auto it = _guildCache.begin(); it != _guildCache.end();) - { - GuildCache& cached = it->second; - cached.memberCount = 0; - cached.faction = 2; - cached.status = 0; - } + _guildCache.clear(); + + for (auto& nameEntry : _guildNames) + nameEntry.second = true; } void PlayerbotGuildMgr::LoadGuildNames() diff --git a/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.h b/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.h index 5d85ce9e7..f2fa10c76 100644 --- a/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.h +++ b/modules/mod-playerbots/src/Mgr/Guild/PlayerbotGuildMgr.h @@ -1,5 +1,11 @@ -#ifndef _PLAYERBOT_PLAYERBOTGUILDMGR_H -#define _PLAYERBOT_PLAYERBOTGUILDMGR_H +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PLAYERBOTGUILDMGR_H +#define PLAYERBOTS_PLAYERBOTGUILDMGR_H #include "Guild.h" #include "Player.h" diff --git a/modules/mod-playerbots/src/Mgr/Item/BisListMgr.cpp b/modules/mod-playerbots/src/Mgr/Item/BisListMgr.cpp new file mode 100644 index 000000000..20f58067d --- /dev/null +++ b/modules/mod-playerbots/src/Mgr/Item/BisListMgr.cpp @@ -0,0 +1,93 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#include "BisListMgr.h" + +#include "DatabaseEnv.h" +#include "Field.h" +#include "Log.h" +#include "QueryResult.h" + +void BisListMgr::LoadAll() +{ + _bis.clear(); + + QueryResult result = PlayerbotsDatabase.Query( + "SELECT class, tab, slot, faction, auto_gear_score_limit, item_id FROM playerbots_bis_gear"); + if (!result) + { + LOG_INFO("server.loading", "playerbots_bis_gear table missing or empty"); + return; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + uint8 cls = fields[0].Get(); + uint8 tab = fields[1].Get(); + uint8 slot = fields[2].Get(); + uint8 faction = fields[3].Get(); + uint16 autoGearScoreLimit = fields[4].Get(); + uint32 item = fields[5].Get(); + + _bis[autoGearScoreLimit][MakeKey(cls, tab)][faction][slot] = item; + ++count; + } while (result->NextRow()); + + LOG_INFO("server.loading", "Loaded {} BiS entries across {} item levels", + count, static_cast(_bis.size())); +} + +std::map BisListMgr::GetBisFor(uint16 autoGearScoreLimit, uint8 cls, uint8 tab, uint8 faction) const +{ + auto ilvlIt = _bis.find(autoGearScoreLimit); + if (ilvlIt == _bis.end()) + return {}; + + auto comboIt = ilvlIt->second.find(MakeKey(cls, tab)); + if (comboIt == ilvlIt->second.end()) + return {}; + + std::map result; + + // Base: faction=0 (Both). + auto bothIt = comboIt->second.find(0); + if (bothIt != comboIt->second.end()) + result = bothIt->second; + + // Faction-specific overrides Both. + if (faction == 1 || faction == 2) + { + auto facIt = comboIt->second.find(faction); + if (facIt != comboIt->second.end()) + for (auto const& kv : facIt->second) + result[kv.first] = kv.second; + } + + return result; +} + +std::map BisListMgr::GetBisForNearest(uint16 requestedIlvl, uint16 maxDrop, uint8 cls, uint8 tab, + uint8 faction, uint16* outResolved) const +{ + uint16 floor = requestedIlvl > maxDrop ? requestedIlvl - maxDrop : 1; + for (uint16 try_ilvl = requestedIlvl; try_ilvl >= floor; --try_ilvl) + { + auto result = GetBisFor(try_ilvl, cls, tab, faction); + if (!result.empty()) + { + if (outResolved) + *outResolved = try_ilvl; + return result; + } + if (try_ilvl == 0) + break; + } + if (outResolved) + *outResolved = 0; + return {}; +} diff --git a/modules/mod-playerbots/src/Mgr/Item/BisListMgr.h b/modules/mod-playerbots/src/Mgr/Item/BisListMgr.h new file mode 100644 index 000000000..2d18bc784 --- /dev/null +++ b/modules/mod-playerbots/src/Mgr/Item/BisListMgr.h @@ -0,0 +1,45 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BISLISTMGR_H +#define PLAYERBOTS_BISLISTMGR_H + +#include "Define.h" + +#include + +class BisListMgr +{ +public: + static BisListMgr* instance() + { + static BisListMgr inst; + return &inst; + } + + void LoadAll(); + + // faction: 1=Alliance, 2=Horde. Faction-specific rows override faction=0 (Both). + // Returns slot -> itemId for the matching auto_gear_score_limit tier. Empty map = no data. + std::map GetBisFor(uint16 autoGearScoreLimit, uint8 cls, uint8 tab, uint8 faction) const; + + // Closest-lower fallback: scan ilvls down from requested to (requested - maxDrop), return first non-empty set. + // outResolved receives the matched ilvl (0 if nothing matched within the window). + std::map GetBisForNearest(uint16 requestedIlvl, uint16 maxDrop, uint8 cls, uint8 tab, + uint8 faction, uint16* outResolved = nullptr) const; + +private: + BisListMgr() = default; + + static uint16 MakeKey(uint8 cls, uint8 tab) { return (uint16(cls) << 8) | tab; } + + // autoGearScoreLimit -> (cls<<8|tab) -> faction (0/1/2) -> slot -> itemId + std::map>>> _bis; +}; + +#define sBisListMgr BisListMgr::instance() + +#endif diff --git a/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.cpp b/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.cpp index 5ed8d1b04..637b9945b 100644 --- a/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.cpp +++ b/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ItemVisitors.h" diff --git a/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.h b/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.h index e42b14f89..55691dc4e 100644 --- a/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.h +++ b/modules/mod-playerbots/src/Mgr/Item/ItemVisitors.h @@ -3,8 +3,8 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_ITEMVISITORS_H -#define _PLAYERBOT_ITEMVISITORS_H +#ifndef PLAYERBOTS_ITEMVISITORS_H +#define PLAYERBOTS_ITEMVISITORS_H #include "ChatHelper.h" #include "Common.h" @@ -66,6 +66,29 @@ private: Player* bot; }; +class HasRelicBySubclassVisitor : public IterateItemsVisitor +{ +public: + HasRelicBySubclassVisitor(uint32 subClass) : subClass(subClass) {} + + bool Visit(Item* item) override + { + ItemTemplate const* proto = item->GetTemplate(); + if (proto && proto->InventoryType == INVTYPE_RELIC && proto->SubClass == subClass) + { + found = true; + return false; + } + + return true; + } + + bool found = false; + +private: + uint32 subClass; +}; + class FindItemsByQualityVisitor : public IterateItemsVisitor { public: @@ -302,9 +325,6 @@ public: FindMountVisitor(Player* bot) : FindUsableItemVisitor(bot) {} bool Accept(ItemTemplate const* proto) override; - -private: - uint32 effectId; }; class FindPetVisitor : public FindUsableItemVisitor diff --git a/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.cpp b/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.cpp index a62eb8fe4..1b4cb9780 100644 --- a/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.cpp +++ b/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "LootObjectStack.h" @@ -85,6 +86,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID) { bool onlyHasQuestItems = true; bool hasAnyQuestItems = false; + bool neededQuestItem = false; GameObjectQuestItemList const* items = sObjectMgr->GetGameObjectQuestItemList(go->GetEntry()); for (size_t i = 0; i < MAX_GAMEOBJECT_QUEST_ITEMS; i++) @@ -100,8 +102,12 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID) if (IsNeededForQuest(bot, itemId)) { + // A gathering node can also drop a needed quest item (e.g. + // Root Sample off Barrens herbs); gathering yields both, so + // keep reading the lock below to set skillId rather than + // bailing here. this->guid = lootGUID; - return; + neededQuestItem = true; } const ItemTemplate* proto = sObjectMgr->GetItemTemplate(itemId); @@ -168,7 +174,7 @@ void LootObject::Refresh(Player* bot, ObjectGuid lootGUID) } // If gameobject has only quest items that bot doesn’t need, skip it. - if (hasAnyQuestItems && onlyHasQuestItems) + if (!neededQuestItem && hasAnyQuestItems && onlyHasQuestItems) return; // Otherwise, loot it. diff --git a/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.h b/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.h index 0f87aed70..c21a961db 100644 --- a/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.h +++ b/modules/mod-playerbots/src/Mgr/Item/LootObjectStack.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LOOTOBJECTSTACK_H -#define _PLAYERBOT_LOOTOBJECTSTACK_H +#ifndef PLAYERBOTS_LOOTOBJECTSTACK_H +#define PLAYERBOTS_LOOTOBJECTSTACK_H #include "ObjectGuid.h" diff --git a/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.cpp b/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.cpp index 71fbb9f79..826edb302 100644 --- a/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.cpp +++ b/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.cpp @@ -1,36 +1,37 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "RandomItemMgr.h" +#include "DBCStores.h" #include "ItemTemplate.h" -#include "LootValues.h" #include "Playerbots.h" -char* strstri(char const* str1, char const* str2); -std::set RandomItemMgr::itemCache; - -uint64 BotEquipKey::GetKey() { return level + 100 * clazz + 10000 * slot + 1000000 * quality; } +std::unordered_set RandomItemMgr::itemCache; class RandomItemGuildTaskPredicate : public RandomItemPredicate { public: bool Apply(ItemTemplate const* proto) override { - if (proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM || - proto->Bonding == BIND_WHEN_USE) + if (!proto) + return false; + + if (proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_WHEN_USE || + proto->Bonding == BIND_QUEST_ITEM) return false; if (proto->Quality < ITEM_QUALITY_NORMAL) return false; - if ((proto->Class == ITEM_CLASS_ARMOR || proto->Class == ITEM_CLASS_WEAPON) && + if ((proto->Class == ITEM_CLASS_WEAPON || proto->Class == ITEM_CLASS_ARMOR) && proto->Quality >= ITEM_QUALITY_RARE) return true; - if (proto->Class == ITEM_CLASS_TRADE_GOODS || proto->Class == ITEM_CLASS_CONSUMABLE) + if (proto->Class == ITEM_CLASS_CONSUMABLE || proto->Class == ITEM_CLASS_TRADE_GOODS) return true; return false; @@ -44,8 +45,11 @@ public: bool Apply(ItemTemplate const* proto) override { - if (proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM || - proto->Bonding == BIND_WHEN_USE) + if (!proto) + return false; + + if (proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_WHEN_USE || + proto->Bonding == BIND_QUEST_ITEM) return false; if (proto->Class == ITEM_CLASS_QUEST) @@ -57,7 +61,7 @@ public: if (proto->Quality < desiredQuality || proto->Quality >= ITEM_QUALITY_EPIC) return false; - if (proto->Class == ITEM_CLASS_ARMOR || proto->Class == ITEM_CLASS_WEAPON) + if (proto->Class == ITEM_CLASS_WEAPON || proto->Class == ITEM_CLASS_ARMOR) return true; } else @@ -66,7 +70,7 @@ public: if (proto->Quality < desiredQuality || proto->Quality >= ITEM_QUALITY_RARE) return false; - if (proto->Class == ITEM_CLASS_TRADE_GOODS || proto->Class == ITEM_CLASS_CONSUMABLE) + if (proto->Class == ITEM_CLASS_CONSUMABLE || proto->Class == ITEM_CLASS_TRADE_GOODS) return true; } @@ -80,103 +84,52 @@ private: RandomItemMgr::RandomItemMgr() { - predicates[RANDOM_ITEM_GUILD_TASK] = new RandomItemGuildTaskPredicate(); - predicates[RANDOM_ITEM_GUILD_TASK_REWARD_EQUIP_GREEN] = new RandomItemGuildTaskRewardPredicate(true, false); - predicates[RANDOM_ITEM_GUILD_TASK_REWARD_EQUIP_BLUE] = new RandomItemGuildTaskRewardPredicate(true, true); - predicates[RANDOM_ITEM_GUILD_TASK_REWARD_TRADE] = new RandomItemGuildTaskRewardPredicate(false, false); - predicates[RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE] = new RandomItemGuildTaskRewardPredicate(false, true); - - viableSlots[EQUIPMENT_SLOT_HEAD].insert(INVTYPE_HEAD); - viableSlots[EQUIPMENT_SLOT_NECK].insert(INVTYPE_NECK); - viableSlots[EQUIPMENT_SLOT_SHOULDERS].insert(INVTYPE_SHOULDERS); - viableSlots[EQUIPMENT_SLOT_BODY].insert(INVTYPE_BODY); - viableSlots[EQUIPMENT_SLOT_CHEST].insert(INVTYPE_CHEST); - viableSlots[EQUIPMENT_SLOT_CHEST].insert(INVTYPE_ROBE); - viableSlots[EQUIPMENT_SLOT_WAIST].insert(INVTYPE_WAIST); - viableSlots[EQUIPMENT_SLOT_LEGS].insert(INVTYPE_LEGS); - viableSlots[EQUIPMENT_SLOT_FEET].insert(INVTYPE_FEET); - viableSlots[EQUIPMENT_SLOT_WRISTS].insert(INVTYPE_WRISTS); - viableSlots[EQUIPMENT_SLOT_HANDS].insert(INVTYPE_HANDS); - viableSlots[EQUIPMENT_SLOT_FINGER1].insert(INVTYPE_FINGER); - viableSlots[EQUIPMENT_SLOT_FINGER2].insert(INVTYPE_FINGER); - viableSlots[EQUIPMENT_SLOT_TRINKET1].insert(INVTYPE_TRINKET); - viableSlots[EQUIPMENT_SLOT_TRINKET2].insert(INVTYPE_TRINKET); - viableSlots[EQUIPMENT_SLOT_MAINHAND].insert(INVTYPE_WEAPON); - viableSlots[EQUIPMENT_SLOT_MAINHAND].insert(INVTYPE_2HWEAPON); - viableSlots[EQUIPMENT_SLOT_MAINHAND].insert(INVTYPE_WEAPONMAINHAND); - viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_WEAPON); - viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_2HWEAPON); - viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_SHIELD); - viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_WEAPONMAINHAND); - viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_HOLDABLE); - viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_RANGED); - viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_THROWN); - viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_RANGEDRIGHT); - viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_RELIC); - viableSlots[EQUIPMENT_SLOT_TABARD].insert(INVTYPE_TABARD); - viableSlots[EQUIPMENT_SLOT_BACK].insert(INVTYPE_CLOAK); - - weightStatLink["sta"] = ITEM_MOD_STAMINA; - weightStatLink["str"] = ITEM_MOD_STRENGTH; - weightStatLink["agi"] = ITEM_MOD_AGILITY; - weightStatLink["int"] = ITEM_MOD_INTELLECT; - weightStatLink["spi"] = ITEM_MOD_SPIRIT; - - weightStatLink["splpwr"] = ITEM_MOD_SPELL_POWER; - weightStatLink["atkpwr"] = ITEM_MOD_ATTACK_POWER; - - weightStatLink["exprtng"] = ITEM_MOD_EXPERTISE_RATING; - weightStatLink["critstrkrtng"] = ITEM_MOD_CRIT_RATING; - weightStatLink["hitrtng"] = ITEM_MOD_HIT_RATING; - weightStatLink["hastertng"] = ITEM_MOD_HASTE_RATING; - weightStatLink["armorpenrtng"] = ITEM_MOD_ARMOR_PENETRATION_RATING; - - weightStatLink["defrtng"] = ITEM_MOD_DEFENSE_SKILL_RATING; - weightStatLink["dodgertng"] = ITEM_MOD_DODGE_RATING; - weightStatLink["block"] = ITEM_MOD_BLOCK_VALUE; - weightStatLink["blockrtng"] = ITEM_MOD_BLOCK_RATING; - weightStatLink["parryrtng"] = ITEM_MOD_PARRY_RATING; - - weightStatLink["manargn"] = ITEM_MOD_MANA_REGENERATION; - - weightRatingLink["exprtng"] = CR_EXPERTISE; - weightRatingLink["critstrkrtng"] = CR_CRIT_MELEE; - weightRatingLink["hitrtng"] = CR_HIT_MELEE; - weightRatingLink["hastertng"] = CR_HASTE_MELEE; - weightRatingLink["armorpenrtng"] = CR_ARMOR_PENETRATION; - - weightRatingLink["defrtng"] = CR_DEFENSE_SKILL; - weightRatingLink["dodgertng"] = CR_DODGE; - weightRatingLink["blockrtng"] = CR_BLOCK; - weightRatingLink["parryrtng"] = CR_PARRY; -} - -void RandomItemMgr::Init() -{ - BuildItemInfoCache(); - // BuildEquipCache(); - BuildEquipCacheNew(); - BuildAmmoCache(); - BuildPotionCache(); - BuildFoodCache(); - BuildTradeCache(); -} - -void RandomItemMgr::InitAfterAhBot() -{ - BuildRandomItemCache(); - // BuildRarityCache(); + InitWeaponProficiency(); + InitPredicates(); + InitViableSlots(); + InitWeightLinks(); } RandomItemMgr::~RandomItemMgr() { - for (std::map::iterator i = predicates.begin(); i != predicates.end(); ++i) - delete i->second; + for (auto& pair : predicates) + delete pair.second; predicates.clear(); } -bool RandomItemMgr::HandleConsoleCommand(ChatHandler* handler, char const* args) +void RandomItemMgr::Init() +{ + // Prevent double initialization when the reload command is executed. + // Use exchange so only the first caller continues; others stop. + // NOTE: If someone implements reload/rebuild hash, update all getters from + // returning by reference to returning by value. Also, replace all + // DEFAULT_MAX_LEVEL maxLevel with randomBotMaxLevel and clear caches + // before processing. + if (m_initialized.exchange(true)) + return; + + BuildCacheItemInfo(); + // if (!LoadCacheEquip()) + // BuildCacheEquip(); + BuildCacheEquipNew(); + BuildCacheAmmo(); + BuildCacheFood(); + BuildCachePotion(); + BuildCacheTrade(); + LoadEnchantmentPool(); +} + +void RandomItemMgr::InitAfterAhBot() +{ + if (!LoadCacheRandomItem()) + BuildCacheRandomItem(); + DebugCacheRandomItem(); + // if (!LoadCacheRarity()) + // BuildCacheRarity(); +} + +bool RandomItemMgr::HandleConsoleCommand(ChatHandler* /*handler*/, char const* args) { if (!args || !*args) { @@ -187,14 +140,19 @@ bool RandomItemMgr::HandleConsoleCommand(ChatHandler* handler, char const* args) return false; } -RandomItemList RandomItemMgr::Query(uint32 level, RandomItemType type, RandomItemPredicate* predicate) +RandomItemList RandomItemMgr::Query(uint32 level, RandomItemType type, RandomItemPredicate* predicate) const { - RandomItemList& list = randomItemCache[(level - 1) / 10][type]; + auto const levelItr = randomItemCache.find((level - 1) / 10); + if (levelItr == randomItemCache.end()) + return {}; + + auto const typeItr = levelItr->second.find(type); + if (typeItr == levelItr->second.end()) + return {}; RandomItemList result; - for (RandomItemList::iterator i = list.begin(); i != list.end(); ++i) + for (uint32 const itemId : typeItr->second) { - uint32 itemId = *i; ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); if (!proto) continue; @@ -208,518 +166,900 @@ RandomItemList RandomItemMgr::Query(uint32 level, RandomItemType type, RandomIte return result; } -void RandomItemMgr::BuildRandomItemCache() +uint32 RandomItemMgr::GetUpgrade(Player* player, std::string spec, uint8 slot, uint32 quality, uint32 itemId) { - if (PreparedQueryResult result = - PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RNDITEM_CACHE))) + if (!player) + return 0; + + // get old item statWeight + uint32 oldStatWeight = 0; + uint32 specId = 0; + uint32 closestUpgrade = 0; + uint32 closestUpgradeWeight = 0; + std::vector classspecs; + + for (uint32 specNum = 1; specNum < 5; ++specNum) { - LOG_INFO("server.loading", "Loading random item cache"); - uint32 count = 0; - do - { - Field* fields = result->Fetch(); - uint32 level = fields[0].Get(); - uint32 type = fields[1].Get(); - uint32 itemId = fields[2].Get(); + if (!m_weightScales[player->getClass()][specNum].info.id) + continue; - RandomItemType rit = (RandomItemType)type; - randomItemCache[level][rit].push_back(itemId); - ++count; + classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); - } while (result->NextRow()); - - LOG_INFO("server.loading", "Equipment cache loaded from {} records", count); + if (m_weightScales[player->getClass()][specNum].info.name == spec) + specId = m_weightScales[player->getClass()][specNum].info.id; } - else + if (!specId) + return 0; + + if (itemId && itemInfoCache.find(itemId) != itemInfoCache.end()) { - ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); - LOG_INFO("server.loading", "Building random item cache from {} items", itemTemplates->size()); + oldStatWeight = itemInfoCache[itemId].weights[specId]; - for (auto const& itr : *itemTemplates) + if (oldStatWeight) + LOG_INFO("playerbots", "Old Item: {}, weight: {}", itemId, oldStatWeight); + else + LOG_INFO("playerbots", "Old item has no stat weight"); + } + + for (std::unordered_map::iterator i = itemInfoCache.begin(); i != itemInfoCache.end(); ++i) + { + ItemInfoEntry& info = i->second; + + // skip useless items + if (info.weights[specId] == 0) + continue; + + // skip higher lvl + if (info.minLevel > player->GetLevel()) + continue; + + // skip too low level + if (info.minLevel < (player->GetLevel() - 10)) + continue; + + // skip wrong team + if (info.team != TEAM_NEUTRAL && info.team != player->GetTeamId()) + continue; + + // skip wrong slot + if ((EquipmentSlots)info.slot != (EquipmentSlots)slot) + continue; + + // skip higher quality + if (quality && info.quality != quality) + continue; + + // skip worse items + if (info.weights[specId] <= oldStatWeight) + continue; + + // skip items that only fit in slot, but not stats + if (!itemId && info.weights[specId] == 1 && player->GetLevel() > 40) + continue; + + // skip quest items + if (info.source == ITEM_SOURCE_QUEST) { - ItemTemplate const* proto = &itr.second; - if (!proto) + if (player->GetQuestRewardStatus(info.sourceId) != QUEST_STATUS_COMPLETE) continue; + } - if (proto->Duration & 0x80000000) - continue; + // skip no stats trinkets + if (info.weights[specId] == 1 && (info.slot == EQUIPMENT_SLOT_NECK || info.slot == EQUIPMENT_SLOT_TRINKET1 || + info.slot == EQUIPMENT_SLOT_TRINKET2 || info.slot == EQUIPMENT_SLOT_FINGER1 || + info.slot == EQUIPMENT_SLOT_FINGER2)) + continue; - if (strstri(proto->Name1.c_str(), "qa") || strstri(proto->Name1.c_str(), "test") || - strstri(proto->Name1.c_str(), "deprecated")) - continue; - - if (!proto->ItemLevel) - continue; - - if (!proto->SellPrice) - continue; - - uint32 level = proto->ItemLevel; - for (uint32 type = RANDOM_ITEM_GUILD_TASK; type <= RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE; type++) + // check if item stat score is the best among class specs + uint32 bestSpecId = 0; + uint32 bestSpecScore = 0; + for (std::vector::iterator i = classspecs.begin(); i != classspecs.end(); ++i) + { + if (info.weights[*i] > bestSpecScore) { - RandomItemType rit = (RandomItemType)type; - if (predicates[rit] && !predicates[rit]->Apply(proto)) - continue; - - randomItemCache[level / 10][rit].push_back(itr.first); - - PlayerbotsDatabasePreparedStatement* stmt = - PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RNDITEM_CACHE); - stmt->SetData(0, level / 10); - stmt->SetData(1, type); - stmt->SetData(2, itr.first); - PlayerbotsDatabase.Execute(stmt); + bestSpecId = *i; + bestSpecScore = info.weights[specId]; } } - uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel; - if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) - maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); + if (bestSpecId && bestSpecId != specId && player->GetLevel() > 40) + return 0; - for (uint32 level = 0; level < maxLevel / 10; level++) + if (!closestUpgrade) { - for (uint32 type = RANDOM_ITEM_GUILD_TASK; type <= RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE; type++) + closestUpgrade = info.itemId; + closestUpgradeWeight = info.weights[specId]; + } + + // pick closest upgrade + if (info.weights[specId] < closestUpgradeWeight) + { + closestUpgrade = info.itemId; + closestUpgradeWeight = info.weights[specId]; + } + } + + if (closestUpgrade) + LOG_INFO("playerbots", "New Item: {}, weight: {}", closestUpgrade, closestUpgradeWeight); + + return closestUpgrade; +} + +std::vector RandomItemMgr::GetUpgradeList(Player* player, std::string spec, uint8 slot, uint32 quality, + uint32 itemId, uint32 /*amount*/) +{ + std::vector listItems; + if (!player) + return listItems; + + // get old item statWeight + uint32 oldStatWeight = 0; + uint32 specId = 0; + uint32 closestUpgradeWeight = 0; + std::vector classspecs; + + for (uint32 specNum = 1; specNum < 5; ++specNum) + { + if (!m_weightScales[player->getClass()][specNum].info.id) + continue; + + classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); + + if (m_weightScales[player->getClass()][specNum].info.name == spec) + specId = m_weightScales[player->getClass()][specNum].info.id; + } + + if (!specId) + return listItems; + + if (itemId && itemInfoCache.find(itemId) != itemInfoCache.end()) + { + oldStatWeight = itemInfoCache[itemId].weights[specId]; + + if (oldStatWeight) + LOG_INFO("playerbots", "Old Item: {}, weight: {}", itemId, oldStatWeight); + else + LOG_INFO("playerbots", "Old item has no stat weight"); + } + + for (std::unordered_map::iterator i = itemInfoCache.begin(); i != itemInfoCache.end(); ++i) + { + ItemInfoEntry& info = i->second; + + // skip useless items + if (info.weights[specId] == 0) + continue; + + // skip higher lvl + if (info.minLevel > player->GetLevel()) + continue; + + // skip too low level + if ((int32)info.minLevel < (int32)(player->GetLevel() - 20)) + continue; + + // skip wrong team + if (info.team != TEAM_NEUTRAL && info.team != player->GetTeamId()) + continue; + + // skip wrong slot + if ((EquipmentSlots)info.slot != (EquipmentSlots)slot) + continue; + + // skip higher quality + if (quality && info.quality != quality) + continue; + + // skip worse items + if (info.weights[specId] <= oldStatWeight) + continue; + + // skip items that only fit in slot, but not stats + if (!itemId && info.weights[specId] == 1 && player->GetLevel() > 40) + continue; + + // skip quest items + if (info.source == ITEM_SOURCE_QUEST) + { + if (player->GetQuestRewardStatus(info.sourceId) != QUEST_STATUS_COMPLETE) + continue; + } + + // skip no stats trinkets + if (info.weights[specId] < 2 && (info.slot == EQUIPMENT_SLOT_NECK || info.slot == EQUIPMENT_SLOT_TRINKET1 || + info.slot == EQUIPMENT_SLOT_TRINKET2 || info.slot == EQUIPMENT_SLOT_FINGER1 || + info.slot == EQUIPMENT_SLOT_FINGER2)) + continue; + + // if (player->GetLevel() >= 40) + //{ + // // check if item stat score is the best among class specs + // uint32 bestSpecId = 0; + // uint32 bestSpecScore = 0; + // for (vector::iterator i = classspecs.begin(); i != classspecs.end(); ++i) + // { + // if (info->weights[*i] > bestSpecScore) + // { + // bestSpecId = *i; + // bestSpecScore = info->weights[specId]; + // } + // } + + // if (bestSpecId && bestSpecId != specId) + // continue; + //} + + listItems.push_back(info.itemId); + // continue; + + // pick closest upgrade + if (info.weights[specId] > closestUpgradeWeight) + { + closestUpgradeWeight = info.weights[specId]; + } + } + + if (listItems.size()) + LOG_INFO("playerbots", "New Items: {}, Old item:%d, New items max: {}", listItems.size(), oldStatWeight, + closestUpgradeWeight); + + return listItems; +} + +bool RandomItemMgr::HasStatWeight(uint32 itemId) +{ + auto itr = itemInfoCache.find(itemId); + return itr != itemInfoCache.end(); +} + +uint32 RandomItemMgr::CalculateStatWeight(ItemTemplate const* proto, uint8 playerclass, uint8 spec) +{ + if (!proto) + return false; + + uint32 statWeight = 0; + bool isCasterItem = false; + bool isAttackItem = false; + bool noCaster = (Classes)playerclass == CLASS_WARRIOR || (Classes)playerclass == CLASS_ROGUE || + (Classes)playerclass == CLASS_DEATH_KNIGHT || (Classes)playerclass == CLASS_HUNTER; + uint32 spellPower = 0; + uint32 spellHeal = 0; + uint32 attackPower = 0; + bool hasInt = false; + bool hasMana = !((Classes)playerclass == CLASS_WARRIOR || (Classes)playerclass == CLASS_ROGUE || + (Classes)playerclass == CLASS_DEATH_KNIGHT); + + if (proto->SubClass == ITEM_SUBCLASS_ARMOR_LIBRAM || proto->SubClass == ITEM_SUBCLASS_ARMOR_IDOL || + proto->SubClass == ITEM_SUBCLASS_ARMOR_TOTEM || proto->SubClass == ITEM_SUBCLASS_ARMOR_SIGIL) + return (uint32)(proto->Quality + proto->ItemLevel); + + // check basic item stats + int32 basicStatsWeight = 0; + for (uint8 j = 0; j < MAX_ITEM_PROTO_STATS; ++j) + { + uint32 statType; + int32 val; + std::string weightName; + + // if (j >= proto->StatsCount) + // continue; + + statType = proto->ItemStat[j].ItemStatType; + val = proto->ItemStat[j].ItemStatValue; + + if (val == 0) + continue; + + for (std::unordered_map::iterator i = weightStatLink.begin(); i != weightStatLink.end(); ++i) + { + uint32 modd = i->second; + if (modd == statType) { - RandomItemList list = randomItemCache[level][(RandomItemType)type]; - LOG_INFO("playerbots", " Level {}..{} Type {} - {} random items cached", level * 10, level * 10 + 9, - type, list.size()); + weightName = i->first; + break; + } + } - for (RandomItemList::iterator i = list.begin(); i != list.end(); ++i) + if (weightName.empty()) + continue; + + uint32 singleStat = CalculateSingleStatWeight(playerclass, spec, weightName, val); + basicStatsWeight += singleStat; + + if (val) + { + if (weightName == "int" && !noCaster) + isCasterItem = true; + + if (weightName == "int") + hasInt = true; + + if (weightName == "splpwr") + isCasterItem = true; + + if (weightName == "str") + isAttackItem = true; + + if (weightName == "agi") + isAttackItem = true; + + if (weightName == "atkpwr") + isAttackItem = true; + } + } + + // check armor & block + statWeight += CalculateSingleStatWeight(playerclass, spec, "armor", proto->Armor); + statWeight += CalculateSingleStatWeight(playerclass, spec, "block", proto->Block); + + // check weapon dps + if (proto->IsWeaponVellum()) + { + //WeaponAttackType attType = BASE_ATTACK; //not used, line marked for removal. + + uint32 dps = 0; + for (uint8 i = 0; i < MAX_ITEM_PROTO_DAMAGES; i++) + { + if (proto->Damage[i].DamageMax == 0) + break; + + dps = (proto->Damage[i].DamageMin + proto->Damage[i].DamageMax) / (float)(proto->Delay / 1000.0f) / 2; + if (dps) + { + if (proto->IsRangedWeapon()) + statWeight += CalculateSingleStatWeight(playerclass, spec, "rgddps", dps); + else + statWeight += CalculateSingleStatWeight(playerclass, spec, "mledps", dps); + } + } + } + + // check item spells + for (auto const& spellData : proto->Spells) + { + // no spell + if (!spellData.SpellId) + continue; + + // apply only at-equip spells + if (spellData.SpellTrigger != ITEM_SPELLTRIGGER_ON_EQUIP) + continue; + + // check if it is valid spell + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellData.SpellId); + if (!spellInfo) + continue; + + uint32 spellDamage = 0; + uint32 spellHealing = 0; + for (uint32 j = 0; j < MAX_SPELL_EFFECTS; j++) + { + if (spellInfo->Effects[j].Effect == SPELL_EFFECT_APPLY_AURA && spellInfo->Effects[j].BasePoints) + { + // spell damage + // SPELL_AURA_MOD_DAMAGE_DONE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_DAMAGE_DONE) { - uint32 itemId = *i; - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (!proto) - continue; + spellDamage = spellInfo->Effects[j].BasePoints + 1; + } - LOG_DEBUG("playerbots", " [{}] {}", itemId, proto->Name1.c_str()); + // spell healing + // SPELL_AURA_MOD_HEALING_DONE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_HEALING_DONE) + { + spellHealing = spellInfo->Effects[j].BasePoints + 1; + } + + // check spell power + if (spellDamage && spellDamage == spellHealing) + { + isCasterItem = true; + spellPower += CalculateSingleStatWeight(playerclass, spec, "splpwr", spellDamage); + } + + // spell hit rating (pre tbc) + // SPELL_AURA_MOD_SPELL_HIT_CHANCE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SPELL_HIT_CHANCE) + { + statWeight += CalculateSingleStatWeight(playerclass, spec, "spellhitrtng", + spellInfo->Effects[j].BasePoints + 1); + } + + // spell crit rating (pre tbc) + // SPELL_AURA_MOD_SPELL_CRIT_CHANCE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE) + { + statWeight += CalculateSingleStatWeight(playerclass, spec, "spellcritstrkrtng", + spellInfo->Effects[j].BasePoints + 1); + } + + // spell penetration + // SPELL_AURA_MOD_TARGET_RESISTANCE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_TARGET_RESISTANCE) + { + // check if magic type + if (spellInfo->Effects[j].MiscValue == SPELL_SCHOOL_MASK_SPELL) + statWeight += CalculateSingleStatWeight(playerclass, spec, "spellpenrtng", + abs(spellInfo->Effects[j].BasePoints + 1)); + } + + // check attack power + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) + { + isAttackItem = true; + attackPower += + CalculateSingleStatWeight(playerclass, spec, "atkpwr", spellInfo->Effects[j].BasePoints + 1); + } + + // check ranged ap + // SPELL_AURA_MOD_RANGED_ATTACK_POWER + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_RANGED_ATTACK_POWER) + { + isAttackItem = true; + attackPower += + CalculateSingleStatWeight(playerclass, spec, "atkpwr", spellInfo->Effects[j].BasePoints + 1); + } + + // check block + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SHIELD_BLOCKVALUE) + { + statWeight += + CalculateSingleStatWeight(playerclass, spec, "block", spellInfo->Effects[j].BasePoints + 1); + } + + // block chance + // SPELL_AURA_MOD_BLOCK_PERCENT + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_BLOCK_PERCENT) + { + statWeight += + CalculateSingleStatWeight(playerclass, spec, "blockrtng", spellInfo->Effects[j].BasePoints + 1); + } + + // armor penetration + // SPELL_AURA_MOD_TARGET_RESISTANCE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_TARGET_RESISTANCE) + { + // check if physical type + if (spellInfo->Effects[j].MiscValue == SPELL_SCHOOL_MASK_NORMAL) + statWeight += CalculateSingleStatWeight(playerclass, spec, "armorpenrtng", + abs(spellInfo->Effects[j].BasePoints + 1)); + } + + // hit rating (pre tbc) + // SPELL_AURA_MOD_HIT_CHANCE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_HIT_CHANCE) + { + statWeight += + CalculateSingleStatWeight(playerclass, spec, "hitrtng", spellInfo->Effects[j].BasePoints + 1); + } + + // crit rating (pre tbc) + // SPELL_AURA_MOD_HIT_CHANCE + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_CRIT_PCT) + { + statWeight += CalculateSingleStatWeight(playerclass, spec, "critstrkrtng", + spellInfo->Effects[j].BasePoints + 1); + } + + // check defense SPELL_AURA_MOD_SKILL + // check block + // if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SKILL) + //{ + // statWeight += CalculateSingleStatWeight(playerclass, spec, "block", + // spellInfo->Effects[j].BasePoints + 1); + //} + + // ratings + // enum CombatRating + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_RATING) + { + for (uint32 rating = 0; rating < MAX_COMBAT_RATING; ++rating) + { + if (spellInfo->Effects[j].MiscValue & (1 << rating)) + { + int32 val = spellInfo->Effects[j].BasePoints + 1; + std::string weightName; + + for (std::unordered_map::iterator i = weightRatingLink.begin(); + i != weightRatingLink.end(); ++i) + { + uint32 modd = i->second; + if (modd == rating) + { + weightName = i->first; + break; + } + } + + if (weightName.empty()) + continue; + + statWeight += CalculateSingleStatWeight(playerclass, spec, weightName, val); + } + } + } + + // mana regen + // SPELL_AURA_MOD_POWER_REGEN + if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN) + { + statWeight += + CalculateSingleStatWeight(playerclass, spec, "manargn", spellInfo->Effects[j].BasePoints + 1); } } } } + + // check for caster item + if (isCasterItem || hasInt) + { + if ((!hasMana || noCaster) && spellPower) + return 0; + + if (!hasMana && hasInt) + return 0; + + if ((!hasMana && noCaster && playerclass != CLASS_PALADIN) && spellPower > attackPower) + return 0; + + bool playerCaster = false; + for (std::vector::iterator i = m_weightScales[playerclass][spec].stats.begin(); + i != m_weightScales[playerclass][spec].stats.end(); ++i) + { + if (i->stat == "splpwr" || i->stat == "int" || i->stat == "manargn") + { + playerCaster = true; + } + } + + if (!playerCaster) + return 0; + } + + // check for caster item + if (isAttackItem) + { + if (hasMana && !noCaster && !(hasInt || spellPower)) + return 0; + // if (!noCaster && attackPower) + // return 0; + + bool playerAttacker = false; + for (std::vector::iterator i = m_weightScales[playerclass][spec].stats.begin(); + i != m_weightScales[playerclass][spec].stats.end(); ++i) + { + if (i->stat == "str" || i->stat == "agi" || i->stat == "atkpwr" || i->stat == "mledps" || + i->stat == "rgddps") + { + playerAttacker = true; + } + } + + if (!playerAttacker) + return 0; + } + + statWeight += spellPower; + statWeight += spellHeal; + statWeight += attackPower; + + // handle negative stats + if (basicStatsWeight < 0) + { + uint32 const absVal = static_cast(abs(basicStatsWeight)); + statWeight = (absVal >= statWeight) ? 0 : statWeight - absVal; + } + else + statWeight += static_cast(basicStatsWeight); + + return statWeight; } -uint32 RandomItemMgr::GetRandomItem(uint32 level, RandomItemType type, RandomItemPredicate* predicate) +uint32 RandomItemMgr::CalculateSingleStatWeight(uint8 playerclass, uint8 spec, std::string stat, uint32 value) { - RandomItemList const& list = Query(level, type, predicate); + uint32 statWeight = 0; + for (std::vector::iterator i = m_weightScales[playerclass][spec].stats.begin(); + i != m_weightScales[playerclass][spec].stats.end(); ++i) + { + if (stat == i->stat) + { + statWeight = i->weight * value; + // if (statWeight) + // LOG_INFO("playerbots", "stat: {}, val: {}, weight: {}, total: {}, class: {}, spec: {}", + // stat, value, i->weight, statWeight, playerclass, m_weightScales[playerclass][spec].info.name); + return statWeight; + } + } + + return statWeight; +} + +uint32 RandomItemMgr::GetStatWeight(Player* player, uint32 itemId) +{ + if (!player || !itemId) + return 0; + + auto itr = itemInfoCache.find(itemId); + if (itr == itemInfoCache.end()) + return 0; + + uint32 statWeight = 0; + uint32 specId = 0; + std::vector classspecs; + + std::string const& specName = AiFactory::GetPlayerSpecName(player); + if (specName.empty()) + return 0; + + for (uint32 specNum = 1; specNum < 5; ++specNum) + { + if (!m_weightScales[player->getClass()][specNum].info.id) + continue; + + classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); + + if (m_weightScales[player->getClass()][specNum].info.name == specName) + specId = m_weightScales[player->getClass()][specNum].info.id; + + if (m_weightScales[player->getClass()][specNum].info.name == specName) + { + specId = specNum; + break; + } + } + + if (!specId) + return 0; + + statWeight = itr->second.weights[specId]; + + return statWeight; +} + +uint32 RandomItemMgr::GetLiveStatWeight(Player* player, uint32 itemId) +{ + if (!player || !itemId) + return 0; + + auto itr = itemInfoCache.find(itemId); + if (itr == itemInfoCache.end()) + return 0; + + uint32 statWeight = 0; + uint32 specId = 0; + std::vector classspecs; + + std::string const& specName = AiFactory::GetPlayerSpecName(player); + if (specName.empty()) + return 0; + + for (uint32 specNum = 1; specNum < 5; ++specNum) + { + if (!m_weightScales[player->getClass()][specNum].info.id) + continue; + + // for bestSpec check + // classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); + + if (m_weightScales[player->getClass()][specNum].info.name == specName) + specId = m_weightScales[player->getClass()][specNum].info.id; + } + if (!specId) + return 0; + + statWeight = itr->second.weights[specId]; + + // skip higher lvl + if (itr->second.minLevel > player->GetLevel()) + return 0; + + // skip too low level + // if ((int32)info->minLevel < (int32)(player->GetLevel() - 20)) + // return 0; + + // skip wrong team + if (itr->second.team != TEAM_NEUTRAL && itr->second.team != player->GetTeamId()) + return 0; + + // skip quest items + if (itr->second.source == ITEM_SOURCE_QUEST && itr->second.sourceId) + { + if (player->GetQuestRewardStatus(itr->second.sourceId) != QUEST_STATUS_COMPLETE) + return 0; + } + + // skip pvp items + if (itr->second.source == ITEM_SOURCE_PVP) + { + if (!player->GetHonorPoints() && !player->GetArenaPoints()) + return 0; + } + + // skip no stats trinkets + if (itr->second.weights[specId] == 1 && + (itr->second.slot == EQUIPMENT_SLOT_NECK || itr->second.slot == EQUIPMENT_SLOT_TRINKET1 || + itr->second.slot == EQUIPMENT_SLOT_TRINKET2 || itr->second.slot == EQUIPMENT_SLOT_FINGER1 || + itr->second.slot == EQUIPMENT_SLOT_FINGER2)) + return 0; + + // skip items that only fit in slot, but not stats + if (!itemId && itr->second.weights[specId] == 1 && player->GetLevel() > 20) + return 0; + + // check if item stat score is the best among class specs + /*uint32 bestSpecId = 0; + uint32 bestSpecScore = 0; + for (vector::iterator i = classspecs.begin(); i != classspecs.end(); ++i) + { + if (itemCache[itemId]->weights[*i] > bestSpecScore && itemCache[itemId]->weights[*i] > 1) + { + bestSpecId = *i; + bestSpecScore = itemCache[itemId]->weights[specId]; + } + } + + if (bestSpecId && bestSpecId != specId && player->GetLevel() >= 60) + return 0;*/ + + return statWeight; +} + +uint32 RandomItemMgr::GetMinLevelFromCache(uint32 itemId) const +{ + auto itr = itemInfoCache.find(itemId); + if (itr == itemInfoCache.end()) + return 0; + + return itr->second.minLevel; +} + +RandomItemList const& RandomItemMgr::GetEquipment(uint32 level, uint8 clazz, uint8 slot, uint32 quality) const +{ + static RandomItemList const empty; + + level = NormalizeLevel(level); + + BotEquipKey const key(level, clazz, slot, quality); + auto const itr = equipCache.find(key); + if (itr == equipCache.end()) + return empty; + + return itr->second; +} + +RandomItemList const& RandomItemMgr::GetEquipmentNew(uint32 level, InventoryType invType) const +{ + static RandomItemList const empty; + + level = NormalizeLevel(level); + + auto const levelItr = equipCacheNew.find(level); + if (levelItr == equipCacheNew.end()) + return empty; + + auto const typeItr = levelItr->second.find(invType); + if (typeItr == levelItr->second.end()) + return empty; + + return typeItr->second; +} + +uint32 RandomItemMgr::GetRandomItem(uint32 level, RandomItemType type, RandomItemPredicate* predicate) const +{ + level = NormalizeLevel(level); + + RandomItemList const list = Query(level, type, predicate); if (list.empty()) return 0; - uint32 index = urand(0, list.size() - 1); - uint32 itemId = list[index]; - - return itemId; + return Acore::Containers::SelectRandomContainerElement(list); } -bool RandomItemMgr::CanEquipItem(BotEquipKey key, ItemTemplate const* proto) +uint32 RandomItemMgr::GetAmmo(uint32 level, uint32 subClass) const { - if (proto->Duration & 0x80000000) - return false; + level = NormalizeLevel(level); - if (proto->Quality != key.quality) - return false; + auto const levelItr = ammoCache.find(level); + if (levelItr == ammoCache.end()) + return 0; - if (proto->Bonding == BIND_QUEST_ITEM || proto->Bonding == BIND_WHEN_USE) - return false; + auto const subItr = levelItr->second.find(subClass); + if (subItr == levelItr->second.end() || subItr->second.empty()) + return 0; - if (proto->Class == ITEM_CLASS_CONTAINER) - return true; + std::vector const& ammo = subItr->second; + if (!sPlayerbotAIConfig.limitGearExpansion) + return ammo.front(); - std::set slots = viableSlots[(EquipmentSlots)key.slot]; - if (slots.find((InventoryType)proto->InventoryType) == slots.end()) - return false; + static constexpr uint32 EXPANSION_ITEM_ID_TBC = 23728; // approx. first item in TBC content (patch 2.0) + static constexpr uint32 EXPANSION_ITEM_ID_WOTLK = 35570; // approx. first item in WotLK content (patch 3.0) + uint32 const maxEntryId = level <= 60 ? EXPANSION_ITEM_ID_TBC : + level <= 70 ? EXPANSION_ITEM_ID_WOTLK : + std::numeric_limits::max(); - uint32 requiredLevel = proto->RequiredLevel; - if (!requiredLevel) + for (uint32 entry : ammo) { - requiredLevel = GetMinLevelFromCache(proto->ItemId); + if (entry < maxEntryId) + return entry; } - if (!requiredLevel) - requiredLevel = key.level; - - uint32 level = key.level; - - uint32 delta = 2; - if (level < 15) - delta = 15; - else if (level < 40) - delta = 10; // urand(5, 10); - else if (level < 60) - delta = 6; // urand(3, 7); - else if (level < 70) - delta = 9; // urand(2, 5); - else if (level < 80) - delta = 9; // urand(2, 4); - else if (level == 80) - delta = 9; // urand(2, 4); - - if (key.quality > ITEM_QUALITY_NORMAL && (requiredLevel > level || requiredLevel < level - delta)) - return false; - - // for (uint32 gap = 60; gap <= 80; gap += 10) - // { - // if (level > gap && requiredLevel <= gap) - // return false; - // } - - return true; + return 0; } -bool RandomItemMgr::CanEquipItemNew(ItemTemplate const* proto) +uint32 RandomItemMgr::GetRandomPotion(uint32 level, uint32 effect) const { - if (proto->Duration & 0x80000000) - return false; + level = NormalizeLevel(level); - if (proto->Bonding == BIND_QUEST_ITEM || proto->Bonding == BIND_WHEN_USE) - return false; + auto const levelItr = potionCache.find(level); + if (levelItr == potionCache.end()) + return 0; - if (proto->Class == ITEM_CLASS_CONTAINER) - return false; + auto const effectItr = levelItr->second.find(effect); + if (effectItr == levelItr->second.end() || effectItr->second.empty()) + return 0; - bool properSlot = false; - for (std::map >::iterator i = viableSlots.begin(); i != viableSlots.end(); - ++i) - { - std::set const& slots = viableSlots[(EquipmentSlots)i->first]; - if (slots.find((InventoryType)proto->InventoryType) != slots.end()) - properSlot = true; - } - - return properSlot; + return Acore::Containers::SelectRandomContainerElement(effectItr->second); } -void RandomItemMgr::AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank) +uint32 RandomItemMgr::GetRandomFood(uint32 level, uint32 category) const { - switch (mod) - { - case ITEM_MOD_HEALTH: - case ITEM_MOD_STAMINA: - case ITEM_MOD_MANA: - case ITEM_MOD_INTELLECT: - case ITEM_MOD_SPIRIT: - ++sp; - break; - } + level = NormalizeLevel(level); - switch (mod) - { - case ITEM_MOD_AGILITY: - case ITEM_MOD_STRENGTH: - case ITEM_MOD_HEALTH: - case ITEM_MOD_STAMINA: - ++tank; - break; - } + uint32 const bucket = (level - 1) / 10; - switch (mod) - { - case ITEM_MOD_HEALTH: - case ITEM_MOD_STAMINA: - case ITEM_MOD_AGILITY: - case ITEM_MOD_STRENGTH: - ++ap; - break; - } + auto const levelItr = foodCache.find(bucket); + if (levelItr == foodCache.end()) + return 0; + + auto const catItr = levelItr->second.find(category); + if (catItr == levelItr->second.end() || catItr->second.empty()) + return 0; + + return Acore::Containers::SelectRandomContainerElement(catItr->second); } -bool RandomItemMgr::CheckItemStats(uint8 clazz, uint8 sp, uint8 ap, uint8 tank) +uint32 RandomItemMgr::GetRandomTrade(uint32 level) const { - switch (clazz) - { - case CLASS_PRIEST: - case CLASS_MAGE: - case CLASS_WARLOCK: - if (!sp || ap > sp || tank > sp) - return false; - break; - case CLASS_PALADIN: - case CLASS_WARRIOR: - if ((!ap && !tank) || sp > ap || sp > tank) - return false; - break; - case CLASS_HUNTER: - case CLASS_ROGUE: - if (!ap || sp > ap || sp > tank) - return false; - break; - } + level = NormalizeLevel(level); + uint32 const bucket = (level - 1) / 10; - return sp || ap || tank; + auto const itr = tradeCache.find(bucket); + if (itr == tradeCache.end() || itr->second.empty()) + return 0; + + return Acore::Containers::SelectRandomContainerElement(itr->second); } -std::vector RandomItemMgr::GetCachedEquipments(uint32 requiredLevel, uint32 inventoryType) +float RandomItemMgr::GetItemRarity(uint32 itemId) const { - return equipCacheNew[requiredLevel][inventoryType]; + auto const itr = rarityCache.find(itemId); + return itr != rarityCache.end() ? itr->second : 0.0f; } -bool RandomItemMgr::ShouldEquipArmorForSpec(uint8 playerclass, uint8 spec, ItemTemplate const* proto) +std::vector const& RandomItemMgr::GetEnchantmentPool(uint32 entry) const { - if (proto->InventoryType == INVTYPE_TABARD) - return true; + static std::vector const empty; + auto it = enchPoolCache.find(entry); + if (it == enchPoolCache.end()) + return empty; + return it->second; +} - if (!m_weightScales[playerclass][spec].info.id) +bool RandomItemMgr::CanEquipArmor(ItemTemplate const* proto, uint8 clazz, uint32 level) const +{ + // skip null proto or invalid class + if (!proto || clazz >= MAX_CLASSES) return false; - std::unordered_set resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH}; - - switch (playerclass) - { - case CLASS_WARRIOR: - { - if (proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - if (m_weightScales[playerclass][spec].info.name == "arms" || - m_weightScales[playerclass][spec].info.name == "fury") - { - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_LEATHER, ITEM_SUBCLASS_ARMOR_MAIL, - ITEM_SUBCLASS_ARMOR_PLATE}; - } - else - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_MAIL, ITEM_SUBCLASS_ARMOR_PLATE}; - break; - } - case CLASS_DEATH_KNIGHT: - { - if (proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_SIGIL, ITEM_SUBCLASS_ARMOR_PLATE}; - } - case CLASS_PALADIN: - { - if (m_weightScales[playerclass][spec].info.name != "holy" && proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - if (m_weightScales[playerclass][spec].info.name != "holy") - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_MAIL, ITEM_SUBCLASS_ARMOR_PLATE, ITEM_SUBCLASS_ARMOR_LIBRAM}; - else - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER, ITEM_SUBCLASS_ARMOR_MAIL, - ITEM_SUBCLASS_ARMOR_PLATE, ITEM_SUBCLASS_ARMOR_LIBRAM}; - break; - } - case CLASS_HUNTER: - { - if (proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER, ITEM_SUBCLASS_ARMOR_MAIL}; - break; - } - case CLASS_ROGUE: - { - if (proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER}; - break; - } - case CLASS_PRIEST: - { - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH}; - break; - } - case CLASS_SHAMAN: - { - if (m_weightScales[playerclass][spec].info.name == "enhance" && proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_TOTEM, ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER, - ITEM_SUBCLASS_ARMOR_MAIL}; - break; - } - case CLASS_MAGE: - case CLASS_WARLOCK: - { - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH}; - break; - } - case CLASS_DRUID: - { - if ((m_weightScales[playerclass][spec].info.name == "feraltank" || - m_weightScales[playerclass][spec].info.name == "feraldps") && - proto->InventoryType == INVTYPE_HOLDABLE) - return false; - - resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_IDOL, ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER}; - break; - } - } - - return resultArmorSubClass.find(proto->SubClass) != resultArmorSubClass.end(); -} - -bool RandomItemMgr::ShouldEquipWeaponForSpec(uint8 playerclass, uint8 spec, ItemTemplate const* proto) -{ - EquipmentSlots slot_mh = EQUIPMENT_SLOT_START; - EquipmentSlots slot_oh = EQUIPMENT_SLOT_START; - EquipmentSlots slot_rh = EQUIPMENT_SLOT_START; - for (std::map >::iterator i = viableSlots.begin(); i != viableSlots.end(); - ++i) - { - std::set slots = viableSlots[(EquipmentSlots)i->first]; - if (slots.find((InventoryType)proto->InventoryType) != slots.end()) - { - if (i->first == EQUIPMENT_SLOT_MAINHAND) - slot_mh = i->first; - if (i->first == EQUIPMENT_SLOT_OFFHAND) - slot_oh = i->first; - if (i->first == EQUIPMENT_SLOT_RANGED) - slot_rh = i->first; - } - } - - if (slot_mh == EQUIPMENT_SLOT_START && slot_oh == EQUIPMENT_SLOT_START && slot_rh == EQUIPMENT_SLOT_START) + // skip non-armor and out-of-range armor subclasses + if (proto->Class != ITEM_CLASS_ARMOR || proto->SubClass >= MAX_ITEM_SUBCLASS_ARMOR) return false; - if (!m_weightScales[playerclass][spec].info.id) + // skip items with no viable equip slot + if (!GetViableSlots(static_cast(proto->InventoryType))) return false; - std::unordered_set mh_weapons; - std::unordered_set oh_weapons; - std::unordered_set r_weapons; - - switch (playerclass) - { - case CLASS_WARRIOR: - { - if (m_weightScales[playerclass][spec].info.name == "prot") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE, - ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_FIST}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD}; - r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}; - } - else - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2, - ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_POLEARM}; - r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}; - } - break; - } - case CLASS_PALADIN: - { - if (m_weightScales[playerclass][spec].info.name == "prot") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD}; - r_weapons = {ITEM_SUBCLASS_ARMOR_LIBRAM}; - } - else if (m_weightScales[playerclass][spec].info.name == "holy") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD, ITEM_SUBCLASS_ARMOR_MISC}; - r_weapons = {ITEM_SUBCLASS_ARMOR_LIBRAM}; - } - else - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2, - ITEM_SUBCLASS_WEAPON_POLEARM}; - r_weapons = {ITEM_SUBCLASS_ARMOR_LIBRAM}; - } - break; - } - case CLASS_HUNTER: - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_STAFF, - ITEM_SUBCLASS_WEAPON_POLEARM}; - r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}; - break; - } - case CLASS_ROGUE: - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_DAGGER}; - oh_weapons = {ITEM_SUBCLASS_WEAPON_DAGGER}; - r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}; - break; - } - case CLASS_PRIEST: - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_MACE}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC}; - r_weapons = {ITEM_SUBCLASS_WEAPON_WAND}; - break; - } - case CLASS_SHAMAN: - { - if (m_weightScales[playerclass][spec].info.name == "resto") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE, - ITEM_SUBCLASS_WEAPON_FIST}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC, ITEM_SUBCLASS_ARMOR_SHIELD}; - r_weapons = {ITEM_SUBCLASS_ARMOR_TOTEM}; - } - else if (m_weightScales[playerclass][spec].info.name == "enhance") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_MACE2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_DAGGER, - ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE, ITEM_SUBCLASS_WEAPON_FIST}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD}; - r_weapons = {ITEM_SUBCLASS_ARMOR_TOTEM}; - } - else - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF}; - r_weapons = {ITEM_SUBCLASS_ARMOR_TOTEM}; - } - break; - } - case CLASS_MAGE: - case CLASS_WARLOCK: - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_SWORD}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC}; - r_weapons = {ITEM_SUBCLASS_WEAPON_WAND}; - break; - } - case CLASS_DRUID: - { - if (m_weightScales[playerclass][spec].info.name == "feraltank") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_MACE2}; - r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL}; - } - else if (m_weightScales[playerclass][spec].info.name == "resto") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_FIST, - ITEM_SUBCLASS_WEAPON_MACE}; - oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC}; - r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL}; - } - else if (m_weightScales[playerclass][spec].info.name == "feraldps") - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_MACE2}; - r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL}; - } - else - { - mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF}; - r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL}; - } - break; - } - } - - if (slot_mh == EQUIPMENT_SLOT_MAINHAND) - { - return mh_weapons.find(proto->SubClass) != mh_weapons.end(); - } - - if (slot_oh == EQUIPMENT_SLOT_OFFHAND) - { - return oh_weapons.find(proto->SubClass) != oh_weapons.end(); - } - - if (slot_rh == EQUIPMENT_SLOT_RANGED) - { - return r_weapons.find(proto->SubClass) != r_weapons.end(); - } - - return false; -} - -bool RandomItemMgr::CanEquipArmor(uint8 clazz, uint32 level, ItemTemplate const* proto) -{ + // skip additional checks for tabards - always equippable if (proto->InventoryType == INVTYPE_TABARD) return true; @@ -727,114 +1067,1006 @@ bool RandomItemMgr::CanEquipArmor(uint8 clazz, uint32 level, ItemTemplate const* proto->SubClass == ITEM_SUBCLASS_ARMOR_SHIELD) return true; - if ((clazz == CLASS_WARRIOR || clazz == CLASS_PALADIN) && level >= 40) + uint32 const requiredSubClass = [&]() -> uint32 { - if (proto->SubClass != ITEM_SUBCLASS_ARMOR_PLATE && proto->InventoryType != INVTYPE_CLOAK) - return false; - } + if (clazz == CLASS_WARRIOR || clazz == CLASS_PALADIN) + return level >= 40 ? ITEM_SUBCLASS_ARMOR_PLATE : ITEM_SUBCLASS_ARMOR_MAIL; + if (clazz == CLASS_HUNTER || clazz == CLASS_SHAMAN) + return level >= 40 ? ITEM_SUBCLASS_ARMOR_MAIL : ITEM_SUBCLASS_ARMOR_LEATHER; + if (clazz == CLASS_DRUID || clazz == CLASS_ROGUE) + return ITEM_SUBCLASS_ARMOR_LEATHER; + return ITEM_SUBCLASS_ARMOR_CLOTH; // mage/warlock/priest + }(); - if (((clazz == CLASS_WARRIOR || clazz == CLASS_PALADIN) && level < 40) || - ((clazz == CLASS_HUNTER || clazz == CLASS_SHAMAN) && level >= 40)) - { - if (proto->SubClass != ITEM_SUBCLASS_ARMOR_MAIL && proto->InventoryType != INVTYPE_CLOAK) - return false; - } + // skip armor of wrong subclass (cloaks are exempt) + if (proto->InventoryType != INVTYPE_CLOAK && proto->SubClass != requiredSubClass) + return false; - if (((clazz == CLASS_HUNTER || clazz == CLASS_SHAMAN) && level < 40) || - (clazz == CLASS_DRUID || clazz == CLASS_ROGUE)) - { - if (proto->SubClass != ITEM_SUBCLASS_ARMOR_LEATHER && proto->InventoryType != INVTYPE_CLOAK) - return false; - } - - if (proto->Quality <= ITEM_QUALITY_NORMAL) + // skip stats calculation for poor/normal quality items (grey/white) + if (proto->Quality < ITEM_QUALITY_UNCOMMON) return true; - uint8 sp = 0, ap = 0, tank = 0; + uint8 sp = 0; + uint8 ap = 0; + uint8 tank = 0; + for (uint8 j = 0; j < MAX_ITEM_PROTO_STATS; ++j) { - // for ItemStatValue != 0 + // skip items with no stats value if (!proto->ItemStat[j].ItemStatValue) continue; AddItemStats(proto->ItemStat[j].ItemStatType, sp, ap, tank); } - return CheckItemStats(clazz, sp, ap, tank); + return CanUseItemStats(clazz, sp, ap, tank); } -bool RandomItemMgr::CanEquipWeapon(uint8 clazz, ItemTemplate const* proto) +bool RandomItemMgr::CanEquipWeapon(ItemTemplate const* proto, uint8 clazz) const { + // skip null proto or invalid class + if (!proto || clazz >= MAX_CLASSES) + return false; + + // skip non-weapon and out-of-range weapon subclasses + if (proto->Class != ITEM_CLASS_WEAPON || proto->SubClass >= MAX_ITEM_SUBCLASS_WEAPON) + return false; + + // skip items with no viable equip slot + if (!GetViableSlots(static_cast(proto->InventoryType))) + return false; + + // skip weapons the class has no proficiency for + return (m_weaponProficiency[clazz] & (1u << proto->SubClass)) != 0; +} + +bool RandomItemMgr::ShouldEquipArmorForSpec(ItemTemplate const* proto, uint8 clazz, uint8 spec) const +{ + if (!proto) + return false; + + // skip additional checks for tabards - always equippable + if (proto->InventoryType == INVTYPE_TABARD) + return true; + + std::unordered_map const& specMap = m_weightScales[clazz]; + auto const itr = specMap.find(spec); + if (itr == specMap.end() || !itr->second.info.id) + return false; + + WeightScale const& scale = itr->second; + std::string const& specName = scale.info.name; + uint32 const subClass = proto->SubClass; + bool const isHoldable = proto->InventoryType == INVTYPE_HOLDABLE; + switch (clazz) { - case CLASS_PRIEST: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && proto->SubClass != ITEM_SUBCLASS_WEAPON_WAND && - proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER) + case CLASS_DEATH_KNIGHT: + if (isHoldable) return false; - break; + return subClass == ITEM_SUBCLASS_ARMOR_SIGIL || subClass == ITEM_SUBCLASS_ARMOR_PLATE; + + case CLASS_WARRIOR: + if (isHoldable) + return false; + if (specName == "arms" || specName == "fury") + return subClass == ITEM_SUBCLASS_ARMOR_LEATHER || subClass == ITEM_SUBCLASS_ARMOR_MAIL || + subClass == ITEM_SUBCLASS_ARMOR_PLATE; + return subClass == ITEM_SUBCLASS_ARMOR_MAIL || subClass == ITEM_SUBCLASS_ARMOR_PLATE; + + case CLASS_PALADIN: + if (specName != "holy") + { + if (isHoldable) + return false; + + return subClass == ITEM_SUBCLASS_ARMOR_MAIL || subClass == ITEM_SUBCLASS_ARMOR_PLATE || + subClass == ITEM_SUBCLASS_ARMOR_LIBRAM; + } + return subClass == ITEM_SUBCLASS_ARMOR_CLOTH || subClass == ITEM_SUBCLASS_ARMOR_LEATHER || + subClass == ITEM_SUBCLASS_ARMOR_MAIL || subClass == ITEM_SUBCLASS_ARMOR_PLATE || + subClass == ITEM_SUBCLASS_ARMOR_LIBRAM; + + case CLASS_HUNTER: + if (isHoldable) + return false; + return subClass == ITEM_SUBCLASS_ARMOR_CLOTH || subClass == ITEM_SUBCLASS_ARMOR_LEATHER || + subClass == ITEM_SUBCLASS_ARMOR_MAIL; + + case CLASS_ROGUE: + if (isHoldable) + return false; + return subClass == ITEM_SUBCLASS_ARMOR_CLOTH || subClass == ITEM_SUBCLASS_ARMOR_LEATHER; + + case CLASS_PRIEST: case CLASS_MAGE: case CLASS_WARLOCK: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && proto->SubClass != ITEM_SUBCLASS_WEAPON_WAND && - proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD) - return false; - break; - case CLASS_WARRIOR: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE && - proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD2 && - proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD && - proto->SubClass != ITEM_SUBCLASS_WEAPON_GUN && proto->SubClass != ITEM_SUBCLASS_WEAPON_CROSSBOW && - proto->SubClass != ITEM_SUBCLASS_WEAPON_BOW && proto->SubClass != ITEM_SUBCLASS_WEAPON_THROWN && - proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && - proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF) - return false; - break; - case CLASS_PALADIN: - case CLASS_DEATH_KNIGHT: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM && - proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && - proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && - proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD) - return false; - break; + return subClass == ITEM_SUBCLASS_ARMOR_CLOTH; + case CLASS_SHAMAN: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE && - proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 && - proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && - proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF) + if (specName == "enhance" && isHoldable) return false; - break; + return subClass == ITEM_SUBCLASS_ARMOR_TOTEM || subClass == ITEM_SUBCLASS_ARMOR_CLOTH || + subClass == ITEM_SUBCLASS_ARMOR_LEATHER || subClass == ITEM_SUBCLASS_ARMOR_MAIL; + case CLASS_DRUID: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 && - proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && - proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM) + if ((specName == "feraltank" || specName == "feraldps") && isHoldable) return false; - break; - case CLASS_HUNTER: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_BOW && - proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE && - proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD && - proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && proto->SubClass != ITEM_SUBCLASS_WEAPON_GUN && - proto->SubClass != ITEM_SUBCLASS_WEAPON_CROSSBOW && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && - proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM) - return false; - break; - case CLASS_ROGUE: - if (proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD && - proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && - proto->SubClass != ITEM_SUBCLASS_WEAPON_GUN && proto->SubClass != ITEM_SUBCLASS_WEAPON_CROSSBOW && - proto->SubClass != ITEM_SUBCLASS_WEAPON_BOW && proto->SubClass != ITEM_SUBCLASS_WEAPON_THROWN && - proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE) - return false; - break; + return subClass == ITEM_SUBCLASS_ARMOR_IDOL || subClass == ITEM_SUBCLASS_ARMOR_CLOTH || + subClass == ITEM_SUBCLASS_ARMOR_LEATHER; + + default: + return false; } +} + +bool RandomItemMgr::ShouldEquipWeaponForSpec(ItemTemplate const* proto, uint8 clazz, uint8 spec) const +{ + if (!proto) + return false; + + std::unordered_map const& specMap = m_weightScales[clazz]; + auto const itr = specMap.find(spec); + if (itr == specMap.end() || !itr->second.info.id) + return false; + + bool hasMH = false, hasOH = false, hasRH = false; + + if (auto const* slots = GetViableSlots(static_cast(proto->InventoryType))) + { + for (EquipmentSlots slot : *slots) + { + if (slot == EQUIPMENT_SLOT_MAINHAND) hasMH = true; + if (slot == EQUIPMENT_SLOT_OFFHAND) hasOH = true; + if (slot == EQUIPMENT_SLOT_RANGED) hasRH = true; + } + } + + if (!hasMH && !hasOH && !hasRH) + return false; + + WeightScale const& scale = itr->second; + std::string const& specName = scale.info.name; + uint32 const subClass = proto->SubClass; + + auto inSlot = [&](bool hasSlot, std::initializer_list list) { + if (!hasSlot) return false; + for (uint32 sc : list) if (sc == subClass) return true; + return false; + }; + + switch (clazz) + { + case CLASS_DEATH_KNIGHT: + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2, + ITEM_SUBCLASS_WEAPON_POLEARM}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_SIGIL}); + + case CLASS_WARRIOR: + if (specName == "prot") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE, + ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_FIST}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_SHIELD}) || + inSlot(hasRH, {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}); + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2, + ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_POLEARM}) || + inSlot(hasRH, {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}); + + case CLASS_PALADIN: + if (specName == "prot") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_SHIELD}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_LIBRAM}); + if (specName == "holy") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_SHIELD, ITEM_SUBCLASS_ARMOR_MISC}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_LIBRAM}); + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2, + ITEM_SUBCLASS_WEAPON_POLEARM}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_LIBRAM}); + + case CLASS_HUNTER: + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_STAFF, + ITEM_SUBCLASS_WEAPON_POLEARM}) || + inSlot(hasRH, {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}); + + case CLASS_ROGUE: + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_DAGGER}) || + inSlot(hasOH, {ITEM_SUBCLASS_WEAPON_DAGGER}) || + inSlot(hasRH, {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN}); + + case CLASS_PRIEST: + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_MACE}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_MISC}) || + inSlot(hasRH, {ITEM_SUBCLASS_WEAPON_WAND}); + + case CLASS_SHAMAN: + if (specName == "resto") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE, + ITEM_SUBCLASS_WEAPON_FIST}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_MISC, ITEM_SUBCLASS_ARMOR_SHIELD}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_TOTEM}); + if (specName == "enhance") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_MACE2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_DAGGER, + ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE, ITEM_SUBCLASS_WEAPON_FIST}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_SHIELD}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_TOTEM}); + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_STAFF}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_TOTEM}); + + case CLASS_MAGE: + case CLASS_WARLOCK: + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_SWORD}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_MISC}) || + inSlot(hasRH, {ITEM_SUBCLASS_WEAPON_WAND}); + + case CLASS_DRUID: + if (specName == "feraltank" || specName == "feraldps") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_MACE2}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_IDOL}); + if (specName == "resto") + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_FIST, + ITEM_SUBCLASS_WEAPON_MACE}) || + inSlot(hasOH, {ITEM_SUBCLASS_ARMOR_MISC}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_IDOL}); + return inSlot(hasMH, {ITEM_SUBCLASS_WEAPON_STAFF}) || + inSlot(hasRH, {ITEM_SUBCLASS_ARMOR_IDOL}); + + default: + return false; + } +} + +uint32 RandomItemMgr::GetQuestIdForItem(uint32 itemId) const +{ + for (auto const& [id, quest] : sObjectMgr->GetQuestTemplates()) + { + for (uint32 i = 0; i < quest->GetRewItemsCount(); ++i) + { + if (quest->RewardItemId[i] == itemId) + return quest->GetQuestId(); + } + + for (uint32 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) + { + if (quest->RewardChoiceItemId[i] == itemId) + return quest->GetQuestId(); + } + } + + return 0; +} + +std::vector RandomItemMgr::GetQuestIdsForItem(uint32 itemId) const +{ + std::vector questIds; + + for (auto const& [id, quest] : sObjectMgr->GetQuestTemplates()) + { + for (uint32 i = 0; i < quest->GetRewItemsCount(); ++i) + { + if (quest->RewardItemId[i] == itemId) + { + questIds.push_back(quest->GetQuestId()); + break; + } + } + + for (uint32 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) + { + if (quest->RewardChoiceItemId[i] == itemId) + { + questIds.push_back(quest->GetQuestId()); + break; + } + } + } + + return questIds; +} + +bool RandomItemMgr::IsInternalItem(ItemTemplate const* proto) +{ + if (!proto) + return false; + + if (proto->HasFlag(ITEM_FLAG_DEPRECATED)) + return true; + + char const* name = proto->Name1.c_str(); + return strstr(name, "NPC ") || // 4859 + strstr(name, "Monster ") || // 574 + strstr(name, "Test") || // 427 -- "(Test)", "Test ", " Test" + strstr(name, "Deprecated ") || // 399 + strstr(name, "[PH]") || // 138 + strstr(name, "TEST") || // 108 -- "(TEST)", "TEST ", " TEST", "(JEFFTEST)" + strstr(name, "Unused ") || // 19 + strstr(name, "zzOLD") || // 11 + strstr(name, "(test)") || // 8 + strstr(name, "(OLD)") || // 7 + strstr(name, "QR") || // 5 + strstr(name, "2200 "); // 4 +} + +bool RandomItemMgr::IsValidItem(ItemTemplate const* proto) +{ + if (!proto) + return false; + + // check items with no item level + if (proto->ItemLevel == 0) + return false; + + // check temporary/expiring items + if (proto->Duration) + return false; + + // check area-, or map-restricted items + if (proto->Area || proto->Map) + return false; + + // check city-rank-, or pvp-rank-restricted items + if (proto->RequiredCityRank || proto->RequiredHonorRank) + return false; + + // check class-restricted items + if ((proto->AllowableClass & CLASSMASK_ALL_PLAYABLE) == 0) + return false; + + constexpr uint32 RACEMASK_ALL_PLAYABLE = + (1 << (RACE_HUMAN - 1)) | + (1 << (RACE_ORC - 1)) | + (1 << (RACE_DWARF - 1)) | + (1 << (RACE_NIGHTELF - 1)) | + (1 << (RACE_UNDEAD_PLAYER - 1)) | + (1 << (RACE_TAUREN - 1)) | + (1 << (RACE_GNOME - 1)) | + (1 << (RACE_TROLL - 1)) | + (1 << (RACE_BLOODELF - 1)) | + (1 << (RACE_DRAENEI - 1)); + + // check race-restricted items + if ((proto->AllowableRace & RACEMASK_ALL_PLAYABLE) == 0) + return false; + + // check test/internal items + if (IsInternalItem(proto)) + return false; + + // skip items flagged as unobtainable + if (sPlayerbotAIConfig.unobtainableItems.contains(proto->ItemId)) + return false; return true; } -void RandomItemMgr::BuildItemInfoCache() +static bool IsCraftedBySpellInfo(uint32 itemId, SpellInfo const* spellInfo) { - //uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); //not used, line marked for removal. + for (uint8 x = 0; x < MAX_SPELL_REAGENTS; ++x) + { + if (spellInfo->Reagent[x] <= 0) + continue; + if (itemId == static_cast(spellInfo->Reagent[x])) + return true; + } + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + if (spellInfo->Effects[i].Effect == SPELL_EFFECT_CREATE_ITEM && + spellInfo->Effects[i].ItemType == itemId) + return true; + } + + return false; +} + +static bool IsCraftedBySpell(uint32 itemId, uint32 spellId) +{ + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + if (!spellInfo) + return false; + + if (IsCraftedBySpellInfo(itemId, spellInfo)) + return true; + + for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) + { + SpellInfo const* triggerSpellInfo = sSpellMgr->GetSpellInfo(spellInfo->Effects[i].TriggerSpell); + if (!triggerSpellInfo) + continue; + + if (IsCraftedBySpellInfo(itemId, triggerSpellInfo)) + return true; + } + + return false; +} + +static bool IsCraftedBySkill(uint32 itemId, uint32 skillId) +{ + for (SkillLineAbilityEntry const* skillLine : GetSkillLineAbilitiesBySkillLine(skillId)) + { + if (IsCraftedBySpell(itemId, skillLine->Spell)) + return true; + } + + std::vector const* itemTemplates = sObjectMgr->GetItemTemplateStoreFast(); + for (ItemTemplate const* recipe : *itemTemplates) + { + if (!recipe || recipe->Class != ITEM_CLASS_RECIPE) + continue; + + uint32 skillType = 0; + switch (recipe->SubClass) + { + case ITEM_SUBCLASS_LEATHERWORKING_PATTERN: skillType = SKILL_LEATHERWORKING; break; + case ITEM_SUBCLASS_TAILORING_PATTERN: skillType = SKILL_TAILORING; break; + case ITEM_SUBCLASS_ENGINEERING_SCHEMATIC: skillType = SKILL_ENGINEERING; break; + case ITEM_SUBCLASS_BLACKSMITHING: skillType = SKILL_BLACKSMITHING; break; + case ITEM_SUBCLASS_COOKING_RECIPE: skillType = SKILL_COOKING; break; + case ITEM_SUBCLASS_ALCHEMY_RECIPE: skillType = SKILL_ALCHEMY; break; + case ITEM_SUBCLASS_FIRST_AID_MANUAL: skillType = SKILL_FIRST_AID; break; + case ITEM_SUBCLASS_ENCHANTING_FORMULA: skillType = SKILL_ENCHANTING; break; + case ITEM_SUBCLASS_JEWELCRAFTING_RECIPE: skillType = SKILL_JEWELCRAFTING; break; + case ITEM_SUBCLASS_FISHING_MANUAL: skillType = SKILL_FISHING; break; + default: break; + } + + if (!skillType) + continue; + + for (uint32 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) + { + if (IsCraftedBySpell(itemId, recipe->Spells[i].SpellId)) + return true; + } + } + + return false; +} + +bool RandomItemMgr::IsUsedBySkill(ItemTemplate const* proto, uint32 skillId) +{ + if (!proto) + return false; + + if (itemCache.contains(proto->ItemId)) + return true; + + switch (proto->Class) + { + case ITEM_CLASS_TRADE_GOODS: + case ITEM_CLASS_MISC: + case ITEM_CLASS_REAGENT: + case ITEM_CLASS_GEM: + if (IsCraftedBySkill(proto->ItemId, skillId)) + { + itemCache.insert(proto->ItemId); + return true; + } + break; + default: + break; + } + + return false; +} + +static uint32 GetWeaponSkillId(uint32 subClass) +{ + switch (subClass) + { + case ITEM_SUBCLASS_WEAPON_AXE: return SKILL_AXES; + case ITEM_SUBCLASS_WEAPON_AXE2: return SKILL_2H_AXES; + case ITEM_SUBCLASS_WEAPON_BOW: return SKILL_BOWS; + case ITEM_SUBCLASS_WEAPON_GUN: return SKILL_GUNS; + case ITEM_SUBCLASS_WEAPON_MACE: return SKILL_MACES; + case ITEM_SUBCLASS_WEAPON_MACE2: return SKILL_2H_MACES; + case ITEM_SUBCLASS_WEAPON_POLEARM: return SKILL_POLEARMS; + case ITEM_SUBCLASS_WEAPON_SWORD: return SKILL_SWORDS; + case ITEM_SUBCLASS_WEAPON_SWORD2: return SKILL_2H_SWORDS; + case ITEM_SUBCLASS_WEAPON_STAFF: return SKILL_STAVES; + case ITEM_SUBCLASS_WEAPON_FIST: return SKILL_FIST_WEAPONS; + case ITEM_SUBCLASS_WEAPON_DAGGER: return SKILL_DAGGERS; + case ITEM_SUBCLASS_WEAPON_THROWN: return SKILL_THROWN; + case ITEM_SUBCLASS_WEAPON_CROSSBOW: return SKILL_CROSSBOWS; + case ITEM_SUBCLASS_WEAPON_WAND: return SKILL_WANDS; + default: return SKILL_NONE; + } +} + +void RandomItemMgr::InitWeaponProficiency() +{ + m_weaponProficiency.fill(0); + + for (uint32 subClass = 0; subClass < MAX_ITEM_SUBCLASS_WEAPON; ++subClass) + { + uint32 const skillId = GetWeaponSkillId(subClass); + if (!skillId) + continue; // ITEM_SUBCLASS_WEAPON_SPEAR, FISHING_POLE, OBSOLETE, etc. + + for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls) + { + if (!((1u << (cls - 1)) & CLASSMASK_ALL_PLAYABLE)) + continue; + + // Weapon proficiencies in WotLK are class-based, not race-based, so + // using RACE_HUMAN here is acceptable as a placeholder. Otherwise, + // method GetSkillRaceClassInfo should be splitted into two separate + // methods for race and class. + if (GetSkillRaceClassInfo(skillId, RACE_HUMAN, cls)) + m_weaponProficiency[cls] |= (1u << subClass); + } + } +} + +void RandomItemMgr::InitPredicates() +{ + predicates[RANDOM_ITEM_GUILD_TASK] = new RandomItemGuildTaskPredicate(); + predicates[RANDOM_ITEM_GUILD_TASK_REWARD_EQUIP_GREEN] = new RandomItemGuildTaskRewardPredicate(true, false); + predicates[RANDOM_ITEM_GUILD_TASK_REWARD_EQUIP_BLUE] = new RandomItemGuildTaskRewardPredicate(true, true); + predicates[RANDOM_ITEM_GUILD_TASK_REWARD_TRADE] = new RandomItemGuildTaskRewardPredicate(false, false); + predicates[RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE] = new RandomItemGuildTaskRewardPredicate(false, true); +} + +void RandomItemMgr::InitViableSlots() +{ + auto addEquipmentSlot = [&](InventoryType invType, EquipmentSlots slot) + { + viableSlots[invType].push_back(slot); + }; + + addEquipmentSlot(INVTYPE_HEAD, EQUIPMENT_SLOT_HEAD); + addEquipmentSlot(INVTYPE_NECK, EQUIPMENT_SLOT_NECK); + addEquipmentSlot(INVTYPE_SHOULDERS, EQUIPMENT_SLOT_SHOULDERS); + addEquipmentSlot(INVTYPE_BODY, EQUIPMENT_SLOT_BODY); + addEquipmentSlot(INVTYPE_CHEST, EQUIPMENT_SLOT_CHEST); + addEquipmentSlot(INVTYPE_ROBE, EQUIPMENT_SLOT_CHEST); + addEquipmentSlot(INVTYPE_WAIST, EQUIPMENT_SLOT_WAIST); + addEquipmentSlot(INVTYPE_LEGS, EQUIPMENT_SLOT_LEGS); + addEquipmentSlot(INVTYPE_FEET, EQUIPMENT_SLOT_FEET); + addEquipmentSlot(INVTYPE_WRISTS, EQUIPMENT_SLOT_WRISTS); + addEquipmentSlot(INVTYPE_HANDS, EQUIPMENT_SLOT_HANDS); + addEquipmentSlot(INVTYPE_FINGER, EQUIPMENT_SLOT_FINGER1); + addEquipmentSlot(INVTYPE_FINGER, EQUIPMENT_SLOT_FINGER2); + addEquipmentSlot(INVTYPE_TRINKET, EQUIPMENT_SLOT_TRINKET1); + addEquipmentSlot(INVTYPE_TRINKET, EQUIPMENT_SLOT_TRINKET2); + + addEquipmentSlot(INVTYPE_WEAPON, EQUIPMENT_SLOT_MAINHAND); + addEquipmentSlot(INVTYPE_2HWEAPON, EQUIPMENT_SLOT_MAINHAND); + addEquipmentSlot(INVTYPE_WEAPONMAINHAND, EQUIPMENT_SLOT_MAINHAND); + + addEquipmentSlot(INVTYPE_WEAPON, EQUIPMENT_SLOT_OFFHAND); + addEquipmentSlot(INVTYPE_WEAPONOFFHAND, EQUIPMENT_SLOT_OFFHAND); + addEquipmentSlot(INVTYPE_SHIELD, EQUIPMENT_SLOT_OFFHAND); + addEquipmentSlot(INVTYPE_HOLDABLE, EQUIPMENT_SLOT_OFFHAND); + + addEquipmentSlot(INVTYPE_RANGED, EQUIPMENT_SLOT_RANGED); + addEquipmentSlot(INVTYPE_THROWN, EQUIPMENT_SLOT_RANGED); + addEquipmentSlot(INVTYPE_RANGEDRIGHT, EQUIPMENT_SLOT_RANGED); + addEquipmentSlot(INVTYPE_RELIC, EQUIPMENT_SLOT_RANGED); + + addEquipmentSlot(INVTYPE_TABARD, EQUIPMENT_SLOT_TABARD); + addEquipmentSlot(INVTYPE_CLOAK, EQUIPMENT_SLOT_BACK); +} + +void RandomItemMgr::InitWeightLinks() +{ + // primary stats + weightStatLink["sta"] = ITEM_MOD_STAMINA; + weightStatLink["str"] = ITEM_MOD_STRENGTH; + weightStatLink["agi"] = ITEM_MOD_AGILITY; + weightStatLink["int"] = ITEM_MOD_INTELLECT; + weightStatLink["spi"] = ITEM_MOD_SPIRIT; + + // offensive stats + weightStatLink["splpwr"] = ITEM_MOD_SPELL_POWER; + weightStatLink["atkpwr"] = ITEM_MOD_ATTACK_POWER; + weightStatLink["exprtng"] = ITEM_MOD_EXPERTISE_RATING; + weightStatLink["critstrkrtng"] = ITEM_MOD_CRIT_RATING; + weightStatLink["hitrtng"] = ITEM_MOD_HIT_RATING; + weightStatLink["hastertng"] = ITEM_MOD_HASTE_RATING; + weightStatLink["armorpenrtng"] = ITEM_MOD_ARMOR_PENETRATION_RATING; + + // defensive stats + weightStatLink["defrtng"] = ITEM_MOD_DEFENSE_SKILL_RATING; + weightStatLink["dodgertng"] = ITEM_MOD_DODGE_RATING; + weightStatLink["block"] = ITEM_MOD_BLOCK_VALUE; + weightStatLink["blockrtng"] = ITEM_MOD_BLOCK_RATING; + weightStatLink["parryrtng"] = ITEM_MOD_PARRY_RATING; + + // misc + weightStatLink["manargn"] = ITEM_MOD_MANA_REGENERATION; + + // combat rating links + weightRatingLink["exprtng"] = CR_EXPERTISE; + weightRatingLink["critstrkrtng"] = CR_CRIT_MELEE; + weightRatingLink["hitrtng"] = CR_HIT_MELEE; + weightRatingLink["hastertng"] = CR_HASTE_MELEE; + weightRatingLink["armorpenrtng"] = CR_ARMOR_PENETRATION; + weightRatingLink["defrtng"] = CR_DEFENSE_SKILL; + weightRatingLink["dodgertng"] = CR_DODGE; + weightRatingLink["blockrtng"] = CR_BLOCK; + weightRatingLink["parryrtng"] = CR_PARRY; +} + +bool RandomItemMgr::LoadCacheEquip() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Loading equipment cache..."); + + PreparedQueryResult result = + PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_EQUIP_CACHE)); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 equipment items. DB table `playerbots_equip_cache` is empty!"); + LOG_INFO("server.loading", " "); + return false; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + uint8 clazz = fields[0].Get(); + uint32 level = fields[1].Get(); + uint8 slot = fields[2].Get(); + uint32 quality = fields[3].Get(); + uint32 itemId = fields[4].Get(); + + BotEquipKey key(level, clazz, slot, quality); + equipCache[key].push_back(itemId); + + ++count; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} equipment records in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); + + return true; +} + +bool RandomItemMgr::LoadCacheRandomItem() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Loading random item cache..."); + + PreparedQueryResult result = + PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RNDITEM_CACHE)); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 random items. DB table `playerbots_rnditem_cache` is empty!"); + LOG_INFO("server.loading", " "); + return false; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + uint32 bucket = fields[0].Get(); + uint32 type = fields[1].Get(); + uint32 itemId = fields[2].Get(); + + RandomItemType const rit = static_cast(type); + + randomItemCache[bucket][rit].push_back(itemId); + + ++count; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} random item records in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); + + return true; +} + +bool RandomItemMgr::LoadCacheRarity() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Loading items rarity cache..."); + + PreparedQueryResult result = + PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RARITY_CACHE)); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 rarity items. DB table `playerbots_rarity_cache` is empty!"); + LOG_INFO("server.loading", " "); + return false; + } + + do + { + Field* fields = result->Fetch(); + uint32 itemId = fields[0].Get(); + float rarity = fields[1].Get(); + + rarityCache[itemId] = rarity; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} items rarity records in {} ms", rarityCache.size(), + GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); + + return true; +} + +void RandomItemMgr::LoadEnchantmentPool() +{ + enchPoolCache.clear(); + + QueryResult result = WorldDatabase.Query("SELECT entry, ench FROM item_enchantment_template"); + if (!result) + { + LOG_WARN("playerbots", "item_enchantment_template empty; bot autogear cannot evaluate random suffixes"); + return; + } + + uint32 count = 0; + do + { + Field* fields = result->Fetch(); + uint32 entry = fields[0].Get(); + uint32 ench = fields[1].Get(); + enchPoolCache[entry].push_back(ench); + ++count; + } while (result->NextRow()); + + LOG_INFO("playerbots", "Loaded {} item enchantment pool rows for bot autogear", count); +} + +void RandomItemMgr::BuildCacheRandomItem() +{ + uint32 const oldMSTime = getMSTime(); + + ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + LOG_INFO("server.loading", "Building random item cache from {} items", itemTemplates->size()); + + PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction(); + + uint32 count = 0; + for (auto const& itr : *itemTemplates) + { + ItemTemplate const* proto = &itr.second; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; + + // skip items with no sell price + if (proto->SellPrice == 0) + continue; + + uint32 const level = proto->ItemLevel; + uint32 const bucket = (level - 1) / 10; + + for (uint8 type = RANDOM_ITEM_GUILD_TASK; type <= RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE; ++type) + { + RandomItemType const rit = static_cast(type); + if (predicates[rit] && !predicates[rit]->Apply(proto)) + continue; + + randomItemCache[bucket][rit].push_back(itr.first); + ++count; + + PlayerbotsDatabasePreparedStatement* stmt = + PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RNDITEM_CACHE); + stmt->SetData(0, bucket); + stmt->SetData(1, type); + stmt->SetData(2, itr.first); + trans->Append(stmt); + } + } + + PlayerbotsDatabase.CommitTransaction(trans); + + LOG_INFO("server.loading", ">> Cached total {} random items in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void RandomItemMgr::BuildCacheEquip() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Building equipment cache for {} levels...", DEFAULT_MAX_LEVEL); + + ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + + PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction(); + + bool weaponOK = false; + bool armorOKBelow40Lvl = false; + bool armorOKAbove40Lvl = false; + + uint32 count = 0; + for (auto const& itr : *itemTemplates) + { + ItemTemplate const* proto = &itr.second; + + // skip non-equipment classes + if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR) + continue; + + // skip legendary, artifact and heirloom quality + if (proto->Quality > ITEM_QUALITY_EPIC) + continue; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; + + // skip items with no viable equip slot + auto const* slots = GetViableSlots(static_cast(proto->InventoryType)); + if (!slots) + continue; + + for (uint8 clazz = CLASS_WARRIOR; clazz < MAX_CLASSES; ++clazz) + { + // skip non-playable classes + if (((1u << (clazz - 1)) & CLASSMASK_ALL_PLAYABLE) == 0) + continue; + + // skip classes that cannot use this item + if ((proto->AllowableClass & (1u << (clazz - 1))) == 0) + continue; + + // cache CanEquip results before processing level loop + weaponOK = proto->Class == ITEM_CLASS_WEAPON && CanEquipWeapon(proto, clazz); + armorOKBelow40Lvl = proto->Class == ITEM_CLASS_ARMOR && CanEquipArmor(proto, clazz, 1); + armorOKAbove40Lvl = proto->Class == ITEM_CLASS_ARMOR && CanEquipArmor(proto, clazz, 40); + + for (uint32 level = 1; level <= DEFAULT_MAX_LEVEL; ++level) + { + // skip if item cannot be equipped at this level + if (!CanEquipItem(proto, level)) + continue; + + // skip weapons the class cannot use + if (proto->Class == ITEM_CLASS_WEAPON && !weaponOK) + continue; + + // skip armor the class cannot use at this level bracket + if (proto->Class == ITEM_CLASS_ARMOR) + { + if ((level < 40 && !armorOKBelow40Lvl) || (level >= 40 && !armorOKAbove40Lvl)) + continue; + } + + for (EquipmentSlots slot : *slots) + { + BotEquipKey key(level, clazz, static_cast(slot), proto->Quality); + + equipCache[key].push_back(itr.first); + ++count; + + PlayerbotsDatabasePreparedStatement* stmt = + PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_EQUIP_CACHE); + stmt->SetData(0, clazz); + stmt->SetData(1, level); + stmt->SetData(2, slot); + stmt->SetData(3, proto->Quality); + stmt->SetData(4, proto->ItemId); + trans->Append(stmt); + } + } + } + } + + PlayerbotsDatabase.CommitTransaction(trans); + + LOG_INFO("server.loading", ">> Cached total {} equipment entries in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void RandomItemMgr::BuildCacheEquipNew() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Building equipment cache (new) for {} levels...", DEFAULT_MAX_LEVEL); + + std::unordered_set questItemIds; + uint32 count = 0; + + auto processQuestItem = [&](uint32 itemId, int32 itemQuestLevel) + { + // skip empty or already-processed item ids + if (!itemId || questItemIds.contains(itemId)) + return; + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + return; + + // skip non-equipment classes + if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR) + return; + + // skip legendary, artifact and heirloom quality + if (proto->Quality > ITEM_QUALITY_EPIC) + return; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + return; + + // skip items with no viable equip slot + InventoryType invType = static_cast(proto->InventoryType); + if (!GetViableSlots(invType)) + return; + + uint32 const requiredLevel = static_cast( + std::max(0, std::max(static_cast(proto->RequiredLevel), itemQuestLevel))); + + equipCacheNew[requiredLevel][invType].push_back(itemId); + questItemIds.insert(itemId); + ++count; + }; + + for (auto const& [_, quest] : sObjectMgr->GetQuestTemplates()) + { + // skip repeatable quests + if (quest->IsRepeatable()) + continue; + + // skip quests with invalid or out-of-range level + int32 const questLevel = quest->GetQuestLevel(); + if (questLevel <= 0 || static_cast(questLevel) > DEFAULT_MAX_LEVEL) + continue; + + // skip class-restricted quests + if (quest->GetRequiredClasses()) + continue; + + for (uint32 i = 0; i < quest->GetRewItemsCount(); ++i) + processQuestItem(quest->RewardItemId[i], questLevel); + + for (uint32 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) + processQuestItem(quest->RewardChoiceItemId[i], questLevel); + } + + ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + for (auto const& itr : *itemTemplates) + { + ItemTemplate const* proto = &itr.second; + + // skip non-equipment classes + if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR) + continue; + + // skip legendary, artifact and heirloom quality + if (proto->Quality > ITEM_QUALITY_EPIC) + continue; + + // skip items already added from quests + if (questItemIds.contains(proto->ItemId)) + continue; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; + + // skip items with no viable equip slot + InventoryType invType = static_cast(proto->InventoryType); + if (!GetViableSlots(invType)) + continue; + + equipCacheNew[proto->RequiredLevel][invType].push_back(proto->ItemId); + ++count; + } + + LOG_INFO("server.loading", ">> Cached total {} equipment entries in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void RandomItemMgr::BuildCacheItemInfo() +{ // load weightscales LOG_INFO("playerbots", "Loading weightscales info"); @@ -906,93 +2138,76 @@ void RandomItemMgr::BuildItemInfoCache() return; } - // vendor items - LOG_INFO("playerbots", "Loading vendor item list..."); + /** + * DEADCODE: Remove or rewrite the code below in the next refactor. Also + * move weightscales loading operations into separate methods. + */ - std::set vendorItems; - vendorItems.clear(); - if (QueryResult result = WorldDatabase.Query("SELECT item FROM npc_vendor")) - { - do - { - Field* fields = result->Fetch(); - int32 entry = fields[0].Get(); - if (entry <= 0) - continue; + // // vendor items + // LOG_INFO("playerbots", "Loading vendor item list..."); - vendorItems.insert(entry); - } while (result->NextRow()); - } + // std::set vendorItems; + // vendorItems.clear(); + // if (QueryResult result = WorldDatabase.Query("SELECT item FROM npc_vendor")) + // { + // do + // { + // Field* fields = result->Fetch(); + // int32 entry = fields[0].Get(); + // if (entry <= 0) + // continue; - LOG_INFO("playerbots", "Loaded {} vendor items...", vendorItems.size()); + // vendorItems.insert(entry); + // } while (result->NextRow()); + // } - // calculate drop source - LOG_INFO("playerbots", "Loading loot templates..."); - DropMap* dropMap = new DropMap; + // LOG_INFO("playerbots", "Loaded {} vendor items...", vendorItems.size()); - if (CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates()) - { - for (CreatureTemplateContainer::const_iterator itr = creatures->begin(); itr != creatures->end(); ++itr) - { - uint32 sEntry = itr->first; - if (LootTemplateAccess const* lTemplateA = - DropMapValue::GetLootTemplate(ObjectGuid::Create(sEntry, uint32(1)), LOOT_CORPSE)) - for (auto const& lItem : lTemplateA->Entries) - dropMap->insert(std::make_pair(lItem->itemid, sEntry)); - } - } + // // calculate drop source + // LOG_INFO("playerbots", "Loading loot templates..."); + // DropMap* dropMap = new DropMap; - if (GameObjectTemplateContainer const* gameobjects = sObjectMgr->GetGameObjectTemplates()) - { - for (auto const& itr : *gameobjects) - { - uint32 sEntry = itr.first; - if (LootTemplateAccess const* lTemplateA = DropMapValue::GetLootTemplate( - ObjectGuid::Create(sEntry, uint32(1)), LOOT_CORPSE)) - for (auto const& lItem : lTemplateA->Entries) - dropMap->insert(std::make_pair(lItem->itemid, sEntry)); - } - } + // if (CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates()) + // { + // for (CreatureTemplateContainer::const_iterator itr = creatures->begin(); itr != creatures->end(); ++itr) + // { + // uint32 sEntry = itr->first; + // if (LootTemplateAccess const* lTemplateA = + // DropMapValue::GetLootTemplate(ObjectGuid::Create(sEntry, uint32(1)), LOOT_CORPSE)) + // for (auto const& lItem : lTemplateA->Entries) + // dropMap->insert(std::make_pair(lItem->itemid, sEntry)); + // } + // } - LOG_INFO("playerbots", "Loaded {} loot templates...", dropMap->size()); + // if (GameObjectTemplateContainer const* gameobjects = sObjectMgr->GetGameObjectTemplates()) + // { + // for (auto const& itr : *gameobjects) + // { + // uint32 sEntry = itr.first; + // if (LootTemplateAccess const* lTemplateA = DropMapValue::GetLootTemplate( + // ObjectGuid::Create(sEntry, uint32(1)), LOOT_CORPSE)) + // for (auto const& lItem : lTemplateA->Entries) + // dropMap->insert(std::make_pair(lItem->itemid, sEntry)); + // } + // } - ItemTemplateContainer const* itemTemplate = sObjectMgr->GetItemTemplateStore(); - LOG_INFO("playerbots", "Calculating stat weights for {} items...", itemTemplate->size()); + // LOG_INFO("playerbots", "Loaded {} loot templates...", dropMap->size()); - PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction(); + // ItemTemplateContainer const* itemTemplate = sObjectMgr->GetItemTemplateStore(); + // LOG_INFO("playerbots", "Calculating stat weights for {} items...", itemTemplate->size()); + + /*PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction(); for (auto const& itr : *itemTemplate) { ItemTemplate const* proto = &itr.second; - if (!proto) - continue; - // skip test items - if (strstr(proto->Name1.c_str(), "(Test)") || strstr(proto->Name1.c_str(), "(TEST)") || - strstr(proto->Name1.c_str(), "(test)") || strstr(proto->Name1.c_str(), "(JEFFTEST)") || - strstr(proto->Name1.c_str(), "Test ") || strstr(proto->Name1.c_str(), "Test") || - strstr(proto->Name1.c_str(), "TEST") || strstr(proto->Name1.c_str(), "TEST ") || - strstr(proto->Name1.c_str(), " TEST") || strstr(proto->Name1.c_str(), "2200 ") || - strstr(proto->Name1.c_str(), "Deprecated ") || strstr(proto->Name1.c_str(), "Unused ") || - strstr(proto->Name1.c_str(), "Monster ") || strstr(proto->Name1.c_str(), "[PH]") || - strstr(proto->Name1.c_str(), "(OLD)") || strstr(proto->Name1.c_str(), "QR") || - strstr(proto->Name1.c_str(), "zzOLD")) - { - itemForTest.insert(proto->ItemId); - continue; - } - - if (proto->HasFlag(ITEM_FLAG_DEPRECATED)) - { - itemForTest.insert(proto->ItemId); - continue; - } - // skip items with rank/rep requirements - /*if (proto->RequiredHonorRank > 0 || - proto->RequiredSkillRank > 0 || - proto->RequiredCityRank > 0 || - proto->RequiredReputationRank > 0) - continue;*/ + // // skip items with rank/rep requirements + // if (proto->RequiredHonorRank > 0 || + // proto->RequiredSkillRank > 0 || + // proto->RequiredCityRank > 0 || + // proto->RequiredReputationRank > 0) + // continue; // if (proto->RequiredHonorRank > 0 || proto->RequiredSkillRank > 0 || proto->RequiredCityRank > 0) // continue; @@ -1264,1642 +2479,567 @@ void RandomItemMgr::BuildItemInfoCache() // itemInfoCache[cacheInfo.itemId] = std::move(cacheInfo); } - PlayerbotsDatabase.CommitTransaction(trans); + PlayerbotsDatabase.CommitTransaction(trans);*/ } -uint32 RandomItemMgr::CalculateStatWeight(uint8 playerclass, uint8 spec, ItemTemplate const* proto) +void RandomItemMgr::BuildCacheAmmo() { - uint32 statWeight = 0; - bool isCasterItem = false; - bool isAttackItem = false; - bool noCaster = (Classes)playerclass == CLASS_WARRIOR || (Classes)playerclass == CLASS_ROGUE || - (Classes)playerclass == CLASS_DEATH_KNIGHT || (Classes)playerclass == CLASS_HUNTER; - uint32 spellPower = 0; - uint32 spellHeal = 0; - uint32 attackPower = 0; - bool hasInt = false; - bool hasMana = !((Classes)playerclass == CLASS_WARRIOR || (Classes)playerclass == CLASS_ROGUE || - (Classes)playerclass == CLASS_DEATH_KNIGHT); + uint32 const oldMSTime = getMSTime(); - if (proto->SubClass == ITEM_SUBCLASS_ARMOR_LIBRAM || proto->SubClass == ITEM_SUBCLASS_ARMOR_IDOL || - proto->SubClass == ITEM_SUBCLASS_ARMOR_TOTEM || proto->SubClass == ITEM_SUBCLASS_ARMOR_SIGIL) - return (uint32)(proto->Quality + proto->ItemLevel); + LOG_INFO("server.loading", "Building ammo cache for {} levels...", DEFAULT_MAX_LEVEL); - // check basic item stats - int32 basicStatsWeight = 0; - for (uint8 j = 0; j < MAX_ITEM_PROTO_STATS; ++j) + struct AmmoEntry { - uint32 statType; - int32 val; - std::string weightName; + uint32 entry; + uint32 subClass; + uint32 itemLevel; + uint32 requiredLevel; + uint32 stackable; + }; - // if (j >= proto->StatsCount) - // continue; + std::vector ammoItems; - statType = proto->ItemStat[j].ItemStatType; - val = proto->ItemStat[j].ItemStatValue; + ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + for (auto const& itr : *itemTemplates) + { + ItemTemplate const* proto = &itr.second; - if (val == 0) + // skip non-projectile items + if (proto->Class != ITEM_CLASS_PROJECTILE) continue; - for (std::map::iterator i = weightStatLink.begin(); i != weightStatLink.end(); ++i) - { - uint32 modd = i->second; - if (modd == statType) - { - weightName = i->first; - break; - } - } - - if (weightName.empty()) + // skip non-arrow and non-bullet subclasses + if (proto->SubClass != ITEM_SUBCLASS_ARROW && proto->SubClass != ITEM_SUBCLASS_BULLET) continue; - uint32 singleStat = CalculateSingleStatWeight(playerclass, spec, weightName, val); - basicStatsWeight += singleStat; + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; - if (val) + // skip items with no required level + // NOTE: This filters only three items: 3464 (Feathered Arrow), 3465 + // (Exploding Shot), and 4960 (Flash Pellet). In theory we could + // set RequiredLevel = ItemLevel and use that instead. + if (proto->RequiredLevel == 0) + continue; + + // skip items with no damage value + if (proto->Damage[0].DamageMin == 0.0f) + continue; + + ammoItems.push_back({itr.first, proto->SubClass, proto->ItemLevel, proto->RequiredLevel, + proto->GetMaxStackSize()}); + } + + // we want higher stack sizes first, then higher item levels, to ensure bots + // use the best available ammo for their current level + std::sort(ammoItems.begin(), ammoItems.end(), [](AmmoEntry const& a, AmmoEntry const& b) + { + if (a.stackable != b.stackable) + return a.stackable > b.stackable; + return a.itemLevel > b.itemLevel; + }); + + uint32 count = 0; + for (uint32 level = 1; level <= DEFAULT_MAX_LEVEL; ++level) + { + for (AmmoEntry const& ammo : ammoItems) { - if (weightName == "int" && !noCaster) - isCasterItem = true; + // skip ammo above bot's current level + if (ammo.requiredLevel > level) + continue; - if (weightName == "int") - hasInt = true; - - if (weightName == "splpwr") - isCasterItem = true; - - if (weightName == "str") - isAttackItem = true; - - if (weightName == "agi") - isAttackItem = true; - - if (weightName == "atkpwr") - isAttackItem = true; + ammoCache[level][ammo.subClass].push_back(ammo.entry); + ++count; } } - // check armor & block - statWeight += CalculateSingleStatWeight(playerclass, spec, "armor", proto->Armor); - statWeight += CalculateSingleStatWeight(playerclass, spec, "block", proto->Block); + LOG_INFO("server.loading", ">> Cached total {} ammo entries in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} - // check weapon dps - if (proto->IsWeaponVellum()) +void RandomItemMgr::BuildCacheFood() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Building food cache for {} levels...", DEFAULT_MAX_LEVEL); + + constexpr uint32 MAX_FOOD_LEVEL_DELTA = 10; + + uint32 count = 0; + ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); + for (auto const& itr : *itemTemplates) { - //WeaponAttackType attType = BASE_ATTACK; //not used, line marked for removal. + ItemTemplate const* proto = &itr.second; - uint32 dps = 0; - for (uint8 i = 0; i < MAX_ITEM_PROTO_DAMAGES; i++) - { - if (proto->Damage[i].DamageMax == 0) - break; - - dps = (proto->Damage[i].DamageMin + proto->Damage[i].DamageMax) / (float)(proto->Delay / 1000.0f) / 2; - if (dps) - { - if (proto->IsRangedWeapon()) - statWeight += CalculateSingleStatWeight(playerclass, spec, "rgddps", dps); - else - statWeight += CalculateSingleStatWeight(playerclass, spec, "mledps", dps); - } - } - } - - // check item spells - for (auto const& spellData : proto->Spells) - { - // no spell - if (!spellData.SpellId) + // skip non-consumable items + if (proto->Class != ITEM_CLASS_CONSUMABLE) continue; - // apply only at-equip spells - if (spellData.SpellTrigger != ITEM_SPELLTRIGGER_ON_EQUIP) + // skip non-food and non-consumable subclasses + if (proto->SubClass != ITEM_SUBCLASS_CONSUMABLE && proto->SubClass != ITEM_SUBCLASS_FOOD) continue; - // check if it is valid spell - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellData.SpellId); + // skip bound items + if (proto->Bonding != NO_BIND) + continue; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; + + // skip items requiring a profession skill + if (proto->RequiredSkill) + continue; + + // skip items that are neither food nor drink + if (proto->Spells[0].SpellCategory != SPELL_CATEGORY_FOOD && + proto->Spells[0].SpellCategory != SPELL_CATEGORY_DRINK) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(proto->Spells[0].SpellId); if (!spellInfo) continue; - uint32 spellDamage = 0; - uint32 spellHealing = 0; - for (uint32 j = 0; j < MAX_SPELL_EFFECTS; j++) + // skip items with spell effect inebriate (alcohol) + if (spellInfo->HasEffect(SPELL_EFFECT_INEBRIATE)) + continue; + + uint32 requiredLevel = proto->RequiredLevel; + uint32 category = proto->Spells[0].SpellCategory; + for (uint32 level = 1; level <= DEFAULT_MAX_LEVEL; level += 10) { - if (spellInfo->Effects[j].Effect == SPELL_EFFECT_APPLY_AURA && spellInfo->Effects[j].BasePoints) + if (requiredLevel) { - // spell damage - // SPELL_AURA_MOD_DAMAGE_DONE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_DAMAGE_DONE) - { - spellDamage = spellInfo->Effects[j].BasePoints + 1; - } + // skip items above the level bucket + if (requiredLevel > level) + continue; - // spell healing - // SPELL_AURA_MOD_HEALING_DONE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_HEALING_DONE) - { - spellHealing = spellInfo->Effects[j].BasePoints + 1; - } - - // check spell power - if (spellDamage && spellDamage == spellHealing) - { - isCasterItem = true; - spellPower += CalculateSingleStatWeight(playerclass, spec, "splpwr", spellDamage); - } - - // spell hit rating (pre tbc) - // SPELL_AURA_MOD_SPELL_HIT_CHANCE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SPELL_HIT_CHANCE) - { - statWeight += CalculateSingleStatWeight(playerclass, spec, "spellhitrtng", - spellInfo->Effects[j].BasePoints + 1); - } - - // spell crit rating (pre tbc) - // SPELL_AURA_MOD_SPELL_CRIT_CHANCE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE) - { - statWeight += CalculateSingleStatWeight(playerclass, spec, "spellcritstrkrtng", - spellInfo->Effects[j].BasePoints + 1); - } - - // spell penetration - // SPELL_AURA_MOD_TARGET_RESISTANCE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_TARGET_RESISTANCE) - { - // check if magic type - if (spellInfo->Effects[j].MiscValue == SPELL_SCHOOL_MASK_SPELL) - statWeight += CalculateSingleStatWeight(playerclass, spec, "spellpenrtng", - abs(spellInfo->Effects[j].BasePoints + 1)); - } - - // check attack power - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER) - { - isAttackItem = true; - attackPower += - CalculateSingleStatWeight(playerclass, spec, "atkpwr", spellInfo->Effects[j].BasePoints + 1); - } - - // check ranged ap - // SPELL_AURA_MOD_RANGED_ATTACK_POWER - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_RANGED_ATTACK_POWER) - { - isAttackItem = true; - attackPower += - CalculateSingleStatWeight(playerclass, spec, "atkpwr", spellInfo->Effects[j].BasePoints + 1); - } - - // check block - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SHIELD_BLOCKVALUE) - { - statWeight += - CalculateSingleStatWeight(playerclass, spec, "block", spellInfo->Effects[j].BasePoints + 1); - } - - // block chance - // SPELL_AURA_MOD_BLOCK_PERCENT - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_BLOCK_PERCENT) - { - statWeight += - CalculateSingleStatWeight(playerclass, spec, "blockrtng", spellInfo->Effects[j].BasePoints + 1); - } - - // armor penetration - // SPELL_AURA_MOD_TARGET_RESISTANCE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_TARGET_RESISTANCE) - { - // check if physical type - if (spellInfo->Effects[j].MiscValue == SPELL_SCHOOL_MASK_NORMAL) - statWeight += CalculateSingleStatWeight(playerclass, spec, "armorpenrtng", - abs(spellInfo->Effects[j].BasePoints + 1)); - } - - // hit rating (pre tbc) - // SPELL_AURA_MOD_HIT_CHANCE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_HIT_CHANCE) - { - statWeight += - CalculateSingleStatWeight(playerclass, spec, "hitrtng", spellInfo->Effects[j].BasePoints + 1); - } - - // crit rating (pre tbc) - // SPELL_AURA_MOD_HIT_CHANCE - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_CRIT_PCT) - { - statWeight += CalculateSingleStatWeight(playerclass, spec, "critstrkrtng", - spellInfo->Effects[j].BasePoints + 1); - } - - // check defense SPELL_AURA_MOD_SKILL - // check block - // if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SKILL) - //{ - // statWeight += CalculateSingleStatWeight(playerclass, spec, "block", - // spellInfo->Effects[j].BasePoints + 1); - //} - - // ratings - // enum CombatRating - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_RATING) - { - for (uint32 rating = 0; rating < MAX_COMBAT_RATING; ++rating) - { - if (spellInfo->Effects[j].MiscValue & (1 << rating)) - { - int32 val = spellInfo->Effects[j].BasePoints + 1; - std::string weightName; - - for (std::map::iterator i = weightRatingLink.begin(); - i != weightRatingLink.end(); ++i) - { - uint32 modd = i->second; - if (modd == rating) - { - weightName = i->first; - break; - } - } - - if (weightName.empty()) - continue; - - statWeight += CalculateSingleStatWeight(playerclass, spec, weightName, val); - } - } - } - - // mana regen - // SPELL_AURA_MOD_POWER_REGEN - if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN) - { - statWeight += - CalculateSingleStatWeight(playerclass, spec, "manargn", spellInfo->Effects[j].BasePoints + 1); - } + // skip items below the level bucket window + if (level > MAX_FOOD_LEVEL_DELTA && requiredLevel < level - MAX_FOOD_LEVEL_DELTA) + continue; } - } - } - // check for caster item - if (isCasterItem || hasInt) - { - if ((!hasMana || noCaster) && spellPower) - return 0; - - if (!hasMana && hasInt) - return 0; - - if ((!hasMana && noCaster && playerclass != CLASS_PALADIN) && spellPower > attackPower) - return 0; - - bool playerCaster = false; - for (std::vector::iterator i = m_weightScales[playerclass][spec].stats.begin(); - i != m_weightScales[playerclass][spec].stats.end(); ++i) - { - if (i->stat == "splpwr" || i->stat == "int" || i->stat == "manargn") - { - playerCaster = true; - } - } - - if (!playerCaster) - return 0; - } - - // check for caster item - if (isAttackItem) - { - if (hasMana && !noCaster && !(hasInt || spellPower)) - return 0; - // if (!noCaster && attackPower) - // return 0; - - bool playerAttacker = false; - for (std::vector::iterator i = m_weightScales[playerclass][spec].stats.begin(); - i != m_weightScales[playerclass][spec].stats.end(); ++i) - { - if (i->stat == "str" || i->stat == "agi" || i->stat == "atkpwr" || i->stat == "mledps" || - i->stat == "rgddps") - { - playerAttacker = true; - } - } - - if (!playerAttacker) - return 0; - } - - statWeight += spellPower; - statWeight += spellHeal; - statWeight += attackPower; - - // handle negative stats - if (basicStatsWeight < 0 && (abs(basicStatsWeight) >= statWeight)) - statWeight = 0; - else - statWeight += basicStatsWeight; - - return statWeight; -} - -uint32 RandomItemMgr::CalculateSingleStatWeight(uint8 playerclass, uint8 spec, std::string stat, uint32 value) -{ - uint32 statWeight = 0; - for (std::vector::iterator i = m_weightScales[playerclass][spec].stats.begin(); - i != m_weightScales[playerclass][spec].stats.end(); ++i) - { - if (stat == i->stat) - { - statWeight = i->weight * value; - // if (statWeight) - // LOG_INFO("playerbots", "stat: {}, val: {}, weight: {}, total: {}, class: {}, spec: {}", - // stat, value, i->weight, statWeight, playerclass, m_weightScales[playerclass][spec].info.name); - return statWeight; - } - } - - return statWeight; -} - -uint32 RandomItemMgr::GetQuestIdForItem(uint32 itemId) -{ - bool isQuest = false; - uint32 questId = 0; - ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates(); - for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i) - { - Quest const* quest = i->second; - - uint32 rewItemCount = quest->GetRewItemsCount(); - for (uint32 i = 0; i < rewItemCount; ++i) - { - if (!quest->RewardItemId[i]) - continue; - - if (quest->RewardItemId[i] == itemId) - { - isQuest = true; - questId = quest->GetQuestId(); - break; - } - } - - uint32 rewChocieItemCount = quest->GetRewChoiceItemsCount(); - for (uint32 i = 0; i < rewChocieItemCount; ++i) - { - if (!quest->RewardChoiceItemId[i]) - continue; - - if (quest->RewardChoiceItemId[i] == itemId) - { - isQuest = true; - questId = quest->GetQuestId(); - break; - } - } - if (isQuest) - break; - } - return questId; -} - -std::vector RandomItemMgr::GetQuestIdsForItem(uint32 itemId) -{ - std::vector questIds; - ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates(); - for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i) - { - Quest const* quest = i->second; - - uint32 rewItemCount = quest->GetRewItemsCount(); - for (uint32 i = 0; i < rewItemCount; ++i) - { - if (!quest->RewardItemId[i]) - continue; - - if (quest->RewardItemId[i] == itemId) - { - questIds.push_back(quest->GetQuestId()); - break; - } - } - - uint32 rewChocieItemCount = quest->GetRewChoiceItemsCount(); - for (uint32 i = 0; i < rewChocieItemCount; ++i) - { - if (!quest->RewardChoiceItemId[i]) - continue; - - if (quest->RewardChoiceItemId[i] == itemId) - { - questIds.push_back(quest->GetQuestId()); - break; - } - } - } - - return questIds; -} - -uint32 RandomItemMgr::GetUpgrade(Player* player, std::string spec, uint8 slot, uint32 quality, uint32 itemId) -{ - if (!player) - return 0; - - // get old item statWeight - uint32 oldStatWeight = 0; - uint32 specId = 0; - uint32 closestUpgrade = 0; - uint32 closestUpgradeWeight = 0; - std::vector classspecs; - - for (uint32 specNum = 1; specNum < 5; ++specNum) - { - if (!m_weightScales[player->getClass()][specNum].info.id) - continue; - - classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); - - if (m_weightScales[player->getClass()][specNum].info.name == spec) - specId = m_weightScales[player->getClass()][specNum].info.id; - } - if (!specId) - return 0; - - if (itemId && itemInfoCache.find(itemId) != itemInfoCache.end()) - { - oldStatWeight = itemInfoCache[itemId].weights[specId]; - - if (oldStatWeight) - LOG_INFO("playerbots", "Old Item: {}, weight: {}", itemId, oldStatWeight); - else - LOG_INFO("playerbots", "Old item has no stat weight"); - } - - for (std::map::iterator i = itemInfoCache.begin(); i != itemInfoCache.end(); ++i) - { - ItemInfoEntry& info = i->second; - - // skip useless items - if (info.weights[specId] == 0) - continue; - - // skip higher lvl - if (info.minLevel > player->GetLevel()) - continue; - - // skip too low level - if (info.minLevel < (player->GetLevel() - 10)) - continue; - - // skip wrong team - if (info.team != TEAM_NEUTRAL && info.team != player->GetTeamId()) - continue; - - // skip wrong slot - if ((EquipmentSlots)info.slot != (EquipmentSlots)slot) - continue; - - // skip higher quality - if (quality && info.quality != quality) - continue; - - // skip worse items - if (info.weights[specId] <= oldStatWeight) - continue; - - // skip items that only fit in slot, but not stats - if (!itemId && info.weights[specId] == 1 && player->GetLevel() > 40) - continue; - - // skip quest items - if (info.source == ITEM_SOURCE_QUEST) - { - if (player->GetQuestRewardStatus(info.sourceId) != QUEST_STATUS_COMPLETE) - continue; - } - - // skip no stats trinkets - if (info.weights[specId] == 1 && info.slot == EQUIPMENT_SLOT_NECK || info.slot == EQUIPMENT_SLOT_TRINKET1 || - info.slot == EQUIPMENT_SLOT_TRINKET2 || info.slot == EQUIPMENT_SLOT_FINGER1 || - info.slot == EQUIPMENT_SLOT_FINGER2) - continue; - - // check if item stat score is the best among class specs - uint32 bestSpecId = 0; - uint32 bestSpecScore = 0; - for (std::vector::iterator i = classspecs.begin(); i != classspecs.end(); ++i) - { - if (info.weights[*i] > bestSpecScore) - { - bestSpecId = *i; - bestSpecScore = info.weights[specId]; - } - } - - if (bestSpecId && bestSpecId != specId && player->GetLevel() > 40) - return 0; - - if (!closestUpgrade) - { - closestUpgrade = info.itemId; - closestUpgradeWeight = info.weights[specId]; - } - - // pick closest upgrade - if (info.weights[specId] < closestUpgradeWeight) - { - closestUpgrade = info.itemId; - closestUpgradeWeight = info.weights[specId]; - } - } - - if (closestUpgrade) - LOG_INFO("playerbots", "New Item: {}, weight: {}", closestUpgrade, closestUpgradeWeight); - - return closestUpgrade; -} - -std::vector RandomItemMgr::GetUpgradeList(Player* player, std::string spec, uint8 slot, uint32 quality, - uint32 itemId, uint32 amount) -{ - std::vector listItems; - if (!player) - return listItems; - - // get old item statWeight - uint32 oldStatWeight = 0; - uint32 specId = 0; - uint32 closestUpgrade = 0; - uint32 closestUpgradeWeight = 0; - std::vector classspecs; - - for (uint32 specNum = 1; specNum < 5; ++specNum) - { - if (!m_weightScales[player->getClass()][specNum].info.id) - continue; - - classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); - - if (m_weightScales[player->getClass()][specNum].info.name == spec) - specId = m_weightScales[player->getClass()][specNum].info.id; - } - - if (!specId) - return listItems; - - if (itemId && itemInfoCache.find(itemId) != itemInfoCache.end()) - { - oldStatWeight = itemInfoCache[itemId].weights[specId]; - - if (oldStatWeight) - LOG_INFO("playerbots", "Old Item: {}, weight: {}", itemId, oldStatWeight); - else - LOG_INFO("playerbots", "Old item has no stat weight"); - } - - for (std::map::iterator i = itemInfoCache.begin(); i != itemInfoCache.end(); ++i) - { - ItemInfoEntry& info = i->second; - - // skip useless items - if (info.weights[specId] == 0) - continue; - - // skip higher lvl - if (info.minLevel > player->GetLevel()) - continue; - - // skip too low level - if ((int32)info.minLevel < (int32)(player->GetLevel() - 20)) - continue; - - // skip wrong team - if (info.team != TEAM_NEUTRAL && info.team != player->GetTeamId()) - continue; - - // skip wrong slot - if ((EquipmentSlots)info.slot != (EquipmentSlots)slot) - continue; - - // skip higher quality - if (quality && info.quality != quality) - continue; - - // skip worse items - if (info.weights[specId] <= oldStatWeight) - continue; - - // skip items that only fit in slot, but not stats - if (!itemId && info.weights[specId] == 1 && player->GetLevel() > 40) - continue; - - // skip quest items - if (info.source == ITEM_SOURCE_QUEST) - { - if (player->GetQuestRewardStatus(info.sourceId) != QUEST_STATUS_COMPLETE) - continue; - } - - // skip no stats trinkets - if (info.weights[specId] < 2 && (info.slot == EQUIPMENT_SLOT_NECK || info.slot == EQUIPMENT_SLOT_TRINKET1 || - info.slot == EQUIPMENT_SLOT_TRINKET2 || info.slot == EQUIPMENT_SLOT_FINGER1 || - info.slot == EQUIPMENT_SLOT_FINGER2)) - continue; - - // if (player->GetLevel() >= 40) - //{ - // // check if item stat score is the best among class specs - // uint32 bestSpecId = 0; - // uint32 bestSpecScore = 0; - // for (vector::iterator i = classspecs.begin(); i != classspecs.end(); ++i) - // { - // if (info->weights[*i] > bestSpecScore) - // { - // bestSpecId = *i; - // bestSpecScore = info->weights[specId]; - // } - // } - - // if (bestSpecId && bestSpecId != specId) - // continue; - //} - - listItems.push_back(info.itemId); - // continue; - - // pick closest upgrade - if (info.weights[specId] > closestUpgradeWeight) - { - closestUpgrade = info.itemId; - closestUpgradeWeight = info.weights[specId]; - } - } - - if (listItems.size()) - LOG_INFO("playerbots", "New Items: {}, Old item:%d, New items max: {}", listItems.size(), oldStatWeight, - closestUpgradeWeight); - - return listItems; -} - -bool RandomItemMgr::HasStatWeight(uint32 itemId) -{ - auto itr = itemInfoCache.find(itemId); - return itr != itemInfoCache.end(); -} - -uint32 RandomItemMgr::GetMinLevelFromCache(uint32 itemId) -{ - auto itr = itemInfoCache.find(itemId); - if (itr == itemInfoCache.end()) - return 0; - - return itr->second.minLevel; -} - -uint32 RandomItemMgr::GetStatWeight(Player* player, uint32 itemId) -{ - if (!player || !itemId) - return 0; - - auto itr = itemInfoCache.find(itemId); - if (itr == itemInfoCache.end()) - return 0; - - uint32 statWeight = 0; - uint32 specId = 0; - std::vector classspecs; - - std::string const& specName = AiFactory::GetPlayerSpecName(player); - if (specName.empty()) - return 0; - - for (uint32 specNum = 1; specNum < 5; ++specNum) - { - if (!m_weightScales[player->getClass()][specNum].info.id) - continue; - - classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); - - if (m_weightScales[player->getClass()][specNum].info.name == specName) - specId = m_weightScales[player->getClass()][specNum].info.id; - - if (m_weightScales[player->getClass()][specNum].info.name == specName) - { - specId = specNum; - break; - } - } - - if (!specId) - return 0; - - statWeight = itr->second.weights[specId]; - - return statWeight; -} - -uint32 RandomItemMgr::GetLiveStatWeight(Player* player, uint32 itemId) -{ - if (!player || !itemId) - return 0; - - auto itr = itemInfoCache.find(itemId); - if (itr == itemInfoCache.end()) - return 0; - - uint32 statWeight = 0; - uint32 specId = 0; - std::vector classspecs; - - std::string const& specName = AiFactory::GetPlayerSpecName(player); - if (specName.empty()) - return 0; - - for (uint32 specNum = 1; specNum < 5; ++specNum) - { - if (!m_weightScales[player->getClass()][specNum].info.id) - continue; - - // for bestSpec check - // classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id); - - if (m_weightScales[player->getClass()][specNum].info.name == specName) - specId = m_weightScales[player->getClass()][specNum].info.id; - } - if (!specId) - return 0; - - statWeight = itr->second.weights[specId]; - - // skip higher lvl - if (itr->second.minLevel > player->GetLevel()) - return 0; - - // skip too low level - // if ((int32)info->minLevel < (int32)(player->GetLevel() - 20)) - // return 0; - - // skip wrong team - if (itr->second.team != TEAM_NEUTRAL && itr->second.team != player->GetTeamId()) - return 0; - - // skip quest items - if (itr->second.source == ITEM_SOURCE_QUEST && itr->second.sourceId) - { - if (player->GetQuestRewardStatus(itr->second.sourceId) != QUEST_STATUS_COMPLETE) - return 0; - } - - // skip pvp items - if (itr->second.source == ITEM_SOURCE_PVP) - { - if (!player->GetHonorPoints() && !player->GetArenaPoints()) - return 0; - } - - // skip no stats trinkets - if (itr->second.weights[specId] == 1 && - (itr->second.slot == EQUIPMENT_SLOT_NECK || itr->second.slot == EQUIPMENT_SLOT_TRINKET1 || - itr->second.slot == EQUIPMENT_SLOT_TRINKET2 || itr->second.slot == EQUIPMENT_SLOT_FINGER1 || - itr->second.slot == EQUIPMENT_SLOT_FINGER2)) - return 0; - - // skip items that only fit in slot, but not stats - if (!itemId && itr->second.weights[specId] == 1 && player->GetLevel() > 20) - return 0; - - // check if item stat score is the best among class specs - /*uint32 bestSpecId = 0; - uint32 bestSpecScore = 0; - for (vector::iterator i = classspecs.begin(); i != classspecs.end(); ++i) - { - if (itemCache[itemId]->weights[*i] > bestSpecScore && itemCache[itemId]->weights[*i] > 1) - { - bestSpecId = *i; - bestSpecScore = itemCache[itemId]->weights[specId]; - } - } - - if (bestSpecId && bestSpecId != specId && player->GetLevel() >= 60) - return 0;*/ - - return statWeight; -} - -void RandomItemMgr::BuildEquipCache() -{ - uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel; - if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) - maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); - - PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_EQUIP_CACHE); - if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt)) - { - LOG_INFO("server.loading", - "Loading equipment cache for {} classes, {} levels, {} slots, {} quality from {} items", MAX_CLASSES, - maxLevel, EQUIPMENT_SLOT_END, ITEM_QUALITY_ARTIFACT, itemTemplates->size()); - - uint32 count = 0; - do - { - Field* fields = result->Fetch(); - uint8 clazz = fields[0].Get(); - uint32 level = fields[1].Get(); - uint8 slot = fields[2].Get(); - uint32 quality = fields[3].Get(); - uint32 itemId = fields[4].Get(); - - BotEquipKey key(level, clazz, slot, quality); - equipCache[key].push_back(itemId); + foodCache[(level - 1) / 10][category].push_back(itr.first); ++count; - } while (result->NextRow()); - - LOG_INFO("playerbots", "Equipment cache loaded from {} records", count); - } - else - { - uint64 total = MAX_CLASSES * maxLevel * EQUIPMENT_SLOT_END * ITEM_QUALITY_ARTIFACT; - LOG_INFO("server.loading", - "Building equipment cache for {} classes, {} levels, {} slots, {} quality from {} items ({} total)", - MAX_CLASSES, maxLevel, EQUIPMENT_SLOT_END, ITEM_QUALITY_ARTIFACT, itemTemplates->size(), total); - - for (uint8 class_ = CLASS_WARRIOR; class_ < MAX_CLASSES; ++class_) - { - // skip nonexistent classes - if (!((1 << (class_ - 1)) & CLASSMASK_ALL_PLAYABLE) || !sChrClassesStore.LookupEntry(class_)) - continue; - - for (uint32 level = 1; level <= maxLevel; ++level) - { - for (uint8 slot = 0; slot < EQUIPMENT_SLOT_END; ++slot) - { - for (uint32 quality = ITEM_QUALITY_POOR; quality <= ITEM_QUALITY_ARTIFACT; ++quality) - { - BotEquipKey key(level, class_, slot, quality); - - RandomItemList items; - for (auto const& itr : *itemTemplates) - { - ItemTemplate const* proto = &itr.second; - if (!proto) - continue; - - if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR && - proto->Class != ITEM_CLASS_CONTAINER && proto->Class != ITEM_CLASS_PROJECTILE) - continue; - - if (!CanEquipItem(key, proto)) - continue; - - if (proto->Class == ITEM_CLASS_ARMOR && - (slot == EQUIPMENT_SLOT_HEAD || slot == EQUIPMENT_SLOT_SHOULDERS || - slot == EQUIPMENT_SLOT_CHEST || slot == EQUIPMENT_SLOT_WAIST || - slot == EQUIPMENT_SLOT_LEGS || slot == EQUIPMENT_SLOT_FEET || - slot == EQUIPMENT_SLOT_WRISTS || slot == EQUIPMENT_SLOT_HANDS) && - !CanEquipArmor(key.clazz, key.level, proto)) - continue; - - if (proto->Class == ITEM_CLASS_WEAPON && !CanEquipWeapon(key.clazz, proto)) - continue; - - if (slot == EQUIPMENT_SLOT_OFFHAND && key.clazz == CLASS_ROGUE && - proto->Class != ITEM_CLASS_WEAPON) - continue; - - items.push_back(itr.first); - - PlayerbotsDatabasePreparedStatement* stmt = - PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_EQUIP_CACHE); - stmt->SetData(0, class_); - stmt->SetData(1, level); - stmt->SetData(2, slot); - stmt->SetData(3, quality); - stmt->SetData(4, proto->ItemId); - PlayerbotsDatabase.Execute(stmt); - } - - equipCache[key] = items; - - LOG_DEBUG("playerbots", - "Equipment cache for class: {}, level {}, slot {}, quality {}: {} items", class_, - level, slot, quality, items.size()); - } - } - } } - - LOG_INFO("server.loading", "Equipment cache saved to DB"); } + + LOG_INFO("server.loading", ">> Cached total {} food entries in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); } -void RandomItemMgr::BuildEquipCacheNew() +void RandomItemMgr::BuildCachePotion() { - LOG_INFO("playerbots", "Loading equipments cache..."); + uint32 const oldMSTime = getMSTime(); - std::unordered_set questItemIds; - ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates(); - for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i) - { - //uint32 questId = i->first; //not used in this scope, line marked for removal. - Quest const* quest = i->second; + LOG_INFO("server.loading", "Building potion cache for {} levels...", DEFAULT_MAX_LEVEL); - if (quest->IsRepeatable()) - continue; - - if (quest->GetQuestLevel() <= 0) - continue; - - if (quest->GetRequiredClasses()) - continue; - - for (int j = 0; j < quest->GetRewChoiceItemsCount(); j++) - if (uint32 itemId = quest->RewardChoiceItemId[j]) - { - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR) - continue; - int requiredLevel = std::max((int)proto->RequiredLevel, quest->GetQuestLevel()); - equipCacheNew[requiredLevel][proto->InventoryType].push_back(itemId); - questItemIds.insert(itemId); - } - - for (int j = 0; j < quest->GetRewItemsCount(); j++) - if (uint32 itemId = quest->RewardItemId[j]) - { - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); - if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR) - continue; - int requiredLevel = std::max((int)proto->RequiredLevel, quest->GetQuestLevel()); - equipCacheNew[requiredLevel][proto->InventoryType].push_back(itemId); - questItemIds.insert(itemId); - } - } + constexpr uint32 MAX_POTION_LEVEL_DELTA = 13; + uint32 count = 0; ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); for (auto const& itr : *itemTemplates) { ItemTemplate const* proto = &itr.second; - if (!proto) - continue; - uint32 itemId = proto->ItemId; - if (questItemIds.find(itemId) != questItemIds.end()) + // skip non-consumable items + if (proto->Class != ITEM_CLASS_CONSUMABLE) continue; - if (IsTestItem(itemId)) + // skip non-potion and non-flask subclasses + if (proto->SubClass != ITEM_SUBCLASS_POTION && proto->SubClass != ITEM_SUBCLASS_FLASK) + continue; + + // skip bound items + if (proto->Bonding != NO_BIND) + continue; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; + + // skip items requiring a profession skill + if (proto->RequiredSkill) + continue; + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(proto->Spells[0].SpellId); + if (!spellInfo) + continue; + + // skip potions whose primary effect is not heal or energize + if (spellInfo->Effects[EFFECT_0].Effect != SPELL_EFFECT_HEAL && + spellInfo->Effects[EFFECT_0].Effect != SPELL_EFFECT_ENERGIZE) + continue; + + // do not accept potions/flasks with more than one spell effects, only + // first one effect (EFFECT_0) should be set and eq. heal/energize + bool hasOtherEffects = false; + for (uint8 i = EFFECT_1; i < MAX_SPELL_EFFECTS; ++i) { - continue; + if (spellInfo->Effects[i].Effect != 0) + { + hasOtherEffects = true; + break; + } } - // Unobtainable or unusable items - if (itemId == 12468 || // Chilton Wand - itemId == 22784 || // Sunwell Orb - itemId == 46978) // Totem of the Earthen Ring + // skip potions with secondary spell effects + if (hasOtherEffects) continue; - equipCacheNew[proto->RequiredLevel][proto->InventoryType].push_back(itemId); + uint32 requiredLevel = proto->RequiredLevel; + uint32 effect = spellInfo->Effects[EFFECT_0].Effect; + for (uint32 level = std::max(1u, requiredLevel); level <= DEFAULT_MAX_LEVEL; ++level) + { + // skip levels outside the potion relevance window + if (level > MAX_POTION_LEVEL_DELTA && level - requiredLevel > MAX_POTION_LEVEL_DELTA) + break; + + potionCache[level][effect].push_back(itr.first); + ++count; + } } + + LOG_INFO("server.loading", ">> Cached total {} potion entries in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); } -RandomItemList RandomItemMgr::Query(uint32 level, uint8 clazz, uint8 slot, uint32 quality) +void RandomItemMgr::BuildCacheTrade() { - // return equipCache[key]; - BotEquipKey key(level, clazz, slot, quality); - RandomItemList items; + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Building trade cache for {} levels...", DEFAULT_MAX_LEVEL); + + constexpr uint32 MAX_TRADE_LEVEL_DELTA = 10; + + uint32 count = 0; ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); for (auto const& itr : *itemTemplates) { ItemTemplate const* proto = &itr.second; - if (!proto) + + // skip non-trade-goods + if (proto->Class != ITEM_CLASS_TRADE_GOODS) continue; - if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR && - proto->Class != ITEM_CLASS_CONTAINER && proto->Class != ITEM_CLASS_PROJECTILE) + // skip bound items + if (proto->Bonding != NO_BIND) continue; - if (!CanEquipItem(key, proto)) + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) continue; - if (proto->Class == ITEM_CLASS_ARMOR && - (slot == EQUIPMENT_SLOT_HEAD || slot == EQUIPMENT_SLOT_SHOULDERS || slot == EQUIPMENT_SLOT_CHEST || - slot == EQUIPMENT_SLOT_WAIST || slot == EQUIPMENT_SLOT_LEGS || slot == EQUIPMENT_SLOT_FEET || - slot == EQUIPMENT_SLOT_WRISTS || slot == EQUIPMENT_SLOT_HANDS) && - !CanEquipArmor(key.clazz, key.level, proto)) + // skip items requiring a profession skill + if (proto->RequiredSkill) continue; - if (proto->Class == ITEM_CLASS_WEAPON && !CanEquipWeapon(key.clazz, proto)) - continue; - - if (slot == EQUIPMENT_SLOT_OFFHAND && key.clazz == CLASS_ROGUE && proto->Class != ITEM_CLASS_WEAPON) - continue; - - items.push_back(itr.first); - } - return items; -} - -void RandomItemMgr::BuildAmmoCache() -{ - uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("server.loading", "Building ammo cache for {} levels", maxLevel); - - uint32 counter = 0; - for (uint32 level = 1; level <= maxLevel; level += 1) - { - for (uint32 subClass = ITEM_SUBCLASS_ARROW; subClass <= ITEM_SUBCLASS_BULLET; subClass++) + uint32 requiredLevel = proto->RequiredLevel; + for (uint32 level = 1; level <= DEFAULT_MAX_LEVEL; level += 10) { - QueryResult results = WorldDatabase.Query( - "SELECT entry FROM item_template WHERE class = {} AND subclass = {} AND RequiredLevel <= {} AND duration = 0 " - "AND (Flags & 16) = 0 AND dmg_min1 != 0 AND RequiredLevel != 0 " - "ORDER BY stackable DESC, ItemLevel DESC", - ITEM_CLASS_PROJECTILE, subClass, level); - if (!results) - continue; - do - { - Field* fields = results->Fetch(); - uint32 entry = fields[0].Get(); - ammoCache[level][subClass].push_back(entry); - ++counter; - } while (results->NextRow()); - } - } - - LOG_INFO("server.loading", "Cached {} ammo", counter); // TEST -} - -std::vector RandomItemMgr::GetAmmo(uint32 level, uint32 subClass) { return ammoCache[level][subClass]; } - -void RandomItemMgr::BuildPotionCache() -{ - uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("playerbots", "Building potion cache for {} levels", maxLevel); - - ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); - - uint32 counter = 0; - for (uint32 level = 1; level <= maxLevel; level++) - { - uint32 effects[] = {SPELL_EFFECT_HEAL, SPELL_EFFECT_ENERGIZE}; - for (uint8 i = 0; i < 2; ++i) - { - uint32 effect = effects[i]; - - for (auto const& itr : *itemTemplates) - { - ItemTemplate const* proto = &itr.second; - if (!proto) - continue; - - if (proto->Class != ITEM_CLASS_CONSUMABLE || - (proto->SubClass != ITEM_SUBCLASS_POTION && proto->SubClass != ITEM_SUBCLASS_FLASK) || - proto->Bonding != NO_BIND) - continue; - - uint32 requiredLevel = proto->RequiredLevel; - if (requiredLevel > level || (level > 13 && requiredLevel < level - 13)) - continue; - - if (proto->RequiredSkill) - continue; - - if (proto->Area || proto->Map || proto->RequiredCityRank || proto->RequiredHonorRank) - continue; - - if (proto->Duration & 0x80000000) - continue; - - if (proto->AllowableClass != -1) - continue; - - bool hybrid = false; - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(proto->Spells[0].SpellId); - if (!spellInfo) - continue; - // do not accept hybrid potion - for (uint8 i = 1; i < 3; i++) - { - if (spellInfo->Effects[i].Effect != 0) - { - hybrid = true; - break; - } - } - if (hybrid) - continue; - - if (spellInfo->Effects[0].Effect == effect) - potionCache[level][effect].push_back(itr.first); - } - } - } - - for (uint32 level = 1; level <= maxLevel; level++) - { - uint32 effects[] = {SPELL_EFFECT_HEAL, SPELL_EFFECT_ENERGIZE}; - for (uint8 i = 0; i < 2; ++i) - { - uint32 effect = effects[i]; - uint32 size = potionCache[level][effect].size(); - counter += size; - } - } - - LOG_INFO("playerbots", "Cached {} potions", counter); -} - -void RandomItemMgr::BuildFoodCache() -{ - uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel; - if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) - maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("server.loading", "Building food cache for {} levels", maxLevel); - - ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); - - uint32 counter = 0; - for (uint32 level = 1; level <= maxLevel + 1; level += 10) - { - uint32 categories[] = {11, 59}; - for (int i = 0; i < 2; ++i) - { - uint32 category = categories[i]; - - for (auto const& itr : *itemTemplates) - { - ItemTemplate const* proto = &itr.second; - if (!proto) - continue; - - if (proto->Class != ITEM_CLASS_CONSUMABLE || - (proto->SubClass != ITEM_SUBCLASS_FOOD && proto->SubClass != ITEM_SUBCLASS_CONSUMABLE) || - (proto->Spells[0].SpellCategory != category) || proto->Bonding != NO_BIND) - continue; - - if (proto->RequiredLevel && (proto->RequiredLevel > level || proto->RequiredLevel < level - 10)) - continue; - - if (proto->RequiredSkill) - continue; - - if (proto->Area || proto->Map || proto->RequiredCityRank || proto->RequiredHonorRank) - continue; - - if (proto->Duration & 0x80000000) - continue; - - foodCache[level / 10][category].push_back(itr.first); - } - } - } - - for (uint32 level = 1; level <= maxLevel + 1; level += 10) - { - uint32 categories[] = {11, 59}; - for (uint8 i = 0; i < 2; ++i) - { - uint32 category = categories[i]; - uint32 size = foodCache[level / 10][category].size(); - ++counter; - LOG_DEBUG("server.loading", "Food cache for level={}, category={}: {} items", level, category, size); - } - } - - LOG_INFO("server.loading", "Cached {} types of food", counter); -} - -uint32 RandomItemMgr::GetRandomPotion(uint32 level, uint32 effect) -{ - const std::vector &potions = potionCache[level][effect]; - if (potions.empty()) - return 0; - - return potions[urand(0, potions.size() - 1)]; -} - -uint32 RandomItemMgr::GetFood(uint32 level, uint32 category) -{ - std::initializer_list items; - std::vector food; - if (category == 11) - { - if (level < 5) - items = {787, 117, 4540, 2680}; - else if (level < 15) - items = {2287, 4592, 4541, 21072}; - else if (level < 25) - items = {3770, 16170, 4542, 20074}; - else if (level < 35) - items = {4594, 3771, 1707, 4457}; - else if (level < 45) - items = {4599, 4601, 21552, 17222 /*21030, 16168 */}; - else if (level < 55) - items = {8950, 8952, 8957, 21023 /*21033, 21031 */}; - else if (level < 65) - items = {29292, 27859, 30458, 27662}; - else if (level < 75) - items = {29450, 29451, 29452}; - else - items = {35947}; - } - - if (category == 59) - { - if (level < 5) - items = {159, 117}; - else if (level < 15) - items = {1179, 21072}; - else if (level < 25) - items = {1205}; - else if (level < 35) - items = {1708}; - else if (level < 45) - items = {1645}; - else if (level < 55) - items = {8766}; - else if (level < 65) - items = {28399}; - else if (level < 75) - items = {27860}; - else - items = {33445}; - } - - food.insert(food.end(), items); - if (food.empty()) - return 0; - return food[urand(0, food.size() - 1)]; -} - -uint32 RandomItemMgr::GetRandomFood(uint32 level, uint32 category) -{ - std::vector food = foodCache[(level - 1) / 10][category]; - if (food.empty()) - return 0; - - return food[urand(0, food.size() - 1)]; -} - -void RandomItemMgr::BuildTradeCache() -{ - uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel; - if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) - maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("server.loading", "Building trade cache for {} levels", maxLevel); - - ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); - - uint32 counter = 0; - for (uint32 level = 1; level <= maxLevel + 1; level += 10) - { - for (auto const& itr : *itemTemplates) - { - ItemTemplate const* proto = &itr.second; - if (!proto) - continue; - - if (proto->Class != ITEM_CLASS_TRADE_GOODS || proto->Bonding != NO_BIND) - continue; - + // skip items whose item level is below the current bucket if (proto->ItemLevel < level) continue; - if (proto->RequiredLevel && (proto->RequiredLevel > level || proto->RequiredLevel < level - 10)) - continue; + if (requiredLevel) + { + // skip items above the level bucket + if (requiredLevel > level) + continue; - if (proto->RequiredSkill) - continue; + // skip items below the level bucket window + if (level > MAX_TRADE_LEVEL_DELTA && requiredLevel < level - MAX_TRADE_LEVEL_DELTA) + continue; + } - tradeCache[level / 10].push_back(itr.first); - } - } - - for (uint32 level = 1; level <= maxLevel + 1; level += 10) - { - uint32 size = tradeCache[level / 10].size(); - LOG_DEBUG("server.loading", "Trade cache for level={}: {} items", level, size); - ++counter; - } - - LOG_INFO("server.loading", "Cached {} trade items", counter); // TEST -} - -uint32 RandomItemMgr::GetRandomTrade(uint32 level) -{ - std::vector trade = tradeCache[(level - 1) / 10]; - if (trade.empty()) - return 0; - - return trade[urand(0, trade.size() - 1)]; -} - -void RandomItemMgr::BuildRarityCache() -{ - if (PreparedQueryResult result = - PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RARITY_CACHE))) - { - LOG_INFO("playerbots", "Loading item rarity cache"); - - uint32 count = 0; - do - { - Field* fields = result->Fetch(); - uint32 itemId = fields[0].Get(); - float rarity = fields[1].Get(); - - rarityCache[itemId] = rarity; + tradeCache[(level - 1) / 10].push_back(itr.first); ++count; - - } while (result->NextRow()); - - LOG_INFO("playerbots", "Item rarity cache loaded from {} records", count); + } } - else + + LOG_INFO("server.loading", ">> Cached total {} trade entries in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void RandomItemMgr::BuildCacheRarity() +{ + uint32 const oldMSTime = getMSTime(); + + LOG_INFO("server.loading", "Building items rarity cache..."); + + QueryResult result = WorldDatabase.Query( + "SELECT item, MAX(q.chance) AS max_chance FROM ( " + // <-- creature + "SELECT lt.item, " + " AVG(CASE " + " WHEN lt.GroupId = 0 THEN lt.Chance " + " WHEN lt.Chance > 0 THEN lt.Chance " + " ELSE IFNULL(100 - lt.group_sum, 100) / NULLIF(lt.group_zero, 0) " + " END) AS chance " + "FROM ( " + " SELECT *, " + " SUM(CASE WHEN Chance > 0 THEN Chance ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_sum, " + " SUM(CASE WHEN Chance = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_zero " + " FROM creature_loot_template WHERE item != 0) lt " + "JOIN creature_template ct ON ct.LootId = lt.entry " + "JOIN creature c ON c.id1 = ct.entry " + "GROUP BY lt.item " + "UNION ALL " + // <-- gameobject + "SELECT lt.item, " + " AVG(CASE " + " WHEN lt.GroupId = 0 THEN lt.Chance " + " WHEN lt.Chance > 0 THEN lt.Chance " + " ELSE IFNULL(100 - lt.group_sum, 100) / NULLIF(lt.group_zero, 0) " + " END) AS chance " + "FROM (SELECT *, " + " SUM(CASE WHEN Chance > 0 THEN Chance ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_sum, " + " SUM(CASE WHEN Chance = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_zero " + " FROM gameobject_loot_template WHERE item != 0) lt " + "JOIN gameobject_template ct ON ct.data1 = lt.entry " + "JOIN gameobject c ON c.id = ct.entry " + "GROUP BY lt.item " + "UNION ALL " + // <-- disenchant + "SELECT lt.item, " + " AVG(CASE " + " WHEN lt.GroupId = 0 THEN lt.Chance " + " WHEN lt.Chance > 0 THEN lt.Chance " + " ELSE IFNULL(100 - lt.group_sum, 100) / NULLIF(lt.group_zero, 0) " + " END) AS chance " + "FROM (SELECT *, " + " SUM(CASE WHEN Chance > 0 THEN Chance ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_sum, " + " SUM(CASE WHEN Chance = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_zero " + " FROM disenchant_loot_template WHERE item != 0) lt " + "JOIN item_template ct ON ct.DisenchantID = lt.entry " + "GROUP BY lt.item " + "UNION ALL " + // <-- fishing + "SELECT lt.item, " + " AVG(CASE " + " WHEN lt.GroupId = 0 THEN lt.Chance " + " WHEN lt.Chance > 0 THEN lt.Chance " + " ELSE IFNULL(100 - lt.group_sum, 100) / NULLIF(lt.group_zero, 0) " + " END) AS chance " + "FROM (SELECT *, " + " SUM(CASE WHEN Chance > 0 THEN Chance ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_sum, " + " SUM(CASE WHEN Chance = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_zero " + " FROM fishing_loot_template WHERE item != 0) lt " + "GROUP BY lt.item " + "UNION ALL " + // <-- skinning + "SELECT lt.item, " + " AVG(CASE " + " WHEN lt.GroupId = 0 THEN lt.Chance " + " WHEN lt.Chance > 0 THEN lt.Chance " + " ELSE IFNULL(100 - lt.group_sum, 100) * IFNULL(1.0 / NULLIF(lt.group_zero, 0), 1) " + " END) AS chance " + "FROM (SELECT *, " + " SUM(CASE WHEN Chance > 0 THEN Chance ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_sum, " + " SUM(CASE WHEN Chance = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY entry, GroupId) AS group_zero " + " FROM skinning_loot_template WHERE item != 0) lt " + "JOIN creature_template ct ON ct.skinloot = lt.entry " + "JOIN creature c ON c.id1 = ct.entry " + "GROUP BY lt.item) q GROUP BY item HAVING max_chance > 0.01 ORDER BY item"); + + if (!result) { - ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore(); - LOG_INFO("playerbots", "Building item rarity cache from {} items", itemTemplates->size()); + LOG_WARN("server.loading", ">> Cached 0 rarity entries. SQL query result is empty!"); + LOG_INFO("server.loading", " "); + return; + } - for (auto const& itr : *itemTemplates) + PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction(); + + do + { + Field* fields = result->Fetch(); + uint32 itemId = fields[0].Get(); + float rarity = fields[1].Get(); + + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + continue; + + // skip poor, legendary, artifact and heirloom quality + // NOTE: There are only 5 items with loot source and above Epic quality: + // Warglaive of Azzinoth (2), Bindings of the Windseeker (2), and + // Shadowfrost Shard. The last two are quest-related items. + if (proto->Quality < ITEM_QUALITY_NORMAL || proto->Quality > ITEM_QUALITY_EPIC) + continue; + + // skip items that fail basic validatoin (level, duration, flags, etc.) + if (!IsValidItem(proto)) + continue; + + rarityCache[itemId] = rarity; + + PlayerbotsDatabasePreparedStatement* stmt = + PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RARITY_CACHE); + stmt->SetData(0, itemId); + stmt->SetData(1, rarity); + trans->Append(stmt); + } while (result->NextRow()); + + PlayerbotsDatabase.CommitTransaction(trans); + + LOG_INFO("server.loading", ">> Cached total {} rarity entries in {} ms", rarityCache.size(), + GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +void RandomItemMgr::DebugCacheRandomItem() +{ + // NOTE: Not sure if anyone ever used this data, because it produces a huge + // amount of output to the file/console. It might be better to create + // separate command that shows the cache contents on demand. + + if (!sLog->ShouldLog("playerbots", LogLevel::LOG_LEVEL_DEBUG)) + return; + + uint32 const maxBucket = (DEFAULT_MAX_LEVEL - 1) / 10; + + for (uint32 bucket = 0; bucket <= maxBucket; ++bucket) + { + auto const bucketItr = randomItemCache.find(bucket); + if (bucketItr == randomItemCache.end()) + continue; + + uint32 const minLevel = bucket * 10 + 1; + uint32 const maxBucketLevel = std::min(minLevel + 9, static_cast(DEFAULT_MAX_LEVEL)); + + for (auto const& [type, items] : bucketItr->second) { - ItemTemplate const* proto = &itr.second; - if (!proto) - continue; + LOG_DEBUG("playerbots", " Bucket {} (levels {}..{}) Type {} - {} random items cached", + bucket, minLevel, maxBucketLevel, type, items.size()); - if (proto->Duration & 0x80000000) - continue; - - if (proto->Quality == ITEM_QUALITY_POOR) - continue; - - if (strstri(proto->Name1.c_str(), "qa") || strstri(proto->Name1.c_str(), "test") || - strstri(proto->Name1.c_str(), "deprecated")) - continue; - - if (!proto->ItemLevel) - continue; - - QueryResult results = WorldDatabase.Query( - "SELECT MAX(q.chance) FROM ( " - // "-- Creature " - "SELECT " - "AVG ( " - " CASE " - " WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance " - " WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance " - " ELSE " - " IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM creature_loot_template lt1 WHERE lt1.groupid = " - "lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) " - " / (SELECT COUNT(*) FROM creature_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry = " - "lt.entry AND lt1.ChanceOrQuestChance = 0) " - " END " - ") chance, 'creature' type " - "FROM creature_loot_template lt " - "JOIN creature_template ct ON ct.LootId = lt.entry " - "JOIN creature c ON c.id1 = ct.entry " - "WHERE lt.item = {} " - "union all " - // "-- Gameobject " - "SELECT " - "AVG ( " - " CASE " - " WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance " - " WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance " - " ELSE " - " IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM gameobject_loot_template lt1 WHERE lt1.groupid " - "= lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) " - " / (SELECT COUNT(*) FROM gameobject_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry " - "= lt.entry AND lt1.ChanceOrQuestChance = 0) " - " END " - ") chance, 'gameobject' type " - "FROM gameobject_loot_template lt " - "JOIN gameobject_template ct ON ct.data1 = lt.entry " - "JOIN gameobject c ON c.id1 = ct.entry " - "WHERE lt.item = {} " - "union all " - // "-- Disenchant " - "SELECT " - "AVG ( " - " CASE " - " WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance " - " WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance " - " ELSE " - " IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM disenchant_loot_template lt1 WHERE lt1.groupid " - "= lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) " - " / (SELECT COUNT(*) FROM disenchant_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry " - "= lt.entry AND lt1.ChanceOrQuestChance = 0) " - " END " - ") chance, 'disenchant' type " - "FROM disenchant_loot_template lt " - "JOIN item_template ct ON ct.DisenchantID = lt.entry " - "WHERE lt.item = {} " - "union all " - // "-- Fishing " - "SELECT " - "AVG ( " - " CASE " - " WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance " - " WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance " - " ELSE " - " IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM fishing_loot_template lt1 WHERE lt1.groupid = " - "lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) " - " / (SELECT COUNT(*) FROM fishing_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry = " - "lt.entry AND lt1.ChanceOrQuestChance = 0) " - " END " - ") chance, 'fishing' type " - "FROM fishing_loot_template lt " - "WHERE lt.item = {} " - "union all " - // "-- Skinning " - "SELECT " - "AVG ( " - " CASE " - " WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance " - " WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance " - " ELSE " - " IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM skinning_loot_template lt1 WHERE lt1.groupid = " - "lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) " - " * IFNULL((SELECT 1/COUNT(*) FROM skinning_loot_template lt1 WHERE lt1.groupid = lt.groupid AND " - "lt1.entry = lt.entry AND lt1.ChanceOrQuestChance = 0), 1) " - " END " - ") chance, 'skinning' type " - "FROM skinning_loot_template lt " - "JOIN creature_template ct ON ct.SkinningLootId = lt.entry " - "JOIN creature c ON c.id1 = ct.entry " - "WHERE lt.item = {}) q; ", - itr.first, itr.first, itr.first, itr.first, itr.first); - - if (results) + for (uint32 const itemId : items) { - Field* fields = results->Fetch(); - float rarity = fields[0].Get(); - if (rarity > 0.01) - { - rarityCache[itr.first] = rarity; + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + continue; - PlayerbotsDatabasePreparedStatement* stmt = - PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RARITY_CACHE); - stmt->SetData(0, itr.first); - stmt->SetData(1, rarity); - PlayerbotsDatabase.Execute(stmt); - } - } - } - - LOG_INFO("playerbots", "Item rarity cache built from {} items", itemTemplates->size()); - } -} - -float RandomItemMgr::GetItemRarity(uint32 itemId) { return rarityCache[itemId]; } - -inline bool IsCraftedBySpellInfo(ItemTemplate const* proto, SpellInfo const* spellInfo) -{ - for (uint32 x = 0; x < MAX_SPELL_REAGENTS; ++x) - { - if (spellInfo->Reagent[x] <= 0) - { - continue; - } - - if (proto->ItemId == spellInfo->Reagent[x]) - return true; - - } - - for (uint8 i = 0; i < 3; ++i) - { - if (spellInfo->Effects[i].Effect == SPELL_EFFECT_CREATE_ITEM) - { - if (spellInfo->Effects[i].ItemType == proto->ItemId) - return true; - } - } - - return false; -} - -inline bool IsCraftedBySpell(ItemTemplate const* proto, uint32 spellId) -{ - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); - if (!spellInfo) - return false; - - return IsCraftedBySpellInfo(proto, spellInfo); -} - -inline bool IsCraftedBy(ItemTemplate const* proto, uint32 spellId) -{ - if (IsCraftedBySpell(proto, spellId)) - return true; - - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); - if (!spellInfo) - return false; - - for (uint32 effect = 0; effect < 3; ++effect) - { - uint32 craftId = spellInfo->Effects[effect].TriggerSpell; - SpellInfo const* craftSpellInfo = sSpellMgr->GetSpellInfo(craftId); - if (!craftSpellInfo) - continue; - - if (IsCraftedBySpellInfo(proto, craftSpellInfo)) - return true; - } - - return false; -} - -inline bool ContainsInternal(ItemTemplate const* proto, uint32 skillId) -{ - for (uint32 j = 0; j < sSkillLineAbilityStore.GetNumRows(); ++j) - { - SkillLineAbilityEntry const* skillLine = sSkillLineAbilityStore.LookupEntry(j); - if (!skillLine || skillLine->ID != skillId) - continue; - - if (IsCraftedBy(proto, skillLine->Spell)) - return true; - } - - CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates(); - for (CreatureTemplateContainer::const_iterator itr = creatures->begin(); itr != creatures->end(); ++itr) - { - Trainer::Trainer* trainer = sObjectMgr->GetTrainer(itr->first); - - if (!trainer) - continue; - - if (trainer->GetTrainerType() != Trainer::Type::Tradeskill) - continue; - - for (auto& spell : trainer->GetSpells()) - { - if (spell.ReqSkillLine != skillId) - continue; - - if (IsCraftedBy(proto, spell.SpellId)) - return true; - } - } - - std::vector const* itemTemplates = sObjectMgr->GetItemTemplateStoreFast(); - for (ItemTemplate const* recipe : *itemTemplates) - { - if (!recipe) - continue; - - if (recipe->Class == ITEM_CLASS_RECIPE && - ((recipe->SubClass == ITEM_SUBCLASS_LEATHERWORKING_PATTERN && skillId == SKILL_LEATHERWORKING) || - (recipe->SubClass == ITEM_SUBCLASS_TAILORING_PATTERN && skillId == SKILL_TAILORING) || - (recipe->SubClass == ITEM_SUBCLASS_ENGINEERING_SCHEMATIC && skillId == SKILL_ENGINEERING) || - (recipe->SubClass == ITEM_SUBCLASS_BLACKSMITHING && skillId == SKILL_BLACKSMITHING) || - (recipe->SubClass == ITEM_SUBCLASS_COOKING_RECIPE && skillId == SKILL_COOKING) || - (recipe->SubClass == ITEM_SUBCLASS_ALCHEMY_RECIPE && skillId == SKILL_ALCHEMY) || - (recipe->SubClass == ITEM_SUBCLASS_FIRST_AID_MANUAL && skillId == SKILL_FIRST_AID) || - (recipe->SubClass == ITEM_SUBCLASS_ENCHANTING_FORMULA && skillId == SKILL_ENCHANTING) || - (recipe->SubClass == ITEM_SUBCLASS_JEWELCRAFTING_RECIPE && skillId == SKILL_JEWELCRAFTING) || - (recipe->SubClass == ITEM_SUBCLASS_FISHING_MANUAL && skillId == SKILL_FISHING))) - { - for (uint32 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) - { - if (IsCraftedBy(proto, recipe->Spells[i].SpellId)) - return true; + LOG_DEBUG("playerbots", " [{}] {}", itemId, proto->Name1.c_str()); } } } - - return false; } -bool RandomItemMgr::IsUsedBySkill(ItemTemplate const* proto, uint32 skillId) +void RandomItemMgr::AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank) { - if (itemCache.find(proto->ItemId) != itemCache.end()) - return true; - - switch (proto->Class) + switch (mod) { - case ITEM_CLASS_TRADE_GOODS: - case ITEM_CLASS_MISC: - case ITEM_CLASS_REAGENT: - case ITEM_CLASS_GEM: + case ITEM_MOD_HEALTH: + case ITEM_MOD_STAMINA: + ++sp; ++ap; ++tank; // universal stats - benefit all roles + break; + case ITEM_MOD_AGILITY: + case ITEM_MOD_STRENGTH: + ++ap; ++tank; // physical stats - no spellpower benefit + break; + case ITEM_MOD_MANA: + case ITEM_MOD_INTELLECT: + case ITEM_MOD_SPIRIT: + ++sp; // caster-only stats break; default: + break; + } +} + +bool RandomItemMgr::CanUseItemStats(uint8 clazz, uint8 sp, uint8 ap, uint8 tank) +{ + switch (clazz) + { + case CLASS_MAGE: + case CLASS_PRIEST: + case CLASS_WARLOCK: + // pure casters: must have sp, and sp must dominate + if (!sp || ap > sp || tank > sp) + return false; + break; + case CLASS_DEATH_KNIGHT: + case CLASS_PALADIN: + case CLASS_WARRIOR: + // pure melee/tank: must have ap and tank, sp must not dominate + if ((!ap && !tank) || sp > ap || sp > tank) + return false; + break; + case CLASS_HUNTER: + case CLASS_ROGUE: + // physical DPS: must have ap, sp must not dominate + if (!ap || sp > ap || sp > tank) + return false; + break; + default: + // CLASS_SHAMAN, CLASS_DRUID - hybrid classes, accept any item that + // has at least one relevant stat + break; + } + + return sp || ap || tank; +} + +bool RandomItemMgr::CanEquipItem(ItemTemplate const* proto, uint32 level) const +{ + if (!proto) + return false; + + // skip items whose required level exceeds the bot's level + if (proto->RequiredLevel && proto->RequiredLevel > level) + return false; + + // skip use-bound and quest-bound items + if (proto->Bonding > BIND_WHEN_EQUIPPED) + return false; + + // skip items with no viable equip slot + if (!GetViableSlots(static_cast(proto->InventoryType))) + return false; + + if (proto->Quality > ITEM_QUALITY_NORMAL) + { + uint32 requiredLevel = proto->RequiredLevel; + /* + * DEADCODE: Uncomment the code below if BuildCacheItemInfo method will + * process itemInfoCache; otherwise remove it completely. + */ + + // if (!requiredLevel) + // requiredLevel = GetMinLevelFromCache(proto->ItemId); + + if (!requiredLevel) + requiredLevel = level; + + uint32 delta = 2; + if (level < 15) + delta = 15; + else if (level < 40) + delta = 10; + else if (level < 60) + delta = 6; + else + delta = 9; + + // junk (poor/normal) - take anything that fits the slot; real gear should + // match the level relevance window + if (requiredLevel > level || (level >= delta && requiredLevel < level - delta)) return false; } - bool contains = ContainsInternal(proto, skillId); - if (contains) - itemCache.insert(proto->ItemId); - - return contains; + return true; +} + +std::vector const* RandomItemMgr::GetViableSlots(InventoryType invType) const +{ + auto it = viableSlots.find(invType); + return it != viableSlots.end() ? &it->second : nullptr; +} + +uint32 RandomItemMgr::NormalizeLevel(uint32 level) const +{ + uint32 const levelCap = std::min(sPlayerbotAIConfig.randomBotMaxLevel, + static_cast(DEFAULT_MAX_LEVEL)); + return std::min(level, levelCap); } diff --git a/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.h b/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.h index cf9130039..96abbfd6d 100644 --- a/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.h +++ b/modules/mod-playerbots/src/Mgr/Item/RandomItemMgr.h @@ -1,16 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_RANDOMITEMMGR_H -#define _PLAYERBOT_RANDOMITEMMGR_H +#ifndef PLAYERBOTS_RANDOMITEMMGR_H +#define PLAYERBOTS_RANDOMITEMMGR_H +#include +#include #include -#include +#include #include #include +#include "Player.h" #include "AiFactory.h" #include "ItemTemplate.h" #include "Player.h" @@ -64,9 +68,7 @@ struct ItemInfoEntry : minLevel(0), source(0), sourceId(0), team(0), repRank(0), repFaction(0), quality(0), slot(0), itemId(0) { for (uint8 i = 1; i <= MAX_STAT_SCALES; ++i) - { weights[i] = 0; - } } std::map weights; @@ -95,40 +97,70 @@ struct WeightScale class RandomItemPredicate { public: - virtual ~RandomItemPredicate(){}; + virtual ~RandomItemPredicate() {}; virtual bool Apply(ItemTemplate const* proto) = 0; }; typedef std::vector RandomItemList; -typedef std::map RandomItemCache; +typedef std::unordered_map RandomItemCache; class BotEquipKey { public: - BotEquipKey() : level(0), clazz(0), slot(0), quality(0), key(GetKey()) {} + BotEquipKey() : level(0), clazz(0), slot(0), quality(0), m_key(GetKey()) {} BotEquipKey(uint32 level, uint8 clazz, uint8 slot, uint32 quality) - : level(level), clazz(clazz), slot(slot), quality(quality), key(GetKey()) - { - } - BotEquipKey(BotEquipKey const& other) - : level(other.level), clazz(other.clazz), slot(other.slot), quality(other.quality), key(GetKey()) + : level(level), clazz(clazz), slot(slot), quality(quality), m_key(GetKey()) { } - bool operator<(BotEquipKey const& other) const { return other.key < this->key; } + BotEquipKey(BotEquipKey const&) = default; + BotEquipKey& operator=(BotEquipKey const&) = default; + + bool operator<(BotEquipKey const& other) const { return m_key < other.m_key; } + bool operator==(BotEquipKey const& other) const { return m_key == other.m_key; } uint32 level; uint8 clazz; uint8 slot; uint32 quality; - uint64 key; private: - uint64 GetKey(); + uint64 m_key; + + uint64 GetKey() const + { + // bit layout - 8 bits per field, masked to prevent uint32 overflow bleed + return (static_cast(level & 0xFFu) << 24) | + (static_cast(clazz & 0xFFu) << 16) | + (static_cast(slot & 0xFFu) << 8) | + (static_cast(quality & 0xFFu)); + } }; -typedef std::map BotEquipCache; +namespace std +{ + template<> + struct hash + { + public: + std::size_t operator()(BotEquipKey const& key) const + { + // identical bit layout to BotEquipKey::GetKey() + uint64 const packed = + (static_cast(key.level & 0xFFu) << 24) | + (static_cast(key.clazz & 0xFFu) << 16) | + (static_cast(key.slot & 0xFFu) << 8) | + (static_cast(key.quality & 0xFFu)); + + return std::hash()(packed); + } + }; +} + +typedef std::unordered_map BotEquipCache; +typedef std::unordered_map EquipByInventoryType; +typedef std::unordered_map BotEquipCacheNew; class RandomItemMgr { @@ -143,54 +175,76 @@ public: public: void Init(); void InitAfterAhBot(); - static bool HandleConsoleCommand(ChatHandler* handler, char const* args); - RandomItemList Query(uint32 level, RandomItemType type, RandomItemPredicate* predicate); - RandomItemList Query(uint32 level, uint8 clazz, uint8 slot, uint32 quality); - uint32 GetUpgrade(Player* player, std::string spec, uint8 slot, uint32 quality, uint32 itemId); - std::vector GetUpgradeList(Player* player, std::string spec, uint8 slot, uint32 quality, uint32 itemId, - uint32 amount = 1); - bool HasStatWeight(uint32 itemId); - uint32 GetMinLevelFromCache(uint32 itemId); - uint32 GetStatWeight(Player* player, uint32 itemId); - uint32 GetLiveStatWeight(Player* player, uint32 itemId); - uint32 GetRandomItem(uint32 level, RandomItemType type, RandomItemPredicate* predicate = nullptr); - std::vector GetAmmo(uint32 level, uint32 subClass); - uint32 GetRandomPotion(uint32 level, uint32 effect); - uint32 GetRandomFood(uint32 level, uint32 category); - uint32 GetFood(uint32 level, uint32 category); - uint32 GetRandomTrade(uint32 level); - uint32 CalculateStatWeight(uint8 playerclass, uint8 spec, ItemTemplate const* proto); - uint32 CalculateSingleStatWeight(uint8 playerclass, uint8 spec, std::string stat, uint32 value); - bool CanEquipArmor(uint8 clazz, uint32 level, ItemTemplate const* proto); - bool ShouldEquipArmorForSpec(uint8 playerclass, uint8 spec, ItemTemplate const* proto); - bool CanEquipWeapon(uint8 clazz, ItemTemplate const* proto); - bool ShouldEquipWeaponForSpec(uint8 playerclass, uint8 spec, ItemTemplate const* proto); - float GetItemRarity(uint32 itemId); - uint32 GetQuestIdForItem(uint32 itemId); - std::vector GetQuestIdsForItem(uint32 itemId); - static bool IsUsedBySkill(ItemTemplate const* proto, uint32 skillId); - bool IsTestItem(uint32 itemId) { return itemForTest.find(itemId) != itemForTest.end(); } - std::vector GetCachedEquipments(uint32 requiredLevel, uint32 inventoryType); + + [[nodiscard]] static bool HandleConsoleCommand(ChatHandler* handler, char const* args); + + [[nodiscard]] RandomItemList Query(uint32 level, RandomItemType type, RandomItemPredicate* predicate) const; + + [[nodiscard]] uint32 GetUpgrade(Player* player, std::string spec, uint8 slot, uint32 quality, uint32 itemId); + [[nodiscard]] std::vector GetUpgradeList(Player* player, std::string spec, uint8 slot, uint32 quality, + uint32 itemId, uint32 amount = 1); + + [[nodiscard]] bool HasStatWeight(uint32 itemId); + [[nodiscard]] uint32 CalculateStatWeight(ItemTemplate const* proto, uint8 playerclass, uint8 spec); + [[nodiscard]] uint32 CalculateSingleStatWeight(uint8 playerclass, uint8 spec, std::string stat, uint32 value); + [[nodiscard]] uint32 GetStatWeight(Player* player, uint32 itemId); + [[nodiscard]] uint32 GetLiveStatWeight(Player* player, uint32 itemId); + + [[nodiscard]] uint32 GetMinLevelFromCache(uint32 itemId) const; + + [[nodiscard]] RandomItemList const& GetEquipment(uint32 level, uint8 clazz, uint8 slot, uint32 quality) const; + [[nodiscard]] RandomItemList const& GetEquipmentNew(uint32 level, InventoryType invType) const; + [[nodiscard]] uint32 GetRandomItem(uint32 level, RandomItemType type, RandomItemPredicate* predicate = nullptr) const; + [[nodiscard]] uint32 GetAmmo(uint32 level, uint32 subClass) const; + [[nodiscard]] uint32 GetRandomPotion(uint32 level, uint32 effect) const; + [[nodiscard]] uint32 GetRandomFood(uint32 level, uint32 category) const; + [[nodiscard]] uint32 GetRandomTrade(uint32 level) const; + [[nodiscard]] float GetItemRarity(uint32 itemId) const; + [[nodiscard]] std::vector const& GetEnchantmentPool(uint32 entry) const; + + [[nodiscard]] bool CanEquipArmor(ItemTemplate const* proto, uint8 clazz, uint32 level) const; + [[nodiscard]] bool CanEquipWeapon(ItemTemplate const* proto, uint8 clazz) const; + [[nodiscard]] bool ShouldEquipArmorForSpec(ItemTemplate const* proto, uint8 clazz, uint8 spec) const; + [[nodiscard]] bool ShouldEquipWeaponForSpec(ItemTemplate const* proto, uint8 clazz, uint8 spec) const; + + [[nodiscard]] uint32 GetQuestIdForItem(uint32 itemId) const; + [[nodiscard]] std::vector GetQuestIdsForItem(uint32 itemId) const; + + [[nodiscard]] static bool IsInternalItem(ItemTemplate const* proto); + [[nodiscard]] static bool IsValidItem(ItemTemplate const* proto); + [[nodiscard]] static bool IsUsedBySkill(ItemTemplate const* proto, uint32 skillId); private: - void BuildRandomItemCache(); - void BuildEquipCache(); - void BuildEquipCacheNew(); - void BuildItemInfoCache(); - void BuildAmmoCache(); - void BuildFoodCache(); - void BuildPotionCache(); - void BuildTradeCache(); - void BuildRarityCache(); - bool CanEquipItem(BotEquipKey key, ItemTemplate const* proto); - bool CanEquipItemNew(ItemTemplate const* proto); - void AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank); - bool CheckItemStats(uint8 clazz, uint8 sp, uint8 ap, uint8 tank); + void InitWeaponProficiency(); + void InitPredicates(); + void InitViableSlots(); + void InitWeightLinks(); + + [[nodiscard]] bool LoadCacheEquip(); + [[nodiscard]] bool LoadCacheRandomItem(); + [[nodiscard]] bool LoadCacheRarity(); + void LoadEnchantmentPool(); + + void BuildCacheRandomItem(); + void BuildCacheEquip(); + void BuildCacheEquipNew(); + void BuildCacheItemInfo(); + void BuildCacheAmmo(); + void BuildCacheFood(); + void BuildCachePotion(); + void BuildCacheTrade(); + void BuildCacheRarity(); + + void DebugCacheRandomItem(); + + static void AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank); + [[nodiscard]] static bool CanUseItemStats(uint8 clazz, uint8 sp, uint8 ap, uint8 tank); + [[nodiscard]] bool CanEquipItem(ItemTemplate const* proto, uint32 level) const; + [[nodiscard]] std::vector const* GetViableSlots(InventoryType invType) const; + [[nodiscard]] uint32 NormalizeLevel(uint32 level) const; private: - // Implemented in RandomItemMgr.cpp RandomItemMgr(); - // Implemented in RandomItemMgr.cpp ~RandomItemMgr(); RandomItemMgr(const RandomItemMgr&) = delete; @@ -199,23 +253,29 @@ private: RandomItemMgr(RandomItemMgr&&) = delete; RandomItemMgr& operator=(RandomItemMgr&&) = delete; - std::map randomItemCache; - std::map predicates; + std::atomic m_initialized{false}; + std::array m_weaponProficiency = {}; + + static std::unordered_set itemCache; + BotEquipCache equipCache; - std::map> viableSlots; - std::map>> ammoCache; - std::map>> potionCache; - std::map>> foodCache; - std::map> tradeCache; - std::map rarityCache; - std::map m_weightScales[MAX_CLASSES]; - std::map weightStatLink; - std::map weightRatingLink; - std::map itemInfoCache; - std::unordered_set itemForTest; - static std::set itemCache; - // equipCacheNew[RequiredLevel][InventoryType] - std::map>> equipCacheNew; + BotEquipCacheNew equipCacheNew; + + std::unordered_map randomItemCache; + std::unordered_map predicates; + std::unordered_map> viableSlots; + std::unordered_map> ammoCache; + std::unordered_map> potionCache; + std::unordered_map> foodCache; + std::unordered_map tradeCache; + std::unordered_map rarityCache; + std::unordered_map m_weightScales[MAX_CLASSES]; + std::unordered_map weightStatLink; + std::unordered_map weightRatingLink; + std::unordered_map itemInfoCache; + + // enchPoolCache[item_enchantment_template.entry] -> list of enchantment ids + std::unordered_map> enchPoolCache; }; #define sRandomItemMgr RandomItemMgr::instance() diff --git a/modules/mod-playerbots/src/Mgr/Item/StatsCollector.cpp b/modules/mod-playerbots/src/Mgr/Item/StatsCollector.cpp index f9c5f6d4c..f041fb60f 100644 --- a/modules/mod-playerbots/src/Mgr/Item/StatsCollector.cpp +++ b/modules/mod-playerbots/src/Mgr/Item/StatsCollector.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "StatsCollector.h" #include "DBCStores.h" @@ -34,7 +40,7 @@ void StatsCollector::CollectItemStats(ItemTemplate const* proto) } stats[STATS_TYPE_ARMOR] += proto->Armor; stats[STATS_TYPE_BLOCK_VALUE] += proto->Block; - for (int i = 0; i < proto->StatsCount; i++) + for (uint32 i = 0; i < proto->StatsCount; i++) { const _ItemStat& stat = proto->ItemStat[i]; const int32& val = stat.ItemStatValue; @@ -45,18 +51,18 @@ void StatsCollector::CollectItemStats(ItemTemplate const* proto) switch (proto->Spells[j].SpellTrigger) { case ITEM_SPELLTRIGGER_ON_USE: - CollectSpellStats(proto->Spells[j].SpellId, 1.0f, proto->Spells[j].SpellCooldown); + CollectSpellStats(proto->Spells[j].SpellId, 1.0f, Milliseconds(proto->Spells[j].SpellCooldown)); break; case ITEM_SPELLTRIGGER_ON_EQUIP: - CollectSpellStats(proto->Spells[j].SpellId, 1.0f, 0); + CollectSpellStats(proto->Spells[j].SpellId, 1.0f, Milliseconds(0)); break; case ITEM_SPELLTRIGGER_CHANCE_ON_HIT: if (type_ & CollectorType::MELEE) { if (proto->Spells[j].SpellPPMRate > 0.01f) - CollectSpellStats(proto->Spells[j].SpellId, 1.0f, 60000 / proto->Spells[j].SpellPPMRate); + CollectSpellStats(proto->Spells[j].SpellId, 1.0f, Milliseconds(static_cast(60000 / proto->Spells[j].SpellPPMRate))); else - CollectSpellStats(proto->Spells[j].SpellId, 1.0f, 60000 / 1.8f); // Default PPM = 1.8 + CollectSpellStats(proto->Spells[j].SpellId, 1.0f, Milliseconds(static_cast(60000 / 1.8f))); // Default PPM = 1.8 } break; default: @@ -71,7 +77,7 @@ void StatsCollector::CollectItemStats(ItemTemplate const* proto) } } -void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, int32 spellCooldown) +void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, Milliseconds spellCooldown) { SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); @@ -81,14 +87,14 @@ void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, int32 s if (SpecialSpellFilter(spellId)) return; - SpellProcEntry const* eventEntry = sSpellMgr->GetSpellProcEntry(spellInfo->Id); + const SpellProcEntry* eventEntry = sSpellMgr->GetSpellProcEntry(spellInfo->Id); - uint32 triggerCooldown = eventEntry ? eventEntry->Cooldown.count() : 0; + Milliseconds triggerCooldown = eventEntry ? eventEntry->Cooldown : 0ms; bool canNextTrigger = true; uint32 procFlags; - float procChance; + uint32 procChance; if (eventEntry && eventEntry->ProcFlags) procFlags = eventEntry->ProcFlags; else @@ -142,11 +148,11 @@ void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, int32 s break; float coverage; - if (spellCooldown <= 2000 || spellInfo->GetDuration() == -1) + if (spellCooldown.count() <= 2000 || spellInfo->GetDuration() == -1) coverage = 1.0f; else coverage = - std::min(1.0f, (float)spellInfo->GetDuration() / (spellInfo->GetDuration() + spellCooldown)); + std::min(1.0f, (float)spellInfo->GetDuration() / (spellInfo->GetDuration() + spellCooldown.count())); multiplier *= coverage; HandleApplyAura(effectInfo, multiplier, canNextTrigger, triggerCooldown); @@ -155,9 +161,9 @@ void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, int32 s case SPELL_EFFECT_HEAL: { /// @todo Handle spell without cooldown - if (!spellCooldown) + if (!spellCooldown.count()) break; - float normalizedCd = std::max((float)spellCooldown / 1000, 5.0f); + float normalizedCd = std::max((float)spellCooldown.count() / 1000, 5.0f); int32 val = AverageValue(effectInfo); float transfer_multiplier = 1; stats[STATS_TYPE_HEAL_POWER] += (float)val / normalizedCd * multiplier * transfer_multiplier; @@ -166,11 +172,11 @@ void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, int32 s case SPELL_EFFECT_ENERGIZE: { /// @todo Handle spell without cooldown - if (!spellCooldown) + if (!spellCooldown.count()) break; if (effectInfo.MiscValue != POWER_MANA) break; - float normalizedCd = std::max((float)spellCooldown / 1000, 5.0f); + float normalizedCd = std::max((float)spellCooldown.count() / 1000, 5.0f); int32 val = AverageValue(effectInfo); float transfer_multiplier = 0.2; stats[STATS_TYPE_MANA_REGENERATION] += (float)val / normalizedCd * multiplier * transfer_multiplier; @@ -179,9 +185,9 @@ void StatsCollector::CollectSpellStats(uint32 spellId, float multiplier, int32 s case SPELL_EFFECT_SCHOOL_DAMAGE: { /// @todo Handle spell without cooldown - if (!spellCooldown) + if (!spellCooldown.count()) break; - float normalizedCd = std::max((float)spellCooldown / 1000, 5.0f); + float normalizedCd = std::max((float)spellCooldown.count() / 1000, 5.0f); int32 val = AverageValue(effectInfo); if (type_ & (CollectorType::MELEE | CollectorType::RANGED)) { @@ -352,7 +358,7 @@ bool StatsCollector::SpecialEnchantFilter(uint32 enchantSpellId) bool StatsCollector::CanBeTriggeredByType(SpellInfo const* spellInfo, uint32 procFlags, bool strict) { - SpellProcEntry const* eventEntry = sSpellMgr->GetSpellProcEntry(spellInfo->Id); + const SpellProcEntry* eventEntry = sSpellMgr->GetSpellProcEntry(spellInfo->Id); uint32 spellFamilyName = 0; if (eventEntry) { @@ -548,7 +554,7 @@ void StatsCollector::CollectByItemStatType(uint32 itemStatType, int32 val) } void StatsCollector::HandleApplyAura(const SpellEffectInfo& effectInfo, float multiplier, bool canNextTrigger, - uint32 triggerCooldown) + Milliseconds triggerCooldown) { if (effectInfo.Effect != SPELL_EFFECT_APPLY_AURA) return; diff --git a/modules/mod-playerbots/src/Mgr/Item/StatsCollector.h b/modules/mod-playerbots/src/Mgr/Item/StatsCollector.h index 1e54546f5..1e7c03e41 100644 --- a/modules/mod-playerbots/src/Mgr/Item/StatsCollector.h +++ b/modules/mod-playerbots/src/Mgr/Item/StatsCollector.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_STATSCOLLECTOR_H -#define _PLAYERBOT_STATSCOLLECTOR_H +#ifndef PLAYERBOTS_STATSCOLLECTOR_H +#define PLAYERBOTS_STATSCOLLECTOR_H #include "ItemTemplate.h" #include "SpellInfo.h" @@ -65,7 +66,7 @@ public: StatsCollector(StatsCollector& stats) = default; void Reset(); void CollectItemStats(ItemTemplate const* proto); - void CollectSpellStats(uint32 spellId, float multiplier = 1.0f, int32 spellCooldown = -1); + void CollectSpellStats(uint32 spellId, float multiplier = 1.0f, Milliseconds spellCooldown = -1ms); void CollectEnchantStats(SpellItemEnchantmentEntry const* enchant, uint32 default_enchant_amount = 0); bool CanBeTriggeredByType(SpellInfo const* spellInfo, uint32 procFlags, bool strict = true); bool CheckSpellValidation(uint32 spellFamilyName, flag96 spelFalimyFlags, bool strict = true); @@ -79,7 +80,7 @@ private: bool SpecialEnchantFilter(uint32 enchantSpellId); void HandleApplyAura(const SpellEffectInfo& effectInfo, float multiplier, bool canNextTrigger, - uint32 triggerCooldown); + Milliseconds triggerCooldown); float AverageValue(const SpellEffectInfo& effectInfo); private: diff --git a/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.cpp b/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.cpp index 1b3f3dcfc..4e61dd09e 100644 --- a/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.cpp +++ b/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.cpp @@ -14,12 +14,40 @@ #include "ObjectMgr.h" #include "PlayerbotAI.h" #include "PlayerbotFactory.h" +#include "RandomItemMgr.h" #include "SharedDefines.h" #include "SpellAuraDefines.h" #include "SpellMgr.h" #include "StatsCollector.h" #include "Unit.h" +namespace +{ +constexpr uint32 SPELL_MOLTEN_ARMOR_RANKS[] = { 30482, 43045, 43046 }; +constexpr uint32 SPELL_FEL_ARMOR_RANKS[] = { 28176, 28189, 47892, 47893 }; +constexpr uint32 SPELL_CAREFUL_AIM = 34484; +constexpr uint32 SPELL_HUNTER_VS_WILD = 56341; +constexpr uint32 SPELL_ARMORED_TO_THE_TEETH = 61222; +constexpr uint32 SPELL_MENTAL_DEXTERITY = 51885; +constexpr uint32 SPELL_ROGUE_SWORD_SPECIALIZATION = 13964; +constexpr uint32 SPELL_POLEAXE_SPECIALIZATION = 12785; +constexpr uint32 SPELL_NERVES_OF_COLD_STEEL = 50138; +constexpr uint32 SPELL_SHADOW_FOCUS = 15835; +constexpr uint32 SPELL_ARCANE_FOCUS = 12840; +} + +template +bool HasAnySpell(Player* player, uint32 const (&spellIds)[Size]) +{ + for (uint32 const spellId : spellIds) + { + if (player->HasSpell(spellId)) + return true; + } + + return false; +} + StatsWeightCalculator::StatsWeightCalculator(Player* player) : player_(player) { if (PlayerbotAI::IsHeal(player)) @@ -61,7 +89,7 @@ void StatsWeightCalculator::Reset() } } -float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyIds) +float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyIds, int32 slot) { ItemTemplate const* proto = &sObjectMgr->GetItemTemplateStore()->at(itemId); @@ -100,10 +128,12 @@ float StatsWeightCalculator::CalculateItem(uint32 itemId, int32 randomPropertyId weight_ *= PlayerbotFactory::CalcMixedGearScore(lvl, ITEM_QUALITY_EPIC); else weight_ *= PlayerbotFactory::CalcMixedGearScore(proto->ItemLevel, proto->Quality); - - return weight_; } - // If quality/level blending is disabled, also return the calculated weight. + + // Apply weapon speed governance if slot is provided and this is a weapon + if (sPlayerbotAIConfig.preferredSpecWeapons && slot >= 0 && proto->Class == ITEM_CLASS_WEAPON) + weight_ *= ApplyPreferredSpecWeapons(proto, slot); + return weight_; } @@ -177,6 +207,53 @@ void StatsWeightCalculator::CalculateRandomProperty(int32 randomPropertyId, uint } } +int32 StatsWeightCalculator::PickBestRandomPropertyId(uint32 itemId) +{ + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); + if (!proto) + return 0; + + bool isSuffix = false; + uint32 poolEntry = proto->RandomProperty; + if (!poolEntry) + { + poolEntry = proto->RandomSuffix; + isSuffix = true; + } + if (!poolEntry) + return 0; + + std::vector const& pool = sRandomItemMgr.GetEnchantmentPool(poolEntry); + if (pool.empty()) + return 0; + + Reset(); + GenerateWeights(player_); + + int32 bestId = 0; + float bestScore = 0.0f; + for (uint32 enchId : pool) + { + int32 candidate = isSuffix ? -static_cast(enchId) : static_cast(enchId); + + collector_->Reset(); + CalculateRandomProperty(candidate, itemId); + + float score = 0.0f; + for (uint32 i = 0; i < STATS_TYPE_MAX; ++i) + score += stats_weights_[i] * collector_->stats[i]; + + if (bestId == 0 || score > bestScore) + { + bestId = candidate; + bestScore = score; + } + } + + collector_->Reset(); + return bestId; +} + void StatsWeightCalculator::GenerateWeights(Player* player) { GenerateBasicWeights(player); @@ -201,6 +278,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.7f; stats_weights_[STATS_TYPE_CRIT] += 1.4f; stats_weights_[STATS_TYPE_HASTE] += 1.6f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 7.5f; } else if (cls == CLASS_HUNTER && tab == HUNTER_TAB_MARKSMANSHIP) @@ -211,6 +289,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 2.0f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 10.0f; } else if (cls == CLASS_ROGUE && tab == ROGUE_TAB_COMBAT) @@ -222,6 +301,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.4f; stats_weights_[STATS_TYPE_HASTE] += 1.7f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } @@ -246,64 +326,69 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.1f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.1f; stats_weights_[STATS_TYPE_MELEE_DPS] += 5.0f; } else if (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY) { - stats_weights_[STATS_TYPE_AGILITY] += 1.8f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.6f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.8f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.8f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 2.1f; stats_weights_[STATS_TYPE_HIT] += 2.3f; stats_weights_[STATS_TYPE_CRIT] += 2.2f; - stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; + stats_weights_[STATS_TYPE_DEFENSE] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_ARMS) { - stats_weights_[STATS_TYPE_AGILITY] += 1.6f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.3f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.8f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.8f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.7f; stats_weights_[STATS_TYPE_HIT] += 2.0f; stats_weights_[STATS_TYPE_CRIT] += 1.9f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; + stats_weights_[STATS_TYPE_DEFENSE] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 1.4f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_FROST) { - stats_weights_[STATS_TYPE_AGILITY] += 1.7f; - stats_weights_[STATS_TYPE_STRENGTH] += 2.8f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; + stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 2.7f; stats_weights_[STATS_TYPE_HIT] += 2.3f; stats_weights_[STATS_TYPE_CRIT] += 2.2f; stats_weights_[STATS_TYPE_HASTE] += 2.1f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 2.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 7.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_UNHOLY) { - stats_weights_[STATS_TYPE_AGILITY] += 0.9f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.3f; stats_weights_[STATS_TYPE_HIT] += 2.2f; stats_weights_[STATS_TYPE_CRIT] += 1.7f; stats_weights_[STATS_TYPE_HASTE] += 1.8f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 1.5f; stats_weights_[STATS_TYPE_MELEE_DPS] += 5.0f; } else if (cls == CLASS_PALADIN && tab == PALADIN_TAB_RETRIBUTION) { - stats_weights_[STATS_TYPE_AGILITY] += 1.6f; + stats_weights_[STATS_TYPE_AGILITY] += 0.5f; stats_weights_[STATS_TYPE_STRENGTH] += 2.5f; - stats_weights_[STATS_TYPE_INTELLECT] += 0.1f; - stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 0.3f; + stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 1.5f; stats_weights_[STATS_TYPE_HIT] += 1.9f; stats_weights_[STATS_TYPE_CRIT] += 1.7f; @@ -317,7 +402,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_STRENGTH] += 1.1f; stats_weights_[STATS_TYPE_INTELLECT] += 0.3f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 1.0f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 0.95f; + stats_weights_[STATS_TYPE_SPELL_POWER] += 0.5f; stats_weights_[STATS_TYPE_ARMOR_PENETRATION] += 0.9f; stats_weights_[STATS_TYPE_HIT] += 2.1f; stats_weights_[STATS_TYPE_CRIT] += 1.5f; @@ -336,6 +421,7 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.1f; stats_weights_[STATS_TYPE_CRIT] += 0.8f; stats_weights_[STATS_TYPE_HASTE] += 1.0f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if (cls == CLASS_MAGE && tab == MAGE_TAB_FIRE) @@ -346,15 +432,17 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_HIT] += 1.2f; stats_weights_[STATS_TYPE_CRIT] += 1.1f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if (cls == CLASS_SHAMAN && tab == SHAMAN_TAB_ELEMENTAL) { - stats_weights_[STATS_TYPE_INTELLECT] += 0.25f; - stats_weights_[STATS_TYPE_SPELL_POWER] += 1.0f; + stats_weights_[STATS_TYPE_INTELLECT] += 0.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] += 1.2f; stats_weights_[STATS_TYPE_HIT] += 1.1f; stats_weights_[STATS_TYPE_CRIT] += 0.8f; stats_weights_[STATS_TYPE_HASTE] += 1.0f; + stats_weights_[STATS_TYPE_MANA_REGENERATION] += 0.5f; } else if ((cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY) || (cls == CLASS_SHAMAN && tab == SHAMAN_TAB_RESTORATION)) @@ -375,14 +463,15 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_MANA_REGENERATION] += 0.9f; stats_weights_[STATS_TYPE_CRIT] += 0.6f; stats_weights_[STATS_TYPE_HASTE] += 0.8f; + stats_weights_[STATS_TYPE_ATTACK_POWER] -= 1.0f; stats_weights_[STATS_TYPE_RANGED_DPS] += 1.0f; } else if ((cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) || (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION)) { - stats_weights_[STATS_TYPE_AGILITY] += 2.0f; - stats_weights_[STATS_TYPE_STRENGTH] += 1.0f; - stats_weights_[STATS_TYPE_STAMINA] += 3.5f; + stats_weights_[STATS_TYPE_AGILITY] += 0.2f; + stats_weights_[STATS_TYPE_STRENGTH] += 1.3f; + stats_weights_[STATS_TYPE_STAMINA] += 3.0f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.2f; stats_weights_[STATS_TYPE_DEFENSE] += 2.5f; stats_weights_[STATS_TYPE_PARRY] += 2.0f; @@ -392,26 +481,26 @@ void StatsWeightCalculator::GenerateBasicWeights(Player* player) stats_weights_[STATS_TYPE_BLOCK_VALUE] += 0.5f; stats_weights_[STATS_TYPE_ARMOR] += 0.15f; stats_weights_[STATS_TYPE_HIT] += 2.0f; - stats_weights_[STATS_TYPE_CRIT] += 0.2f; - stats_weights_[STATS_TYPE_HASTE] += 0.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 2.0f; stats_weights_[STATS_TYPE_EXPERTISE] += 3.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 2.0f; } else if (cls == CLASS_DEATH_KNIGHT && tab == DEATH_KNIGHT_TAB_BLOOD) { - stats_weights_[STATS_TYPE_AGILITY] += 2.0f; - stats_weights_[STATS_TYPE_STRENGTH] += 1.0f; - stats_weights_[STATS_TYPE_STAMINA] += 3.5f; + stats_weights_[STATS_TYPE_AGILITY] += 0.2f; + stats_weights_[STATS_TYPE_STRENGTH] += 1.3f; + stats_weights_[STATS_TYPE_STAMINA] += 3.0f; stats_weights_[STATS_TYPE_ATTACK_POWER] += 0.2f; - stats_weights_[STATS_TYPE_DEFENSE] += 3.5f; + stats_weights_[STATS_TYPE_DEFENSE] += 2.5f; stats_weights_[STATS_TYPE_PARRY] += 2.0f; stats_weights_[STATS_TYPE_DODGE] += 2.0f; + stats_weights_[STATS_TYPE_BLOCK_RATING] -= 2.0f; + stats_weights_[STATS_TYPE_BLOCK_VALUE] -= 2.0f; // stats_weights_[STATS_TYPE_RESILIENCE] += 2.0f; stats_weights_[STATS_TYPE_ARMOR] += 0.15f; stats_weights_[STATS_TYPE_HIT] += 2.0f; - stats_weights_[STATS_TYPE_CRIT] += 0.5f; - stats_weights_[STATS_TYPE_HASTE] += 0.5f; - stats_weights_[STATS_TYPE_EXPERTISE] += 3.5f; + stats_weights_[STATS_TYPE_SPELL_POWER] -= 1.0f; + stats_weights_[STATS_TYPE_EXPERTISE] += 3.0f; stats_weights_[STATS_TYPE_MELEE_DPS] += 2.0f; } else @@ -439,21 +528,41 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player) // int tab = AiFactory::GetPlayerSpecTab(player); if (cls == CLASS_HUNTER) { - if (player->HasAura(34484)) + if (player->HasAura(SPELL_CAREFUL_AIM)) stats_weights_[STATS_TYPE_INTELLECT] += 1.1f; - if (player->HasAura(56341)) + if (player->HasAura(SPELL_HUNTER_VS_WILD)) stats_weights_[STATS_TYPE_STAMINA] += 0.3f; } else if (cls == CLASS_WARRIOR) { - if (player->HasAura(61222)) + if (player->HasAura(SPELL_ARMORED_TO_THE_TEETH)) stats_weights_[STATS_TYPE_ARMOR] += 0.03f; } else if (cls == CLASS_SHAMAN) { - if (player->HasAura(51885)) + if (player->HasAura(SPELL_MENTAL_DEXTERITY)) stats_weights_[STATS_TYPE_INTELLECT] += 1.1f; } + else if (cls == CLASS_MAGE) + { + if (!HasAnySpell(player, SPELL_MOLTEN_ARMOR_RANKS)) + { + if (tab != MAGE_TAB_FIRE) + stats_weights_[STATS_TYPE_SPIRIT] -= 0.6f; + else + stats_weights_[STATS_TYPE_SPIRIT] -= 0.7f; + } + } + else if (cls == CLASS_WARLOCK) + { + if (!HasAnySpell(player, SPELL_FEL_ARMOR_RANKS)) + stats_weights_[STATS_TYPE_SPIRIT] -= 0.4f; + } + + if (pvpSpec_ && !exclude_resilience_) + stats_weights_[STATS_TYPE_RESILIENCE] += 7.0f; + else if (!pvpSpec_) + stats_weights_[STATS_TYPE_RESILIENCE] -= 3.0f; } void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto) @@ -500,7 +609,7 @@ void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate con weight_ *= multiplier; } -void StatsWeightCalculator::CalculateSocketBonus(Player* player, ItemTemplate const* proto) +void StatsWeightCalculator::CalculateSocketBonus(Player* /*player*/, ItemTemplate const* proto) { uint32 socketNum = 0; for (uint32 enchant_slot = SOCK_ENCHANTMENT_SLOT; enchant_slot < SOCK_ENCHANTMENT_SLOT + MAX_GEM_SOCKETS; @@ -544,7 +653,8 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_FURY && !player_->CanTitanGrip() && player_->CanDualWield()) || (cls == CLASS_WARRIOR && tab == WARRIOR_TAB_PROTECTION) || - (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION))) + (cls == CLASS_PALADIN && tab == PALADIN_TAB_PROTECTION) || + (cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY))) { weight_ *= 0.1; } @@ -563,11 +673,16 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) weight_ *= 0.1; } // caster's main hand (cannot duel weapon but can equip two-hands stuff) - if (cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID || - (cls == CLASS_SHAMAN && !player_->CanDualWield())) + if ((cls == CLASS_MAGE || cls == CLASS_PRIEST || cls == CLASS_WARLOCK || cls == CLASS_DRUID || + (cls == CLASS_SHAMAN && !player_->CanDualWield())) && + !(cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY)) { weight_ *= 0.65; } + if (cls == CLASS_PALADIN && tab == PALADIN_TAB_HOLY) + { + weight_ *= 0.8; + } } // fury with titan's grip if ((!isDoubleHand || proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || @@ -588,17 +703,17 @@ void StatsWeightCalculator::CalculateItemTypePenalty(ItemTemplate const* proto) weight_ *= 1.5; } - if (cls == CLASS_ROGUE && player_->HasAura(13964) && + if (cls == CLASS_ROGUE && player_->HasAura(SPELL_ROGUE_SWORD_SPECIALIZATION) && (proto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD || proto->SubClass == ITEM_SUBCLASS_WEAPON_AXE)) { weight_ *= 1.1; } - if (cls == CLASS_WARRIOR && player_->HasAura(12785) && + if (cls == CLASS_WARRIOR && player_->HasAura(SPELL_POLEAXE_SPECIALIZATION) && (proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM || proto->SubClass == ITEM_SUBCLASS_WEAPON_AXE2)) { weight_ *= 1.1; } - if (cls == CLASS_DEATH_KNIGHT && player_->HasAura(50138) && !isDoubleHand) + if (cls == CLASS_DEATH_KNIGHT && player_->HasAura(SPELL_NERVES_OF_COLD_STEEL) && !isDoubleHand) { weight_ *= 1.3; } @@ -637,9 +752,9 @@ void StatsWeightCalculator::ApplyOverflowPenalty(Player* player) player->GetTotalAuraModifier(SPELL_AURA_MOD_INCREASES_SPELL_PCT_TO_HIT); // suppression (18176) hit_current += player->GetRatingBonusValue(CR_HIT_SPELL); - if (cls == CLASS_PRIEST && tab == PRIEST_TAB_SHADOW && player->HasAura(15835)) // Shadow Focus + if (cls == CLASS_PRIEST && tab == PRIEST_TAB_SHADOW && player->HasAura(SPELL_SHADOW_FOCUS)) hit_current += 3; - if (cls == CLASS_MAGE && tab == MAGE_TAB_ARCANE && player->HasAura(12840)) // Arcane Focus + if (cls == CLASS_MAGE && tab == MAGE_TAB_ARCANE && player->HasAura(SPELL_ARCANE_FOCUS)) hit_current += 3; hit_overflow = SPELL_HIT_OVERFLOW; @@ -738,3 +853,163 @@ void StatsWeightCalculator::ApplyWeightFinetune(Player* player) } } } + +float StatsWeightCalculator::ApplyPreferredSpecWeapons(ItemTemplate const* proto, int32 slot) +{ + // Multiply score by 3x when this weapon's delay matches the spec-ideal speed. + float weight = 2.0f; + + // Applies to mainhand, offhand, and ranged slots only. + if (slot != EQUIPMENT_SLOT_MAINHAND && + slot != EQUIPMENT_SLOT_OFFHAND && + slot != EQUIPMENT_SLOT_RANGED) + return 1.0f; + + uint32 delay = proto->Delay; // milliseconds + float boost = 1.0f + weight; // applied on a match + + // Hunter: melee weapons are stat sticks — speed irrelevant. + // Ranged weapons scale Aimed/Chimera/Explosive Shot from top-end damage, + // so a slow ranged weapon (>=2600 ms) is strongly preferred. + if (cls == CLASS_HUNTER) + { + if (slot == EQUIPMENT_SLOT_RANGED && delay >= 2600) + return boost; + return 1.0f; + } + + // Feral Druid: forms normalise attack speed; raw weapon Delay is irrelevant. + if (cls == CLASS_DRUID && tab == DRUID_TAB_FERAL) + return 1.0f; + + switch (cls) + { + case CLASS_WARRIOR: + if (tab == WARRIOR_TAB_ARMS) + { + // Arms: slow 2H axes or polearms in mainhand only (Axe Specialization: +5% crit). + bool isAxeOrPolearm = (proto->SubClass == ITEM_SUBCLASS_WEAPON_AXE2 || + proto->SubClass == ITEM_SUBCLASS_WEAPON_POLEARM); + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400 && isAxeOrPolearm) + return boost; + } + else if (tab == WARRIOR_TAB_FURY) + { + if (!player_->CanDualWield()) + { + // Pre-DW: treat like Arms — slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (player_->CanTitanGrip()) + { + // Titan's Grip: slow 2H (>=3400) in both hands. + if (delay >= 3400) + return boost; + } + else + { + // 1H DW: slow 1H (>=2600) in both hands. + // 2H must be excluded — delay >= 2600 would otherwise pass + // for a 2H heirloom (~3600ms) just as it did for Enhancement. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + if (delay >= 2600) + return boost; + } + } + else if (tab == WARRIOR_TAB_PROTECTION) + { + // Prot: slow 1H (>=2600) in mainhand. Shield in offhand, no speed bonus. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + } + break; + + case CLASS_PALADIN: + if (tab == PALADIN_TAB_RETRIBUTION) + { + // Ret: slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (tab == PALADIN_TAB_PROTECTION) + { + // Prot: slow 1H (>=2600) in mainhand. Shield in offhand. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + } + break; + + case CLASS_DEATH_KNIGHT: + if (tab == DEATH_KNIGHT_TAB_BLOOD || tab == DEATH_KNIGHT_TAB_UNHOLY) + { + // Blood / Unholy: slow 2H in mainhand only. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else if (tab == DEATH_KNIGHT_TAB_FROST) + { + // Frost DK has Dual Wield innately — always dual-wields 1H. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + if (delay >= 2600) + return boost; + } + break; + + case CLASS_SHAMAN: + if (tab == SHAMAN_TAB_ENHANCEMENT) + { + if (!player_->CanDualWield()) + { + // Pre-Dual Wield: Enhancement plays like a 2H spec. + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 3400) + return boost; + } + else + { + // Post-Dual Wield: slow 1H (>=2600) in both hands. + if (proto->InventoryType == INVTYPE_2HWEAPON) + break; + + if (delay >= 2600) + { + float mult = boost; + if (slot == EQUIPMENT_SLOT_OFFHAND) + { + Item* mh = player_->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (mh && mh->GetTemplate() && mh->GetTemplate()->Delay == delay) + mult *= boost; // synchronized: ×(1+weight)² total = ×9 for 2.0f weight + } + return mult; + } + } + } + break; + + case CLASS_ROGUE: + if (tab == ROGUE_TAB_COMBAT) + { + // Combat: slow MH (>=2600), fast OH (<=1500). + if (slot == EQUIPMENT_SLOT_MAINHAND && delay >= 2600) + return boost; + if (slot == EQUIPMENT_SLOT_OFFHAND && delay <= 1500) + return boost; + } + else // Assassination or Subtlety: slow dagger MH, fast dagger OH. + { + bool isDagger = (proto->SubClass == ITEM_SUBCLASS_WEAPON_DAGGER); + if (slot == EQUIPMENT_SLOT_MAINHAND && isDagger && delay >= 1700) + return boost; + if (slot == EQUIPMENT_SLOT_OFFHAND && isDagger && delay <= 1500) + return boost; + } + break; + + default: + break; + } + + return 1.0f; +} diff --git a/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.h b/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.h index 4390e6af1..f82aa5c2a 100644 --- a/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.h +++ b/modules/mod-playerbots/src/Mgr/Item/StatsWeightCalculator.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_GEARSCORECALCULATOR_H -#define _PLAYERBOT_GEARSCORECALCULATOR_H +#ifndef PLAYERBOTS_STATSWEIGHTCALCULATOR_H +#define PLAYERBOTS_STATSWEIGHTCALCULATOR_H #include "Player.h" #include "StatsCollector.h" @@ -28,12 +29,15 @@ class StatsWeightCalculator public: StatsWeightCalculator(Player* player); void Reset(); - float CalculateItem(uint32 itemId, int32 randomPropertyId = 0); + float CalculateItem(uint32 itemId, int32 randomPropertyId = 0, int32 slot = -1); float CalculateEnchant(uint32 enchantId); + int32 PickBestRandomPropertyId(uint32 itemId); void SetOverflowPenalty(bool apply) { enable_overflow_penalty_ = apply; } void SetItemSetBonus(bool apply) { enable_item_set_bonus_ = apply; } void SetQualityBlend(bool apply) { enable_quality_blend_ = apply; } + void SetPvpSpec(bool isPvp) { pvpSpec_ = isPvp; } + void SetExcludeResilience(bool exclude) { exclude_resilience_ = exclude; } private: void GenerateWeights(Player* player); @@ -45,6 +49,7 @@ public: void CalculateSocketBonus(Player* player, ItemTemplate const* proto); void CalculateItemTypePenalty(ItemTemplate const* proto); + float ApplyPreferredSpecWeapons(ItemTemplate const* proto, int32 slot); bool NotBestArmorType(uint32 item_subclass_armor); @@ -65,6 +70,8 @@ private: float weight_; float stats_weights_[STATS_TYPE_MAX]; + bool pvpSpec_ = false; + bool exclude_resilience_ = false; }; #endif diff --git a/modules/mod-playerbots/src/Mgr/Move/FleeManager.cpp b/modules/mod-playerbots/src/Mgr/Move/FleeManager.cpp index e82f01daf..e7577a864 100644 --- a/modules/mod-playerbots/src/Mgr/Move/FleeManager.cpp +++ b/modules/mod-playerbots/src/Mgr/Move/FleeManager.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "FleeManager.h" diff --git a/modules/mod-playerbots/src/Mgr/Move/FleeManager.h b/modules/mod-playerbots/src/Mgr/Move/FleeManager.h index 79629769f..6ea83dcf5 100644 --- a/modules/mod-playerbots/src/Mgr/Move/FleeManager.h +++ b/modules/mod-playerbots/src/Mgr/Move/FleeManager.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_FLEEMANAGER_H -#define _PLAYERBOT_FLEEMANAGER_H +#ifndef PLAYERBOTS_FLEEMANAGER_H +#define PLAYERBOTS_FLEEMANAGER_H #include diff --git a/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.cpp b/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.cpp index 4a3bd3655..482bbc979 100644 --- a/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.cpp +++ b/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlayerbotSecurity.h" @@ -27,7 +28,7 @@ PlayerbotSecurityLevel PlayerbotSecurity::LevelFor(Player* from, DenyReason* rea } // GMs always have full access - if (from->GetSession()->GetSecurity() >= SEC_GAMEMASTER) + if (from->CanBeGameMaster()) return PLAYERBOT_SECURITY_ALLOW_ALL; PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); @@ -188,9 +189,8 @@ bool PlayerbotSecurity::CheckLevelFor(PlayerbotSecurityLevel level, bool silent, Player* master = botAI->GetMaster(); if (master && botAI->IsOpposing(master)) - if (WorldSession* session = master->GetSession()) - if (session->GetSecurity() < SEC_GAMEMASTER) - return false; + if (master->GetSession() && !master->CanBeGameMaster()) + return false; std::ostringstream out; diff --git a/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.h b/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.h index e73108dc1..711a1e006 100644 --- a/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.h +++ b/modules/mod-playerbots/src/Mgr/Security/PlayerbotSecurity.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTSECURITY_H -#define _PLAYERBOT_PLAYERBOTSECURITY_H +#ifndef PLAYERBOTS_PLAYERBOTSECURITY_H +#define PLAYERBOTS_PLAYERBOTSECURITY_H #include diff --git a/modules/mod-playerbots/src/Mgr/Talent/Talentspec.cpp b/modules/mod-playerbots/src/Mgr/Talent/Talentspec.cpp index 7ee760ed4..050c1544b 100644 --- a/modules/mod-playerbots/src/Mgr/Talent/Talentspec.cpp +++ b/modules/mod-playerbots/src/Mgr/Talent/Talentspec.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Talentspec.h" @@ -142,7 +143,7 @@ bool TalentSpec::CheckTalents(uint32 level, std::ostringstream* out) } // Set the talents for the bots to the current spec. -void TalentSpec::ApplyTalents(Player* bot, std::ostringstream* out) +void TalentSpec::ApplyTalents(Player* bot, std::ostringstream* /*out*/) { for (auto& entry : talents) { @@ -316,7 +317,7 @@ std::vector TalentSpec::GetTalentTree(uint32 tabpag std::vector retList; for (auto& entry : talents) - if (entry.tabPage() == tabpage) + if (entry.tabPage() == uint32(tabpage)) retList.push_back(entry); return retList; @@ -332,7 +333,7 @@ uint32 TalentSpec::GetTalentPoints(std::vector& talents, int32 uint32 tPoints = 0; for (auto& entry : talents) - if (entry.tabPage() == tabpage) + if (entry.tabPage() == uint32(tabpage)) tPoints = tPoints + entry.rank; return tPoints; @@ -397,7 +398,7 @@ uint32 TalentSpec::highestTree() return 0; } -std::string const TalentSpec::FormatSpec(Player* bot) +std::string const TalentSpec::FormatSpec(Player* /*bot*/) { // uint8 cls = bot->getClass(); //not used, (used in lined 403), line marked for removal. @@ -448,7 +449,7 @@ std::vector TalentSpec::SubTalentList(std::vector, 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TALENTSPEC_H -#define _PLAYERBOT_TALENTSPEC_H +#ifndef PLAYERBOTS_TALENTSPEC_H +#define PLAYERBOTS_TALENTSPEC_H #include #include diff --git a/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.cpp b/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.cpp index 278a14db5..8ba63d6cd 100644 --- a/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.cpp +++ b/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.cpp @@ -1,7 +1,9 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ + #include "DatabaseEnv.h" #include "WorldSessionMgr.h" #include "Random.h" @@ -194,8 +196,7 @@ void PlayerbotTextMgr::AddLocalePriority(uint32 locale) { if (locale >= TOTAL_LOCALES) { - LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds TOTAL_LOCALES ({})", locale, - TOTAL_LOCALES - 1); + LOG_WARN("playerbots", "Ignoring locale {} for bot texts because it exceeds TOTAL_LOCALES ({})", locale, TOTAL_LOCALES - 1); return; } diff --git a/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.h b/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.h index 1613630f9..563405cf3 100644 --- a/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.h +++ b/modules/mod-playerbots/src/Mgr/Text/PlayerbotTextMgr.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERBOTTEXTMGR_H -#define _PLAYERBOT_PLAYERBOTTEXTMGR_H +#ifndef PLAYERBOTS_PLAYERBOTTEXTMGR_H +#define PLAYERBOTS_PLAYERBOTTEXTMGR_H #include #include diff --git a/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.cpp b/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.cpp index 2909df5e5..e6a618ca5 100644 --- a/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.cpp +++ b/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TravelMgr.h" @@ -8,12 +9,18 @@ #include #include +#include "AreaDefines.h" +#include "Creature.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "TravelNode.h" #include "Talentspec.h" #include "ChatHelper.h" -#include "MMapFactory.h" +#include "MapCollisionData.h" #include "MapMgr.h" #include "PathGenerator.h" #include "Playerbots.h" +#include "RaceMgr.h" #include "TransportMgr.h" #include "VMapFactory.h" #include "VMapMgr2.h" @@ -21,6 +28,64 @@ #include "Corpse.h" #include "CellImpl.h" +// Navigation data + +struct Capital +{ + uint32 zoneId; + TeamId team; + char const* name; + std::vector bankers; +}; + +static const std::vector capitals = { + { AREA_STORMWIND_CITY, TEAM_ALLIANCE, "Stormwind", {2455, 2456, 2457} }, + { AREA_IRONFORGE, TEAM_ALLIANCE, "Ironforge", {2460, 2461, 5099} }, + { AREA_DARNASSUS, TEAM_ALLIANCE, "Darnassus", {4155, 4208, 4209} }, + { AREA_THE_EXODAR, TEAM_ALLIANCE, "Exodar", {17773, 18350, 16710} }, + { AREA_ORGRIMMAR, TEAM_HORDE, "Orgrimmar", {3320, 3309, 3318} }, + { AREA_UNDERCITY, TEAM_HORDE, "Undercity", {4549, 2459, 2458, 4550} }, + { AREA_THUNDER_BLUFF, TEAM_HORDE, "Thunder Bluff", {2996, 8356, 8357} }, + { AREA_SILVERMOON_CITY, TEAM_HORDE, "Silvermoon", {17631, 17632, 17633, 16615, 16616, 16617} }, + { AREA_SHATTRATH_CITY, TEAM_NEUTRAL, "Shattrath", {19246, 19338, 19034, 19318} }, + { AREA_DALARAN, TEAM_NEUTRAL, "Dalaran", {30604, 30605, 30607, 28675, 28676, 28677, 29530} } +}; + +static Capital const* FindCapitalByZone(uint32 zoneId) +{ + for (Capital const& capital : capitals) + if (capital.zoneId == zoneId) + return &capital; + return nullptr; +} + +static Capital const* FindCapitalByBanker(uint16 bankerEntry) +{ + for (Capital const& capital : capitals) + for (uint16 bankerId : capital.bankers) + if (bankerId == bankerEntry) + return &capital; + return nullptr; +} + +static int GetCityWeight(uint32 zoneId) +{ + switch (zoneId) + { + case AREA_STORMWIND_CITY: return sPlayerbotAIConfig.weightTeleToStormwind; + case AREA_IRONFORGE: return sPlayerbotAIConfig.weightTeleToIronforge; + case AREA_DARNASSUS: return sPlayerbotAIConfig.weightTeleToDarnassus; + case AREA_THE_EXODAR: return sPlayerbotAIConfig.weightTeleToExodar; + case AREA_ORGRIMMAR: return sPlayerbotAIConfig.weightTeleToOrgrimmar; + case AREA_UNDERCITY: return sPlayerbotAIConfig.weightTeleToUndercity; + case AREA_THUNDER_BLUFF: return sPlayerbotAIConfig.weightTeleToThunderBluff; + case AREA_SILVERMOON_CITY: return sPlayerbotAIConfig.weightTeleToSilvermoonCity; + case AREA_SHATTRATH_CITY: return sPlayerbotAIConfig.weightTeleToShattrathCity; + case AREA_DALARAN: return sPlayerbotAIConfig.weightTeleToDalaran; + } + return 0; +} + WorldPosition::WorldPosition(std::string const str) { std::vector tokens = split(str, '|'); @@ -228,13 +293,13 @@ WorldPosition WorldPosition::offset(WorldPosition* center) float WorldPosition::size() { - return sqrt(pow(GetPositionX(), 2.0) + pow(GetPositionY(), 2.0) + pow(GetPositionZ(), 2.0)); + return GetExactDist(0.0f, 0.0f, 0.0f); } float WorldPosition::distance(WorldPosition* center) { if (GetMapId() == center->GetMapId()) - return relPoint(center).size(); + return GetExactDist(center->GetPositionX(), center->GetPositionY(), center->GetPositionZ()); // this -> mapTransfer | mapTransfer -> center return TravelMgr::instance().mapTransDistance(*this, *center); @@ -243,7 +308,7 @@ float WorldPosition::distance(WorldPosition* center) float WorldPosition::fDist(WorldPosition* center) { if (GetMapId() == center->GetMapId()) - return sqrt(sqDistance2d(center)); + return GetExactDist2d(center->GetPositionX(), center->GetPositionY()); // this -> mapTransfer | mapTransfer -> center return TravelMgr::instance().fastMapTransDistance(*this, *center); @@ -476,7 +541,7 @@ std::string const WorldPosition::getAreaName(bool fullName, bool zoneName) return areaName; } -std::set WorldPosition::getTransports(uint32 entry) +std::set WorldPosition::getTransports(uint32 /*entry*/) { /* if (!entry) @@ -617,10 +682,11 @@ std::vector WorldPosition::frommGridCoord(mGridCoord GridCoord) return retVec; } +// TODO: Cleanup — make this actually work. void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y) { std::string const fileName = "load_map_grid.csv"; - +/* if (isOverworld() && false || false) { if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y)) @@ -675,22 +741,22 @@ void WorldPosition::loadMapAndVMap(uint32 mapId, uint8 x, uint8 y) sPlayerbotAIConfig.log(fileName, out.str().c_str()); } } +*/ + if (!TravelMgr::instance().isBadMmap(mapId, x, y)) + { + // load navmesh + Map* map = getMap(); + if (map && map->GetMapCollisionData().LoadMMapTile(x, y) == MMAP::MMAP_LOAD_RESULT_ERROR) + TravelMgr::instance().addBadMmap(mapId, x, y); - if (!TravelMgr::instance().isBadMmap(mapId, x, y)) + if (sPlayerbotAIConfig.hasLog(fileName)) { - // load navmesh - if (!MMAP::MMapFactory::createOrGetMMapMgr()->loadMap(mapId, x, y)) - TravelMgr::instance().addBadMmap(mapId, x, y); - - if (sPlayerbotAIConfig.hasLog(fileName)) - { - std::ostringstream out; - out << sPlayerbotAIConfig.GetTimestampStr(); - out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1") - << ","; - printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true); - sPlayerbotAIConfig.log(fileName, out.str().c_str()); - } + std::ostringstream out; + out << sPlayerbotAIConfig.GetTimestampStr(); + out << "+00,\"mmap\", " << x << "," << y << "," << (TravelMgr::instance().isBadMmap(mapId, x, y) ? "0" : "1") + << ","; + printWKT(fromGridCoord(GridCoord(x, y)), out, 1, true); + sPlayerbotAIConfig.log(fileName, out.str().c_str()); } } } @@ -844,7 +910,7 @@ uint32 WorldPosition::getUnitsAggro(GuidVector& units, Player* bot) void FindPointCreatureData::operator()(CreatureData const& creatureData) { - if (!entry || creatureData.id1 == entry) + if (!entry || creatureData.id == entry) if ((!point || creatureData.mapid == point.GetMapId()) && (!radius || point.sqDistance(WorldPosition(creatureData.mapid, creatureData.posX, creatureData.posY, creatureData.posZ)) < radius * radius)) @@ -995,7 +1061,7 @@ bool GuidPosition::IsCreatureOrGOAccessible() GuidPosition::GuidPosition(WorldObject* wo) : ObjectGuid(wo->GetGUID()), WorldPosition(wo), loadedFromDB(false) {} GuidPosition::GuidPosition(CreatureData const& creData) - : ObjectGuid(HighGuid::Unit, creData.id1, creData.spawnId), + : ObjectGuid(HighGuid::Unit, creData.id, creData.spawnId), WorldPosition(creData.mapid, creData.posX, creData.posY, creData.posZ, creData.orientation) { loadedFromDB = true; @@ -1193,7 +1259,7 @@ bool QuestObjectiveTravelDestination::isActive(Player* bot) GuidVector targets = AI_VALUE(GuidVector, "possible targets"); for (auto& target : targets) - if (target.GetEntry() == getEntry() && target.IsCreature() && botAI->GetCreature(target) && + if (target.GetEntry() == uint32(getEntry()) && target.IsCreature() && botAI->GetCreature(target) && botAI->GetCreature(target)->IsAlive()) return true; @@ -1245,7 +1311,7 @@ bool RpgTravelDestination::isActive(Player* bot) for (ObjectGuid const guid : ignoreList) { - if (guid.GetEntry() == getEntry()) + if (guid.GetEntry() == uint32(getEntry())) return false; } @@ -1390,7 +1456,7 @@ bool BossTravelDestination::isActive(Player* bot) GuidVector targets = AI_VALUE(GuidVector, "possible targets"); for (auto& target : targets) - if (target.GetEntry() == getEntry() && target.IsCreature() && botAI->GetCreature(target) && + if (target.GetEntry() == uint32(getEntry()) && target.IsCreature() && botAI->GetCreature(target) && botAI->GetCreature(target)->IsAlive()) return true; @@ -1820,14 +1886,14 @@ void TravelMgr::LoadQuestTravelTable() for (auto& creatureData : WorldPosition().getCreaturesNear()) { t_unit.type = 0; - t_unit.entry = creatureData->id1; + t_unit.entry = creatureData->id; t_unit.map = creatureData->mapid; t_unit.x = creatureData->posX; t_unit.y = creatureData->posY; t_unit.z = creatureData->posZ; t_unit.o = creatureData->orientation; - entryCount[creatureData->id1]++; + entryCount[creatureData->id]++; units.push_back(t_unit); } @@ -1855,7 +1921,7 @@ void TravelMgr::LoadQuestTravelTable() /* // 0 1 2 3 4 5 6 7 8 std::string const query = "SELECT 0,guid,id,map,position_x,position_y,position_z,orientation, (SELECT COUNT(*) FROM - creature k WHERE c.id1 = k.id1) FROM creature c UNION ALL SELECT + creature k WHERE c.id = k.id) FROM creature c UNION ALL SELECT 1,guid,id,map,position_x,position_y,position_z,orientation, (SELECT COUNT(*) FROM gameobject h WHERE h.id = g.id) FROM gameobject g"; @@ -2853,7 +2919,7 @@ void TravelMgr::LoadQuestTravelTable() { for (CreatureData const* cData : WorldPosition().getCreaturesNear()) { - CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(cData->id1); + CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(cData->id); if (!cInfo) continue; @@ -3335,7 +3401,7 @@ void TravelMgr::LoadQuestTravelTable() std::ostringstream out; - for (uint8 race = RACE_HUMAN; race < MAX_RACES; race++) + for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); race++) { for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls) { @@ -3771,7 +3837,7 @@ uint32 TravelMgr::getDialogStatus(Player* pPlayer, int32 questgiver, Quest const // Selects a random WorldPosition from a list. Use a distance weighted distribution. std::vector TravelMgr::getNextPoint(WorldPosition* center, std::vector points, - uint32 amount) + uint32 /*amount*/) { std::vector retVec; @@ -4286,3 +4352,524 @@ void TravelMgr::printObj(WorldObject* obj, std::string const type) } } } + +void TravelMgr::Init() +{ + if (sPlayerbotAIConfig.enabled) + { + PrepareZone2LevelBracket(); + PrepareDestinationCache(); + } + sTravelNodeMap.InitTaxiGraph(); + LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); +} + +TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const +{ + auto const& flightMasterCache = + (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + + FlightMasterInfo const* nearest = nullptr; + float nearestDistance = std::numeric_limits::max(); + + for (auto const& [dbGuid, info] : flightMasterCache) + { + if (info.pos.GetMapId() != bot->GetMapId()) + continue; + + float distance = bot->GetExactDist2dSq(info.pos); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearest = &info; + } + } + + return nearest; +} + +std::vector TravelMgr::GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode) const +{ + auto const& cache = (team == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + std::unordered_set seen; + std::vector result; + for (auto const& [entry, info] : cache) + { + if (info.zoneId != zoneId || info.taxiNodeId == 0 || info.taxiNodeId == excludeNode) + continue; + if (seen.insert(info.taxiNodeId).second) + result.push_back(info.taxiNodeId); + } + return result; +} + +std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot) +{ + std::vector> validDestinations; + + FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot); + if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f) + return validDestinations; + + uint32 fromNode = nearestFlightMaster->taxiNodeId; + if (!fromNode) + return validDestinations; + + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + if (!startNode) + return validDestinations; + + uint32 botLevel = bot->GetLevel(); + + // Bots already in a capital shouldn't have another capital picked as a + // flight destination — that just shuffles them between cities. + bool botInCapital = false; + if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId())) + botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0; + + //Simplify destination delection. Its either target cities (Based on config value) or target world. + std::vector candidateZones; + if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + { + TeamId botTeam = bot->GetTeamId(); + for (Capital const& capital : capitals) + { + if (capital.team != TEAM_NEUTRAL && capital.team != botTeam) + continue; + candidateZones.push_back(capital.zoneId); + } + } + if (candidateZones.empty()) + { + for (auto const& [zoneId, bracket] : zone2LevelBracket) + { + if (botLevel < bracket.low || botLevel > bracket.high) + continue; + if (GetFlightNodesInZone(zoneId, bot->GetTeamId(), fromNode).empty()) + continue; + candidateZones.push_back(zoneId); + } + } + + if (candidateZones.empty()) + return validDestinations; + + while (!candidateZones.empty()) + { + uint32 zoneIndex = urand(0, candidateZones.size() - 1); + uint32 pickedZone = candidateZones[zoneIndex]; + + std::vector usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode); + + if (!usableNodes.empty()) + { + uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)]; + std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode); + if (!path.empty()) + { + validDestinations.push_back(std::move(path)); + return validDestinations; + } + } + + candidateZones.erase(candidateZones.begin() + zoneIndex); + } + + return validDestinations; +} + +const std::vector TravelMgr::GetTeleportLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + uint8 isAlliance = bot->GetTeamId() == TEAM_ALLIANCE; + if (sPlayerbotAIConfig.enableNewRpgStrategy) + return isAlliance ? allianceHubsPerLevelCache[level] : hordeHubsPerLevelCache[level]; + + return locsPerLevelCache[level]; +} + +const std::vector TravelMgr::GetTravelHubs(Player* bot) +{ + std::vector locs = bot->GetTeamId() == TEAM_ALLIANCE + ? allianceHubsPerLevelCache[bot->GetLevel()] + : hordeHubsPerLevelCache[bot->GetLevel()]; + return locs; +} + +std::vector TravelMgr::GetCityLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + + std::vector fallbackLocations; + for (auto& bLoc : bankerLocsPerLevelCache[level]) + fallbackLocations.push_back(bLoc.loc); + + if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) + return fallbackLocations; + + TeamId botTeamId = bot->GetTeamId(); + std::unordered_set validBankerCities; + for (auto& loc : bankerLocsPerLevelCache[level]) + { + Capital const* capital = FindCapitalByBanker(loc.entry); + if (!capital) + continue; + + TeamId cityTeamId = capital->team; + + if (cityTeamId == botTeamId || + (cityTeamId == TEAM_NEUTRAL) + ) + validBankerCities.insert(capital->zoneId); + } + // Fallback if no valid cities + if (validBankerCities.empty()) + return fallbackLocations; + + // Apply weights to valid cities + std::vector weightedCities; + for (uint32 zoneId : validBankerCities) + { + int weight = GetCityWeight(zoneId); + if (weight <= 0) + continue; + + for (int i = 0; i < weight; ++i) + weightedCities.push_back(zoneId); + } + + // Fallback if no valid cities + if (weightedCities.empty()) + return fallbackLocations; + + // Pick a weighted city randomly, then a random banker in that city + uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + Capital const* selectedCapital = FindCapitalByZone(selectedCity); + if (!selectedCapital) + return fallbackLocations; + auto const& bankers = selectedCapital->bankers; + uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; + auto locIt = bankerEntryToLocation.find(selectedBankerEntry); + if (locIt != bankerEntryToLocation.end()) + return { locIt->second }; + // Fallback if something went wrong + return fallbackLocations; +} + +void TravelMgr::PrepareZone2LevelBracket() +{ + // Classic WoW - starter zones + zone2LevelBracket[AREA_DUN_MOROGH] = {5, 12}; + zone2LevelBracket[AREA_ELWYNN_FOREST] = {5, 12}; + zone2LevelBracket[AREA_DUROTAR] = {5, 12}; + zone2LevelBracket[AREA_TIRISFAL_GLADES] = {5, 12}; + zone2LevelBracket[AREA_TELDRASSIL] = {5, 12}; + zone2LevelBracket[AREA_MULGORE] = {5, 12}; + zone2LevelBracket[AREA_EVERSONG_WOODS] = {5, 12}; + zone2LevelBracket[AREA_AZUREMYST_ISLE] = {5, 12}; + + // Classic WoW - low level zones + zone2LevelBracket[AREA_THE_BARRENS] = {10, 25}; + zone2LevelBracket[AREA_LOCH_MODAN] = {10, 20}; + zone2LevelBracket[AREA_WESTFALL] = {10, 21}; + zone2LevelBracket[AREA_SILVERPINE_FOREST] = {10, 23}; + zone2LevelBracket[AREA_DARKSHORE] = {10, 21}; + zone2LevelBracket[AREA_GHOSTLANDS] = {10, 22}; + zone2LevelBracket[AREA_BLOODMYST_ISLE] = {10, 21}; + + // Classic WoW - mid-level zones + zone2LevelBracket[AREA_DUSKWOOD] = {19, 33}; + zone2LevelBracket[AREA_WETLANDS] = {21, 30}; + zone2LevelBracket[AREA_REDRIDGE_MOUNTAINS] = {16, 28}; + zone2LevelBracket[AREA_HILLSBRAD_FOOTHILLS] = {20, 34}; + zone2LevelBracket[AREA_ASHENVALE] = {18, 33}; + zone2LevelBracket[AREA_THOUSAND_NEEDLES] = {24, 36}; + zone2LevelBracket[AREA_STONETALON_MOUNTAINS] = {16, 29}; + + // Classic WoW - 30-52 zones + zone2LevelBracket[AREA_BADLANDS] = {36, 46}; + zone2LevelBracket[AREA_SWAMP_OF_SORROWS] = {36, 46}; + zone2LevelBracket[AREA_DUSTWALLOW_MARSH] = {35, 46}; + zone2LevelBracket[AREA_AZSHARA] = {45, 52}; + zone2LevelBracket[AREA_STRANGLETHORN_VALE] = {32, 47}; + zone2LevelBracket[AREA_ARATHI_HIGHLANDS] = {30, 42}; + zone2LevelBracket[AREA_THE_HINTERLANDS] = {42, 51}; + zone2LevelBracket[AREA_SEARING_GORGE] = {45, 51}; + zone2LevelBracket[AREA_FERALAS] = {40, 52}; + zone2LevelBracket[AREA_DESOLACE] = {30, 41}; + zone2LevelBracket[AREA_TANARIS] = {41, 52}; + + // Classic WoW - top level zones + zone2LevelBracket[AREA_BLASTED_LANDS] = {52, 57}; + zone2LevelBracket[AREA_WESTERN_PLAGUELANDS] = {50, 60}; + zone2LevelBracket[AREA_BURNING_STEPPES] = {51, 60}; + zone2LevelBracket[AREA_EASTERN_PLAGUELANDS] = {54, 62}; + zone2LevelBracket[361] = {47, 57}; // Felwood (no AREA_ define) + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater (no AREA_ define) + zone2LevelBracket[AREA_WINTERSPRING] = {54, 61}; + zone2LevelBracket[AREA_SILITHUS] = {54, 63}; + + // The Burning Crusade zones + zone2LevelBracket[AREA_HELLFIRE_PENINSULA] = {58, 66}; + zone2LevelBracket[AREA_NAGRAND] = {64, 70}; + zone2LevelBracket[AREA_TEROKKAR_FOREST] = {62, 73}; + zone2LevelBracket[AREA_SHADOWMOON_VALLEY] = {66, 73}; + zone2LevelBracket[AREA_ZANGARMARSH] = {60, 67}; + zone2LevelBracket[AREA_BLADES_EDGE_MOUNTAINS] = {64, 73}; + zone2LevelBracket[AREA_NETHERSTORM] = {67, 73}; + zone2LevelBracket[AREA_ISLE_OF_QUEL_DANAS] = {68, 73}; + + // Wrath of the Lich King zones + zone2LevelBracket[AREA_DRAGONBLIGHT] = {71, 77}; + zone2LevelBracket[AREA_ZUL_DRAK] = {74, 80}; + zone2LevelBracket[AREA_THE_STORM_PEAKS] = {77, 80}; + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier (no AREA_ define) + zone2LevelBracket[AREA_GRIZZLY_HILLS] = {72, 78}; + zone2LevelBracket[AREA_HOWLING_FJORD] = {68, 74}; + zone2LevelBracket[AREA_CRYSTALSONG_FOREST] = {77, 80}; + zone2LevelBracket[AREA_BOREAN_TUNDRA] = {68, 75}; + zone2LevelBracket[AREA_SHOLAZAR_BASIN] = {75, 80}; + zone2LevelBracket[AREA_WINTERGRASP] = {79, 80}; + + // Override with values from config + for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) + zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; +} + +void TravelMgr::PrepareDestinationCache() +{ + uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); + uint32 flightMastersCount = 0; + uint32 innkeepersCount = 0; + uint32 bankerCount = 0; + + LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); + // Temporary map to group creatures by entry and area + std::map, std::vector> tempLocsCache; + std::map>> tempCreatureCache; + for (auto const& [guid, creatureData] : sObjectMgr->GetAllCreatureData()) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureData.id); + if (!creatureTemplate) + continue; + + uint16 mapId = creatureData.mapid; + if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId) + == sPlayerbotAIConfig.randomBotMaps.end()) + continue; + + float x = creatureData.posX; + float y = creatureData.posY; + float z = creatureData.posZ; + float orient = creatureData.orientation; + uint32 templateEntry = creatureData.id; + + Map* map = sMapMgr->FindMap(mapId, 0); + if (!map) + continue; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); + if (!area) + continue; + + uint32 areaId = area->zone ? area->zone : area->ID; + + // CREATURES + if (creatureTemplate->npcflag == 0 && + creatureTemplate->lootid != 0 && + creatureTemplate->maxlevel - creatureTemplate->minlevel < 3 && + creatureTemplate->Entry != 32820 && creatureTemplate->Entry != 24196 && + creatureTemplate->Entry != 30627 && creatureTemplate->Entry != 30617 && + creatureData.spawntimesecs < 1000 && + creatureTemplate->faction != 11 && creatureTemplate->faction != 71 && + creatureTemplate->faction != 79 && creatureTemplate->faction != 85 && + creatureTemplate->faction != 188 && creatureTemplate->faction != 1575 && + (creatureTemplate->unit_flags & 256) == 0 && + (creatureTemplate->unit_flags & 4096) == 0 && + creatureTemplate->rank == 0) + { + int32 roundX = static_cast(std::lround(x / 50.0f)); + int32 roundY = static_cast(std::lround(y / 50.0f)); + int32 roundZ = static_cast(std::lround(z / 50.0f)); + tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); + tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); + } + // FLIGHT MASTERS + // Entry 29480 is Grimwing (Storm Peaks) + // Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one. + else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || + creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && + creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) + { + FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); + bool forHorde = !(factionEntry->hostileMask & 4); + bool forAlliance = !(factionEntry->hostileMask & 2); + + if (creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER) + { + WorldPosition pos(mapId, x, y, z, orient); + if (forHorde) + { + FlightMasterInfo info; + info.pos = pos; + info.zoneId = areaId; + info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_HORDE); + info.templateEntry = templateEntry; + info.dbGuid = guid; + hordeFlightMasterCache[guid] = info; + } + + if (forAlliance) + { + FlightMasterInfo info; + info.pos = pos; + info.zoneId = areaId; + info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_ALLIANCE); + info.templateEntry = templateEntry; + info.dbGuid = guid; + allianceFlightMasterCache[guid] = info; + } + flightMastersCount++; + + // Zones that have flight masters but no innkeepers — use flight master as hub + static const std::set zonesWithoutInnkeeper = { + AREA_BLASTED_LANDS, + AREA_AZSHARA, + AREA_WESTERN_PLAGUELANDS, + AREA_BURNING_STEPPES, + AREA_SEARING_GORGE, + 361, // Felwood (47-57) + 490, // Un'Goro Crater (49-56) + AREA_CRYSTALSONG_FOREST, + AREA_WINTERGRASP + }; + if (zonesWithoutInnkeeper.count(areaId)) + { + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (uint32 i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + } + } + } + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) + { + if (zone2LevelBracket.find(areaId) == zone2LevelBracket.end()) + continue; + + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (uint32 i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + innkeepersCount++; + } + } + } + // === BANKERS === + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_BANKER && + creatureTemplate->npcflag != 135298 && + creatureTemplate->minlevel != 55 && + creatureTemplate->minlevel != 65 && + creatureTemplate->faction != 35 && creatureTemplate->faction != 474 && + creatureTemplate->faction != 69 && creatureTemplate->faction != 57 && + creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && + creatureTemplate->Entry != 29282) + { + BankerLocation bLoc; + bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); + bLoc.entry = templateEntry; + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (uint32 l = 1; l <= maxLevel; l++) + { + // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) + if (l <=60 && level > 45) + continue; + + // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) + if ((l >=61 && l <=70) && (level < 60 || level > 70)) + continue; + + // Bots 71+ go to Dalaran bankers (all have minlevel 75) + if ((l >=71) && level != 75) + continue; + + bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); + bankerEntryToLocation[bLoc.entry] = bLoc.loc; + } + bankerCount++; + } + } + + // Process temporary caches + for (auto const& [gridTuple, creatureDataList] : tempLocsCache) + { + if (creatureDataList.size() >= 2) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id); + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; + l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) + { + if (l < 1 || l > int32(maxLevel)) + continue; + + locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple), + static_cast(std::get<1>(gridTuple)) * 50.0f, + static_cast(std::get<2>(gridTuple)) * 50.0f, + static_cast(std::get<3>(gridTuple)) * 50.0f)); + } + } + } + for (auto const& [entry, areaMap] : tempCreatureCache) + { + for (auto const& [area, locList] : areaMap) + { + if (locList.size() > 3) + continue; + + float totalX = 0, totalY = 0, totalZ = 0; + for (auto const& loc : locList) + { + totalX += loc.GetPositionX(); + totalY += loc.GetPositionY(); + totalZ += loc.GetPositionZ(); + } + float avgX = totalX / locList.size(); + float avgY = totalY / locList.size(); + float avgZ = totalZ / locList.size(); + creatureSpawnsByTemplate[entry].push_back(WorldLocation(locList[0].GetMapId(), avgX, avgY, avgZ, 0)); + } + } + // Add travel hubs based on player start locations + for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) + { + for (uint32 j = 1; j < MAX_CLASSES; j++) + { + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); + + if (!info) + continue; + + WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); + + for (int32 l = 1; l <= 5; l++) + { + if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) + allianceHubsPerLevelCache[(uint8)l].push_back(pos); + else + hordeHubsPerLevelCache[(uint8)l].push_back(pos); + } + break; + } + } + LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); +} diff --git a/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.h b/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.h index 7e2a40b16..cb24cf053 100644 --- a/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.h +++ b/modules/mod-playerbots/src/Mgr/Travel/TravelMgr.h @@ -1,12 +1,14 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRAVELMGR_H -#define _PLAYERBOT_TRAVELMGR_H +#ifndef PLAYERBOTS_TRAVELMGR_H +#define PLAYERBOTS_TRAVELMGR_H #include +#include #include #include "AiObject.h" @@ -15,6 +17,7 @@ #include "GridDefines.h" #include "PlayerbotAIConfig.h" +class Creature; class GuidPosition; class ObjectGuid; class Quest; @@ -179,8 +182,7 @@ public: // Quick square distance in 2d plane. float sqDistance2d(WorldPosition center) { - return (GetPositionX() - center.GetPositionX()) * (GetPositionX() - center.GetPositionX()) + - (GetPositionY() - center.GetPositionY()) * (GetPositionY() - center.GetPositionY()); + return GetExactDist2dSq(center.GetPositionX(), center.GetPositionY()); } // Quick square distance calculation without map check. Used for getting the minimum distant points. @@ -193,8 +195,7 @@ public: float sqDistance2d(WorldPosition* center) { - return (GetPositionX() - center->GetPositionX()) * (GetPositionX() - center->GetPositionX()) + - (GetPositionY() - center->GetPositionY()) * (GetPositionY() - center->GetPositionY()); + return GetExactDist2dSq(center->GetPositionX(), center->GetPositionY()); } float sqDistance(WorldPosition* center) @@ -846,6 +847,21 @@ protected: class TravelMgr { public: + struct NpcLocation + { + WorldLocation loc; + uint32 entry; + }; + + struct FlightMasterInfo + { + WorldPosition pos; + uint32 zoneId; // resolved once at cache load + uint32 taxiNodeId; // DBC taxi node nearest to this flight master + uint32 templateEntry; // creature template ID (for ObjectGuid construction) + uint32 dbGuid; // DB spawn GUID (for ObjectGuid construction) + }; + static TravelMgr& instance() { static TravelMgr instance; @@ -856,6 +872,18 @@ public: void Clear(); void LoadQuestTravelTable(); + // Navigation + void Init(); + + FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const; + std::vector> GetOptimalFlightDestinations(Player* bot); + const std::vector GetTeleportLocations(Player* bot); + const std::vector GetTravelHubs(Player* bot); + std::vector GetCityLocations(Player* bot); + std::vector GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const; + bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer); + const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } + template void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g) { @@ -945,6 +973,37 @@ private: TravelMgr(TravelMgr&&) = delete; TravelMgr& operator=(TravelMgr&&) = delete; + + // Navigation initialization + void PrepareZone2LevelBracket(); + void PrepareDestinationCache(); + + // Internal types + struct LevelBracket + { + uint32 low; + uint32 high; + bool InsideBracket(uint32 val) const { return val >= low && val <= high; } + }; + + struct BankerLocation + { + WorldLocation loc; + uint32 entry; + }; + + // Navigation caches + std::map allianceFlightMasterCache; + std::map hordeFlightMasterCache; + std::map> allianceHubsPerLevelCache; + std::map> hordeHubsPerLevelCache; + std::map> bankerLocsPerLevelCache; + std::unordered_map bankerEntryToLocation; + std::map> locsPerLevelCache; + std::unordered_map> creatureSpawnsByTemplate; + std::map zone2LevelBracket; }; +#define sTravelMgr TravelMgr::instance() + #endif diff --git a/modules/mod-playerbots/src/Mgr/Travel/TravelNode.cpp b/modules/mod-playerbots/src/Mgr/Travel/TravelNode.cpp index bdf9cc952..7da250116 100644 --- a/modules/mod-playerbots/src/Mgr/Travel/TravelNode.cpp +++ b/modules/mod-playerbots/src/Mgr/Travel/TravelNode.cpp @@ -1,16 +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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "TravelNode.h" #include #include +#include #include "BudgetValues.h" #include "PathGenerator.h" #include "Playerbots.h" +#include "RaceMgr.h" #include "ServerFacade.h" #include "TransportMgr.h" @@ -54,7 +57,7 @@ void TravelNodePath::calculateCost(bool distanceOnly) { for (CreatureData const* cData : point.getCreaturesNear(50)) // Agro radius + 5 { - CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(cData->id1); + CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(cData->id); if (cInfo) { FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(cInfo->faction); @@ -1595,7 +1598,7 @@ void TravelNodeMap::generateNpcNodes() WorldPosition guidP(creatureData->mapid, creatureData->posX, creatureData->posY, creatureData->posZ, creatureData->orientation); - CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(creatureData->id1); + CreatureTemplate const* cInfo = sObjectMgr->GetCreatureTemplate(creatureData->id); if (!cInfo) continue; @@ -1660,7 +1663,7 @@ void TravelNodeMap::generateStartNodes() startNames[RACE_GNOME] = "Dwarf and Gnome"; startNames[RACE_TROLL] = "Orc and Troll"; - for (uint32 i = 0; i < MAX_RACES; i++) + for (uint32 i = 0; i < sRaceMgr->GetMaxRaces(); i++) { for (uint32 j = 0; j < MAX_CLASSES; j++) { @@ -1752,190 +1755,75 @@ void TravelNodeMap::generateTransportNodes() for (auto const& itr : *sObjectMgr->GetGameObjectTemplates()) { GameObjectTemplate const* data = &itr.second; - if (data && (data->type == GAMEOBJECT_TYPE_TRANSPORT || data->type == GAMEOBJECT_TYPE_MO_TRANSPORT)) + if (!data || (data->type != GAMEOBJECT_TYPE_TRANSPORT && data->type != GAMEOBJECT_TYPE_MO_TRANSPORT)) + continue; + + uint32 pathId = data->moTransport.taxiPathId; + float moveSpeed = data->moTransport.moveSpeed; + if (pathId >= sTaxiPathNodesByPath.size()) + continue; + + TaxiPathNodeList const& path = sTaxiPathNodesByPath[pathId]; + + // Keep only transports with taxi paths (boats/zeppelins). + if (path.empty()) + continue; + + std::vector ppath; + TravelNode* prevNode = nullptr; + + // Loop over the path and connect stop locations. + for (auto& p : path) { - TransportAnimation const* animation = sTransportMgr->GetTransportAnimInfo(itr.first); + WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); - uint32 pathId = data->moTransport.taxiPathId; - float moveSpeed = data->moTransport.moveSpeed; - if (pathId >= sTaxiPathNodesByPath.size()) - continue; + if (prevNode) + ppath.push_back(pos); - TaxiPathNodeList const& path = sTaxiPathNodesByPath[pathId]; - - std::vector ppath; - TravelNode* prevNode = nullptr; - - // Elevators/Trams - if (path.empty()) + if (p->delay > 0) { - if (animation) + TravelNode* node = TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); + + if (!prevNode) { - TransportPathContainer aPath = animation->Path; - float timeStart; - - for (auto& transport : WorldPosition().getGameObjectsNear(0, itr.first)) - { - prevNode = nullptr; - WorldPosition basePos(transport->mapid, transport->posX, transport->posY, transport->posZ, - transport->orientation); - WorldPosition lPos = WorldPosition(); - - for (auto& p : aPath) - { - float dx = -1 * p.second->X; - float dy = -1 * p.second->Y; - - WorldPosition pos = - WorldPosition(basePos.GetMapId(), basePos.GetPositionX() + dx, - basePos.GetPositionY() + dy, basePos.GetPositionZ() + p.second->Z, - basePos.GetOrientation()); - - if (prevNode) - { - ppath.push_back(pos); - } - - if (pos.distance(&lPos) == 0) - { - TravelNode* node = - TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - - if (!prevNode) - { - ppath.push_back(pos); - timeStart = p.second->TimeSeg; - } - else - { - float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f; - - TravelNodePath travelPath(0.1f, totalTime, (uint8)TravelNodePathType::transport, - itr.first, true); - node->setPathTo(prevNode, travelPath); - ppath.clear(); - ppath.push_back(pos); - timeStart = p.second->TimeSeg; - } - - prevNode = node; - } - - lPos = pos; - } - - if (prevNode) - { - for (auto& p : aPath) - { - float dx = -1 * p.second->X; - float dy = -1 * p.second->Y; - WorldPosition pos = - WorldPosition(basePos.GetMapId(), basePos.GetPositionX() + dx, - basePos.GetPositionY() + dy, basePos.GetPositionZ() + p.second->Z, - basePos.GetOrientation()); - - ppath.push_back(pos); - - if (pos.distance(&lPos) == 0) - { - TravelNode* node = - TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - if (node != prevNode) - { - if (p.second->TimeSeg < timeStart) - timeStart = 0; - - float totalTime = (p.second->TimeSeg - timeStart) / 1000.0f; - - TravelNodePath travelPath(0.1f, totalTime, (uint8)TravelNodePathType::transport, - itr.first, true); - travelPath.setPath(ppath); - node->setPathTo(prevNode, travelPath); - ppath.clear(); - ppath.push_back(pos); - timeStart = p.second->TimeSeg; - } - } - - lPos = pos; - } - } - - ppath.clear(); - } + ppath.push_back(pos); } - } - else // Boats/Zepelins - { - // Loop over the path and connect stop locations. - for (auto& p : path) + else { - WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); - - // if (data->displayId == 3015) - // pos.setZ(pos.getZ() + 6.0f); - // else if (data->displayId == 3031) - // pos.setZ(pos.getZ() - 17.0f); - - if (prevNode) - { - ppath.push_back(pos); - } - - if (p->delay > 0) - { - TravelNode* node = TravelNodeMap::instance().addNode(pos, data->name, true, true, true, itr.first); - - if (!prevNode) - { - ppath.push_back(pos); - } - else - { - TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, true); - travelPath.setPathAndCost(ppath, moveSpeed); - node->setPathTo(prevNode, travelPath); - ppath.clear(); - ppath.push_back(pos); - } - - prevNode = node; - } + TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, true); + travelPath.setPathAndCost(ppath, moveSpeed); + node->setPathTo(prevNode, travelPath); + ppath.clear(); + ppath.push_back(pos); } - if (prevNode) - { - // Continue from start until first stop and connect to end. - for (auto& p : path) - { - WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); - - // if (data->displayId == 3015) - // pos.setZ(pos.getZ() + 6.0f); - // else if (data->displayId == 3031) - // pos.setZ(pos.getZ() - 17.0f); - - ppath.push_back(pos); - - if (p->delay > 0) - { - TravelNode* node = TravelNodeMap::instance().getNode(pos, nullptr, 5.0f); - - if (node != prevNode) - { - TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, - true); - travelPath.setPathAndCost(ppath, moveSpeed); - - node->setPathTo(prevNode, travelPath); - } - } - } - } - ppath.clear(); + prevNode = node; } } + + if (!prevNode) + continue; + + // Continue from start until first stop and connect to end. + for (auto& p : path) + { + WorldPosition pos = WorldPosition(p->mapid, p->x, p->y, p->z, 0); + ppath.push_back(pos); + + if (p->delay > 0) + { + TravelNode* node = TravelNodeMap::instance().getNode(pos, nullptr, 5.0f); + + if (node != prevNode) + { + TravelNodePath travelPath(0.1f, 0.0, (uint8)TravelNodePathType::transport, itr.first, true); + travelPath.setPathAndCost(ppath, moveSpeed); + + node->setPathTo(prevNode, travelPath); + } + } + } + ppath.clear(); } } @@ -2561,3 +2449,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId) return WorldPosition(mapId, 0, 0, 0, 0); } + +// ============================================================ +// TravelNodeMap taxi graph (BFS-based flight path lookup) +// ============================================================ + +void TravelNodeMap::InitTaxiGraph() +{ + BuildTaxiGraph(); + ComputeAllPaths(); +} + +std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) +{ + if (fromNode == toNode) + return {}; + + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode); + + if (!startNode || !endNode) + return {}; + + auto cacheItr = taxiPathCache.find(fromNode); + if (cacheItr == taxiPathCache.end()) + return {}; + + auto toNodeItr = cacheItr->second.find(toNode); + if (toNodeItr == cacheItr->second.end()) + return {}; + + return toNodeItr->second; +} + +void TravelNodeMap::BuildTaxiGraph() +{ + taxiGraph.clear(); + std::unordered_map> tempGraph; + for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i) + { + TaxiPathEntry const* path = sTaxiPathStore.LookupEntry(i); + if (!path) + continue; + + if (path->to == 0 || path->to == uint32(-1)) + continue; + + tempGraph[path->from].insert(path->to); + tempGraph[path->to].insert(path->from); + } + for (auto const& [node, neighbors] : tempGraph) + taxiGraph[node] = std::vector(neighbors.begin(), neighbors.end()); +} + +void TravelNodeMap::ComputeAllPaths() +{ + std::set allNodes; + for (auto const& [source, neighbors] : taxiGraph) + allNodes.insert(source); + + for (uint32 source : allNodes) + { + auto parentMap = BFS(source); + + for (uint32 target : allNodes) + { + if (source == target) + continue; + + auto path = BuildPath(source, target, parentMap); + if (!path.empty()) + taxiPathCache[source][target] = path; + } + } +} + +std::unordered_map TravelNodeMap::BFS(uint32 fromNode) +{ + std::queue workQueue; + std::unordered_set visited; + std::unordered_map parentMap; + + workQueue.push(fromNode); + visited.insert(fromNode); + parentMap[fromNode] = 0; + + while (!workQueue.empty()) + { + uint32 current = workQueue.front(); + workQueue.pop(); + + for (uint32 next : taxiGraph.at(current)) + { + if (visited.count(next)) + continue; + + visited.insert(next); + parentMap[next] = current; + workQueue.push(next); + } + } + return parentMap; +} + +std::vector TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap) +{ + if (!parentMap.count(toNode)) + return {}; // unreachable + + std::vector path; + uint32 current = toNode; + while (current != fromNode) + { + path.push_back(current); + auto it = parentMap.find(current); + if (it == parentMap.end() || it->second == 0) + break; + current = it->second; + } + + path.push_back(fromNode); + std::reverse(path.begin(), path.end()); + return path; +} diff --git a/modules/mod-playerbots/src/Mgr/Travel/TravelNode.h b/modules/mod-playerbots/src/Mgr/Travel/TravelNode.h index 4dc235721..afb5196e9 100644 --- a/modules/mod-playerbots/src/Mgr/Travel/TravelNode.h +++ b/modules/mod-playerbots/src/Mgr/Travel/TravelNode.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_TRAVELNODE_H -#define _PLAYERBOT_TRAVELNODE_H +#ifndef PLAYERBOTS_TRAVELNODE_H +#define PLAYERBOTS_TRAVELNODE_H #include @@ -580,6 +581,10 @@ public: void calcMapOffset(); WorldPosition getMapOffset(uint32 mapId); + // Taxi graph (BFS-based path lookup between taxi nodes) + void InitTaxiGraph(); + std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); + std::shared_timed_mutex m_nMapMtx; std::unordered_map> teleportNodes; @@ -593,6 +598,16 @@ private: TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap& operator=(TravelNodeMap&&) = delete; + // Taxi graph internals + void BuildTaxiGraph(); + void ComputeAllPaths(); + std::unordered_map BFS(uint32 startNode); + std::vector BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap); + + std::unordered_map> taxiGraph; + std::map>> taxiPathCache; + std::vector m_nodes; std::vector> mapOffsets; diff --git a/modules/mod-playerbots/src/PlayerbotAIConfig.cpp b/modules/mod-playerbots/src/PlayerbotAIConfig.cpp index 09143ca25..cd696c745 100644 --- a/modules/mod-playerbots/src/PlayerbotAIConfig.cpp +++ b/modules/mod-playerbots/src/PlayerbotAIConfig.cpp @@ -1,10 +1,12 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlayerbotAIConfig.h" #include +#include "BisListMgr.h" #include "Config.h" #include "NewRpgInfo.h" #include "PlayerbotDungeonRepository.h" @@ -15,6 +17,7 @@ #include "RandomPlayerbotFactory.h" #include "RandomPlayerbotMgr.h" #include "Talentspec.h" +#include "TravelMgr.h" template void LoadList(std::string const value, T& list) @@ -84,8 +87,6 @@ bool PlayerbotAIConfig::Initialize() sitDelay = sConfigMgr->GetOption("AiPlayerbot.SitDelay", 20000); returnDelay = sConfigMgr->GetOption("AiPlayerbot.ReturnDelay", 2000); lootDelay = sConfigMgr->GetOption("AiPlayerbot.LootDelay", 1000); - minBotsForGreaterBuff = sConfigMgr->GetOption("AiPlayerbot.MinBotsForGreaterBuff", 3); - rpWarningCooldown = sConfigMgr->GetOption("AiPlayerbot.RPWarningCooldown", 30); disabledWithoutRealPlayerLoginDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLoginDelay", 30); disabledWithoutRealPlayerLogoutDelay = sConfigMgr->GetOption("AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay", 300); @@ -116,6 +117,35 @@ bool PlayerbotAIConfig::Initialize() highMana = sConfigMgr->GetOption("AiPlayerbot.HighMana", 65); autoSaveMana = sConfigMgr->GetOption("AiPlayerbot.AutoSaveMana", true); saveManaThreshold = sConfigMgr->GetOption("AiPlayerbot.SaveManaThreshold", 60); + switch (sConfigMgr->GetOption("AiPlayerbot.AutoGreaterBlessings", 1)) + { + case 0: + autoGreaterBlessings = AutoPartyBuffMode::DISABLED; + break; + case 2: + autoGreaterBlessings = AutoPartyBuffMode::GROUP_OR_RAID; + break; + case 1: + default: + autoGreaterBlessings = AutoPartyBuffMode::RAID_ONLY; + break; + } + switch (sConfigMgr->GetOption("AiPlayerbot.AutoPartyBuffs", 2)) + { + case 0: + autoPartyBuffs = AutoPartyBuffMode::DISABLED; + break; + case 1: + autoPartyBuffs = AutoPartyBuffMode::RAID_ONLY; + break; + case 2: + default: + autoPartyBuffs = AutoPartyBuffMode::GROUP_OR_RAID; + break; + } + tellWhenMissingBuffReagents = sConfigMgr->GetOption("AiPlayerbot.TellWhenMissingBuffReagents", true); + missingBuffReagentMessageCooldown = sConfigMgr->GetOption( + "AiPlayerbot.MissingBuffReagentMessageCooldown", 300); autoAvoidAoe = sConfigMgr->GetOption("AiPlayerbot.AutoAvoidAoe", true); maxAoeAvoidRadius = sConfigMgr->GetOption("AiPlayerbot.MaxAoeAvoidRadius", 15.0f); LoadSet>(sConfigMgr->GetOption("AiPlayerbot.AoeAvoidSpellWhitelist", "50759,57491,13810,29946"), @@ -123,9 +153,10 @@ bool PlayerbotAIConfig::Initialize() tellWhenAvoidAoe = sConfigMgr->GetOption("AiPlayerbot.TellWhenAvoidAoe", false); randomGearLoweringChance = sConfigMgr->GetOption("AiPlayerbot.RandomGearLoweringChance", 0.0f); - incrementalGearInit = sConfigMgr->GetOption("AiPlayerbot.IncrementalGearInit", true); randomGearQualityLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearQualityLimit", 3); randomGearScoreLimit = sConfigMgr->GetOption("AiPlayerbot.RandomGearScoreLimit", 0); + preferClassArmorType = sConfigMgr->GetOption("AiPlayerbot.PreferClassArmorType", false); + preferredSpecWeapons = sConfigMgr->GetOption("AiPlayerbot.PreferredSpecWeapons", false); randomBotMinLevelChance = sConfigMgr->GetOption("AiPlayerbot.RandomBotMinLevelChance", 0.1f); randomBotMaxLevelChance = sConfigMgr->GetOption("AiPlayerbot.RandomBotMaxLevelChance", 0.1f); @@ -168,11 +199,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,6 +214,15 @@ bool PlayerbotAIConfig::Initialize() "179490,141596,160836,160845,179516,176224,181085,176112,128308,128403," "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"), + attunementQuests); + + LoadSet>( + sConfigMgr->GetOption("AiPlayerbot.UnobtainableItems", "12468,44869,44870,46978"), + unobtainableItems); + botAutologin = sConfigMgr->GetOption("AiPlayerbot.BotAutologin", false); randomBotAutologin = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutologin", true); minRandomBots = sConfigMgr->GetOption("AiPlayerbot.MinRandomBots", 500); @@ -226,6 +268,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); @@ -313,32 +357,27 @@ bool PlayerbotAIConfig::Initialize() // does not depend on global chance broadcastChanceGuildManagement = sConfigMgr->GetOption("AiPlayerbot.BroadcastChanceGuildManagement", 30000); - //////////////////////////// toxicLinksRepliesChance = sConfigMgr->GetOption("AiPlayerbot.ToxicLinksRepliesChance", 30); // 0-100 thunderfuryRepliesChance = sConfigMgr->GetOption("AiPlayerbot.ThunderfuryRepliesChance", 40); // 0-100 guildRepliesRate = sConfigMgr->GetOption("AiPlayerbot.GuildRepliesRate", 100); // 0-100 - suggestDungeonsInLowerCaseRandomly = - sConfigMgr->GetOption("AiPlayerbot.SuggestDungeonsInLowerCaseRandomly", false); - - ////////////////////////// !CHAT randomBotJoinBG = sConfigMgr->GetOption("AiPlayerbot.RandomBotJoinBG", true); randomBotAutoJoinBG = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBG", false); - randomBotAutoJoinArenaBracket = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinArenaBracket", 7); + randomBotAutoJoinArenaBracket = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinArenaBracket", 14); - randomBotAutoJoinICBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinICBrackets", "0,1"); - randomBotAutoJoinEYBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinEYBrackets", "0,1,2"); - randomBotAutoJoinAVBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinAVBrackets", "0,1,2,3"); - randomBotAutoJoinABBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinABBrackets", "0,1,2,3,4,5,6"); - randomBotAutoJoinWSBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinWSBrackets", "0,1,2,3,4,5,6,7"); + randomBotAutoJoinWSBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinWSBrackets", "7"); + randomBotAutoJoinABBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinABBrackets", "6"); + randomBotAutoJoinAVBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinAVBrackets", "3"); + randomBotAutoJoinEYBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinEYBrackets", "2"); + randomBotAutoJoinICBrackets = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinICBrackets", "1"); - randomBotAutoJoinBGICCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGICCount", 0); - randomBotAutoJoinBGEYCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGEYCount", 0); + randomBotAutoJoinBGWSCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGWSCount", 1); + randomBotAutoJoinBGABCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGABCount", 1); randomBotAutoJoinBGAVCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGAVCount", 0); - randomBotAutoJoinBGABCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGABCount", 0); - randomBotAutoJoinBGWSCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGWSCount", 0); + randomBotAutoJoinBGEYCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGEYCount", 1); + randomBotAutoJoinBGICCount = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGICCount", 0); randomBotAutoJoinBGRatedArena2v2Count = sConfigMgr->GetOption("AiPlayerbot.RandomBotAutoJoinBGRatedArena2v2Count", 0); @@ -354,7 +393,6 @@ bool PlayerbotAIConfig::Initialize() randomBotMaxLevel = sConfigMgr->GetOption("AiPlayerbot.RandomBotMaxLevel", 80); if (randomBotMaxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) randomBotMaxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - randomBotLoginAtStartup = sConfigMgr->GetOption("AiPlayerbot.RandomBotLoginAtStartup", true); randomBotTeleLowerLevel = sConfigMgr->GetOption("AiPlayerbot.RandomBotTeleLowerLevel", 1); randomBotTeleHigherLevel = sConfigMgr->GetOption("AiPlayerbot.RandomBotTeleHigherLevel", 3); openGoSpell = sConfigMgr->GetOption("AiPlayerbot.OpenGoSpell", 6477); @@ -414,10 +452,10 @@ bool PlayerbotAIConfig::Initialize() useFastFlyMountAtMinLevel = sConfigMgr->GetOption("AiPlayerbot.UseFastFlyMountAtMinLevel", 70); // stagger bot flightpath takeoff - delayMin = sConfigMgr->GetOption("AiPlayerbot.BotTaxiDelayMinMs", 350u); - delayMax = sConfigMgr->GetOption("AiPlayerbot.BotTaxiDelayMaxMs", 5000u); - gapMs = sConfigMgr->GetOption("AiPlayerbot.BotTaxiGapMs", 200u); - gapJitterMs = sConfigMgr->GetOption("AiPlayerbot.BotTaxiGapJitterMs", 100u); + botTaxiDelayMin = sConfigMgr->GetOption("AiPlayerbot.BotTaxiDelayMinMs", 350); + botTaxiDelayMax = sConfigMgr->GetOption("AiPlayerbot.BotTaxiDelayMaxMs", 5000); + botTaxiGapMs = sConfigMgr->GetOption("AiPlayerbot.BotTaxiGapMs", 200); + botTaxiGapJitterMs = sConfigMgr->GetOption("AiPlayerbot.BotTaxiGapJitterMs", 100); LOG_INFO("server.loading", "Loading TalentSpecs..."); @@ -505,6 +543,7 @@ bool PlayerbotAIConfig::Initialize() LoadListString>(sConfigMgr->GetOption("AiPlayerbot.AllowedLogFiles", ""), allowedLogFiles); + enableAutoTradeOnItemMention = sConfigMgr->GetOption("AiPlayerbot.EnableAutoTradeOnItemMention", true); LoadListString>(sConfigMgr->GetOption("AiPlayerbot.TradeActionExcludedPrefixes", ""), tradeActionExcludedPrefixes); @@ -519,6 +558,8 @@ bool PlayerbotAIConfig::Initialize() randomBotGuildSizeMax = sConfigMgr->GetOption("AiPlayerbot.RandomBotGuildSizeMax", 15); deleteRandomBotGuilds = sConfigMgr->GetOption("AiPlayerbot.DeleteRandomBotGuilds", false); + botSendMailEnabled = sConfigMgr->GetOption("AiPlayerbot.BotSendMailEnabled", true); + guildTaskEnabled = sConfigMgr->GetOption("AiPlayerbot.EnableGuildTasks", false); minGuildTaskChangeTime = sConfigMgr->GetOption("AiPlayerbot.MinGuildTaskChangeTime", 3 * 24 * 3600); maxGuildTaskChangeTime = sConfigMgr->GetOption("AiPlayerbot.MaxGuildTaskChangeTime", 4 * 24 * 3600); @@ -530,9 +571,33 @@ bool PlayerbotAIConfig::Initialize() guildTaskKillTaskDistance = sConfigMgr->GetOption("AiPlayerbot.GuildTaskKillTaskDistance", 2000); targetPosRecalcDistance = sConfigMgr->GetOption("AiPlayerbot.TargetPosRecalcDistance", 0.1f); - // cosmetics (by lidocain) - randomBotShowCloak = sConfigMgr->GetOption("AiPlayerbot.RandomBotShowCloak", true); - randomBotShowHelmet = sConfigMgr->GetOption("AiPlayerbot.RandomBotShowHelmet", true); + //cosmetics + switch (sConfigMgr->GetOption("AiPlayerbot.RandomBotShowHelmet", 1)) + { + case 0: + randomBotShowHelmet = ShowHideCosmetic::ALWAYS_HIDE; + break; + case 2: + randomBotShowHelmet = ShowHideCosmetic::RANDOMIZE; + break; + case 1: + default: + randomBotShowHelmet = ShowHideCosmetic::ALWAYS_SHOW; + break; + } + switch (sConfigMgr->GetOption("AiPlayerbot.RandomBotShowCloak", 1)) + { + case 0: + randomBotShowCloak = ShowHideCosmetic::ALWAYS_HIDE; + break; + case 2: + randomBotShowCloak = ShowHideCosmetic::RANDOMIZE; + break; + case 1: + default: + randomBotShowCloak = ShowHideCosmetic::ALWAYS_SHOW; + break; + } // SPP switches enableGreet = sConfigMgr->GetOption("AiPlayerbot.EnableGreet", true); @@ -541,8 +606,8 @@ bool PlayerbotAIConfig::Initialize() disableRandomLevels = sConfigMgr->GetOption("AiPlayerbot.DisableRandomLevels", false); randomBotRandomPassword = sConfigMgr->GetOption("AiPlayerbot.RandomBotRandomPassword", true); downgradeMaxLevelBot = sConfigMgr->GetOption("AiPlayerbot.DowngradeMaxLevelBot", true); - equipmentPersistence = sConfigMgr->GetOption("AiPlayerbot.EquipmentPersistence", false); - equipmentPersistenceLevel = sConfigMgr->GetOption("AiPlayerbot.EquipmentPersistenceLevel", 80); + equipAndSpecPersistence = sConfigMgr->GetOption("AiPlayerbot.EquipAndSpecPersistence", true); + equipAndSpecPersistenceLevel = sConfigMgr->GetOption("AiPlayerbot.EquipAndSpecPersistenceLevel", 1); groupInvitationPermission = sConfigMgr->GetOption("AiPlayerbot.GroupInvitationPermission", 1); keepAltsInGroup = sConfigMgr->GetOption("AiPlayerbot.KeepAltsInGroup", false); allowSummonInCombat = sConfigMgr->GetOption("AiPlayerbot.AllowSummonInCombat", true); @@ -551,6 +616,7 @@ bool PlayerbotAIConfig::Initialize() reviveBotWhenSummoned = sConfigMgr->GetOption("AiPlayerbot.ReviveBotWhenSummoned", 1); botRepairWhenSummon = sConfigMgr->GetOption("AiPlayerbot.BotRepairWhenSummon", true); autoInitOnly = sConfigMgr->GetOption("AiPlayerbot.AutoInitOnly", false); + resetInstanceIdForAltBots = sConfigMgr->GetOption("AiPlayerbot.ResetInstanceIdForAltBots", false); autoInitEquipLevelLimitRatio = sConfigMgr->GetOption("AiPlayerbot.AutoInitEquipLevelLimitRatio", 1.0); maxAddedBots = sConfigMgr->GetOption("AiPlayerbot.MaxAddedBots", 40); @@ -580,6 +646,7 @@ bool PlayerbotAIConfig::Initialize() autoGearCommand = sConfigMgr->GetOption("AiPlayerbot.AutoGearCommand", 1); autoGearCommandAltBots = sConfigMgr->GetOption("AiPlayerbot.AutoGearCommandAltBots", 1); + autoGearBisCommand = sConfigMgr->GetOption("AiPlayerbot.AutoGearBisCommand", 0); autoGearQualityLimit = sConfigMgr->GetOption("AiPlayerbot.AutoGearQualityLimit", 3); autoGearScoreLimit = sConfigMgr->GetOption("AiPlayerbot.AutoGearScoreLimit", 0); @@ -588,11 +655,12 @@ bool PlayerbotAIConfig::Initialize() randomBotHordeRatio = sConfigMgr->GetOption("AiPlayerbot.RandomBotHordeRatio", 50); disableDeathKnightLogin = sConfigMgr->GetOption("AiPlayerbot.DisableDeathKnightLogin", 0); limitTalentsExpansion = sConfigMgr->GetOption("AiPlayerbot.LimitTalentsExpansion", 0); - botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 100); + botActiveAlone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAlone", 10); + BotActiveAloneDurationSeconds = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneDurationSeconds", 30); BotActiveAloneForceWhenInRadius = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInRadius", 150); BotActiveAloneForceWhenInZone = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInZone", 1); BotActiveAloneForceWhenInMap = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInMap", 0); - BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 1); + BotActiveAloneForceWhenIsFriend = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenIsFriend", 0); BotActiveAloneForceWhenInGuild = sConfigMgr->GetOption("AiPlayerbot.BotActiveAloneForceWhenInGuild", 1); botActiveAloneSmartScale = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScale", 1); botActiveAloneSmartScaleDiffLimitfloor = sConfigMgr->GetOption("AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor", 50); @@ -614,7 +682,10 @@ bool PlayerbotAIConfig::Initialize() // SPP automation freeMethodLoot = sConfigMgr->GetOption("AiPlayerbot.FreeMethodLoot", false); - lootRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootRollLevel", 1); + lootNeedRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootNeedRollLevel", 1); + lootRollRecipe = sConfigMgr->GetOption("AiPlayerbot.LootRollRecipe", false); + lootRollDisenchant = sConfigMgr->GetOption("AiPlayerbot.LootRollDisenchant", false); + lootGreedRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootGreedRollLevel", false); autoPickReward = sConfigMgr->GetOption("AiPlayerbot.AutoPickReward", "yes"); autoEquipUpgradeLoot = sConfigMgr->GetOption("AiPlayerbot.AutoEquipUpgradeLoot", true); equipUpgradeThreshold = sConfigMgr->GetOption("AiPlayerbot.EquipUpgradeThreshold", 1.1f); @@ -622,14 +693,14 @@ bool PlayerbotAIConfig::Initialize() syncQuestWithPlayer = sConfigMgr->GetOption("AiPlayerbot.SyncQuestWithPlayer", true); syncQuestForPlayer = sConfigMgr->GetOption("AiPlayerbot.SyncQuestForPlayer", false); dropObsoleteQuests = sConfigMgr->GetOption("AiPlayerbot.DropObsoleteQuests", true); - autoTrainSpells = sConfigMgr->GetOption("AiPlayerbot.AutoTrainSpells", "yes"); + allowLearnTrainerSpells = sConfigMgr->GetOption("AiPlayerbot.AllowLearnTrainerSpells", true); autoPickTalents = sConfigMgr->GetOption("AiPlayerbot.AutoPickTalents", true); - autoUpgradeEquip = sConfigMgr->GetOption("AiPlayerbot.AutoUpgradeEquip", false); + autoUpgradeEquip = sConfigMgr->GetOption("AiPlayerbot.AutoUpgradeEquip", true); hunterWolfPet = sConfigMgr->GetOption("AiPlayerbot.HunterWolfPet", 0); defaultPetStance = sConfigMgr->GetOption("AiPlayerbot.DefaultPetStance", 1); petChatCommandDebug = sConfigMgr->GetOption("AiPlayerbot.PetChatCommandDebug", 0); autoLearnTrainerSpells = sConfigMgr->GetOption("AiPlayerbot.AutoLearnTrainerSpells", true); - autoLearnQuestSpells = sConfigMgr->GetOption("AiPlayerbot.AutoLearnQuestSpells", false); + autoLearnQuestSpells = sConfigMgr->GetOption("AiPlayerbot.AutoLearnQuestSpells", true); autoTeleportForLevel = sConfigMgr->GetOption("AiPlayerbot.AutoTeleportForLevel", false); autoDoQuests = sConfigMgr->GetOption("AiPlayerbot.AutoDoQuests", true); enableNewRpgStrategy = sConfigMgr->GetOption("AiPlayerbot.EnableNewRpgStrategy", true); @@ -641,6 +712,7 @@ bool PlayerbotAIConfig::Initialize() RpgStatusProbWeight[RPG_DO_QUEST] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.DoQuest", 60); RpgStatusProbWeight[RPG_TRAVEL_FLIGHT] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.TravelFlight", 15); RpgStatusProbWeight[RPG_REST] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.Rest", 5); + RpgStatusProbWeight[RPG_OUTDOOR_PVP] = sConfigMgr->GetOption("AiPlayerbot.RpgStatusProbWeight.OutdoorPvp", 10); syncLevelWithPlayers = sConfigMgr->GetOption("AiPlayerbot.SyncLevelWithPlayers", false); randomBotGroupNearby = sConfigMgr->GetOption("AiPlayerbot.RandomBotGroupNearby", false); @@ -672,6 +744,7 @@ bool PlayerbotAIConfig::Initialize() PlayerbotGuildMgr::instance().Init(); sRandomItemMgr.Init(); sRandomItemMgr.InitAfterAhBot(); + sBisListMgr->LoadAll(); PlayerbotTextMgr::instance().LoadBotTexts(); PlayerbotTextMgr::instance().LoadBotTextChance(); PlayerbotFactory::Init(); @@ -682,6 +755,7 @@ bool PlayerbotAIConfig::Initialize() { PlayerbotDungeonRepository::instance().LoadDungeonSuggestions(); } + sTravelMgr.Init(); excludedHunterPetFamilies.clear(); LoadList>(sConfigMgr->GetOption("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies); @@ -902,14 +976,14 @@ std::vector> PlayerbotAIConfig::ParseTempTalentsOrder(uint32 } for (int tab = 0; tab < 3; tab++) { - if (tab_links.size() <= tab) + if (tab_links.size() <= (size_t)tab) { break; } std::sort(spells[tab].begin(), spells[tab].end(), [&](TalentEntry const* lhs, TalentEntry const* rhs) { return lhs->Row != rhs->Row ? lhs->Row < rhs->Row : lhs->Col < rhs->Col; }); - for (int i = 0; i < tab_links[tab].size(); i++) + for (uint32 i = 0; i < tab_links[tab].size(); i++) { if (i >= spells[tab].size()) { @@ -958,7 +1032,7 @@ std::vector> PlayerbotAIConfig::ParseTempPetTalentsOrder(uin std::sort(spells.begin(), spells.end(), [&](TalentEntry const* lhs, TalentEntry const* rhs) { return lhs->Row != rhs->Row ? lhs->Row < rhs->Row : lhs->Col < rhs->Col; }); - for (int i = 0; i < tab_link.size(); i++) + for (uint32 i = 0; i < tab_link.size(); i++) { if (i >= spells.size()) { diff --git a/modules/mod-playerbots/src/PlayerbotAIConfig.h b/modules/mod-playerbots/src/PlayerbotAIConfig.h index 14f9ef26f..1ef64dcb3 100644 --- a/modules/mod-playerbots/src/PlayerbotAIConfig.h +++ b/modules/mod-playerbots/src/PlayerbotAIConfig.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLAYERbotAICONFIG_H -#define _PLAYERBOT_PLAYERbotAICONFIG_H +#ifndef PLAYERBOTS_PLAYERBOTAICONFIG_H +#define PLAYERBOTS_PLAYERBOTAICONFIG_H #include #include @@ -40,6 +41,20 @@ enum class HealingManaEfficiency : uint8 SUPERIOR = 32 }; +enum class ShowHideCosmetic : uint8 +{ + ALWAYS_HIDE = 0, + ALWAYS_SHOW = 1, + RANDOMIZE = 2 +}; + +enum class AutoPartyBuffMode : uint8 +{ + DISABLED = 0, + RAID_ONLY = 1, + GROUP_OR_RAID = 2 +}; + enum NewRpgStatus : int { //Initial Status @@ -56,7 +71,8 @@ enum NewRpgStatus : int RPG_TRAVEL_FLIGHT = 6, // Taking a break RPG_REST = 7, - RPG_STATUS_END = 8 + RPG_OUTDOOR_PVP = 8, + RPG_STATUS_END = 9 }; #define MAX_SPECNO 20 @@ -94,11 +110,17 @@ public: uint32 lowMana, mediumMana, highMana; bool autoSaveMana; uint32 saveManaThreshold; + AutoPartyBuffMode autoGreaterBlessings; + AutoPartyBuffMode autoPartyBuffs; + bool tellWhenMissingBuffReagents; + uint32 missingBuffReagentMessageCooldown; bool autoAvoidAoe; float maxAoeAvoidRadius; std::set aoeAvoidSpellWhitelist; bool tellWhenAvoidAoe; std::set disallowedGameObjects; + std::set attunementQuests; + std::set unobtainableItems; uint32 openGoSpell; bool randomBotAutologin; @@ -123,9 +145,10 @@ public: std::vector randomBotQuestIds; uint32 randomBotTeleportDistance; float randomGearLoweringChance; - bool incrementalGearInit; int32 randomGearQualityLimit; int32 randomGearScoreLimit; + bool preferClassArmorType; + bool preferredSpecWeapons; float randomBotMinLevelChance, randomBotMaxLevelChance; float randomBotRpgChance; uint32 minRandomBots, maxRandomBots; @@ -142,14 +165,9 @@ public: uint32 disabledWithoutRealPlayerLoginDelay, disabledWithoutRealPlayerLogoutDelay; bool randomBotJoinLfg; - // Buff system - // Min group size to use Greater buffs (Paladin, Mage, Druid). Default: 3 - int32 minBotsForGreaterBuff; - // Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff. Default: 30 - int32 rpWarningCooldown; - // Professions bool enableFishingWithMaster; + uint32 classMatchingProfessionChance; float fishingDistanceFromMaster, fishingDistance, endFishingWithMaster; // chat @@ -219,10 +237,6 @@ public: uint32 guildRepliesRate; - bool suggestDungeonsInLowerCaseRandomly; - - // -- - bool randomBotJoinBG; bool randomBotAutoJoinBG; @@ -244,7 +258,6 @@ public: uint32 randomBotAutoJoinBGRatedArena3v3Count; uint32 randomBotAutoJoinBGRatedArena5v5Count; - bool randomBotLoginAtStartup; uint32 randomBotTeleLowerLevel, randomBotTeleHigherLevel; std::map> zoneBrackets; bool logInGroupOnly, logValuesPerTick; @@ -290,6 +303,7 @@ public: float periodicOnlineOfflineRatio; bool gearscorecheck; bool randomBotPreQuests; + bool botSendMailEnabled; bool guildTaskEnabled; uint32 minGuildTaskChangeTime, maxGuildTaskChangeTime; @@ -301,6 +315,7 @@ public: uint32 iterationsPerTick; std::mutex m_logMtx; + bool enableAutoTradeOnItemMention; std::vector tradeActionExcludedPrefixes; std::vector allowedLogFiles; std::unordered_map> logFiles; @@ -323,8 +338,8 @@ public: uint32 commandServerPort; bool perfMonEnabled; bool summonWhenGroup; - bool randomBotShowHelmet; - bool randomBotShowCloak; + ShowHideCosmetic randomBotShowHelmet; + ShowHideCosmetic randomBotShowCloak; bool randomBotFixedLevel; bool disableRandomLevels; float randomBotXPRate; @@ -333,6 +348,7 @@ public: bool disableDeathKnightLogin; bool limitTalentsExpansion; uint32 botActiveAlone; + uint32 BotActiveAloneDurationSeconds; uint32 BotActiveAloneForceWhenInRadius; bool BotActiveAloneForceWhenInZone; bool BotActiveAloneForceWhenInMap; @@ -345,7 +361,10 @@ public: uint32 botActiveAloneSmartScaleWhenMaxLevel; bool freeMethodLoot; - int32 lootRollLevel; + int32 lootNeedRollLevel; + bool lootGreedRollLevel; + bool lootRollRecipe; + bool lootRollDisenchant; std::string autoPickReward; bool autoEquipUpgradeLoot; float equipUpgradeThreshold; @@ -353,7 +372,7 @@ public: bool syncQuestWithPlayer; bool syncQuestForPlayer; bool dropObsoleteQuests; - std::string autoTrainSpells; + bool allowLearnTrainerSpells; bool autoPickTalents; bool autoUpgradeEquip; int32 hunterWolfPet; @@ -381,8 +400,8 @@ public: uint32 selfBotLevel; bool downgradeMaxLevelBot; - bool equipmentPersistence; - int32 equipmentPersistenceLevel; + bool equipAndSpecPersistence; + int32 equipAndSpecPersistenceLevel; int32 groupInvitationPermission; bool keepAltsInGroup = false; bool KeepAltsInGroup() const { return keepAltsInGroup; } @@ -392,6 +411,7 @@ public: int reviveBotWhenSummoned; bool botRepairWhenSummon; bool autoInitOnly; + bool resetInstanceIdForAltBots; float autoInitEquipLevelLimitRatio; int32 maxAddedBots; int32 addClassCommand; @@ -417,6 +437,7 @@ public: altMaintenanceKeyring, altMaintenanceGemsEnchants; int32 autoGearCommand, autoGearCommandAltBots, autoGearQualityLimit, autoGearScoreLimit; + int32 autoGearBisCommand; uint32 useGroundMountAtMinLevel; uint32 useFastGroundMountAtMinLevel; @@ -424,10 +445,10 @@ public: uint32 useFastFlyMountAtMinLevel; // stagger flightpath takeoff - uint32 delayMin; - uint32 delayMax; - uint32 gapMs; - uint32 gapJitterMs; + uint32 botTaxiDelayMin; + uint32 botTaxiDelayMax; + uint32 botTaxiGapMs; + uint32 botTaxiGapJitterMs; std::string const GetTimestampStr(); bool hasLog(std::string const fileName) diff --git a/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp b/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp index ac8164ae5..982a2fa8a 100644 --- a/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp +++ b/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp @@ -1,16 +1,7 @@ /* - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation; either version 2 of the License, or (at your - * option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "BattleGroundTactics.h" @@ -72,7 +63,7 @@ public: return GuildTaskMgr::HandleConsoleCommand(handler, args); } - static bool HandlePerfMonCommand(ChatHandler* handler, char const* args) + static bool HandlePerfMonCommand(ChatHandler* /*handler*/, char const* args) { if (!strcmp(args, "reset")) { diff --git a/modules/mod-playerbots/src/Script/PlayerbotCommandScript.h b/modules/mod-playerbots/src/Script/PlayerbotCommandScript.h index f17ae7ecf..cc3332313 100644 --- a/modules/mod-playerbots/src/Script/PlayerbotCommandScript.h +++ b/modules/mod-playerbots/src/Script/PlayerbotCommandScript.h @@ -1 +1,12 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_PLAYERBOTCOMMANDSCRIPT_H +#define PLAYERBOTS_PLAYERBOTCOMMANDSCRIPT_H + void AddPlayerbotsCommandscripts(); + +#endif diff --git a/modules/mod-playerbots/src/Script/Playerbots.cpp b/modules/mod-playerbots/src/Script/Playerbots.cpp index c385ee320..680fe5469 100644 --- a/modules/mod-playerbots/src/Script/Playerbots.cpp +++ b/modules/mod-playerbots/src/Script/Playerbots.cpp @@ -1,22 +1,11 @@ /* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by the - * Free Software Foundation; either version 3 of the License, or (at your - * option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . + * 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 "Playerbots.h" +#include "BattlefieldScript.h" #include "Channel.h" #include "Config.h" #include "DatabaseEnv.h" @@ -207,7 +196,7 @@ public: return true; } - bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* guild) override + bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* /*guild*/) override { if (type != CHAT_MSG_GUILD) return true; @@ -433,7 +422,7 @@ public: playerbotMgr->HandleMasterOutgoingPacket(*packet); } - void OnPlayerbotUpdate(uint32 diff) override + void OnPlayerbotUpdate(uint32 /*diff*/) override { sRandomPlayerbotMgr.UpdateSessions(); // Per-bot updates only } @@ -504,10 +493,24 @@ public: void OnBattlegroundEnd(Battleground* bg, TeamId /*winnerTeam*/) override { bgStrategies.erase(bg->GetInstanceID()); } }; +// Workaround for missing InitEnabledHooksIfNeeded for new BattlefieldScript in ScriptMgr +class PlayerbotsBattlefieldScript : public BattlefieldScript +{ +public: + PlayerbotsBattlefieldScript() : BattlefieldScript("PlayerbotsBattlefieldScript") { } +}; + void AddPlayerbotsSecureLoginScripts(); +void AddSC_MagtheridonBotScripts(); +void AddSC_TempestKeepBotScripts(); +void AddSC_HyjalSummitBotScripts(); +void AddSC_IcecrownBotScripts(); +void AddSC_RubySanctumBotScripts(); + void AddPlayerbotsScripts() { + new PlayerbotsBattlefieldScript(); new PlayerbotsDatabaseScript(); new PlayerbotsPlayerScript(); new PlayerbotsMiscScript(); @@ -518,4 +521,9 @@ void AddPlayerbotsScripts() AddPlayerbotsSecureLoginScripts(); AddPlayerbotsCommandscripts(); PlayerBotsGuildValidationScript(); + AddSC_MagtheridonBotScripts(); + AddSC_TempestKeepBotScripts(); + AddSC_HyjalSummitBotScripts(); + AddSC_IcecrownBotScripts(); + AddSC_RubySanctumBotScripts(); } diff --git a/modules/mod-playerbots/src/Script/Playerbots.h b/modules/mod-playerbots/src/Script/Playerbots.h index 6a9036cef..157f58cde 100644 --- a/modules/mod-playerbots/src/Script/Playerbots.h +++ b/modules/mod-playerbots/src/Script/Playerbots.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_H -#define _PLAYERBOT_H +#ifndef PLAYERBOTS_PLAYERBOTS_H +#define PLAYERBOTS_PLAYERBOTS_H #include "AiObjectContext.h" #include "DatabaseEnv.h" diff --git a/modules/mod-playerbots/src/Script/PlayerbotsSecureLogin.cpp b/modules/mod-playerbots/src/Script/PlayerbotsSecureLogin.cpp index fc54a82b7..29186dae7 100644 --- a/modules/mod-playerbots/src/Script/PlayerbotsSecureLogin.cpp +++ b/modules/mod-playerbots/src/Script/PlayerbotsSecureLogin.cpp @@ -1,3 +1,9 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + #include "ScriptMgr.h" #include "Opcodes.h" #include "Player.h" @@ -52,15 +58,14 @@ public: PlayerbotsSecureLoginServerScript() : ServerScript("PlayerbotsSecureLoginServerScript", { SERVERHOOK_CAN_PACKET_RECEIVE }) {} - bool CanPacketReceive(WorldSession* /*session*/, WorldPacket& packet) override + bool CanPacketReceive(WorldSession* /*session*/, WorldPacket const& packet) override { if (packet.GetOpcode() != CMSG_PLAYER_LOGIN) return true; - auto const oldPos = packet.rpos(); + WorldPacket pkt(packet); ObjectGuid loginGuid; - packet >> loginGuid; - packet.rpos(oldPos); + pkt >> loginGuid; if (!loginGuid) return true; diff --git a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperation.h b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperation.h index 6ac303d32..ce499df20 100644 --- a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperation.h +++ b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperation.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_OPERATION_H -#define _PLAYERBOT_OPERATION_H +#ifndef PLAYERBOTS_PLAYERBOTOPERATION_H +#define PLAYERBOTS_PLAYERBOTOPERATION_H #include "Common.h" #include "ObjectGuid.h" diff --git a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperations.h b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperations.h index ee6443cd8..d960e6b8f 100644 --- a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperations.h +++ b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotOperations.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_OPERATIONS_H -#define _PLAYERBOT_OPERATIONS_H +#ifndef PLAYERBOTS_PLAYERBOTOPERATIONS_H +#define PLAYERBOTS_PLAYERBOTOPERATIONS_H #include "Group.h" #include "GroupMgr.h" @@ -242,6 +243,7 @@ public: } group->ChangeLeader(newLeader->GetGUID()); + group->SendUpdate(); LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Changed leader to {}", newLeader->GetName()); return true; } diff --git a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.cpp b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.cpp index 3679d3169..113a38296 100644 --- a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.cpp +++ b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include diff --git a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.h b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.h index bd489829e..827db1cf3 100644 --- a/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.h +++ b/modules/mod-playerbots/src/Script/WorldThr/PlayerbotWorldThreadProcessor.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_WORLD_THREAD_PROCESSOR_H -#define _PLAYERBOT_WORLD_THREAD_PROCESSOR_H +#ifndef PLAYERBOTS_PLAYERBOTWORLDTHREADPROCESSOR_H +#define PLAYERBOTS_PLAYERBOTWORLDTHREADPROCESSOR_H #include #include diff --git a/modules/mod-playerbots/src/Script/WorldThr/Queue.cpp b/modules/mod-playerbots/src/Script/WorldThr/Queue.cpp index 43b1597f8..4d1ecfdc2 100644 --- a/modules/mod-playerbots/src/Script/WorldThr/Queue.cpp +++ b/modules/mod-playerbots/src/Script/WorldThr/Queue.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Queue.h" diff --git a/modules/mod-playerbots/src/Script/WorldThr/Queue.h b/modules/mod-playerbots/src/Script/WorldThr/Queue.h index 439b61aa0..9087a7b42 100644 --- a/modules/mod-playerbots/src/Script/WorldThr/Queue.h +++ b/modules/mod-playerbots/src/Script/WorldThr/Queue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef PLAYERBOT_QUEUE_H -#define PLAYERBOT_QUEUE_H +#ifndef PLAYERBOTS_QUEUE_H +#define PLAYERBOTS_QUEUE_H #include "Action.h" #include "Common.h" diff --git a/modules/mod-playerbots/src/Script/playerbots_loader.cpp b/modules/mod-playerbots/src/Script/playerbots_loader.cpp index bd91e39cc..8cf3dcde1 100644 --- a/modules/mod-playerbots/src/Script/playerbots_loader.cpp +++ b/modules/mod-playerbots/src/Script/playerbots_loader.cpp @@ -1,18 +1,7 @@ /* - * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation; either version 2 of the License, or (at your - * option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ // From SC diff --git a/modules/mod-playerbots/src/Util/BroadcastHelper.cpp b/modules/mod-playerbots/src/Util/BroadcastHelper.cpp index 246344bda..627660136 100644 --- a/modules/mod-playerbots/src/Util/BroadcastHelper.cpp +++ b/modules/mod-playerbots/src/Util/BroadcastHelper.cpp @@ -1,3 +1,8 @@ +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ #include "Playerbots.h" #include "BroadcastHelper.h" @@ -11,7 +16,7 @@ uint8 BroadcastHelper::GetLocale() { uint8 locale = sWorld->GetDefaultDbcLocale(); // -- In case we're using auto detect on config file^M - if (locale >= MAX_LOCALES) + if (locale >= TOTAL_LOCALES) locale = LocaleConstant::LOCALE_enUS; return locale; } diff --git a/modules/mod-playerbots/src/Util/BroadcastHelper.h b/modules/mod-playerbots/src/Util/BroadcastHelper.h index c79d837aa..94e99da04 100644 --- a/modules/mod-playerbots/src/Util/BroadcastHelper.h +++ b/modules/mod-playerbots/src/Util/BroadcastHelper.h @@ -1,4 +1,11 @@ -#pragma once +/* + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. + */ + +#ifndef PLAYERBOTS_BROADCASTHELPER_H +#define PLAYERBOTS_BROADCASTHELPER_H class PlayerbotAI; class Player; @@ -146,3 +153,5 @@ public: Player* bot ); }; + +#endif diff --git a/modules/mod-playerbots/src/Util/Helpers.cpp b/modules/mod-playerbots/src/Util/Helpers.cpp index fab0b4835..b2e83f3ec 100644 --- a/modules/mod-playerbots/src/Util/Helpers.cpp +++ b/modules/mod-playerbots/src/Util/Helpers.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "Helpers.h" diff --git a/modules/mod-playerbots/src/Util/Helpers.h b/modules/mod-playerbots/src/Util/Helpers.h index 90d5b6427..4dfc695b9 100644 --- a/modules/mod-playerbots/src/Util/Helpers.h +++ b/modules/mod-playerbots/src/Util/Helpers.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_HELPERS_H -#define _PLAYERBOT_HELPERS_H +#ifndef PLAYERBOTS_HELPERS_H +#define PLAYERBOTS_HELPERS_H #include #include diff --git a/modules/mod-playerbots/src/Util/LazyCalculatedValue.h b/modules/mod-playerbots/src/Util/LazyCalculatedValue.h index bef54e2dd..aa9c89b50 100644 --- a/modules/mod-playerbots/src/Util/LazyCalculatedValue.h +++ b/modules/mod-playerbots/src/Util/LazyCalculatedValue.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_LAZYCALCULATEDVALUE_H -#define _PLAYERBOT_LAZYCALCULATEDVALUE_H +#ifndef PLAYERBOTS_LAZYCALCULATEDVALUE_H +#define PLAYERBOTS_LAZYCALCULATEDVALUE_H /** * @brief Lazy calculation helper. diff --git a/modules/mod-playerbots/src/Util/PlaceholderHelper.cpp b/modules/mod-playerbots/src/Util/PlaceholderHelper.cpp index e41a050fa..ae0da7731 100644 --- a/modules/mod-playerbots/src/Util/PlaceholderHelper.cpp +++ b/modules/mod-playerbots/src/Util/PlaceholderHelper.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "PlaceholderHelper.h" diff --git a/modules/mod-playerbots/src/Util/PlaceholderHelper.h b/modules/mod-playerbots/src/Util/PlaceholderHelper.h index 5ce57d720..be4403238 100644 --- a/modules/mod-playerbots/src/Util/PlaceholderHelper.h +++ b/modules/mod-playerbots/src/Util/PlaceholderHelper.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_PLACEHOLDERHELPER_H -#define _PLAYERBOT_PLACEHOLDERHELPER_H +#ifndef PLAYERBOTS_PLACEHOLDERHELPER_H +#define PLAYERBOTS_PLACEHOLDERHELPER_H #include diff --git a/modules/mod-playerbots/src/Util/ServerFacade.cpp b/modules/mod-playerbots/src/Util/ServerFacade.cpp index 12a77de63..5a6ef43a3 100644 --- a/modules/mod-playerbots/src/Util/ServerFacade.cpp +++ b/modules/mod-playerbots/src/Util/ServerFacade.cpp @@ -1,6 +1,7 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ #include "ServerFacade.h" @@ -39,7 +40,7 @@ bool ServerFacade::IsDistanceGreaterOrEqualThan(float dist1, float dist2) { retu bool ServerFacade::IsDistanceLessOrEqualThan(float dist1, float dist2) { return !IsDistanceGreaterThan(dist1, dist2); } -void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool force) +void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool /*force*/) { if (!bot) return; @@ -53,8 +54,9 @@ void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool force) bot->SetOrientation(angle); if (!bot->IsRooted()) - bot->SendMovementFlagUpdate(); - // } + // enforce (bool self) true otherwhise when using real-client with self-bot wont + // recieve update; e.g. will not face the target when using (mostly ranged) attack + bot->SendMovementFlagUpdate(true); } Unit* ServerFacade::GetChaseTarget(Unit* target) diff --git a/modules/mod-playerbots/src/Util/ServerFacade.h b/modules/mod-playerbots/src/Util/ServerFacade.h index 978b8e768..d1ec2b751 100644 --- a/modules/mod-playerbots/src/Util/ServerFacade.h +++ b/modules/mod-playerbots/src/Util/ServerFacade.h @@ -1,10 +1,11 @@ /* - * 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. + * This file is part of the mod-playerbots module for AzerothCore. See AUTHORS file for Copyright + * information; released under GNU GPL v2 license, redistribute/modify under version 2 of the License, + * or (at your option) any later version. */ -#ifndef _PLAYERBOT_SERVERFACADE_H -#define _PLAYERBOT_SERVERFACADE_H +#ifndef PLAYERBOTS_SERVERFACADE_H +#define PLAYERBOTS_SERVERFACADE_H class Player; class Unit;