feat(TOK): Implement complete Temple of Kotmogu battleground AI

Implement all 6 TOK functions with full orb interaction, role-based
strategy dispatch, combat engagement, escort formations, carrier
movement with center push logic, and survival retreat behavior.
Includes comprehensive null safety, edge case handling, and logging.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: luis <[email protected]>
This commit is contained in:
agatho
2026-02-06 14:00:26 -03:00
committed by luis
co-authored by Claude
parent d7bc07522c
commit 0d2bcd3c25
3 changed files with 851 additions and 13 deletions
@@ -43,7 +43,8 @@ Create a detailed implementation plan based on `{@artifacts_path}/spec.md`.
Plan created below. The spec defines 4 delivery phases. Each phase maps to one implementation step so that each step is a coherent, buildable, and testable unit. No automated unit tests exist for BattlegroundAI — verification is build + manual runtime testing — so test verification is "build compiles clean" at each step plus manual validation at the end.
### [ ] Step: Implement PickupOrb and basic ExecuteKotmoguStrategy dispatch
### [x] Step: Implement PickupOrb and basic ExecuteKotmoguStrategy dispatch
<!-- chat-id: afa8c1ab-a4f1-4787-8231-62002c0b5a86 -->
**Files modified:**
- `src/modules/Playerbot/PvP/BattlegroundAI.h` (add 3 new method declarations)
@@ -98,7 +99,8 @@ Plan created below. The spec defines 4 delivery phases. Each phase maps to one i
---
### [ ] Step: Implement HuntEnemyOrbCarrier and DefendOrbCarrier
### [x] Step: Implement HuntEnemyOrbCarrier and DefendOrbCarrier
<!-- chat-id: bb24fa42-8537-41c7-a194-96c34faa6e71 -->
**Files modified:**
- `src/modules/Playerbot/PvP/BattlegroundAI.cpp` (replace 2 stubs)
@@ -139,7 +141,8 @@ Plan created below. The spec defines 4 delivery phases. Each phase maps to one i
---
### [ ] Step: Implement EscortOrbCarrier and ExecuteOrbCarrierMovement
### [x] Step: Implement EscortOrbCarrier and ExecuteOrbCarrierMovement
<!-- chat-id: 4ffdbd6c-29d0-406f-9af1-fb34206b35bb -->
**Files modified:**
- `src/modules/Playerbot/PvP/BattlegroundAI.cpp` (replace 2 stubs)
@@ -186,7 +189,8 @@ Plan created below. The spec defines 4 delivery phases. Each phase maps to one i
---
### [ ] Step: Polish, edge cases, and full build verification
### [x] Step: Polish, edge cases, and full build verification
<!-- chat-id: 05be0c66-4e54-4e99-983a-fb8bee818fc2 -->
**Files modified:**
- `src/modules/Playerbot/PvP/BattlegroundAI.cpp` (modifications to all 6 functions)
+837 -8
View File
@@ -24,6 +24,8 @@
#include "../AI/Coordination/Battleground/BattlegroundCoordinator.h"
#include "../AI/Coordination/Battleground/BGSpatialQueryCache.h"
#include "../AI/Coordination/Battleground/Scripts/IBGScript.h"
#include "../AI/Coordination/Battleground/Scripts/Domination/TempleOfKotmoguScript.h"
#include "../AI/Coordination/Battleground/Scripts/Domination/TempleOfKotmoguData.h"
#include "../Movement/BotMovementUtil.h"
#include <algorithm>
#include <cmath>
@@ -33,6 +35,21 @@
constexpr uint32 ALLIANCE_FLAG_AURA = 23333; // Carrying Horde flag
constexpr uint32 HORDE_FLAG_AURA = 23335; // Carrying Alliance flag
// TOK Orb aura IDs
namespace TokData = Playerbot::Coordination::Battleground::TempleOfKotmogu;
constexpr uint32 TOK_ORB_AURAS[] = {
TokData::Spells::ORANGE_ORB_AURA, // 121175
TokData::Spells::BLUE_ORB_AURA, // 121176
TokData::Spells::GREEN_ORB_AURA, // 121177
TokData::Spells::PURPLE_ORB_AURA // 121178
};
constexpr uint32 TOK_ORB_ENTRIES[] = {
TokData::GameObjects::ORANGE_ORB, // 212094
TokData::GameObjects::BLUE_ORB, // 212091
TokData::GameObjects::GREEN_ORB, // 212093
TokData::GameObjects::PURPLE_ORB // 212092
};
namespace Playerbot
{
@@ -1566,8 +1583,42 @@ bool BattlegroundAI::DefendGate(::Player* player)
// ============================================================================
// TEMPLE OF KOTMOGU STRATEGY
// ============================================================================
// Runtime behavior has been moved to TempleOfKotmoguScript (lighthouse pattern).
// This is a thin delegation wrapper.
/// Helper: check if player is carrying any TOK orb via aura check
static bool IsCarryingOrb(::Player* player)
{
for (uint32 aura : TOK_ORB_AURAS)
{
if (player->HasAura(aura))
return true;
}
return false;
}
/// Helper: get orbId (0-3) from orb aura, or -1 if not carrying
static int32 GetCarriedOrbId(::Player* player)
{
for (uint32 i = 0; i < TokData::ORB_COUNT; ++i)
{
if (player->HasAura(TOK_ORB_AURAS[i]))
return static_cast<int32>(i);
}
return -1;
}
/// Helper: get the TOK script from coordinator, returns nullptr on failure
static Coordination::Battleground::TempleOfKotmoguScript* GetTokScript(
BattlegroundCoordinator* coordinator)
{
if (!coordinator)
return nullptr;
auto* script = coordinator->GetScript();
if (!script || script->GetBGType() != BGType::TEMPLE_OF_KOTMOGU)
return nullptr;
return static_cast<Coordination::Battleground::TempleOfKotmoguScript*>(script);
}
void BattlegroundAI::ExecuteKotmoguStrategy(::Player* player)
{
@@ -1575,18 +1626,796 @@ void BattlegroundAI::ExecuteKotmoguStrategy(::Player* player)
return;
BattlegroundCoordinator* coordinator = sBGCoordinatorMgr->GetCoordinatorForPlayer(player);
if (coordinator)
auto* tokScript = GetTokScript(coordinator);
BGRole role = GetPlayerRole(player);
bool holdingOrb = IsCarryingOrb(player);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} role={} holdingOrb={}",
player->GetName(), static_cast<uint32>(role), holdingOrb);
// =========================================================================
// PRIORITY 1: If holding orb, execute carrier movement
// =========================================================================
if (holdingOrb)
{
auto* script = coordinator->GetScript();
if (script && script->GetBGType() == BGType::TEMPLE_OF_KOTMOGU)
ExecuteOrbCarrierMovement(player);
return;
}
// =========================================================================
// PRIORITY 2: Execute role-based behavior
// =========================================================================
switch (role)
{
case BGRole::ORB_CARRIER:
PickupOrb(player);
break;
case BGRole::FLAG_ESCORT:
EscortOrbCarrier(player);
break;
case BGRole::FLAG_HUNTER:
case BGRole::NODE_ATTACKER:
HuntEnemyOrbCarrier(player);
break;
case BGRole::NODE_DEFENDER:
DefendOrbCarrier(player);
break;
case BGRole::HEALER_SUPPORT:
// Healers prioritize escorting carriers, fall back to defend
EscortOrbCarrier(player);
break;
case BGRole::ROAMER:
case BGRole::UNASSIGNED:
default:
// Default: try to pick up an orb, or hunt enemies if all held
if (!PickupOrb(player))
HuntEnemyOrbCarrier(player);
break;
}
}
bool BattlegroundAI::PickupOrb(::Player* player)
{
if (!player || !player->IsInWorld() || !player->IsAlive())
return false;
// Already carrying an orb - nothing to pick up
if (IsCarryingOrb(player))
{
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} already carrying an orb, skipping pickup",
player->GetName());
return false;
}
BattlegroundCoordinator* coordinator = sBGCoordinatorMgr->GetCoordinatorForPlayer(player);
auto* tokScript = GetTokScript(coordinator);
// Get prioritized orb list from script (or use default order)
std::vector<uint32> orbPriority;
if (tokScript)
orbPriority = tokScript->GetOrbPriority(player->GetBGTeam());
else
orbPriority = { TokData::Orbs::ORANGE, TokData::Orbs::BLUE,
TokData::Orbs::GREEN, TokData::Orbs::PURPLE };
// Find nearest unheld orb
float bestDist = std::numeric_limits<float>::max();
uint32 bestOrbId = TokData::ORB_COUNT; // invalid sentinel
Position bestOrbPos;
for (uint32 orbId : orbPriority)
{
if (orbId >= TokData::ORB_COUNT)
continue;
// Skip orbs that are currently held
if (tokScript && tokScript->IsOrbHeld(orbId))
continue;
// Get orb position (prefer dynamic discovery)
Position orbPos = tokScript
? tokScript->GetDynamicOrbPosition(orbId)
: TokData::GetOrbPosition(orbId);
float dist = player->GetExactDist(&orbPos);
if (dist < bestDist)
{
if (script->ExecuteStrategy(player))
return;
bestDist = dist;
bestOrbId = orbId;
bestOrbPos = orbPos;
}
}
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} no coordinator/script available, idle",
// No available orb found
if (bestOrbId >= TokData::ORB_COUNT)
{
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} no available orbs to pick up (all held)",
player->GetName());
return false;
}
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} targeting {} (dist: {:.1f})",
player->GetName(), TokData::GetOrbName(bestOrbId), bestDist);
// Move toward the orb if too far
if (bestDist > OBJECTIVE_RANGE)
{
BotMovementUtil::MoveToPosition(player, bestOrbPos);
return true; // returning true = we're working on it
}
// Within range - search for the orb GameObject and use it
uint32 orbEntry = TOK_ORB_ENTRIES[bestOrbId];
std::list<GameObject*> goList;
player->GetGameObjectListWithEntryInGrid(goList, orbEntry, OBJECTIVE_RANGE);
for (GameObject* go : goList)
{
if (!go || !go->IsWithinDistInMap(player, OBJECTIVE_RANGE))
continue;
GameObjectTemplate const* goInfo = go->GetGOInfo();
if (!goInfo)
continue;
// Orbs are GAMEOBJECT_TYPE_FLAGSTAND in TOK
if (goInfo->type == GAMEOBJECT_TYPE_FLAGSTAND ||
goInfo->type == GAMEOBJECT_TYPE_GOOBER)
{
go->Use(player);
TC_LOG_INFO("playerbots.bg", "[TOK] {} picked up {} (entry {})",
player->GetName(), TokData::GetOrbName(bestOrbId), orbEntry);
return true;
}
}
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} at orb location but no interactable GO found for {} (entry {})",
player->GetName(), TokData::GetOrbName(bestOrbId), orbEntry);
return false;
}
bool BattlegroundAI::DefendOrbCarrier(::Player* player)
{
if (!player || !player->IsInWorld() || !player->IsAlive())
return false;
BattlegroundCoordinator* coordinator = sBGCoordinatorMgr->GetCoordinatorForPlayer(player);
auto* tokScript = GetTokScript(coordinator);
// =========================================================================
// PHASE 1: Find nearest friendly orb carrier
// =========================================================================
::Player* friendlyCarrier = nullptr;
float carrierDist = std::numeric_limits<float>::max();
if (tokScript)
{
for (uint32 orbId = 0; orbId < TokData::ORB_COUNT; ++orbId)
{
if (!tokScript->IsOrbHeld(orbId))
continue;
ObjectGuid holderGuid = tokScript->GetOrbHolder(orbId);
if (holderGuid.IsEmpty())
continue;
::Player* holder = ObjectAccessor::FindPlayer(holderGuid);
if (!holder || !holder->IsAlive() || holder->IsHostileTo(player))
continue; // skip enemy carriers
float dist = player->GetExactDist(holder);
if (dist < carrierDist)
{
carrierDist = dist;
friendlyCarrier = holder;
}
}
}
// =========================================================================
// PHASE 2: If friendly carrier found, defend them
// =========================================================================
if (friendlyCarrier)
{
constexpr float DEFENSE_ESCORT_RANGE = 30.0f;
constexpr float ESCORT_DISTANCE = 8.0f;
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} defending carrier {} (dist: {:.1f})",
player->GetName(), friendlyCarrier->GetName(), carrierDist);
// If too far from carrier, move closer
if (carrierDist > DEFENSE_ESCORT_RANGE)
{
BotMovementUtil::MoveToPosition(player, friendlyCarrier->GetPosition());
return true;
}
// Check for enemies near the carrier — engage them
if (coordinator)
{
auto nearbyEnemies = coordinator->QueryNearbyEnemies(
friendlyCarrier->GetPosition(), DEFENSE_ESCORT_RANGE);
::Player* closestThreat = nullptr;
float closestThreatDist = DEFENSE_ESCORT_RANGE + 1.0f;
for (auto const* snapshot : nearbyEnemies)
{
if (!snapshot || !snapshot->isAlive)
continue;
::Player* enemy = ObjectAccessor::FindPlayer(snapshot->guid);
if (!enemy || !enemy->IsAlive())
continue;
float dist = player->GetExactDist(enemy);
if (dist < closestThreatDist)
{
closestThreatDist = dist;
closestThreat = enemy;
}
}
if (closestThreat)
{
player->SetSelection(closestThreat->GetGUID());
if (closestThreatDist > 5.0f)
BotMovementUtil::ChaseTarget(player, closestThreat, 5.0f);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} engaging threat {} near carrier (dist: {:.1f})",
player->GetName(), closestThreat->GetName(), closestThreatDist);
return true;
}
}
else
{
// Fallback: legacy O(n) enemy search near carrier
std::list<Player*> nearbyPlayers;
friendlyCarrier->GetPlayerListInGrid(nearbyPlayers, DEFENSE_ESCORT_RANGE);
::Player* closestThreat = nullptr;
float closestThreatDist = DEFENSE_ESCORT_RANGE + 1.0f;
for (::Player* nearby : nearbyPlayers)
{
if (!nearby || !nearby->IsAlive() || !nearby->IsHostileTo(player))
continue;
float dist = player->GetExactDist(nearby);
if (dist < closestThreatDist)
{
closestThreatDist = dist;
closestThreat = nearby;
}
}
if (closestThreat)
{
player->SetSelection(closestThreat->GetGUID());
if (closestThreatDist > 5.0f)
BotMovementUtil::ChaseTarget(player, closestThreat, 5.0f);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} engaging threat {} near carrier (legacy, dist: {:.1f})",
player->GetName(), closestThreat->GetName(), closestThreatDist);
return true;
}
}
// No threats — maintain escort distance behind carrier
if (carrierDist > ESCORT_DISTANCE * 1.5f || !BotMovementUtil::IsMoving(player))
{
float angle = friendlyCarrier->GetOrientation() + static_cast<float>(M_PI);
Position escortPos;
escortPos.Relocate(
friendlyCarrier->GetPositionX() + ESCORT_DISTANCE * 0.7f * std::cos(angle),
friendlyCarrier->GetPositionY() + ESCORT_DISTANCE * 0.7f * std::sin(angle),
friendlyCarrier->GetPositionZ()
);
BotMovementUtil::CorrectPositionToGround(player, escortPos);
BotMovementUtil::MoveToPosition(player, escortPos);
}
return true;
}
// =========================================================================
// PHASE 3: No friendly carrier — patrol center area
// =========================================================================
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} no friendly carrier found, patrolling center",
player->GetName());
if (!BotMovementUtil::IsMoving(player))
{
Position patrolPos;
float angle = frand(0.0f, 2.0f * static_cast<float>(M_PI));
float dist = frand(5.0f, 15.0f);
patrolPos.Relocate(
TokData::CENTER_X + dist * std::cos(angle),
TokData::CENTER_Y + dist * std::sin(angle),
TokData::CENTER_Z
);
BotMovementUtil::CorrectPositionToGround(player, patrolPos);
BotMovementUtil::MoveToPosition(player, patrolPos);
}
return true;
}
bool BattlegroundAI::HuntEnemyOrbCarrier(::Player* player)
{
if (!player || !player->IsInWorld() || !player->IsAlive())
return false;
BattlegroundCoordinator* coordinator = sBGCoordinatorMgr->GetCoordinatorForPlayer(player);
auto* tokScript = GetTokScript(coordinator);
// =========================================================================
// PHASE 1: Find enemy orb carriers via script orb tracking
// =========================================================================
::Player* bestTarget = nullptr;
float bestDist = std::numeric_limits<float>::max();
if (tokScript)
{
for (uint32 orbId = 0; orbId < TokData::ORB_COUNT; ++orbId)
{
if (!tokScript->IsOrbHeld(orbId))
continue;
ObjectGuid holderGuid = tokScript->GetOrbHolder(orbId);
if (holderGuid.IsEmpty())
continue;
::Player* holder = ObjectAccessor::FindPlayer(holderGuid);
if (!holder || !holder->IsAlive() || !holder->IsHostileTo(player))
continue;
float dist = player->GetExactDist(holder);
// Prefer carriers in center zone (they score more points)
bool inCenter = tokScript->IsInCenter(
holder->GetPositionX(), holder->GetPositionY());
if (inCenter)
dist *= 0.5f; // Effectively double priority for center carriers
if (dist < bestDist)
{
bestDist = dist;
bestTarget = holder;
}
}
}
// =========================================================================
// PHASE 2: If enemy carrier found, chase and engage
// =========================================================================
if (bestTarget)
{
float actualDist = player->GetExactDist(bestTarget);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} hunting enemy orb carrier {} (dist: {:.1f})",
player->GetName(), bestTarget->GetName(), actualDist);
if (actualDist > 30.0f)
{
BotMovementUtil::MoveToPosition(player, bestTarget->GetPosition());
}
else
{
player->SetSelection(bestTarget->GetGUID());
if (actualDist > 5.0f)
BotMovementUtil::ChaseTarget(player, bestTarget, 5.0f);
}
return true;
}
// =========================================================================
// PHASE 3: No enemy carrier found — attack nearest enemy via spatial cache
// =========================================================================
if (coordinator)
{
float enemyDist = 0.0f;
auto const* nearestEnemy = coordinator->GetNearestEnemy(
player->GetPosition(), 40.0f, &enemyDist);
if (nearestEnemy && nearestEnemy->isAlive)
{
::Player* enemy = ObjectAccessor::FindPlayer(nearestEnemy->guid);
if (enemy && enemy->IsAlive())
{
player->SetSelection(enemy->GetGUID());
if (enemyDist > 5.0f)
BotMovementUtil::ChaseTarget(player, enemy, 5.0f);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} no enemy carrier, engaging nearby enemy {} (dist: {:.1f})",
player->GetName(), enemy->GetName(), enemyDist);
return true;
}
}
}
else
{
// Fallback: legacy O(n) search if no coordinator
std::list<Player*> nearbyPlayers;
player->GetPlayerListInGrid(nearbyPlayers, 40.0f);
::Player* closestEnemy = nullptr;
float closestDist = 41.0f;
for (::Player* nearby : nearbyPlayers)
{
if (!nearby || !nearby->IsAlive() || !nearby->IsHostileTo(player))
continue;
float dist = player->GetExactDist(nearby);
if (dist < closestDist)
{
closestDist = dist;
closestEnemy = nearby;
}
}
if (closestEnemy)
{
player->SetSelection(closestEnemy->GetGUID());
if (closestDist > 5.0f)
BotMovementUtil::ChaseTarget(player, closestEnemy, 5.0f);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} engaging nearby enemy {} (legacy, dist: {:.1f})",
player->GetName(), closestEnemy->GetName(), closestDist);
return true;
}
}
// =========================================================================
// PHASE 4: No enemies nearby — move toward center
// =========================================================================
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} no enemies found, moving toward center",
player->GetName());
Position centerPos(TokData::CENTER_X, TokData::CENTER_Y, TokData::CENTER_Z, 0.0f);
BotMovementUtil::MoveToPosition(player, centerPos);
return true;
}
bool BattlegroundAI::EscortOrbCarrier(::Player* player)
{
if (!player || !player->IsInWorld() || !player->IsAlive())
return false;
BattlegroundCoordinator* coordinator = sBGCoordinatorMgr->GetCoordinatorForPlayer(player);
auto* tokScript = GetTokScript(coordinator);
// =========================================================================
// PHASE 1: Find nearest friendly orb carrier
// =========================================================================
::Player* friendlyCarrier = nullptr;
float carrierDist = std::numeric_limits<float>::max();
if (tokScript)
{
for (uint32 orbId = 0; orbId < TokData::ORB_COUNT; ++orbId)
{
if (!tokScript->IsOrbHeld(orbId))
continue;
ObjectGuid holderGuid = tokScript->GetOrbHolder(orbId);
if (holderGuid.IsEmpty())
continue;
::Player* holder = ObjectAccessor::FindPlayer(holderGuid);
if (!holder || !holder->IsAlive() || holder->IsHostileTo(player))
continue; // skip enemy carriers
float dist = player->GetExactDist(holder);
if (dist < carrierDist)
{
carrierDist = dist;
friendlyCarrier = holder;
}
}
}
// =========================================================================
// PHASE 2: If carrier found, take escort formation
// =========================================================================
if (friendlyCarrier)
{
constexpr float MAX_ESCORT_DISTANCE = 40.0f;
constexpr float ESCORT_DISTANCE = 8.0f;
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} escorting carrier {} (dist: {:.1f})",
player->GetName(), friendlyCarrier->GetName(), carrierDist);
// If too far, just run toward the carrier
if (carrierDist > MAX_ESCORT_DISTANCE)
{
BotMovementUtil::MoveToPosition(player, friendlyCarrier->GetPosition());
return true;
}
// Try to get formation position from script
Position escortPos;
if (tokScript && carrierDist < MAX_ESCORT_DISTANCE)
{
auto formation = tokScript->GetEscortFormation(
friendlyCarrier->GetPositionX(),
friendlyCarrier->GetPositionY(),
friendlyCarrier->GetPositionZ());
if (!formation.empty())
{
uint32 idx = player->GetGUID().GetCounter() % formation.size();
escortPos = formation[idx];
BotMovementUtil::CorrectPositionToGround(player, escortPos);
}
}
// Fallback: offset behind carrier using angle
if (escortPos.GetPositionX() == 0.0f)
{
float angle = friendlyCarrier->GetOrientation() + static_cast<float>(M_PI);
escortPos.Relocate(
friendlyCarrier->GetPositionX() + ESCORT_DISTANCE * 0.7f * std::cos(angle),
friendlyCarrier->GetPositionY() + ESCORT_DISTANCE * 0.7f * std::sin(angle),
friendlyCarrier->GetPositionZ()
);
BotMovementUtil::CorrectPositionToGround(player, escortPos);
}
// Move to escort position if needed
if (carrierDist > ESCORT_DISTANCE * 1.5f || !BotMovementUtil::IsMoving(player))
BotMovementUtil::MoveToPosition(player, escortPos);
// If carrier is in combat, help kill attackers
if (friendlyCarrier->IsInCombat())
{
if (coordinator)
{
auto nearbyEnemies = coordinator->QueryNearbyEnemies(
friendlyCarrier->GetPosition(), 20.0f);
for (auto const* snapshot : nearbyEnemies)
{
if (!snapshot || !snapshot->isAlive)
continue;
::Player* enemy = ObjectAccessor::FindPlayer(snapshot->guid);
if (enemy && enemy->IsAlive())
{
player->SetSelection(enemy->GetGUID());
if (player->GetExactDist(enemy) > 5.0f)
BotMovementUtil::ChaseTarget(player, enemy, 5.0f);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} engaging {} threatening carrier (dist: {:.1f})",
player->GetName(), enemy->GetName(), player->GetExactDist(enemy));
break;
}
}
}
else
{
// Fallback: legacy O(n) search near carrier
std::list<Player*> nearbyPlayers;
friendlyCarrier->GetPlayerListInGrid(nearbyPlayers, 20.0f);
for (::Player* nearby : nearbyPlayers)
{
if (nearby && nearby->IsAlive() && nearby->IsHostileTo(player))
{
player->SetSelection(nearby->GetGUID());
if (player->GetExactDist(nearby) > 5.0f)
BotMovementUtil::ChaseTarget(player, nearby, 5.0f);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} engaging {} threatening carrier (legacy)",
player->GetName(), nearby->GetName());
break;
}
}
}
}
return true;
}
// =========================================================================
// PHASE 3: No friendly carrier — fall back to defend behavior
// =========================================================================
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} no friendly carrier to escort, falling back to defend",
player->GetName());
return DefendOrbCarrier(player);
}
bool BattlegroundAI::ExecuteOrbCarrierMovement(::Player* player)
{
if (!player || !player->IsInWorld() || !player->IsAlive())
return false;
BattlegroundCoordinator* coordinator = sBGCoordinatorMgr->GetCoordinatorForPlayer(player);
auto* tokScript = GetTokScript(coordinator);
// Determine which orb this player is carrying
int32 orbId = GetCarriedOrbId(player);
if (orbId < 0)
{
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} ExecuteOrbCarrierMovement called but not carrying orb",
player->GetName());
return false;
}
bool inCenter = tokScript
? tokScript->IsInCenter(player->GetPositionX(), player->GetPositionY())
: TokData::IsInCenterZone(player->GetPositionX(), player->GetPositionY());
// =========================================================================
// SURVIVAL CHECK: If health low and outnumbered, retreat
// =========================================================================
constexpr float LOW_HEALTH_PCT = 30.0f;
float healthPct = player->GetHealthPct();
if (healthPct < LOW_HEALTH_PCT && coordinator)
{
Position playerPos = player->GetPosition();
uint32 nearbyEnemies = coordinator->CountEnemiesInRadius(playerPos, 30.0f);
uint32 nearbyAllies = coordinator->CountAlliesInRadius(playerPos, 30.0f);
if (nearbyEnemies > nearbyAllies)
{
// Retreat toward nearest ally cluster
auto const* nearestAlly = coordinator->GetNearestAlly(
playerPos, 60.0f, player->GetGUID());
if (nearestAlly)
{
::Player* ally = ObjectAccessor::FindPlayer(nearestAlly->guid);
if (ally && ally->IsAlive())
{
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier LOW HP ({:.0f}%), retreating toward {} (enemies={} allies={})",
player->GetName(), healthPct, ally->GetName(), nearbyEnemies, nearbyAllies);
BotMovementUtil::MoveToPosition(player, ally->GetPosition());
return true;
}
}
// No ally found — retreat toward own spawn
Position spawnPos = (player->GetBGTeam() == ALLIANCE)
? Position(TokData::ALLIANCE_SPAWNS[0])
: Position(TokData::HORDE_SPAWNS[0]);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier LOW HP ({:.0f}%), retreating to spawn",
player->GetName(), healthPct);
BotMovementUtil::MoveToPosition(player, spawnPos);
return true;
}
}
// =========================================================================
// DECISION: Should we push to center?
// =========================================================================
bool shouldPushCenter = tokScript
? tokScript->ShouldPushToCenter(player->GetBGTeam())
: false;
if (shouldPushCenter)
{
// =====================================================================
// CENTER PUSH: Navigate along pre-calculated route to center
// =====================================================================
std::vector<Position> route = tokScript
? tokScript->GetOrbCarrierRoute(static_cast<uint32>(orbId))
: TokData::GetOrbCarrierRoute(static_cast<uint32>(orbId));
if (!route.empty())
{
// Find the next waypoint we haven't reached yet
// (the first waypoint farther than OBJECTIVE_RANGE from us)
Position targetWaypoint = route.back(); // default: center
for (size_t i = 1; i < route.size(); ++i) // skip index 0 (orb spawn)
{
float wpDist = player->GetExactDist(&route[i]);
if (wpDist > OBJECTIVE_RANGE)
{
targetWaypoint = route[i];
break;
}
}
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier pushing to center with {} (dist: {:.1f})",
player->GetName(), TokData::GetOrbName(static_cast<uint32>(orbId)),
player->GetExactDist(&targetWaypoint));
BotMovementUtil::MoveToPosition(player, targetWaypoint);
return true;
}
// Fallback: move directly to center
Position centerPos(TokData::CENTER_X, TokData::CENTER_Y, TokData::CENTER_Z, 0.0f);
BotMovementUtil::MoveToPosition(player, centerPos);
return true;
}
// =========================================================================
// HOLD: Already in center — hold position
// =========================================================================
if (inCenter)
{
// Check if we're outnumbered in center
if (coordinator)
{
Position centerPos(TokData::CENTER_X, TokData::CENTER_Y, TokData::CENTER_Z, 0.0f);
uint32 enemiesInCenter = coordinator->CountEnemiesInRadius(centerPos, TokData::CENTER_RADIUS);
uint32 alliesInCenter = coordinator->CountAlliesInRadius(centerPos, TokData::CENTER_RADIUS);
if (enemiesInCenter > alliesInCenter + 1)
{
// Outnumbered — retreat to a safe orb defense position
auto defensePositions = tokScript
? tokScript->GetOrbDefensePositions(static_cast<uint32>(orbId))
: TokData::GetOrbDefensePositions(static_cast<uint32>(orbId));
if (!defensePositions.empty())
{
uint32 idx = player->GetGUID().GetCounter() % defensePositions.size();
Position defPos = defensePositions[idx];
BotMovementUtil::CorrectPositionToGround(player, defPos);
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier outnumbered in center (enemies={} allies={}), retreating to defense pos",
player->GetName(), enemiesInCenter, alliesInCenter);
BotMovementUtil::MoveToPosition(player, defPos);
return true;
}
}
}
// Safe in center — hold position (small random movement to avoid being static)
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier holding center with {}",
player->GetName(), TokData::GetOrbName(static_cast<uint32>(orbId)));
if (!BotMovementUtil::IsMoving(player))
{
float angle = frand(0.0f, 2.0f * static_cast<float>(M_PI));
float dist = frand(2.0f, 8.0f);
Position holdPos;
holdPos.Relocate(
TokData::CENTER_X + dist * std::cos(angle),
TokData::CENTER_Y + dist * std::sin(angle),
TokData::CENTER_Z
);
BotMovementUtil::CorrectPositionToGround(player, holdPos);
BotMovementUtil::MoveToPosition(player, holdPos);
}
return true;
}
// =========================================================================
// DEFENSIVE: Not pushing center — hold near orb defense position
// =========================================================================
auto defensePositions = tokScript
? tokScript->GetOrbDefensePositions(static_cast<uint32>(orbId))
: TokData::GetOrbDefensePositions(static_cast<uint32>(orbId));
if (!defensePositions.empty())
{
// Pick a position near our orb's defense zone
uint32 idx = player->GetGUID().GetCounter() % defensePositions.size();
Position defPos = defensePositions[idx];
float defDist = player->GetExactDist(&defPos);
if (defDist > OBJECTIVE_RANGE || !BotMovementUtil::IsMoving(player))
{
BotMovementUtil::CorrectPositionToGround(player, defPos);
BotMovementUtil::MoveToPosition(player, defPos);
}
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier holding defensively with {} (dist to def: {:.1f})",
player->GetName(), TokData::GetOrbName(static_cast<uint32>(orbId)), defDist);
return true;
}
// Final fallback: hold at current position
TC_LOG_DEBUG("playerbots.bg", "[TOK] {} carrier holding position with {} (no route/defense data)",
player->GetName(), TokData::GetOrbName(static_cast<uint32>(orbId)));
return true;
}
// ============================================================================
+6 -1
View File
@@ -295,8 +295,13 @@ public:
bool AttackGate(::Player* player);
bool DefendGate(::Player* player);
// Temple of Kotmogu (delegates to TempleOfKotmoguScript::ExecuteStrategy)
// Temple of Kotmogu
void ExecuteKotmoguStrategy(::Player* player);
bool PickupOrb(::Player* player);
bool DefendOrbCarrier(::Player* player);
bool HuntEnemyOrbCarrier(::Player* player);
bool EscortOrbCarrier(::Player* player);
bool ExecuteOrbCarrierMovement(::Player* player);
// Silvershard Mines
void ExecuteSilvershardStrategy(::Player* player);