From 52c3e966416ffe881acd5ff24a375debbbeddde9 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Wed, 3 Dec 2025 04:25:01 -0800 Subject: [PATCH 1/5] Rename groupmaster to groupleader and related variables. (#1875) Fix the naming conventions. Master should be reserved to identify a bots Master. groupleaders are not necessarily group masters and it should be clear what the bot is looking for. (In most solo cases leader=master) --- src/PlayerbotAI.cpp | 6 +-- src/PlayerbotAI.h | 2 +- src/PlayerbotSecurity.cpp | 4 +- src/RandomPlayerbotMgr.cpp | 6 +-- src/strategy/actions/ActionContext.h | 4 +- .../actions/ChooseRpgTargetAction.cpp | 16 +++---- .../actions/ChooseTravelTargetAction.cpp | 2 +- src/strategy/actions/FollowActions.cpp | 16 +++---- src/strategy/actions/FollowActions.h | 4 +- src/strategy/actions/InviteToGroupAction.cpp | 2 +- src/strategy/actions/LeaveGroupAction.cpp | 22 +++++----- .../actions/MoveToTravelTargetAction.cpp | 2 +- src/strategy/actions/MovementActions.cpp | 4 +- .../actions/RandomBotUpdateAction.cpp | 6 +-- src/strategy/actions/ReleaseSpiritAction.cpp | 8 ++-- src/strategy/actions/ResetInstancesAction.cpp | 2 +- .../actions/ReviveFromCorpseAction.cpp | 42 +++++++++---------- src/strategy/actions/RewardAction.cpp | 4 +- src/strategy/actions/RpgSubActions.cpp | 2 +- src/strategy/actions/SecurityCheckAction.cpp | 4 +- src/strategy/actions/TellMasterAction.cpp | 2 +- src/strategy/actions/TravelAction.cpp | 2 +- src/strategy/rogue/RogueTriggers.cpp | 4 +- src/strategy/triggers/RangeTriggers.cpp | 2 +- src/strategy/values/Formations.cpp | 2 +- ...erTargetValue.cpp => GroupLeaderValue.cpp} | 4 +- ...MasterTargetValue.h => GroupLeaderValue.h} | 8 ++-- src/strategy/values/GroupValues.cpp | 14 +++---- src/strategy/values/ValueContext.h | 6 +-- 29 files changed, 101 insertions(+), 101 deletions(-) rename src/strategy/values/{MasterTargetValue.cpp => GroupLeaderValue.cpp} (70%) rename src/strategy/values/{MasterTargetValue.h => GroupLeaderValue.h} (57%) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 3748b84e02..7419574bd2 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -420,7 +420,7 @@ void PlayerbotAI::UpdateAIGroupAndMaster() { botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); - if (botAI->GetMaster() == botAI->GetGroupMaster()) + if (botAI->GetMaster() == botAI->GetGroupLeader()) botAI->TellMaster("Hello, I follow you!"); else botAI->TellMaster(!urand(0, 2) ? "Hello!" : "Hi!"); @@ -4093,7 +4093,7 @@ Player* PlayerbotAI::FindNewMaster() if (!group) return nullptr; - Player* groupLeader = GetGroupMaster(); + Player* groupLeader = GetGroupLeader(); PlayerbotAI* leaderBotAI = GET_PLAYERBOT_AI(groupLeader); if (!leaderBotAI || leaderBotAI->IsRealPlayer()) return groupLeader; @@ -4144,7 +4144,7 @@ bool PlayerbotAI::HasActivePlayerMaster() { return master && !GET_PLAYERBOT_AI(m bool PlayerbotAI::IsAlt() { return HasRealPlayerMaster() && !sRandomPlayerbotMgr->IsRandomBot(bot); } -Player* PlayerbotAI::GetGroupMaster() +Player* PlayerbotAI::GetGroupLeader() { if (!bot->InBattleground()) if (Group* group = bot->GetGroup()) diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index 8c8cff14f2..ad7641b278 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -540,7 +540,7 @@ class PlayerbotAI : public PlayerbotAIBase // Get the group leader or the master of the bot. // Checks if the bot is summoned as alt of a player bool IsAlt(); - Player* GetGroupMaster(); + Player* GetGroupLeader(); // Returns a semi-random (cycling) number that is fixed for each bot. uint32 GetFixedBotNumer(uint32 maxNum = 100, float cyclePerMin = 1); GrouperType GetGrouperType(); diff --git a/src/PlayerbotSecurity.cpp b/src/PlayerbotSecurity.cpp index 45e8598989..822f40e6d4 100644 --- a/src/PlayerbotSecurity.cpp +++ b/src/PlayerbotSecurity.cpp @@ -251,9 +251,9 @@ bool PlayerbotSecurity::CheckLevelFor(PlayerbotSecurityLevel level, bool silent, out << "I am currently leading a group. I can invite you if you want."; break; case PLAYERBOT_DENY_NOT_LEADER: - if (botAI->GetGroupMaster()) + if (botAI->GetGroupLeader()) { - out << "I am in a group with " << botAI->GetGroupMaster()->GetName() + out << "I am in a group with " << botAI->GetGroupLeader()->GetName() << ". You can ask him for invite."; } else diff --git a/src/RandomPlayerbotMgr.cpp b/src/RandomPlayerbotMgr.cpp index 89a960aecb..1f94e35e13 100644 --- a/src/RandomPlayerbotMgr.cpp +++ b/src/RandomPlayerbotMgr.cpp @@ -1480,10 +1480,10 @@ bool RandomPlayerbotMgr::ProcessBot(uint32 bot) if (!sRandomPlayerbotMgr->IsRandomBot(player)) update = false; - if (player->GetGroup() && botAI->GetGroupMaster()) + if (player->GetGroup() && botAI->GetGroupLeader()) { - PlayerbotAI* groupMasterBotAI = GET_PLAYERBOT_AI(botAI->GetGroupMaster()); - if (!groupMasterBotAI || groupMasterBotAI->IsRealPlayer()) + PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader()); + if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer()) { update = false; } diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h index cebd7615a7..09c9145f93 100644 --- a/src/strategy/actions/ActionContext.h +++ b/src/strategy/actions/ActionContext.h @@ -121,7 +121,7 @@ class ActionContext : public NamedObjectContext creators["shoot"] = &ActionContext::shoot; creators["follow"] = &ActionContext::follow; creators["move from group"] = &ActionContext::move_from_group; - creators["flee to master"] = &ActionContext::flee_to_master; + creators["flee to group leader"] = &ActionContext::flee_to_group_leader; creators["runaway"] = &ActionContext::runaway; creators["stay"] = &ActionContext::stay; creators["sit"] = &ActionContext::sit; @@ -318,7 +318,7 @@ class ActionContext : public NamedObjectContext static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); } static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); } static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); } - static Action* flee_to_master(PlayerbotAI* botAI) { return new FleeToMasterAction(botAI); } + static Action* flee_to_group_leader(PlayerbotAI* botAI) { return new FleeToGroupLeaderAction(botAI); } static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); } static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); } static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); } diff --git a/src/strategy/actions/ChooseRpgTargetAction.cpp b/src/strategy/actions/ChooseRpgTargetAction.cpp index 20151658cc..0d441e9b67 100644 --- a/src/strategy/actions/ChooseRpgTargetAction.cpp +++ b/src/strategy/actions/ChooseRpgTargetAction.cpp @@ -311,7 +311,7 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldObject* target) bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) { PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); - Player* gmaster = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Player* realMaster = botAI->GetMaster(); AiObjectContext* context = botAI->GetAiObjectContext(); @@ -327,30 +327,30 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) return false; } - if (!gmaster || bot == gmaster) + if (!groupLeader || bot == groupLeader) return true; if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) return true; - if (bot->GetDistance(gmaster) > sPlayerbotAIConfig->rpgDistance * 2) + if (bot->GetDistance(groupLeader) > sPlayerbotAIConfig->rpgDistance * 2) return false; Formation* formation = AI_VALUE(Formation*, "formation"); - float distance = gmaster->GetDistance2d(pos.getX(), pos.getY()); + float distance = groupLeader->GetDistance2d(pos.getX(), pos.getY()); if (!botAI->HasActivePlayerMaster() && distance < 50.0f) { - Player* player = gmaster; - if (gmaster && !gmaster->isMoving() || + Player* player = groupLeader; + if (groupLeader && !groupLeader->isMoving() || PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance) return true; } - if ((inDungeon || !gmaster->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == gmaster && distance > 5.0f) + if ((inDungeon || !groupLeader->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == groupLeader && distance > 5.0f) return false; - if (!gmaster->isMoving() && distance < 25.0f) + if (!groupLeader->isMoving() && distance < 25.0f) return true; if (distance < formation->GetMaxDistance()) diff --git a/src/strategy/actions/ChooseTravelTargetAction.cpp b/src/strategy/actions/ChooseTravelTargetAction.cpp index 3843f35876..cf6dddd40c 100644 --- a/src/strategy/actions/ChooseTravelTargetAction.cpp +++ b/src/strategy/actions/ChooseTravelTargetAction.cpp @@ -180,7 +180,7 @@ void ChooseTravelTargetAction::getNewTarget(TravelTarget* newTarget, TravelTarge void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget) { // Tell the master where we are going. - if (!bot->GetGroup() || (botAI->GetGroupMaster() == bot)) + if (!bot->GetGroup() || (botAI->GetGroupLeader() == bot)) ReportTravelTarget(newTarget, oldTarget); // If we are heading to a creature/npc clear it from the ignore list. diff --git a/src/strategy/actions/FollowActions.cpp b/src/strategy/actions/FollowActions.cpp index f4a66bf9e7..d168e8cc93 100644 --- a/src/strategy/actions/FollowActions.cpp +++ b/src/strategy/actions/FollowActions.cpp @@ -70,7 +70,7 @@ bool FollowAction::isUseful() if (!target.empty()) fTarget = AI_VALUE(Unit*, target); else - fTarget = AI_VALUE(Unit*, "master target"); + fTarget = AI_VALUE(Unit*, "group leader"); if (fTarget) { @@ -114,9 +114,9 @@ bool FollowAction::CanDeadFollow(Unit* target) return true; } -bool FleeToMasterAction::Execute(Event event) +bool FleeToGroupLeaderAction::Execute(Event event) { - Unit* fTarget = AI_VALUE(Unit*, "master target"); + Unit* fTarget = AI_VALUE(Unit*, "group leader"); bool canFollow = Follow(fTarget); if (!canFollow) { @@ -146,22 +146,22 @@ bool FleeToMasterAction::Execute(Event event) return true; } -bool FleeToMasterAction::isUseful() +bool FleeToGroupLeaderAction::isUseful() { - if (!botAI->GetGroupMaster()) + if (!botAI->GetGroupLeader()) return false; - if (botAI->GetGroupMaster() == bot) + if (botAI->GetGroupLeader() == bot) return false; Unit* target = AI_VALUE(Unit*, "current target"); - if (target && botAI->GetGroupMaster()->GetTarget() == target->GetGUID()) + if (target && botAI->GetGroupLeader()->GetTarget() == target->GetGUID()) return false; if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) return false; - Unit* fTarget = AI_VALUE(Unit*, "master target"); + Unit* fTarget = AI_VALUE(Unit*, "group leader"); if (!CanDeadFollow(fTarget)) return false; diff --git a/src/strategy/actions/FollowActions.h b/src/strategy/actions/FollowActions.h index 5468e34131..9331bbf556 100644 --- a/src/strategy/actions/FollowActions.h +++ b/src/strategy/actions/FollowActions.h @@ -20,10 +20,10 @@ class FollowAction : public MovementAction bool CanDeadFollow(Unit* target); }; -class FleeToMasterAction : public FollowAction +class FleeToGroupLeaderAction : public FollowAction { public: - FleeToMasterAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to master") {} + FleeToGroupLeaderAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to group leader") {} bool Execute(Event event) override; bool isUseful() override; diff --git a/src/strategy/actions/InviteToGroupAction.cpp b/src/strategy/actions/InviteToGroupAction.cpp index 4d0b4df7b9..bec515fefb 100644 --- a/src/strategy/actions/InviteToGroupAction.cpp +++ b/src/strategy/actions/InviteToGroupAction.cpp @@ -141,7 +141,7 @@ bool InviteNearbyToGroupAction::isUseful() if (group->isRaidGroup() && group->IsFull()) return false; - if (botAI->GetGroupMaster() != bot) + if (botAI->GetGroupLeader() != bot) return false; uint32 memberCount = group->GetMembersCount(); diff --git a/src/strategy/actions/LeaveGroupAction.cpp b/src/strategy/actions/LeaveGroupAction.cpp index 039c476bf5..a279c9426c 100644 --- a/src/strategy/actions/LeaveGroupAction.cpp +++ b/src/strategy/actions/LeaveGroupAction.cpp @@ -109,22 +109,22 @@ bool LeaveFarAwayAction::isUseful() if (!bot->GetGroup()) return false; - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Player* trueMaster = botAI->GetMaster(); - if (!master || (bot == master && !botAI->IsRealPlayer())) + if (!groupLeader || (bot == groupLeader && !botAI->IsRealPlayer())) return false; - PlayerbotAI* masterBotAI = nullptr; - if (master) - masterBotAI = GET_PLAYERBOT_AI(master); - if (master && !masterBotAI) + PlayerbotAI* groupLeaderBotAI = nullptr; + if (groupLeader) + groupLeaderBotAI = GET_PLAYERBOT_AI(groupLeader); + if (groupLeader && !groupLeaderBotAI) return false; if (trueMaster && !GET_PLAYERBOT_AI(trueMaster)) return false; if (botAI->IsAlt() && - (!masterBotAI || masterBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player master. + (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer())) // Don't leave group when alt grouped with player groupLeader. return false; if (botAI->GetGrouperType() == GrouperType::SOLO) @@ -138,19 +138,19 @@ bool LeaveFarAwayAction::isUseful() if (dCount > 4 && !botAI->HasRealPlayerMaster()) return true; - if (bot->GetGuildId() == master->GetGuildId()) + if (bot->GetGuildId() == groupLeader->GetGuildId()) { - if (bot->GetLevel() > master->GetLevel() + 5) + if (bot->GetLevel() > groupLeader->GetLevel() + 5) { if (AI_VALUE(bool, "should get money")) return false; } } - if (abs(int32(master->GetLevel() - bot->GetLevel())) > 4) + if (abs(int32(groupLeader->GetLevel() - bot->GetLevel())) > 4) return true; - if (bot->GetMapId() != master->GetMapId() || bot->GetDistance2d(master) >= 2 * sPlayerbotAIConfig->rpgDistance) + if (bot->GetMapId() != groupLeader->GetMapId() || bot->GetDistance2d(groupLeader) >= 2 * sPlayerbotAIConfig->rpgDistance) { return true; } diff --git a/src/strategy/actions/MoveToTravelTargetAction.cpp b/src/strategy/actions/MoveToTravelTargetAction.cpp index a4dec2caac..ed60339b82 100644 --- a/src/strategy/actions/MoveToTravelTargetAction.cpp +++ b/src/strategy/actions/MoveToTravelTargetAction.cpp @@ -18,7 +18,7 @@ bool MoveToTravelTargetAction::Execute(Event event) WorldLocation location = *target->getPosition(); Group* group = bot->GetGroup(); - if (group && !urand(0, 1) && bot == botAI->GetGroupMaster() && !bot->IsInCombat()) + if (group && !urand(0, 1) && bot == botAI->GetGroupLeader() && !bot->IsInCombat()) { for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next()) { diff --git a/src/strategy/actions/MovementActions.cpp b/src/strategy/actions/MovementActions.cpp index 7078557d1e..f66638a7a1 100644 --- a/src/strategy/actions/MovementActions.cpp +++ b/src/strategy/actions/MovementActions.cpp @@ -1102,7 +1102,7 @@ void MovementAction::UpdateMovementState() // { // if (Unit* pTarget = sServerFacade->GetChaseTarget(bot)) // { - // if (pTarget != botAI->GetGroupMaster()) + // if (pTarget != botAI->GetGroupLeader()) // return; // if (!bot->IsWithinMeleeRange(pTarget)) @@ -2663,7 +2663,7 @@ bool DisperseSetAction::Execute(Event event) return true; } -bool RunAwayAction::Execute(Event event) { return Flee(AI_VALUE(Unit*, "master target")); } +bool RunAwayAction::Execute(Event event) { return Flee(AI_VALUE(Unit*, "group leader")); } bool MoveToLootAction::Execute(Event event) { diff --git a/src/strategy/actions/RandomBotUpdateAction.cpp b/src/strategy/actions/RandomBotUpdateAction.cpp index 1536cd6b92..61a8360e59 100644 --- a/src/strategy/actions/RandomBotUpdateAction.cpp +++ b/src/strategy/actions/RandomBotUpdateAction.cpp @@ -13,10 +13,10 @@ bool RandomBotUpdateAction::Execute(Event event) if (!sRandomPlayerbotMgr->IsRandomBot(bot)) return false; - if (bot->GetGroup() && botAI->GetGroupMaster()) + if (bot->GetGroup() && botAI->GetGroupLeader()) { - PlayerbotAI* groupMasterBotAI = GET_PLAYERBOT_AI(botAI->GetGroupMaster()); - if (!groupMasterBotAI || groupMasterBotAI->IsRealPlayer()) + PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader()); + if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer()) return true; } diff --git a/src/strategy/actions/ReleaseSpiritAction.cpp b/src/strategy/actions/ReleaseSpiritAction.cpp index b06fc89263..17fcc42a0b 100644 --- a/src/strategy/actions/ReleaseSpiritAction.cpp +++ b/src/strategy/actions/ReleaseSpiritAction.cpp @@ -168,15 +168,15 @@ bool AutoReleaseSpiritAction::ShouldAutoRelease() const if (!bot->GetGroup()) return true; - Player* groupMaster = botAI->GetGroupMaster(); - if (!groupMaster || groupMaster == bot) + Player* groupLeader = botAI->GetGroupLeader(); + if (!groupLeader || groupLeader == bot) return true; if (!botAI->HasActivePlayerMaster()) return true; if (botAI->HasActivePlayerMaster() && - groupMaster->GetMapId() == bot->GetMapId() && + groupLeader->GetMapId() == bot->GetMapId() && bot->GetMap() && (bot->GetMap()->IsRaid() || bot->GetMap()->IsDungeon())) { @@ -184,7 +184,7 @@ bool AutoReleaseSpiritAction::ShouldAutoRelease() const } return sServerFacade->IsDistanceGreaterThan( - AI_VALUE2(float, "distance", "master target"), + AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->sightDistance); } diff --git a/src/strategy/actions/ResetInstancesAction.cpp b/src/strategy/actions/ResetInstancesAction.cpp index 1c0a6f2480..cce5eef1e3 100644 --- a/src/strategy/actions/ResetInstancesAction.cpp +++ b/src/strategy/actions/ResetInstancesAction.cpp @@ -16,4 +16,4 @@ bool ResetInstancesAction::Execute(Event event) return true; } -bool ResetInstancesAction::isUseful() { return botAI->GetGroupMaster() == bot; }; +bool ResetInstancesAction::isUseful() { return botAI->GetGroupLeader() == bot; }; diff --git a/src/strategy/actions/ReviveFromCorpseAction.cpp b/src/strategy/actions/ReviveFromCorpseAction.cpp index 74237670df..ce0b1fa05d 100644 --- a/src/strategy/actions/ReviveFromCorpseAction.cpp +++ b/src/strategy/actions/ReviveFromCorpseAction.cpp @@ -17,14 +17,14 @@ bool ReviveFromCorpseAction::Execute(Event event) { - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Corpse* corpse = bot->GetCorpse(); - // follow master when master revives + // follow group Leader when group Leader revives WorldPacket& p = event.getPacket(); - if (!p.empty() && p.GetOpcode() == CMSG_RECLAIM_CORPSE && master && !corpse && bot->IsAlive()) + if (!p.empty() && p.GetOpcode() == CMSG_RECLAIM_CORPSE && groupLeader && !corpse && bot->IsAlive()) { - if (sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + if (sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->farDistance)) { if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) @@ -43,10 +43,10 @@ bool ReviveFromCorpseAction::Execute(Event event) // time(nullptr)) // return false; - if (master) + if (groupLeader) { - if (!GET_PLAYERBOT_AI(master) && master->isDead() && master->GetCorpse() && - sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + if (!GET_PLAYERBOT_AI(groupLeader) && groupLeader->isDead() && groupLeader->GetCorpse() && + sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), sPlayerbotAIConfig->farDistance)) return false; } @@ -79,15 +79,15 @@ bool FindCorpseAction::Execute(Event event) if (bot->InBattleground()) return false; - Player* master = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); Corpse* corpse = bot->GetCorpse(); if (!corpse) return false; - // if (master) + // if (groupLeader) // { - // if (!GET_PLAYERBOT_AI(master) && - // sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "master target"), + // if (!GET_PLAYERBOT_AI(groupLeader) && + // sServerFacade->IsDistanceLessThan(AI_VALUE2(float, "distance", "group leader"), // sPlayerbotAIConfig->farDistance)) return false; // } @@ -110,20 +110,20 @@ bool FindCorpseAction::Execute(Event event) WorldPosition botPos(bot); WorldPosition corpsePos(corpse); WorldPosition moveToPos = corpsePos; - WorldPosition masterPos(master); + WorldPosition leaderPos(groupLeader); float reclaimDist = CORPSE_RECLAIM_RADIUS - 5.0f; float corpseDist = botPos.distance(corpsePos); int64 deadTime = time(nullptr) - corpse->GetGhostTime(); - bool moveToMaster = master && master != bot && masterPos.fDist(corpsePos) < reclaimDist; + bool moveToLeader = groupLeader && groupLeader != bot && leaderPos.fDist(corpsePos) < reclaimDist; // Should we ressurect? If so, return false. if (corpseDist < reclaimDist) { - if (moveToMaster) // We are near master. + if (moveToLeader) // We are near group leader. { - if (botPos.fDist(masterPos) < sPlayerbotAIConfig->spellDistance) + if (botPos.fDist(leaderPos) < sPlayerbotAIConfig->spellDistance) return false; } else if (deadTime > 8 * MINUTE) // We have walked too long already. @@ -140,8 +140,8 @@ bool FindCorpseAction::Execute(Event event) // If we are getting close move to a save ressurrection spot instead of just the corpse. if (corpseDist < sPlayerbotAIConfig->reactDistance) { - if (moveToMaster) - moveToPos = masterPos; + if (moveToLeader) + moveToPos = leaderPos; else { FleeManager manager(bot, reclaimDist, 0.0, urand(0, 1), moveToPos); @@ -215,12 +215,12 @@ GraveyardStruct const* SpiritHealerAction::GetGrave(bool startZone) if (!startZone && ClosestGrave) return ClosestGrave; - if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && botAI->GetGroupMaster() && botAI->GetGroupMaster() != bot) + if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && botAI->GetGroupLeader() && botAI->GetGroupLeader() != bot) { - Player* master = botAI->GetGroupMaster(); - if (master && master != bot) + Player* groupLeader = botAI->GetGroupLeader(); + if (groupLeader && groupLeader != bot) { - ClosestGrave = sGraveyard->GetClosestGraveyard(master, bot->GetTeamId()); + ClosestGrave = sGraveyard->GetClosestGraveyard(groupLeader, bot->GetTeamId()); if (ClosestGrave) return ClosestGrave; diff --git a/src/strategy/actions/RewardAction.cpp b/src/strategy/actions/RewardAction.cpp index 1022166d0f..8fe1c6ff72 100644 --- a/src/strategy/actions/RewardAction.cpp +++ b/src/strategy/actions/RewardAction.cpp @@ -35,8 +35,8 @@ bool RewardAction::Execute(Event event) return true; } - Unit* mtar = AI_VALUE(Unit*, "master target"); - if (mtar && Reward(itemId, mtar)) + Unit* groupLeaderUnit = AI_VALUE(Unit*, "group leader"); + if (groupLeaderUnit && Reward(itemId, groupLeaderUnit)) return true; botAI->TellError("Cannot talk to quest giver"); diff --git a/src/strategy/actions/RpgSubActions.cpp b/src/strategy/actions/RpgSubActions.cpp index 784f6bbb20..aa4269fa92 100644 --- a/src/strategy/actions/RpgSubActions.cpp +++ b/src/strategy/actions/RpgSubActions.cpp @@ -76,7 +76,7 @@ void RpgHelper::setFacing(GuidPosition guidPosition) void RpgHelper::setDelay(bool waitForGroup) { - if (!botAI->HasRealPlayerMaster() || (waitForGroup && botAI->GetGroupMaster() == bot && bot->GetGroup())) + if (!botAI->HasRealPlayerMaster() || (waitForGroup && botAI->GetGroupLeader() == bot && bot->GetGroup())) botAI->SetNextCheckDelay(sPlayerbotAIConfig->rpgDelay); else botAI->SetNextCheckDelay(sPlayerbotAIConfig->rpgDelay / 5); diff --git a/src/strategy/actions/SecurityCheckAction.cpp b/src/strategy/actions/SecurityCheckAction.cpp index 1320eb04a6..c47a6e52cb 100644 --- a/src/strategy/actions/SecurityCheckAction.cpp +++ b/src/strategy/actions/SecurityCheckAction.cpp @@ -22,8 +22,8 @@ bool SecurityCheckAction::Execute(Event event) ItemQualities threshold = group->GetLootThreshold(); if (method == MASTER_LOOT || method == FREE_FOR_ALL || threshold > ITEM_QUALITY_UNCOMMON) { - if ((botAI->GetGroupMaster()->GetSession()->GetSecurity() == SEC_PLAYER) && - (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupMaster()->GetGuildId())) + if ((botAI->GetGroupLeader()->GetSession()->GetSecurity() == SEC_PLAYER) && + (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupLeader()->GetGuildId())) { botAI->TellError("I will play with this loot type only if I'm in your guild :/"); botAI->ChangeStrategy("+passive,+stay", BOT_STATE_NON_COMBAT); diff --git a/src/strategy/actions/TellMasterAction.cpp b/src/strategy/actions/TellMasterAction.cpp index 701f93d773..4b8d96d561 100644 --- a/src/strategy/actions/TellMasterAction.cpp +++ b/src/strategy/actions/TellMasterAction.cpp @@ -22,7 +22,7 @@ bool OutOfReactRangeAction::Execute(Event event) bool OutOfReactRangeAction::isUseful() { - bool canFollow = Follow(AI_VALUE(Unit*, "master target")); + bool canFollow = Follow(AI_VALUE(Unit*, "group leader")); if (!canFollow) { return false; diff --git a/src/strategy/actions/TravelAction.cpp b/src/strategy/actions/TravelAction.cpp index 5804fd7d24..f99f8b29d3 100644 --- a/src/strategy/actions/TravelAction.cpp +++ b/src/strategy/actions/TravelAction.cpp @@ -64,7 +64,7 @@ bool MoveToDarkPortalAction::Execute(Event event) { if (bot->GetGroup()) if (bot->GetGroup()->GetLeaderGUID() != bot->GetGUID() && - !GET_PLAYERBOT_AI(GET_PLAYERBOT_AI(bot)->GetGroupMaster())) + !GET_PLAYERBOT_AI(GET_PLAYERBOT_AI(bot)->GetGroupLeader())) return false; if (bot->GetLevel() > 57) diff --git a/src/strategy/rogue/RogueTriggers.cpp b/src/strategy/rogue/RogueTriggers.cpp index 16aada0164..fd9901552e 100644 --- a/src/strategy/rogue/RogueTriggers.cpp +++ b/src/strategy/rogue/RogueTriggers.cpp @@ -22,8 +22,8 @@ bool UnstealthTrigger::IsActive() return botAI->HasAura("stealth", bot) && !AI_VALUE(uint8, "attacker count") && (AI_VALUE2(bool, "moving", "self target") && ((botAI->GetMaster() && - sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "master target"), 10.0f) && - AI_VALUE2(bool, "moving", "master target")) || + sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "group leader"), 10.0f) && + AI_VALUE2(bool, "moving", "group leader")) || !AI_VALUE(uint8, "attacker count"))); } diff --git a/src/strategy/triggers/RangeTriggers.cpp b/src/strategy/triggers/RangeTriggers.cpp index 3c60934fd2..af29f984d1 100644 --- a/src/strategy/triggers/RangeTriggers.cpp +++ b/src/strategy/triggers/RangeTriggers.cpp @@ -213,7 +213,7 @@ PartyMemberToHealOutOfSpellRangeTrigger::PartyMemberToHealOutOfSpellRangeTrigger bool FarFromMasterTrigger::IsActive() { - return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "master target"), distance); + return sServerFacade->IsDistanceGreaterThan(AI_VALUE2(float, "distance", "group leader"), distance); } bool TooCloseToCreatureTrigger::TooCloseToCreature(uint32 creatureId, float range, bool alive) diff --git a/src/strategy/values/Formations.cpp b/src/strategy/values/Formations.cpp index 21c43f402a..30c738252b 100644 --- a/src/strategy/values/Formations.cpp +++ b/src/strategy/values/Formations.cpp @@ -62,7 +62,7 @@ class MeleeFormation : public FollowFormation public: MeleeFormation(PlayerbotAI* botAI) : FollowFormation(botAI, "melee") {} - std::string const GetTargetName() override { return "master target"; } + std::string const GetTargetName() override { return "group leader"; } }; class QueueFormation : public FollowFormation diff --git a/src/strategy/values/MasterTargetValue.cpp b/src/strategy/values/GroupLeaderValue.cpp similarity index 70% rename from src/strategy/values/MasterTargetValue.cpp rename to src/strategy/values/GroupLeaderValue.cpp index 6fddb6d99b..18ed007648 100644 --- a/src/strategy/values/MasterTargetValue.cpp +++ b/src/strategy/values/GroupLeaderValue.cpp @@ -3,8 +3,8 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#include "MasterTargetValue.h" +#include "GroupLeaderValue.h" #include "Playerbots.h" -Unit* MasterTargetValue::Calculate() { return botAI->GetGroupMaster(); } +Unit* GroupLeaderValue::Calculate() { return botAI->GetGroupLeader(); } diff --git a/src/strategy/values/MasterTargetValue.h b/src/strategy/values/GroupLeaderValue.h similarity index 57% rename from src/strategy/values/MasterTargetValue.h rename to src/strategy/values/GroupLeaderValue.h index a9f8f781df..a359620437 100644 --- a/src/strategy/values/MasterTargetValue.h +++ b/src/strategy/values/GroupLeaderValue.h @@ -3,18 +3,18 @@ * and/or modify it under version 3 of the License, or (at your option), any later version. */ -#ifndef _PLAYERBOT_MASTERTARGETVALUE_H -#define _PLAYERBOT_MASTERTARGETVALUE_H +#ifndef _PLAYERBOT_GROUPLEADERVALUE_H +#define _PLAYERBOT_GROUPLEADERVALUE_H #include "Value.h" class PlayerbotAI; class Unit; -class MasterTargetValue : public UnitCalculatedValue +class GroupLeaderValue : public UnitCalculatedValue { public: - MasterTargetValue(PlayerbotAI* botAI, std::string const name = "master target") : UnitCalculatedValue(botAI, name) + GroupLeaderValue(PlayerbotAI* botAI, std::string const name = "group leader") : UnitCalculatedValue(botAI, name) { } diff --git a/src/strategy/values/GroupValues.cpp b/src/strategy/values/GroupValues.cpp index f9cb21028e..01049a7969 100644 --- a/src/strategy/values/GroupValues.cpp +++ b/src/strategy/values/GroupValues.cpp @@ -28,7 +28,7 @@ GuidVector GroupMembersValue::Calculate() bool IsFollowingPartyValue::Calculate() { - if (botAI->GetGroupMaster() == bot) + if (botAI->GetGroupLeader() == bot) return true; if (botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) @@ -39,15 +39,15 @@ bool IsFollowingPartyValue::Calculate() bool IsNearLeaderValue::Calculate() { - Player* groupMaster = botAI->GetGroupMaster(); + Player* groupLeader = botAI->GetGroupLeader(); - if (!groupMaster) + if (!groupLeader) return false; - if (groupMaster == bot) + if (groupLeader == bot) return true; - return sServerFacade->GetDistance2d(bot, botAI->GetGroupMaster()) < sPlayerbotAIConfig->sightDistance; + return sServerFacade->GetDistance2d(bot, botAI->GetGroupLeader()) < sPlayerbotAIConfig->sightDistance; } bool BoolANDValue::Calculate() @@ -154,8 +154,8 @@ bool GroupReadyValue::Calculate() // We only wait for members that are in range otherwise we might be waiting for bots stuck in dead loops // forever. - if (botAI->GetGroupMaster() && - sServerFacade->GetDistance2d(member, botAI->GetGroupMaster()) > sPlayerbotAIConfig->sightDistance) + if (botAI->GetGroupLeader() && + sServerFacade->GetDistance2d(member, botAI->GetGroupLeader()) > sPlayerbotAIConfig->sightDistance) continue; if (member->GetHealthPct() < sPlayerbotAIConfig->almostFullHealth) diff --git a/src/strategy/values/ValueContext.h b/src/strategy/values/ValueContext.h index b8e79d6e5a..adf03be8ea 100644 --- a/src/strategy/values/ValueContext.h +++ b/src/strategy/values/ValueContext.h @@ -30,6 +30,7 @@ #include "Formations.h" #include "GrindTargetValue.h" #include "GroupValues.h" +#include "GroupLeaderValue.h" #include "GuildValues.h" #include "HasAvailableLootValue.h" #include "HasTotemValue.h" @@ -51,7 +52,6 @@ #include "LootStrategyValue.h" #include "MaintenanceValues.h" #include "ManaSaveLevelValue.h" -#include "MasterTargetValue.h" #include "NearestAdsValue.h" #include "NearestCorpsesValue.h" #include "NearestFriendlyPlayersValue.h" @@ -130,7 +130,7 @@ class ValueContext : public NamedObjectContext creators["party member to resurrect"] = &ValueContext::party_member_to_resurrect; creators["current target"] = &ValueContext::current_target; creators["self target"] = &ValueContext::self_target; - creators["master target"] = &ValueContext::master; + creators["group leader"] = &ValueContext::group_leader; creators["line target"] = &ValueContext::line_target; creators["tank target"] = &ValueContext::tank_target; creators["dps target"] = &ValueContext::dps_target; @@ -439,7 +439,7 @@ class ValueContext : public NamedObjectContext 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); } - static UntypedValue* master(PlayerbotAI* botAI) { return new MasterTargetValue(botAI); } + static UntypedValue* group_leader(PlayerbotAI* botAI) { return new GroupLeaderValue(botAI); } static UntypedValue* line_target(PlayerbotAI* botAI) { return new LineTargetValue(botAI); } static UntypedValue* tank_target(PlayerbotAI* botAI) { return new TankTargetValue(botAI); } static UntypedValue* dps_target(PlayerbotAI* botAI) { return new DpsTargetValue(botAI); } From 353c29dfc44dd9779dea1348d8b10eeeb1580efe Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Mon, 8 Dec 2025 03:25:40 -0800 Subject: [PATCH 2/5] Bug: Fix bots leaving LFG groups before master (#1876) I removed bots checking if they should leave group every tick, and will rely on the LeaveGroupFarAway action. I also increased the timer from 5 seconds to 20 seconds. No need to check this that often. --- src/PlayerbotAI.cpp | 6 ++---- src/PlayerbotAI.h | 2 +- src/strategy/generic/GroupStrategy.cpp | 5 ++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 7419574bd2..7975807f88 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -365,7 +365,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Update the bot's group status (moved to helper function) - UpdateAIGroupAndMaster(); + UpdateAIGroupMaster(); // Update internal AI UpdateAIInternal(elapsed, minimal); @@ -373,7 +373,7 @@ void PlayerbotAI::UpdateAI(uint32 elapsed, bool minimal) } // Helper function for UpdateAI to check group membership and handle removal if necessary -void PlayerbotAI::UpdateAIGroupAndMaster() +void PlayerbotAI::UpdateAIGroupMaster() { if (!bot) return; @@ -431,8 +431,6 @@ void PlayerbotAI::UpdateAIGroupAndMaster() botAI->ChangeStrategy("-follow", BOT_STATE_NON_COMBAT); } } - else if (!newMaster && !bot->InBattleground()) - LeaveOrDisbandGroup(); } } diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index ad7641b278..7252cd7720 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -612,7 +612,7 @@ class PlayerbotAI : public PlayerbotAIBase static void _fillGearScoreData(Player* player, Item* item, std::vector* gearScore, uint32& twoHandScore, bool mixed = false); bool IsTellAllowed(PlayerbotSecurityLevel securityLevel = PLAYERBOT_SECURITY_ALLOW_ALL); - void UpdateAIGroupAndMaster(); + void UpdateAIGroupMaster(); Item* FindItemInInventory(std::function checkItem) const; void HandleCommands(); void HandleCommand(uint32 type, const std::string& text, Player& fromPlayer, const uint32 lang = LANG_UNIVERSAL); diff --git a/src/strategy/generic/GroupStrategy.cpp b/src/strategy/generic/GroupStrategy.cpp index 9406e8409d..9cfba6e2fc 100644 --- a/src/strategy/generic/GroupStrategy.cpp +++ b/src/strategy/generic/GroupStrategy.cpp @@ -11,7 +11,6 @@ void GroupStrategy::InitTriggers(std::vector& triggers) { triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("invite nearby", 4.0f), nullptr))); triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("invite guild", 4.0f), nullptr))); - triggers.push_back(new TriggerNode("often", NextAction::array(0, new NextAction("leave far away", 4.0f), nullptr))); - triggers.push_back( - new TriggerNode("seldom", NextAction::array(0, new NextAction("reset instances", 1.0f), nullptr))); + triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("leave far away", 4.0f), nullptr))); + triggers.push_back(new TriggerNode("seldom", NextAction::array(0, new NextAction("reset instances", 1.0f), nullptr))); } From e5b27910539d0ea741e9e2a4341de4aaed6e44ac Mon Sep 17 00:00:00 2001 From: HennyWilly <5954598+HennyWilly@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:29:07 +0100 Subject: [PATCH 3/5] Improve Molten Core Strategy (#1852) This is my first attempt of implementing playerbot strategies. A team of 40 can steamroll Molten Core relatively easy, even with lower item levels. Regardless, this PR adds resistance triggers and actions to mitigate some damage. Additionally, improvements were made for the encounters with Garr, Baron Geddon, Shazzrah and Golemagg. A short summary per boss is listed below. All planned features are included, but feedback is of course appreciated. ### Lucifron - Shadow resistance: mitigate damage from [Impending Doom](https://www.wowhead.com/classic/spell=19702/impending-doom) and [Shadow Shock](https://www.wowhead.com/classic/spell=19460/shadow-shock). ### Magmadar - Fire resistance: mitigate damage from [Lava Bomb](https://www.wowhead.com/classic/spell=19411/lava-bomb) and [Magma Spit](https://www.wowhead.com/classic/spell=19450/magma-spit). - Like King Dred and the fraction commander (Nexus), this fight might profit from an anti-fear strategy in order to counter [Panic](https://www.wowhead.com/classic/spell=19408/panic). Not implemented here. ### Gehennas - Shadow resistance: mitigate damage from [Shadow Bolt](https://www.wowhead.com/classic/spell=19728/shadow-bolt) and increase the chance to resist [Gehennas' Curse](https://www.wowhead.com/classic/spell=19716/gehennas-curse). ### Garr - Fire resistance: mitigate damage from the Firesworn adds ([Immolate](https://www.wowhead.com/classic/spell=20294/immolate) and [Eruption](https://www.wowhead.com/classic/spell=19497/eruption)). - Disabled dps aoe abilities via multiplier. This one is important because multiple exploding adds at once might delete bots rather quick... ### Baron Geddon - Refactored the existing strategy. - Fire resistance: mitigate damage from [Ignite Mana](https://www.wowhead.com/classic/spell=19659/ignite-mana), [Inferno](https://www.wowhead.com/classic/spell=19695/inferno) and [Living Bomb](https://www.wowhead.com/classic/spell=20475/living-bomb). - Better Inferno handling: Before moving away, bots stop attacking and interrupt their spells. Additionally, the new multiplier prevents bots from running back to Geddon while Inferno is still active. ### Shazzrah - Ranged bots now position themselves in a sweet spot that prevents them from getting hit with [Arcane Explosion](https://www.wowhead.com/classic/spell=19712/arcane-explosion) but still close enough to dps and heal. ### Sulfuron Harbinger - Fire resistance: mitigate damage from [Hand of Ragnaros](https://www.wowhead.com/classic/spell=19780/hand-of-ragnaros) and [Immolate](https://www.wowhead.com/classic/spell=20294/immolate). To be fair, this one is quite negligible... ### Golemagg - Fire resistance: mitigate damage from [Magma Splash](https://www.wowhead.com/classic/spell=13880/magma-splash) and [Pyroblast](https://www.wowhead.com/classic/spell=20228/pyroblast). - Disabled dps aoe abilities via multiplier. Kind of a preference on my side. Otherwise, the Core Ragers spam emotes about not wanting to die. ### Majordomo Executus - Shadow resistance: mitigate damage from [Aegis of Ragnaros](https://www.wowhead.com/classic/spell=20620/aegis-of-ragnaros), [Shadow Shock](https://www.wowhead.com/classic/spell=20603/shadow-shock) and [Shadow Bolt](https://www.wowhead.com/classic/spell=21077/shadow-bolt). This one is also negligible, TBF. ### Ragnaros - Fire resistance: mitigate damage from [Wrath of Ragnaros](https://www.wowhead.com/classic/spell=20566/wrath-of-ragnaros) and [Lava Burst](https://www.wowhead.com/classic/spell=21158/lava-burst). --- src/PlayerbotAI.cpp | 10 +- src/PlayerbotAI.h | 2 +- src/strategy/raids/RaidStrategyContext.h | 4 +- .../raids/moltencore/RaidMcActionContext.h | 32 ++- .../raids/moltencore/RaidMcActions.cpp | 214 ++++++++++++++++-- src/strategy/raids/moltencore/RaidMcActions.h | 49 +++- src/strategy/raids/moltencore/RaidMcHelpers.h | 22 ++ .../raids/moltencore/RaidMcMultipliers.cpp | 117 ++++++++++ .../raids/moltencore/RaidMcMultipliers.h | 27 +++ .../raids/moltencore/RaidMcStrategy.cpp | 70 +++++- .../raids/moltencore/RaidMcStrategy.h | 8 +- .../raids/moltencore/RaidMcTriggerContext.h | 30 ++- .../raids/moltencore/RaidMcTriggers.cpp | 36 ++- .../raids/moltencore/RaidMcTriggers.h | 28 +++ 14 files changed, 601 insertions(+), 48 deletions(-) create mode 100644 src/strategy/raids/moltencore/RaidMcHelpers.h create mode 100644 src/strategy/raids/moltencore/RaidMcMultipliers.cpp create mode 100644 src/strategy/raids/moltencore/RaidMcMultipliers.h diff --git a/src/PlayerbotAI.cpp b/src/PlayerbotAI.cpp index 7975807f88..2ef6860a2a 100644 --- a/src/PlayerbotAI.cpp +++ b/src/PlayerbotAI.cpp @@ -1459,7 +1459,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) strategyName = "onyxia"; // Onyxia's Lair break; case 409: - strategyName = "mc"; // Molten Core + strategyName = "moltencore"; // Molten Core break; case 469: strategyName = "bwl"; // Blackwing Lair @@ -2247,7 +2247,7 @@ uint32 PlayerbotAI::GetGroupTankNum(Player* player) bool PlayerbotAI::IsAssistTank(Player* player) { return IsTank(player) && !IsMainTank(player); } -bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) +bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers) { Group* group = player->GetGroup(); if (!group) @@ -2264,6 +2264,9 @@ bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) continue; } + if (ignoreDeadPlayers && !member->IsAlive()) + continue; + if (group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) @@ -2283,6 +2286,9 @@ bool PlayerbotAI::IsAssistTankOfIndex(Player* player, int index) continue; } + if (ignoreDeadPlayers && !member->IsAlive()) + continue; + if (!group->IsAssistant(member->GetGUID()) && IsAssistTank(member)) { if (index == counter) diff --git a/src/PlayerbotAI.h b/src/PlayerbotAI.h index 7252cd7720..3b9eaab1ea 100644 --- a/src/PlayerbotAI.h +++ b/src/PlayerbotAI.h @@ -428,7 +428,7 @@ class PlayerbotAI : public PlayerbotAIBase static bool IsMainTank(Player* player); static uint32 GetGroupTankNum(Player* player); static bool IsAssistTank(Player* player); - static bool IsAssistTankOfIndex(Player* player, int index); + static bool IsAssistTankOfIndex(Player* player, int index, bool ignoreDeadPlayers = false); static bool IsHealAssistantOfIndex(Player* player, int index); static bool IsRangedDpsAssistantOfIndex(Player* player, int index); bool HasAggro(Unit* unit); diff --git a/src/strategy/raids/RaidStrategyContext.h b/src/strategy/raids/RaidStrategyContext.h index f16fa72546..ee2bac3270 100644 --- a/src/strategy/raids/RaidStrategyContext.h +++ b/src/strategy/raids/RaidStrategyContext.h @@ -22,7 +22,7 @@ class RaidStrategyContext : public NamedObjectContext RaidStrategyContext() : NamedObjectContext(false, true) { creators["aq20"] = &RaidStrategyContext::aq20; - creators["mc"] = &RaidStrategyContext::mc; + creators["moltencore"] = &RaidStrategyContext::moltencore; creators["bwl"] = &RaidStrategyContext::bwl; creators["karazhan"] = &RaidStrategyContext::karazhan; creators["magtheridon"] = &RaidStrategyContext::magtheridon; @@ -38,7 +38,7 @@ class RaidStrategyContext : public NamedObjectContext private: static Strategy* aq20(PlayerbotAI* botAI) { return new RaidAq20Strategy(botAI); } - static Strategy* mc(PlayerbotAI* botAI) { return new RaidMcStrategy(botAI); } + 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); } diff --git a/src/strategy/raids/moltencore/RaidMcActionContext.h b/src/strategy/raids/moltencore/RaidMcActionContext.h index 45e6e056cc..79a4a95a85 100644 --- a/src/strategy/raids/moltencore/RaidMcActionContext.h +++ b/src/strategy/raids/moltencore/RaidMcActionContext.h @@ -10,13 +10,39 @@ class RaidMcActionContext : public NamedObjectContext public: RaidMcActionContext() { - creators["mc check should move from group"] = &RaidMcActionContext::check_should_move_from_group; + creators["mc lucifron shadow resistance"] = &RaidMcActionContext::lucifron_shadow_resistance; + creators["mc magmadar fire resistance"] = &RaidMcActionContext::magmadar_fire_resistance; + creators["mc gehennas shadow resistance"] = &RaidMcActionContext::gehennas_shadow_resistance; + creators["mc garr fire resistance"] = &RaidMcActionContext::garr_fire_resistance; + creators["mc baron geddon fire resistance"] = &RaidMcActionContext::baron_geddon_fire_resistance; + creators["mc move from group"] = &RaidMcActionContext::check_should_move_from_group; creators["mc move from baron geddon"] = &RaidMcActionContext::move_from_baron_geddon; + creators["mc shazzrah move away"] = &RaidMcActionContext::shazzrah_move_away; + creators["mc sulfuron harbinger fire resistance"] = &RaidMcActionContext::sulfuron_harbinger_fire_resistance; + creators["mc golemagg fire resistance"] = &RaidMcActionContext::golemagg_fire_resistance; + creators["mc golemagg mark boss"] = &RaidMcActionContext::golemagg_mark_boss; + creators["mc golemagg main tank attack golemagg"] = &RaidMcActionContext::golemagg_main_tank_attack_golemagg; + 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; } private: - static Action* check_should_move_from_group(PlayerbotAI* ai) { return new McCheckShouldMoveFromGroupAction(ai); } - static Action* move_from_baron_geddon(PlayerbotAI* ai) { return new McMoveFromBaronGeddonAction(ai); } + static Action* lucifron_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "lucifron"); } + static Action* magmadar_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "magmadar"); } + static Action* gehennas_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceAction(botAI, "gehennas"); } + static Action* garr_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "garr"); } + static Action* baron_geddon_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "baron geddon"); } + static Action* check_should_move_from_group(PlayerbotAI* botAI) { return new McMoveFromGroupAction(botAI); } + static Action* move_from_baron_geddon(PlayerbotAI* botAI) { return new McMoveFromBaronGeddonAction(botAI); } + static Action* shazzrah_move_away(PlayerbotAI* botAI) { return new McShazzrahMoveAwayAction(botAI); } + static Action* sulfuron_harbinger_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "sulfuron harbinger"); } + static Action* golemagg_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceAction(botAI, "golemagg the incinerator"); } + static Action* golemagg_mark_boss(PlayerbotAI* botAI) { return new McGolemaggMarkBossAction(botAI); } + static Action* golemagg_main_tank_attack_golemagg(PlayerbotAI* botAI) { return new McGolemaggMainTankAttackGolemaggAction(botAI); } + 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"); } }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcActions.cpp b/src/strategy/raids/moltencore/RaidMcActions.cpp index b82a073c40..b18c8b8534 100644 --- a/src/strategy/raids/moltencore/RaidMcActions.cpp +++ b/src/strategy/raids/moltencore/RaidMcActions.cpp @@ -1,43 +1,215 @@ #include "RaidMcActions.h" #include "Playerbots.h" +#include "RtiTargetValue.h" +#include "RaidMcTriggers.h" +#include "RaidMcHelpers.h" -bool McCheckShouldMoveFromGroupAction::Execute(Event event) +static constexpr float LIVING_BOMB_DISTANCE = 20.0f; +static constexpr float INFERNO_DISTANCE = 20.0f; + +// don't get hit by Arcane Explosion but still be in casting range +static constexpr float ARCANE_EXPLOSION_DISTANCE = 26.0f; + +// dedicated tank positions; prevents assist tanks from positioning Core Ragers on steep walls on pull +static const Position GOLEMAGG_TANK_POSITION{795.7308, -994.8848, -207.18661}; +static const Position CORE_RAGER_TANK_POSITION{846.6453, -1019.0639, -198.9819}; + +static constexpr float GOLEMAGGS_TRUST_DISTANCE = 30.0f; +static constexpr float CORE_RAGER_STEP_DISTANCE = 5.0f; + +using namespace MoltenCoreHelpers; + +bool McMoveFromGroupAction::Execute(Event event) +{ + return MoveFromGroup(LIVING_BOMB_DISTANCE); +} + +bool McMoveFromBaronGeddonAction::Execute(Event event) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + { + float distToTravel = INFERNO_DISTANCE - bot->GetDistance2d(boss); + if (distToTravel > 0) + { + // Stop current spell first + bot->AttackStop(); + bot->InterruptNonMeleeSpells(false); + + return MoveAway(boss, distToTravel); + } + } + return false; +} + +bool McShazzrahMoveAwayAction::Execute(Event event) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "shazzrah")) + { + float distToTravel = ARCANE_EXPLOSION_DISTANCE - bot->GetDistance2d(boss); + if (distToTravel > 0) + return MoveAway(boss, distToTravel); + } + return false; +} + +bool McGolemaggMarkBossAction::Execute(Event event) { - if (bot->HasAura(20475)) // barron geddon's living bomb + if (Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) { - if (!botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT)) + if (Group* group = bot->GetGroup()) { - // add/remove from both for now as it will make it more obvious to - // player if this strat remains on after fight somehow - botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT); - return true; + ObjectGuid currentSkullGuid = group->GetTargetIcon(RtiTargetValue::skullIndex); + if (currentSkullGuid.IsEmpty() || currentSkullGuid != boss->GetGUID()) + { + group->SetTargetIcon(RtiTargetValue::skullIndex, bot->GetGUID(), boss->GetGUID()); + return true; + } } } - else if (botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT)) + return false; +} + +bool McGolemaggTankAction::MoveUnitToPosition(Unit* target, const Position& tankPosition, float maxDistance, + float stepDistance) +{ + if (bot->GetVictim() != target) + return Attack(target); + if (target->GetVictim() == bot) { - // add/remove from both for now as it will make it more obvious to - // player if this strat remains on after fight somehow - botAI->ChangeStrategy("-move from group", BOT_STATE_NON_COMBAT); - botAI->ChangeStrategy("-move from group", BOT_STATE_COMBAT); + float distanceToTankPosition = bot->GetExactDist2d(tankPosition.GetPositionX(), tankPosition.GetPositionY()); + if (distanceToTankPosition > maxDistance) + { + float dX = tankPosition.GetPositionX() - bot->GetPositionX(); + float dY = tankPosition.GetPositionY() - bot->GetPositionY(); + float dist = sqrt(dX * dX + dY * dY); + float moveX = bot->GetPositionX() + (dX / dist) * stepDistance; + float moveY = bot->GetPositionY() + (dY / dist) * stepDistance; + return MoveTo(bot->GetMapId(), moveX, moveY, bot->GetPositionZ(), false, false, + false, false, MovementPriority::MOVEMENT_COMBAT, true, + true); + } + } + else if (botAI->DoSpecificAction("taunt spell", Event(), true)) return true; + return false; +} + +bool McGolemaggTankAction::FindCoreRagers(Unit*& coreRager1, Unit*& coreRager2) const +{ + coreRager1 = coreRager2 = nullptr; + for (auto const& target : AI_VALUE(GuidVector, "possible targets no los")) + { + Unit* unit = botAI->GetUnit(target); + if (unit && unit->IsAlive() && unit->GetEntry() == NPC_CORE_RAGER) + { + if (coreRager1 == nullptr) + coreRager1 = unit; + else if (coreRager2 == nullptr) + { + coreRager2 = unit; + break; // There should be no third Core Rager. + } + } + } + return coreRager1 != nullptr && coreRager2 != nullptr; +} + +bool McGolemaggMainTankAttackGolemaggAction::Execute(Event event) +{ + // At this point, we know we are not the last living tank in the group. + if (Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) + { + Unit* coreRager1; + Unit* coreRager2; + if (!FindCoreRagers(coreRager1, coreRager2)) + return false; // safety check + + // We only need to move if the Core Ragers still have Golemagg's Trust + if (coreRager1->HasAura(SPELL_GOLEMAGGS_TRUST) || coreRager2->HasAura(SPELL_GOLEMAGGS_TRUST)) + return MoveUnitToPosition(boss, GOLEMAGG_TANK_POSITION, boss->GetCombatReach()); } return false; } -bool McMoveFromBaronGeddonAction::Execute(Event event) +bool McGolemaggAssistTankAttackCoreRagerAction::Execute(Event event) { - const float radius = 25.0f; // more than should be needed but bots keep trying to run back in - if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + Unit* boss = AI_VALUE2(Unit*, "find target", "golemagg the incinerator"); + if (!boss) + return false; + + // Step 0: Filter additional assist tanks. We only need 2. + bool isFirstAssistTank = PlayerbotAI::IsAssistTankOfIndex(bot, 0, true); + bool isSecondAssistTank = PlayerbotAI::IsAssistTankOfIndex(bot, 1, true); + if (!isFirstAssistTank && !isSecondAssistTank) + return Attack(boss); + + // Step 1: Find both Core Ragers + Unit* coreRager1; + Unit* coreRager2; + if (!FindCoreRagers(coreRager1, coreRager2)) + return false; // safety check + + // Step 2: Assign Core Rager to bot + Unit* myCoreRager = nullptr; + Unit* otherCoreRager = nullptr; + if (isFirstAssistTank) { - long distToTravel = radius - bot->GetDistance(boss); - if (distToTravel > 0) + myCoreRager = coreRager1; + otherCoreRager = coreRager2; + } + else // isSecondAssistTank is always true here + { + myCoreRager = coreRager2; + otherCoreRager = coreRager1; + } + + // Step 3: Select the right target + if (myCoreRager->GetVictim() != bot) + { + // Step 3.1: My Core Rager isn't attacking me. Attack until it does. + if (bot->GetVictim() != myCoreRager) + return Attack(myCoreRager); + return botAI->DoSpecificAction("taunt spell", event, true); + } + + Unit* otherCoreRagerVictim = otherCoreRager->GetVictim(); + if (otherCoreRagerVictim) // Core Rager victim can be NULL + { + // Step 3.2: Check if the other Core Rager isn't attacking its assist tank. + Player* otherCoreRagerPlayerVictim = otherCoreRagerVictim->ToPlayer(); + if (otherCoreRagerPlayerVictim && + !PlayerbotAI::IsAssistTankOfIndex(otherCoreRagerPlayerVictim, 0, true) && + !PlayerbotAI::IsAssistTankOfIndex(otherCoreRagerPlayerVictim, 1, true)) { - // float angle = bot->GetAngle(boss) + M_PI; - // return Move(angle, distToTravel); - return MoveAway(boss, distToTravel); + // Assume we are the only assist tank or the other assist tank is dead => pick up other Core Rager! + if (bot->GetVictim() != otherCoreRager) + return Attack(otherCoreRager); + return botAI->DoSpecificAction("taunt spell", event, true); } } + + if (bot->GetVictim() != myCoreRager) + return Attack(myCoreRager); // Step 3.3: Attack our Core Rager in case we previously switched in 3.2. + + // Step 4: Prevent Golemagg's Trust on Core Ragers + if (myCoreRager->HasAura(SPELL_GOLEMAGGS_TRUST) || + (otherCoreRagerVictim == bot && otherCoreRager->HasAura(SPELL_GOLEMAGGS_TRUST))) + { + // Step 4.1: Move Core Ragers to dedicated tank position (only if Golemagg is far enough away from said position) + float bossDistanceToCoreRagerTankPosition = boss->GetExactDist2d( + CORE_RAGER_TANK_POSITION.GetPositionX(), CORE_RAGER_TANK_POSITION.GetPositionY()); + if (bossDistanceToCoreRagerTankPosition > GOLEMAGGS_TRUST_DISTANCE) + { + float distanceToTankPosition = bot->GetExactDist2d(CORE_RAGER_TANK_POSITION.GetPositionX(), + CORE_RAGER_TANK_POSITION.GetPositionY()); + if (distanceToTankPosition > CORE_RAGER_STEP_DISTANCE) + return MoveUnitToPosition(myCoreRager, CORE_RAGER_TANK_POSITION, CORE_RAGER_STEP_DISTANCE); + } + + // Step 4.2: if boss is too close to tank position, or we are already there, move away from Golemagg to try to out-range Golemagg's Trust + return MoveAway(boss, CORE_RAGER_STEP_DISTANCE, true); + } + return false; } diff --git a/src/strategy/raids/moltencore/RaidMcActions.h b/src/strategy/raids/moltencore/RaidMcActions.h index 6ff18513ff..680b311d3c 100644 --- a/src/strategy/raids/moltencore/RaidMcActions.h +++ b/src/strategy/raids/moltencore/RaidMcActions.h @@ -1,15 +1,16 @@ #ifndef _PLAYERBOT_RAIDMCACTIONS_H #define _PLAYERBOT_RAIDMCACTIONS_H +#include "AttackAction.h" #include "MovementActions.h" #include "PlayerbotAI.h" #include "Playerbots.h" -class McCheckShouldMoveFromGroupAction : public Action +class McMoveFromGroupAction : public MovementAction { public: - McCheckShouldMoveFromGroupAction(PlayerbotAI* botAI, std::string const name = "mc check should move from group") - : Action(botAI, name) {} + McMoveFromGroupAction(PlayerbotAI* botAI, std::string const name = "mc move from group") + : MovementAction(botAI, name) {} bool Execute(Event event) override; }; @@ -21,4 +22,46 @@ class McMoveFromBaronGeddonAction : public MovementAction bool Execute(Event event) override; }; +class McShazzrahMoveAwayAction : public MovementAction +{ +public: + McShazzrahMoveAwayAction(PlayerbotAI* botAI, std::string const name = "mc shazzrah move away") + : MovementAction(botAI, name) {} + bool Execute(Event event) override; +}; + +class McGolemaggMarkBossAction : public Action +{ +public: + McGolemaggMarkBossAction(PlayerbotAI* botAI, std::string const name = "mc golemagg mark boss") + : Action(botAI, name) {}; + bool Execute(Event event) override; +}; + +class McGolemaggTankAction : public AttackAction +{ +public: + McGolemaggTankAction(PlayerbotAI* botAI, std::string const name) + : AttackAction(botAI, name) {} +protected: + bool MoveUnitToPosition(Unit* target, const Position& tankPosition, float maxDistance, float stepDistance = 3.0f); + bool FindCoreRagers(Unit*& coreRager1, Unit*& coreRager2) const; +}; + +class McGolemaggMainTankAttackGolemaggAction : public McGolemaggTankAction +{ +public: + McGolemaggMainTankAttackGolemaggAction(PlayerbotAI* botAI, std::string const name = "mc golemagg main tank attack golemagg") + : McGolemaggTankAction(botAI, name) {}; + bool Execute(Event event) override; +}; + +class McGolemaggAssistTankAttackCoreRagerAction : public McGolemaggTankAction +{ +public: + McGolemaggAssistTankAttackCoreRagerAction(PlayerbotAI* botAI, std::string const name = "mc golemagg assist tank attack core rager") + : McGolemaggTankAction(botAI, name) {}; + bool Execute(Event event) override; +}; + #endif diff --git a/src/strategy/raids/moltencore/RaidMcHelpers.h b/src/strategy/raids/moltencore/RaidMcHelpers.h new file mode 100644 index 0000000000..5dc821246d --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcHelpers.h @@ -0,0 +1,22 @@ +#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/src/strategy/raids/moltencore/RaidMcMultipliers.cpp b/src/strategy/raids/moltencore/RaidMcMultipliers.cpp new file mode 100644 index 0000000000..d1ee936b0d --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcMultipliers.cpp @@ -0,0 +1,117 @@ +#include "RaidMcMultipliers.h" + +#include "Playerbots.h" +#include "ChooseTargetActions.h" +#include "GenericSpellActions.h" +#include "DruidActions.h" +#include "HunterActions.h" +#include "PaladinActions.h" +#include "ShamanActions.h" +#include "WarriorActions.h" +#include "DKActions.h" +#include "RaidMcActions.h" +#include "RaidMcHelpers.h" + +using namespace MoltenCoreHelpers; + +static bool IsDpsBotWithAoeAction(Player* bot, Action* action) +{ + if (PlayerbotAI::IsDps(bot)) + { + if (dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action) || dynamic_cast(action) || + dynamic_cast(action)) + return true; + + if (auto castSpellAction = dynamic_cast(action)) + { + if (castSpellAction->getThreatType() == Action::ActionThreatType::Aoe) + return true; + } + } + return false; +} + +float GarrDisableDpsAoeMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "garr")) + { + if (IsDpsBotWithAoeAction(bot, action)) + return 0.0f; + } + return 1.0f; +} + +static bool IsAllowedGeddonMovementAction(Action* action) +{ + if (dynamic_cast(action) && + !dynamic_cast(action) && + !dynamic_cast(action)) + return false; + + if (dynamic_cast(action)) + return false; + + return true; +} + +float BaronGeddonAbilityMultiplier::GetValue(Action* action) +{ + if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) + { + if (boss->HasAura(SPELL_INFERNO)) + { + if (!IsAllowedGeddonMovementAction(action)) + return 0.0f; + } + } + + // No check for Baron Geddon, because bots may have the bomb even after Geddon died. + if (bot->HasAura(SPELL_LIVING_BOMB)) + { + if (!IsAllowedGeddonMovementAction(action)) + return 0.0f; + } + + return 1.0f; +} + +static bool IsSingleLivingTankInGroup(Player* bot) +{ + 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) + continue; + if (PlayerbotAI::IsTank(member)) + return false; + } + } + return true; +} + +float GolemaggMultiplier::GetValue(Action* action) +{ + if (AI_VALUE2(Unit*, "find target", "golemagg the incinerator")) + { + if (PlayerbotAI::IsTank(bot) && IsSingleLivingTankInGroup(bot)) + { + // Only one tank => Pick up Golemagg and the two Core Ragers + if (dynamic_cast(action) || + dynamic_cast(action)) + return 0.0f; + } + if (PlayerbotAI::IsAssistTank(bot)) + { + // The first two assist tanks manage the Core Ragers. The remaining assist tanks attack the boss. + if (dynamic_cast(action)) + return 0.0f; + } + if (IsDpsBotWithAoeAction(bot, action)) + return 0.0f; + } + return 1.0f; +} diff --git a/src/strategy/raids/moltencore/RaidMcMultipliers.h b/src/strategy/raids/moltencore/RaidMcMultipliers.h new file mode 100644 index 0000000000..56dc7a5f80 --- /dev/null +++ b/src/strategy/raids/moltencore/RaidMcMultipliers.h @@ -0,0 +1,27 @@ +#ifndef _PLAYERBOT_RAIDMCMULTIPLIERS_H +#define _PLAYERBOT_RAIDMCMULTIPLIERS_H + +#include "Multiplier.h" + +class GarrDisableDpsAoeMultiplier : public Multiplier +{ +public: + GarrDisableDpsAoeMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "garr disable dps aoe multiplier") {} + float GetValue(Action* action) override; +}; + +class BaronGeddonAbilityMultiplier : public Multiplier +{ +public: + BaronGeddonAbilityMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "baron geddon ability multiplier") {} + float GetValue(Action* action) override; +}; + +class GolemaggMultiplier : public Multiplier +{ +public: + GolemaggMultiplier(PlayerbotAI* botAI) : Multiplier(botAI, "golemagg multiplier") {} + float GetValue(Action* action) override; +}; + +#endif diff --git a/src/strategy/raids/moltencore/RaidMcStrategy.cpp b/src/strategy/raids/moltencore/RaidMcStrategy.cpp index 67793de906..c6a5042eb5 100644 --- a/src/strategy/raids/moltencore/RaidMcStrategy.cpp +++ b/src/strategy/raids/moltencore/RaidMcStrategy.cpp @@ -1,13 +1,81 @@ #include "RaidMcStrategy.h" +#include "RaidMcMultipliers.h" #include "Strategy.h" void RaidMcStrategy::InitTriggers(std::vector& triggers) { + // Lucifron + triggers.push_back( + new TriggerNode("mc lucifron shadow resistance", + NextAction::array(0, new NextAction("mc lucifron shadow resistance", ACTION_RAID), nullptr))); + + // Magmadar + // TODO: Fear ward / tremor totem, or general anti-fear strat development. Same as King Dred (Drak'Tharon) and faction commander (Nexus). + triggers.push_back( + new TriggerNode("mc magmadar fire resistance", + NextAction::array(0, new NextAction("mc magmadar fire resistance", ACTION_RAID), nullptr))); + + // Gehennas + triggers.push_back( + new TriggerNode("mc gehennas shadow resistance", + NextAction::array(0, new NextAction("mc gehennas shadow resistance", ACTION_RAID), nullptr))); + + // Garr + triggers.push_back( + new TriggerNode("mc garr fire resistance", + NextAction::array(0, new NextAction("mc garr fire resistance", ACTION_RAID), nullptr))); + + // Baron Geddon + triggers.push_back( + new TriggerNode("mc baron geddon fire resistance", + NextAction::array(0, new NextAction("mc baron geddon fire resistance", ACTION_RAID), nullptr))); triggers.push_back( new TriggerNode("mc living bomb debuff", - NextAction::array(0, new NextAction("mc check should move from group", ACTION_RAID), nullptr))); + NextAction::array(0, new NextAction("mc move from group", ACTION_RAID), nullptr))); triggers.push_back( new TriggerNode("mc baron geddon inferno", NextAction::array(0, new NextAction("mc move from baron geddon", ACTION_RAID), nullptr))); + + // Shazzrah + triggers.push_back( + new TriggerNode("mc shazzrah ranged", + NextAction::array(0, new NextAction("mc shazzrah move away", ACTION_RAID), nullptr))); + + // Sulfuron Harbinger + // Alternatively, shadow resistance is also possible. + triggers.push_back( + new TriggerNode("mc sulfuron harbinger fire resistance", + NextAction::array(0, new NextAction("mc sulfuron harbinger fire resistance", ACTION_RAID), nullptr))); + + // Golemagg the Incinerator + triggers.push_back( + new TriggerNode("mc golemagg fire resistance", + NextAction::array(0, new NextAction("mc golemagg fire resistance", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg mark boss", + NextAction::array(0, new NextAction("mc golemagg mark boss", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg is main tank", + NextAction::array(0, new NextAction("mc golemagg main tank attack golemagg", ACTION_RAID), nullptr))); + triggers.push_back( + new TriggerNode("mc golemagg is assist tank", + NextAction::array(0, new NextAction("mc golemagg assist tank attack core rager", ACTION_RAID), nullptr))); + + // Majordomo Executus + triggers.push_back( + new TriggerNode("mc majordomo shadow resistance", + NextAction::array(0, new NextAction("mc majordomo shadow resistance", ACTION_RAID), nullptr))); + + // Ragnaros + triggers.push_back( + new TriggerNode("mc ragnaros fire resistance", + NextAction::array(0, new NextAction("mc ragnaros fire resistance", ACTION_RAID), nullptr))); +} + +void RaidMcStrategy::InitMultipliers(std::vector& multipliers) +{ + multipliers.push_back(new GarrDisableDpsAoeMultiplier(botAI)); + multipliers.push_back(new BaronGeddonAbilityMultiplier(botAI)); + multipliers.push_back(new GolemaggMultiplier(botAI)); } diff --git a/src/strategy/raids/moltencore/RaidMcStrategy.h b/src/strategy/raids/moltencore/RaidMcStrategy.h index 82b7d8f25e..45b503e933 100644 --- a/src/strategy/raids/moltencore/RaidMcStrategy.h +++ b/src/strategy/raids/moltencore/RaidMcStrategy.h @@ -8,10 +8,10 @@ class RaidMcStrategy : public Strategy { public: - RaidMcStrategy(PlayerbotAI* ai) : Strategy(ai) {} - virtual std::string const getName() override { return "mc"; } - virtual void InitTriggers(std::vector& triggers) override; - // virtual void InitMultipliers(std::vector &multipliers) override; + RaidMcStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} + std::string const getName() override { return "moltencore"; } + void InitTriggers(std::vector& triggers) override; + void InitMultipliers(std::vector &multipliers) override; }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcTriggerContext.h b/src/strategy/raids/moltencore/RaidMcTriggerContext.h index 93fe6df0f8..b74958919c 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggerContext.h +++ b/src/strategy/raids/moltencore/RaidMcTriggerContext.h @@ -10,13 +10,39 @@ class RaidMcTriggerContext : public NamedObjectContext public: RaidMcTriggerContext() { + creators["mc lucifron shadow resistance"] = &RaidMcTriggerContext::lucifron_shadow_resistance; + creators["mc magmadar fire resistance"] = &RaidMcTriggerContext::magmadar_fire_resistance; + creators["mc gehennas shadow resistance"] = &RaidMcTriggerContext::gehennas_shadow_resistance; + creators["mc garr fire resistance"] = &RaidMcTriggerContext::garr_fire_resistance; + creators["mc baron geddon fire resistance"] = &RaidMcTriggerContext::baron_geddon_fire_resistance; creators["mc living bomb debuff"] = &RaidMcTriggerContext::living_bomb_debuff; creators["mc baron geddon inferno"] = &RaidMcTriggerContext::baron_geddon_inferno; + creators["mc shazzrah ranged"] = &RaidMcTriggerContext::shazzrah_ranged; + creators["mc sulfuron harbinger fire resistance"] = &RaidMcTriggerContext::sulfuron_harbinger_fire_resistance; + creators["mc golemagg fire resistance"] = &RaidMcTriggerContext::golemagg_fire_resistance; + creators["mc golemagg mark boss"] = &RaidMcTriggerContext::golemagg_mark_boss; + creators["mc golemagg is main tank"] = &RaidMcTriggerContext::golemagg_is_main_tank; + 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; } private: - static Trigger* living_bomb_debuff(PlayerbotAI* ai) { return new McLivingBombDebuffTrigger(ai); } - static Trigger* baron_geddon_inferno(PlayerbotAI* ai) { return new McBaronGeddonInfernoTrigger(ai); } + static Trigger* lucifron_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "lucifron"); } + static Trigger* magmadar_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "magmadar"); } + static Trigger* gehennas_shadow_resistance(PlayerbotAI* botAI) { return new BossShadowResistanceTrigger(botAI, "gehennas"); } + static Trigger* garr_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "garr"); } + static Trigger* baron_geddon_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "baron geddon"); } + static Trigger* living_bomb_debuff(PlayerbotAI* botAI) { return new McLivingBombDebuffTrigger(botAI); } + static Trigger* baron_geddon_inferno(PlayerbotAI* botAI) { return new McBaronGeddonInfernoTrigger(botAI); } + static Trigger* shazzrah_ranged(PlayerbotAI* botAI) { return new McShazzrahRangedTrigger(botAI); } + static Trigger* sulfuron_harbinger_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "sulfuron harbinger"); } + static Trigger* golemagg_fire_resistance(PlayerbotAI* botAI) { return new BossFireResistanceTrigger(botAI, "golemagg the incinerator"); } + static Trigger* golemagg_mark_boss(PlayerbotAI* botAI) { return new McGolemaggMarkBossTrigger(botAI); } + static Trigger* golemagg_is_main_tank(PlayerbotAI* botAI) { return new McGolemaggIsMainTankTrigger(botAI); } + 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"); } }; #endif diff --git a/src/strategy/raids/moltencore/RaidMcTriggers.cpp b/src/strategy/raids/moltencore/RaidMcTriggers.cpp index f77b70f20e..834d703d33 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggers.cpp +++ b/src/strategy/raids/moltencore/RaidMcTriggers.cpp @@ -1,22 +1,40 @@ #include "RaidMcTriggers.h" #include "SharedDefines.h" +#include "RaidMcHelpers.h" + +using namespace MoltenCoreHelpers; bool McLivingBombDebuffTrigger::IsActive() { - // if bot has barron geddon's living bomb, we need to add strat, otherwise we need to remove - // only do when fighting baron geddon (to avoid modifying strat set by player outside this fight) - if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) - { - if (boss->IsInCombat()) - return bot->HasAura(20475) != botAI->HasStrategy("move from group", BotState::BOT_STATE_COMBAT); - } - return false; + // No check for Baron Geddon, because bots may have the bomb even after Geddon died. + return bot->HasAura(SPELL_LIVING_BOMB); } bool McBaronGeddonInfernoTrigger::IsActive() { if (Unit* boss = AI_VALUE2(Unit*, "find target", "baron geddon")) - return boss->HasAura(19695); + return boss->HasAura(SPELL_INFERNO); return false; } + +bool McShazzrahRangedTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "shazzrah") && PlayerbotAI::IsRanged(bot); +} + +bool McGolemaggMarkBossTrigger::IsActive() +{ + // any tank may mark the boss + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsTank(bot); +} + +bool McGolemaggIsMainTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsMainTank(bot); +} + +bool McGolemaggIsAssistTankTrigger::IsActive() +{ + return AI_VALUE2(Unit*, "find target", "golemagg the incinerator") && PlayerbotAI::IsAssistTank(bot); +} diff --git a/src/strategy/raids/moltencore/RaidMcTriggers.h b/src/strategy/raids/moltencore/RaidMcTriggers.h index 9d2fb985a2..4cd84a2df2 100644 --- a/src/strategy/raids/moltencore/RaidMcTriggers.h +++ b/src/strategy/raids/moltencore/RaidMcTriggers.h @@ -19,4 +19,32 @@ class McBaronGeddonInfernoTrigger : public Trigger bool IsActive() override; }; +class McShazzrahRangedTrigger : public Trigger +{ +public: + McShazzrahRangedTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc shazzrah ranged") {} + bool IsActive() override; +}; + +class McGolemaggMarkBossTrigger : public Trigger +{ +public: + McGolemaggMarkBossTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg mark boss") {} + bool IsActive() override; +}; + +class McGolemaggIsMainTankTrigger : public Trigger +{ +public: + McGolemaggIsMainTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg is main tank") {} + bool IsActive() override; +}; + +class McGolemaggIsAssistTankTrigger : public Trigger +{ +public: + McGolemaggIsAssistTankTrigger(PlayerbotAI* botAI) : Trigger(botAI, "mc golemagg is assist tank") {} + bool IsActive() override; +}; + #endif From dde16674c3725b2be1ee393a2d6cfb0481dee962 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Mon, 8 Dec 2025 04:34:16 -0700 Subject: [PATCH 4/5] Fix: Stop pets from fighting in PVP prohibited zones (#1829) Stripped down version of #1818. No new features. Refactors IsPossibleTarget in AttackersValue.cpp to a better style and makes sure pets don't attack in prohibited zones. Testing: Confirmed that aggro pets no longer attack in PVP prohibited areas, but still do outside them. Zim'Torga in Zul'Drak is a good example to test this (ID 4323). Lookout for death knights with a Risen Ally (uncontrolled and naturally aggro) now they respect PVP prohibition like their master. Note: If you manually teleport a bot that is in mid combat to a PVP prohibited area, its aggro pet might still attack, because its master is still in combat strategy. Otherwise the pet will not attack if its master has switched to non-combat. --- ...11_25_00_ai_playerbot_pet_action_texts.sql | 255 ++++++++++++++++++ src/strategy/actions/AttackAction.cpp | 7 +- src/strategy/actions/PetsAction.cpp | 97 +++++-- src/strategy/values/AttackersValue.cpp | 137 +++++++--- 4 files changed, 427 insertions(+), 69 deletions(-) create mode 100644 data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql diff --git a/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql b/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql new file mode 100644 index 0000000000..b7c6c96826 --- /dev/null +++ b/data/sql/playerbots/updates/2025_11_25_00_ai_playerbot_pet_action_texts.sql @@ -0,0 +1,255 @@ +DELETE FROM ai_playerbot_texts WHERE name IN ( + 'pet_usage_error', + 'pet_no_pet_error', + 'pet_stance_report', + 'pet_no_target_error', + 'pet_target_dead_error', + 'pet_invalid_target_error', + 'pet_pvp_prohibited_error', + 'pet_attack_success', + 'pet_attack_failed', + 'pet_follow_success', + 'pet_stay_success', + 'pet_unknown_command_error', + 'pet_stance_set_success', + 'pet_type_pet', + 'pet_type_guardian', + 'pet_stance_aggressive', + 'pet_stance_defensive', + 'pet_stance_passive', + 'pet_stance_unknown' +); + +DELETE FROM ai_playerbot_texts_chance WHERE name IN ( + 'pet_usage_error', + 'pet_no_pet_error', + 'pet_stance_report', + 'pet_no_target_error', + 'pet_target_dead_error', + 'pet_invalid_target_error', + 'pet_pvp_prohibited_error', + 'pet_attack_success', + 'pet_attack_failed', + 'pet_follow_success', + 'pet_stay_success', + 'pet_unknown_command_error', + 'pet_stance_set_success', + 'pet_type_pet', + 'pet_type_guardian', + 'pet_stance_aggressive', + 'pet_stance_defensive', + 'pet_stance_passive', + 'pet_stance_unknown' +); + +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 +(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", +"неизвестная"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES +('pet_usage_error', 100), +('pet_no_pet_error', 100), +('pet_stance_report', 100), +('pet_no_target_error', 100), +('pet_target_dead_error', 100), +('pet_invalid_target_error', 100), +('pet_pvp_prohibited_error', 100), +('pet_attack_success', 100), +('pet_attack_failed', 100), +('pet_follow_success', 100), +('pet_stay_success', 100), +('pet_unknown_command_error', 100), +('pet_stance_set_success', 100), +('pet_type_pet', 100), +('pet_type_guardian', 100), +('pet_stance_aggressive', 100), +('pet_stance_defensive', 100), +('pet_stance_passive', 100), +('pet_stance_unknown', 100); \ No newline at end of file diff --git a/src/strategy/actions/AttackAction.cpp b/src/strategy/actions/AttackAction.cpp index 4ea8694378..38cb96835f 100644 --- a/src/strategy/actions/AttackAction.cpp +++ b/src/strategy/actions/AttackAction.cpp @@ -84,9 +84,10 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/) return false; } - if ((sPlayerbotAIConfig->IsInPvpProhibitedZone(bot->GetZoneId()) || - sPlayerbotAIConfig->IsInPvpProhibitedArea(bot->GetAreaId())) - && (target->IsPlayer() || target->IsPet())) + // Check if bot OR target is in prohibited zone/area + if ((target->IsPlayer() || target->IsPet()) && + (sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) || + sPlayerbotAIConfig->IsPvpProhibited(target->GetZoneId(), target->GetAreaId()))) { if (verbose) botAI->TellError("I cannot attack other players in PvP prohibited areas."); diff --git a/src/strategy/actions/PetsAction.cpp b/src/strategy/actions/PetsAction.cpp index 945a883672..9e9e3172c4 100644 --- a/src/strategy/actions/PetsAction.cpp +++ b/src/strategy/actions/PetsAction.cpp @@ -25,7 +25,9 @@ bool PetsAction::Execute(Event event) if (param.empty()) { // If no parameter is provided, show usage instructions and return. - botAI->TellError("Usage: pet "); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_usage_error", "Usage: pet ", {}); + botAI->TellError(text); return false; } @@ -52,7 +54,9 @@ bool PetsAction::Execute(Event event) // If no pets or guardians are found, notify and return. if (targets.empty()) { - botAI->TellError("You have no pet or guardian pet."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_no_pet_error", "You have no pet or guardian pet.", {}); + botAI->TellError(text); return false; } @@ -63,42 +67,54 @@ bool PetsAction::Execute(Event event) if (param == "aggressive") { react = REACT_AGGRESSIVE; - stanceText = "aggressive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_aggressive", "aggressive", {}); } else if (param == "defensive") { react = REACT_DEFENSIVE; - stanceText = "defensive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_defensive", "defensive", {}); } else if (param == "passive") { react = REACT_PASSIVE; - stanceText = "passive"; + stanceText = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_passive", "passive", {}); } // The "stance" command simply reports the current stance of each pet/guardian. else if (param == "stance") { for (Creature* target : targets) { - std::string type = target->IsPet() ? "pet" : "guardian"; + std::string type = target->IsPet() ? + sPlayerbotTextMgr->GetBotTextOrDefault("pet_type_pet", "pet", {}) : + sPlayerbotTextMgr->GetBotTextOrDefault("pet_type_guardian", "guardian", {}); std::string name = target->GetName(); std::string stance; switch (target->GetReactState()) { case REACT_AGGRESSIVE: - stance = "aggressive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_aggressive", "aggressive", {}); break; case REACT_DEFENSIVE: - stance = "defensive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_defensive", "defensive", {}); break; case REACT_PASSIVE: - stance = "passive"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_passive", "passive", {}); break; default: - stance = "unknown"; + stance = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_unknown", "unknown", {}); break; } - botAI->TellMaster("Current stance of " + type + " \"" + name + "\": " + stance + "."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_report", "Current stance of %type \"%name\": %stance.", + {{"type", type}, {"name", name}, {"stance", stance}}); + botAI->TellMaster(text); } return true; } @@ -121,17 +137,31 @@ bool PetsAction::Execute(Event event) // If no valid target is selected, show an error and return. if (!targetUnit) { - botAI->TellError("No valid target selected by master."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_no_target_error", "No valid target selected by master.", {}); + botAI->TellError(text); return false; } if (!targetUnit->IsAlive()) { - botAI->TellError("Target is not alive."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_target_dead_error", "Target is not alive.", {}); + botAI->TellError(text); return false; } if (!bot->IsValidAttackTarget(targetUnit)) { - botAI->TellError("Target is not a valid attack target for the bot."); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_invalid_target_error", "Target is not a valid attack target for the bot.", {}); + botAI->TellError(text); + return false; + } + if (sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) + && (targetUnit->IsPlayer() || targetUnit->IsPet())) + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_pvp_prohibited_error", "I cannot command my pet to attack players in PvP prohibited areas.", {}); + botAI->TellError(text); return false; } @@ -182,9 +212,17 @@ bool PetsAction::Execute(Event event) } // Inform the master if the command succeeded or failed. if (didAttack && sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to attack your target."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_attack_success", "Pet commanded to attack your target.", {}); + botAI->TellMaster(text); + } else if (!didAttack) - botAI->TellError("Pet did not attack. (Already attacking or unable to attack target)"); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_attack_failed", "Pet did not attack. (Already attacking or unable to attack target)", {}); + botAI->TellError(text); + } return didAttack; } // The "follow" command makes all pets/guardians follow the bot. @@ -192,7 +230,11 @@ bool PetsAction::Execute(Event event) { botAI->PetFollow(); if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to follow."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_follow_success", "Pet commanded to follow.", {}); + botAI->TellMaster(text); + } return true; } // The "stay" command causes all pets/guardians to stop and stay in place. @@ -229,14 +271,20 @@ bool PetsAction::Execute(Event event) } } if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet commanded to stay."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stay_success", "Pet commanded to stay.", {}); + botAI->TellMaster(text); + } return true; } // Unknown command: show usage instructions and return. else { - botAI->TellError("Unknown pet command: " + param + - ". Use: pet "); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_unknown_command_error", "Unknown pet command: %param. Use: pet ", + {{"param", param}}); + botAI->TellError(text); return false; } @@ -251,7 +299,12 @@ bool PetsAction::Execute(Event event) // Inform the master of the new stance if debug is enabled. if (sPlayerbotAIConfig->petChatCommandDebug == 1) - botAI->TellMaster("Pet stance set to " + stanceText + "."); + { + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "pet_stance_set_success", "Pet stance set to %stance.", + {{"stance", stanceText}}); + botAI->TellMaster(text); + } return true; -} +} \ No newline at end of file diff --git a/src/strategy/values/AttackersValue.cpp b/src/strategy/values/AttackersValue.cpp index 745369a3b5..d89f1f8da5 100644 --- a/src/strategy/values/AttackersValue.cpp +++ b/src/strategy/values/AttackersValue.cpp @@ -107,7 +107,6 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set& t { ThreatMgr* threatMgr = ref->GetSource(); Unit* attacker = threatMgr->GetOwner(); - Unit* victim = attacker->GetVictim(); if (player->IsValidAttackTarget(attacker) && player->GetDistance2d(attacker) < sPlayerbotAIConfig->sightDistance) @@ -142,57 +141,107 @@ bool AttackersValue::hasRealThreat(Unit* attacker) (attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast(attacker)); } -bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float range) +bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) { - Creature* c = attacker->ToCreature(); - bool rti = false; - if (attacker && bot->GetGroup()) - rti = bot->GetGroup()->GetTargetIcon(7) == attacker->GetGUID(); - PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); + if (!botAI) + return false; + + // Basic check + if (!attacker) + return false; + + // bool inCannon = botAI->IsInVehicle(false, true); + // bool enemy = botAI->GetAiObjectContext()->GetValue("enemy player target")->Get(); + + // Validity checks + if (!attacker->IsVisible() || !attacker->IsInWorld() || attacker->GetMapId() != bot->GetMapId()) + return false; - bool leaderHasThreat = false; - if (attacker && bot->GetGroup() && botAI->GetMaster()) - leaderHasThreat = attacker->GetThreatMgr().GetThreat(botAI->GetMaster()); + if (attacker->isDead() || attacker->HasSpiritOfRedemptionAura()) + return false; - bool isMemberBotGroup = false; - if (bot->GetGroup() && botAI->GetMaster()) + // Flag checks + if (attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NON_ATTACKABLE_2)) + return false; + + if (attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) || attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) + return false; + + // Relationship checks + if (attacker->IsFriendlyTo(bot)) + return false; + + // Critter exception + if (attacker->GetCreatureType() == CREATURE_TYPE_CRITTER && !attacker->IsInCombat()) + return false; + + // Visibility check + if (!bot->CanSeeOrDetect(attacker)) + return false; + + // PvP prohibition checks + if ((attacker->GetGUID().IsPlayer() || attacker->GetGUID().IsPet()) && + (sPlayerbotAIConfig->IsPvpProhibited(attacker->GetZoneId(), attacker->GetAreaId()) || + sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()))) { - PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(botAI->GetMaster()); - if (masterBotAI && !masterBotAI->IsRealPlayer()) - isMemberBotGroup = true; + // This will stop aggresive pets from starting an attack. + // This will stop currently attacking pets from continuing their attack. + // This will first require the bot to change from a combat strat. It will + // not be reached if the bot only switches targets, including NPC targets. + for (Unit::ControlSet::const_iterator itr = bot->m_Controlled.begin(); + itr != bot->m_Controlled.end(); ++itr) + { + Creature* creature = dynamic_cast(*itr); + if (creature && creature->GetVictim() == attacker) + { + creature->AttackStop(); + if (CharmInfo* charmInfo = creature->GetCharmInfo()) + charmInfo->SetIsCommandAttack(false); + } + } + + return false; } - // bool inCannon = botAI->IsInVehicle(false, true); - // bool enemy = botAI->GetAiObjectContext()->GetValue("enemy player target")->Get(); + // Unflagged player check + if (attacker->IsPlayer() && !attacker->IsPvP() && !attacker->IsFFAPvP() && + (!bot->duel || bot->duel->Opponent != attacker)) + return false; + + // Creature-specific checks + Creature* c = attacker->ToCreature(); + if (c) + { + if (c->IsInEvadeMode()) + return false; + + bool leaderHasThreat = false; + if (bot->GetGroup() && botAI->GetMaster()) + leaderHasThreat = attacker->GetThreatMgr().GetThreat(botAI->GetMaster()); + + bool isMemberBotGroup = false; + if (bot->GetGroup() && botAI->GetMaster()) + { + PlayerbotAI* masterBotAI = GET_PLAYERBOT_AI(botAI->GetMaster()); + if (masterBotAI && !masterBotAI->IsRealPlayer()) + isMemberBotGroup = true; + } + + bool canAttack = (!isMemberBotGroup && botAI->HasStrategy("attack tagged", BOT_STATE_NON_COMBAT)) || + leaderHasThreat || + (!c->hasLootRecipient() && + (!c->GetVictim() || + (c->GetVictim() && + ((!c->GetVictim()->IsPlayer() || bot->IsInSameGroupWith(c->GetVictim()->ToPlayer())) || + (botAI->GetMaster() && c->GetVictim() == botAI->GetMaster()))))) || + c->isTappedBy(bot); + + if (!canAttack) + return false; + } - return attacker && attacker->IsVisible() && attacker->IsInWorld() && attacker->GetMapId() == bot->GetMapId() && - !attacker->isDead() && - !attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_NON_ATTACKABLE_2) && - // (inCannon || !attacker->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE)) && - // attacker->CanSeeOrDetect(bot) && - // !(attacker->HasUnitState(UNIT_STATE_STUNNED) && botAI->HasAura("shackle undead", attacker)) && - // !((attacker->IsPolymorphed() || botAI->HasAura("sap", attacker) || /*attacker->IsCharmed() ||*/ - // attacker->isFeared()) && !rti) && - /*!sServerFacade->IsInRoots(attacker) &&*/ - !attacker->IsFriendlyTo(bot) && !attacker->HasSpiritOfRedemptionAura() && - // !(attacker->GetGUID().IsPet() && enemy) && - !(attacker->GetCreatureType() == CREATURE_TYPE_CRITTER && !attacker->IsInCombat()) && - !attacker->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC) && !attacker->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE) && - bot->CanSeeOrDetect(attacker) && - !(sPlayerbotAIConfig->IsPvpProhibited(attacker->GetZoneId(), attacker->GetAreaId()) && - (attacker->GetGUID().IsPlayer() || attacker->GetGUID().IsPet())) && - !(attacker->IsPlayer() && !attacker->IsPvP() && !attacker->IsFFAPvP() && - (!bot->duel || bot->duel->Opponent != attacker)) && - (!c || - (!c->IsInEvadeMode() && - ((!isMemberBotGroup && botAI->HasStrategy("attack tagged", BOT_STATE_NON_COMBAT)) || leaderHasThreat || - (!c->hasLootRecipient() && - (!c->GetVictim() || - (c->GetVictim() && - ((!c->GetVictim()->IsPlayer() || bot->IsInSameGroupWith(c->GetVictim()->ToPlayer())) || - (botAI->GetMaster() && c->GetVictim() == botAI->GetMaster()))))) || - c->isTappedBy(bot)))); + return true; } bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) From bb569b4d3987b7f15e702620eedb22d2ed5cc269 Mon Sep 17 00:00:00 2001 From: Tecc Date: Mon, 8 Dec 2025 12:35:06 +0100 Subject: [PATCH 5/5] Fix: Arena - PersonalRating and MMR issue for bot teams (#1789) # Fix: Arena PersonalRating and MMR issue for bot teams ## Problem Bot arena teams are created with artificial random ratings (1000-2000 range), but when bots join these teams, their personal ratings and matchmaker ratings (MMR) use default config values instead of being adjusted to match the team's artificial rating. This causes matchmaking issues since the system uses personal ratings for queue calculations. ## Root Cause The issue occurred because `SetRatingForAll()` was called during team creation but only affected the captain. When additional bots were added later via `AddMember()`, they received default values from `CONFIG_ARENA_START_PERSONAL_RATING` and `CONFIG_ARENA_START_MATCHMAKER_RATING` instead of values appropriate for the team's artificial rating. ## Solution After bots are added to arena teams, the fix: 1. Uses `SetRatingForAll()` to align all personal ratings with team rating 2. Adjusts matchmaker ratings based on team context vs default configuration 3. Saves changes to both database tables with proper data types ## Impact - Personal ratings now match team ratings for artificial bot teams - MMR values are adjusted for artificial bot team ratings instead of using default config values - Arena matchmaking functions correctly for bot teams with random ratings - Only affects new arena team assignments after deployment - Existing player teams and normal config behavior are unaffected ## Manual Database Update For existing installations, the provided SQL script could be used to fix bot teams created before this patch. ### Update personal rating ```sql UPDATE arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id SET atm.personalRating = at.rating WHERE a.username LIKE 'rndbot%' AND atm.personalRating != at.rating; ``` ### Update MMR for existing entries ```sql UPDATE character_arena_stats cas JOIN characters c ON cas.guid = c.guid JOIN auth.account a ON c.account = a.id JOIN arena_team_member atm ON cas.guid = atm.guid JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId SET cas.matchMakerRating = GREATEST(at.rating, 1500), -- Use team rating or 1500 minimum cas.maxMMR = GREATEST(cas.maxMMR, cas.matchMakerRating) -- Update maxMMR if needed WHERE a.username LIKE '%rndbot%' AND ( -- Update if MMR doesn't match team context (at.rating > 1500 AND cas.matchMakerRating < at.rating) OR (at.rating <= 1500 AND cas.matchMakerRating != 1500) OR cas.matchMakerRating IS NULL ) AND ( -- Map arena team type to character_arena_stats slot (at.type = 2 AND cas.slot = 0) OR -- 2v2 teams use slot 0 (at.type = 3 AND cas.slot = 1) OR -- 3v3 teams use slot 1 (at.type = 5 AND cas.slot = 2) -- 5v5 teams use slot 2 ); ``` ### Insert missing MMR records for bots without character_arena_stats entries ```sql INSERT INTO character_arena_stats (guid, slot, matchMakerRating, maxMMR) SELECT atm.guid, CASE WHEN at.type = 2 THEN 0 -- 2v2 -> slot 0 WHEN at.type = 3 THEN 1 -- 3v3 -> slot 1 WHEN at.type = 5 THEN 2 -- 5v5 -> slot 2 ELSE 0 END as slot, GREATEST(at.rating, 1500) as matchMakerRating, GREATEST(at.rating, 1500) as maxMMR FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id WHERE a.username LIKE '%rndbot%' AND NOT EXISTS ( SELECT 1 FROM character_arena_stats cas2 WHERE cas2.guid = atm.guid AND cas2.slot = CASE WHEN at.type = 2 THEN 0 WHEN at.type = 3 THEN 1 WHEN at.type = 5 THEN 2 ELSE 0 END ) AND at.rating > 0; ``` ## Related issues Fixes: #1787 Fixes: #1800 ## Verification Queries ### Query 1: Check personal rating alignment ```sql SELECT 'Personal Rating Check' as check_type, COUNT(*) as total_bot_members, SUM(CASE WHEN atm.personalRating = at.rating THEN 1 ELSE 0 END) as correct_ratings, SUM(CASE WHEN atm.personalRating != at.rating THEN 1 ELSE 0 END) as incorrect_ratings, ROUND(AVG(at.rating), 2) as avg_team_rating, ROUND(AVG(atm.personalRating), 2) as avg_personal_rating FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id WHERE a.username LIKE '%rndbot%'; ``` ### Query 2: Check MMR alignment ```sql SELECT 'MMR Alignment Check' as check_type, COUNT(*) as total_mmr_records, SUM(CASE WHEN at.rating > 1500 AND cas.matchMakerRating >= at.rating THEN 1 WHEN at.rating <= 1500 AND cas.matchMakerRating = 1500 THEN 1 ELSE 0 END) as correct_mmr, SUM(CASE WHEN at.rating > 1500 AND cas.matchMakerRating < at.rating THEN 1 WHEN at.rating <= 1500 AND cas.matchMakerRating != 1500 THEN 1 ELSE 0 END) as incorrect_mmr, ROUND(AVG(at.rating), 2) as avg_team_rating, ROUND(AVG(cas.matchMakerRating), 2) as avg_mmr, ROUND(AVG(cas.maxMMR), 2) as avg_max_mmr FROM arena_team_member atm JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id JOIN character_arena_stats cas ON atm.guid = cas.guid WHERE a.username LIKE '%rndbot%' AND ( (at.type = 2 AND cas.slot = 0) OR (at.type = 3 AND cas.slot = 1) OR (at.type = 5 AND cas.slot = 2) ); ``` ### Query 3: Detailed team-by-team analysis ```sql SELECT at.arenaTeamId, at.name as team_name, at.type as team_type, at.rating as team_rating, COUNT(atm.guid) as member_count, GROUP_CONCAT(DISTINCT atm.personalRating) as personal_ratings, GROUP_CONCAT(DISTINCT cas.matchMakerRating) as mmr_values, CASE WHEN COUNT(DISTINCT atm.personalRating) = 1 AND MIN(atm.personalRating) = at.rating THEN 'OK' ELSE 'MISMATCH' END as personal_rating_status, CASE WHEN COUNT(DISTINCT cas.matchMakerRating) = 1 AND ( (at.rating > 1500 AND MIN(cas.matchMakerRating) >= at.rating) OR (at.rating <= 1500 AND MIN(cas.matchMakerRating) = 1500) ) THEN 'OK' ELSE 'MISMATCH' END as mmr_status FROM arena_team at JOIN arena_team_member atm ON at.arenaTeamId = atm.arenaTeamId JOIN characters c ON atm.guid = c.guid JOIN auth.account a ON c.account = a.id LEFT JOIN character_arena_stats cas ON atm.guid = cas.guid AND cas.slot = CASE WHEN at.type = 2 THEN 0 WHEN at.type = 3 THEN 1 WHEN at.type = 5 THEN 2 ELSE 0 END WHERE a.username LIKE '%rndbot%' GROUP BY at.arenaTeamId, at.name, at.type, at.rating ORDER BY at.rating DESC; ``` --- src/factory/PlayerbotFactory.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/factory/PlayerbotFactory.cpp b/src/factory/PlayerbotFactory.cpp index 65ffc444ee..50a216c970 100644 --- a/src/factory/PlayerbotFactory.cpp +++ b/src/factory/PlayerbotFactory.cpp @@ -4099,6 +4099,7 @@ void PlayerbotFactory::InitImmersive() void PlayerbotFactory::InitArenaTeam() { + if (!sPlayerbotAIConfig->IsInRandomAccountList(bot->GetSession()->GetAccountId())) return; @@ -4185,10 +4186,34 @@ void PlayerbotFactory::InitArenaTeam() if (botcaptain && botcaptain->GetTeamId() == bot->GetTeamId()) // need? { + // Add bot to arena team arenateam->AddMember(bot->GetGUID()); - arenateam->SaveToDB(); + + // Only synchronize ratings once the team is full (avoid redundant work) + // The captain was added with incorrect ratings when the team was created, + // so we fix everyone's ratings once the roster is complete + if (arenateam->GetMembersSize() >= (uint32)arenateam->GetType()) + { + uint32 teamRating = arenateam->GetRating(); + + // Use SetRatingForAll to align all members with team rating + arenateam->SetRatingForAll(teamRating); + + // For bot-only teams, keep MMR synchronized with team rating + // This ensures matchmaking reflects the artificial team strength (1000-2000 range) + // instead of being influenced by the global CONFIG_ARENA_START_MATCHMAKER_RATING + for (auto& member : arenateam->GetMembers()) + { + // Set MMR to match personal rating (which already matches team rating) + member.MatchMakerRating = member.PersonalRating; + member.MaxMMR = std::max(member.MaxMMR, member.PersonalRating); + } + // Force save all member data to database + arenateam->SaveToDB(true); + } } } + arenateams.erase(arenateams.begin() + index); }