Merge pull request #2305 from mod-playerbots/test-staging

Test staging
This commit is contained in:
Keleborn 2026-04-17 09:30:34 -07:00 committed by GitHub
commit f275f72957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 850 additions and 27 deletions

View File

@ -1038,6 +1038,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
@ -1045,6 +1046,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

View File

@ -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);

View File

@ -63,6 +63,7 @@
#include "WorldBuffAction.h"
#include "XpGainAction.h"
#include "NewRpgAction.h"
#include "NewRpgOutdoorPvP.h"
#include "FishingAction.h"
#include "CancelChannelAction.h"
#include "WaitForAttackAction.h"
@ -265,6 +266,7 @@ 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;
}
@ -462,6 +464,7 @@ 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); }
};

View File

@ -0,0 +1,219 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU 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 "SetFocusHealTargetsAction.h"
#include "ObjectAccessor.h"
#include "Playerbots.h"
#include "PlayerbotTextMgr.h"
#include <algorithm>
#include <cctype>
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<ObjectGuid> focusHealTargets =
AI_VALUE(std::list<ObjectGuid>, "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<std::string, std::string> 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<ObjectGuid>, "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<std::string> 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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<ObjectGuid>, "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;
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU 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_SETFOCUSHEALTARGETSACTION_H
#define _PLAYERBOT_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

View File

@ -37,6 +37,7 @@
#include "LogLevelAction.h"
#include "LootStrategyAction.h"
#include "LootRollAction.h"
#include "SetFocusHealTargetsAction.h"
#include "MailAction.h"
#include "NamedObjectContext.h"
#include "NewRpgAction.h"
@ -201,6 +202,7 @@ public:
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:
@ -314,6 +316,7 @@ private:
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

View File

@ -146,6 +146,7 @@ public:
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:
@ -271,6 +272,7 @@ private:
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

View File

@ -107,6 +107,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
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("focus heal", { NextAction("focus heal targets", relevance) }));
}
ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI)
@ -200,4 +201,5 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("pet");
supported.push_back("pet attack");
supported.push_back("wait for attack time");
supported.push_back("focus heal");
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU 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_FOCUSTARGETSTRATEGY_H
#define _PLAYERBOT_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

View File

@ -19,6 +19,7 @@
#include "DuelStrategy.h"
#include "EmoteStrategy.h"
#include "FleeStrategy.h"
#include "FocusTargetStrategy.h"
#include "FollowMasterStrategy.h"
#include "GrindingStrategy.h"
#include "GroupStrategy.h"
@ -126,6 +127,7 @@ public:
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:
@ -198,6 +200,7 @@ private:
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<Strategy>

View File

@ -230,6 +230,7 @@ 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;
@ -436,6 +437,7 @@ 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); }

View File

@ -38,6 +38,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<ObjectGuid> const focusHealTargets =
AI_VALUE(std::list<ObjectGuid>, "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 +75,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 +98,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 +112,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))
{

View File

@ -13,6 +13,7 @@
#include "ServerFacade.h"
#include "SharedDefines.h"
#include "NearestGameObjects.h"
#include <unordered_set>
std::vector<uint32> PossibleRpgTargetsValue::allowedNpcFlags;
@ -88,8 +89,11 @@ bool PossibleRpgTargetsValue::AcceptUnit(Unit* unit)
std::vector<uint32> PossibleNewRpgTargetsValue::allowedNpcFlags;
// Sparse starting zones where the default scan range is insufficient for WANDER_NPC (requires >= 3 NPCs)
static const std::unordered_set<uint32> 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())
{
@ -119,6 +123,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<Unit*> targets;
FindUnits(targets);

View File

@ -35,6 +35,8 @@ public:
protected:
void FindUnits(std::list<Unit*>& targets) override;
bool AcceptUnit(Unit* unit) override;
private:
float defaultRange;
};
class PossibleNewRpgGameObjectsValue : public ObjectGuidListCalculatedValue

View File

@ -140,4 +140,11 @@ public:
public:
Unit* Calculate();
};
class FocusHealTargetValue : public ManualSetValue<std::list<ObjectGuid>>
{
public:
FocusHealTargetValue(PlayerbotAI* botAI) : ManualSetValue<std::list<ObjectGuid>>(botAI, {}, "focus heal targets") {}
};
#endif

