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:
committed by
luis
co-authored by
Claude
parent
d7bc07522c
commit
0d2bcd3c25
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user