View File

@ -241,6 +241,7 @@ public:
creators["travel target"] = &ValueContext::travel_target;
creators["talk target"] = &ValueContext::talk_target;
creators["pull target"] = &ValueContext::pull_target;
creators["focus heal targets"] = &ValueContext::focus_heal_targets;
creators["group"] = &ValueContext::group;
creators["range"] = &ValueContext::range;
creators["inside target"] = &ValueContext::inside_target;
@ -497,6 +498,7 @@ 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* 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); }

View File

@ -23,6 +23,16 @@ std::vector<NextAction> CastAbolishPoisonOnPartyAction::getAlternatives()
CastSpellAction::getPrerequisites());
}
bool CastLifebloomOnMainTankAction::isUseful()
{
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;
}
Value<Unit*>* CastEntanglingRootsCcAction::GetTargetValue()
{
return context->GetValue<Unit*>("cc target", "entangling roots");

View File

@ -128,6 +128,14 @@ public:
CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {}
};
class CastLifebloomOnMainTankAction : public BuffOnMainTankAction
{
public:
CastLifebloomOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "lifebloom", true) {}
bool isUseful() override;
};
class CastOmenOfClarityAction : public CastBuffSpellAction
{
public:

View File

@ -79,6 +79,7 @@ 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;
@ -117,6 +118,7 @@ public:
private:
static Trigger* natures_swiftness(PlayerbotAI* botAI) { return new NaturesSwiftnessTrigger(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); }
@ -209,6 +211,7 @@ public:
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;
@ -304,6 +307,7 @@ private:
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); }

View File

@ -56,6 +56,9 @@ void HealDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
new TriggerNode("party member critical health",
{ NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4) }));
triggers.push_back(new TriggerNode("clearcasting",
{ NextAction("lifebloom on main tank", ACTION_CRITICAL_HEAL - 1) }));
triggers.push_back(new TriggerNode(
"group heal setting",
{

View File

@ -61,6 +61,12 @@ public:
OmenOfClarityTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "omen of clarity") {}
};
class ClearcastingTrigger : public HasAuraTrigger
{
public:
ClearcastingTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "clearcasting") {}
};
class RakeTrigger : public DebuffTrigger
{
public:

View File

@ -62,7 +62,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 +140,15 @@ bool NewRpgStatusUpdateAction::Execute(Event /*event*/)
}
break;
}
case RPG_OUTDOOR_PVP:
{
if (info.HasStatusPersisted(statusOutDoorPvPDuration))
{
info.ChangeToIdle();
return true;
}
break;
}
default:
break;
}

View File

@ -47,10 +47,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

View File

@ -12,6 +12,7 @@
#include "NewRpgStrategy.h"
#include "Object.h"
#include "ObjectAccessor.h"
#include "OutdoorPvPMgr.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
@ -222,12 +223,10 @@ 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* center)
{
if (IsWaitingForLastMove(priority))
{
return false;
}
Map* map = bot->GetMap();
const float x = bot->GetPositionX();
@ -1160,6 +1159,11 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
bot->SetStandState(UNIT_STAND_STATE_SIT);
return true;
}
case RPG_OUTDOOR_PVP:
{
botAI->rpgInfo.ChangeToOutdoorPvp();
return true;
}
default:
{
botAI->rpgInfo.ChangeToRest();
@ -1220,6 +1224,17 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, 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;
}

View File

@ -31,7 +31,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 */

View File

@ -0,0 +1,128 @@
#include "NewRpgOutdoorPvP.h"
#include "OutdoorPvP.h"
#include "OutdoorPvPMgr.h"
bool NewRpgOutdoorPvpAction::Execute(Event 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<NewRpgInfo::OutdoorPvP>(&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<OPvPCapturePoint*> 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));
}

View File

@ -0,0 +1,19 @@
#ifndef PLAYERBOT_NEWRPGOUTDOORPVP_H
#define PLAYERBOT_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

View File

@ -47,6 +47,14 @@ void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<u
data = flight;
}
void NewRpgInfo::ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId)
{
startT = getMSTime();
OutdoorPvP pvp;
pvp.capturePointSpawnId = capturePointSpawnId;
data = pvp;
}
void NewRpgInfo::ChangeToRest()
{
startT = getMSTime();
@ -90,6 +98,7 @@ NewRpgStatus NewRpgInfo::GetStatus()
if constexpr (std::is_same_v<T, Rest>) return RPG_REST;
if constexpr (std::is_same_v<T, DoQuest>) return RPG_DO_QUEST;
if constexpr (std::is_same_v<T, TravelFlight>) return RPG_TRAVEL_FLIGHT;
if constexpr (std::is_same_v<T, OutdoorPvP>) return RPG_OUTDOOR_PVP;
return RPG_IDLE;
}, data);
}
@ -153,6 +162,14 @@ std::string NewRpgInfo::ToString()
out << "\ntoNode: " << arg.path[arg.path.size() - 1];
out << "\ninFlight: " << arg.inFlight;
}
else if constexpr (std::is_same_v<T, OutdoorPvP>)
{
out << "OUTDOOR_PVP";
if (!arg.capturePointSpawnId)
out << "\nNo capture point assigned.";
else
out << "\ncapturePointSpawnId: " << arg.capturePointSpawnId;
}
else
out << "UNKNOWN";
}, data);

View File

@ -58,6 +58,11 @@ struct NewRpgInfo
{
Rest() = default;
};
// RPG_OUTDOOR_PVP
struct OutdoorPvP
{
ObjectGuid::LowType capturePointSpawnId{0};
};
struct Idle
{
};
@ -79,7 +84,8 @@ struct NewRpgInfo
WanderRandom,
DoQuest,
Rest,
TravelFlight
TravelFlight,
OutdoorPvP
>;
RpgData data;
@ -91,6 +97,7 @@ struct NewRpgInfo
void ChangeToWanderRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest);
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path);
void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0);
void ChangeToRest();
void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status);

View File

@ -65,6 +65,14 @@ void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
}
)
);
triggers.push_back(
new TriggerNode(
"outdoor pvp status",
{
NextAction("new rpg outdoor pvp", 3.0f)
}
)
);
}
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>&)

View File

@ -54,6 +54,7 @@
#include "Unit.h"
#include "UpdateTime.h"
#include "Vehicle.h"
#include "../../../../src/server/scripts/Spells/spell_dk.cpp"
const int SPELL_TITAN_GRIP = 49152;
@ -2163,7 +2164,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;
}

View File

@ -2861,10 +2861,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,

View File

@ -20,6 +20,13 @@
#include "StatsCollector.h"
#include "Unit.h"
namespace
{
constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_1 = 30482;
constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_2 = 43045;
constexpr uint32 SPELL_MOLTEN_ARMOR_RANK_3 = 43046;
}
StatsWeightCalculator::StatsWeightCalculator(Player* player) : player_(player)
{
if (PlayerbotAI::IsHeal(player))
@ -454,6 +461,16 @@ void StatsWeightCalculator::GenerateAdditionalWeights(Player* player)
if (player->HasAura(51885))
stats_weights_[STATS_TYPE_INTELLECT] += 1.1f;
}
else if (cls == CLASS_MAGE)
{
if (!player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_1)
&& !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_2)
&& !player->HasSpell(SPELL_MOLTEN_ARMOR_RANK_3))
{
stats_weights_[STATS_TYPE_INTELLECT] += 0.2f;
stats_weights_[STATS_TYPE_SPIRIT] -= 0.0f;
}
}
}
void StatsWeightCalculator::CalculateItemSetMod(Player* player, ItemTemplate const* proto)

View File

@ -652,6 +652,7 @@ bool PlayerbotAIConfig::Initialize()
RpgStatusProbWeight[RPG_DO_QUEST] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.DoQuest", 60);
RpgStatusProbWeight[RPG_TRAVEL_FLIGHT] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.TravelFlight", 15);
RpgStatusProbWeight[RPG_REST] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.Rest", 5);
RpgStatusProbWeight[RPG_OUTDOOR_PVP] = sConfigMgr->GetOption<int32>("AiPlayerbot.RpgStatusProbWeight.OutdoorPvp", 10);
syncLevelWithPlayers = sConfigMgr->GetOption<bool>("AiPlayerbot.SyncLevelWithPlayers", false);
randomBotGroupNearby = sConfigMgr->GetOption<bool>("AiPlayerbot.RandomBotGroupNearby", false);

View File

@ -56,7 +56,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

View File

@ -53,8 +53,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)