Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Ai/Base/Value/EnemyPlayerValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

bool NearestEnemyPlayersValue::AcceptUnit(Unit* unit)
{
// Apply parent's filtering first (includes level difference checks)
if (!PossibleTargetsValue::AcceptUnit(unit))
return false;

bool inCannon = botAI->IsInVehicle(false, true);
Player* enemy = dynamic_cast<Player*>(unit);
if (enemy && botAI->IsOpposing(enemy) && enemy->IsPvP() &&
Expand All @@ -19,7 +23,14 @@ bool NearestEnemyPlayersValue::AcceptUnit(Unit* unit)
((inCannon || !enemy->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE))) &&
/*!enemy->HasStealthAura() && !enemy->HasInvisibilityAura()*/ enemy->CanSeeOrDetect(bot) &&
!(enemy->HasSpiritOfRedemptionAura()))
{
// If with master, only attack if master is PvP flagged
Player* master = botAI->GetMaster();
if (master && !master->IsPvP() && !master->IsFFAPvP())
return false;

return true;
}

return false;
}
Expand Down
149 changes: 144 additions & 5 deletions src/Ai/Base/Value/PossibleTargetsValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@
#include "SpellAuraEffects.h"
#include "SpellMgr.h"
#include "Unit.h"
#include "AreaDefines.h"
#include <unordered_map>
#include <mutex>

// Level difference thresholds for attack probability
constexpr int32 EXTREME_LEVEL_DIFF = 5; // Don't attack if enemy is this much higher
constexpr int32 HIGH_LEVEL_DIFF = 4; // 25% chance at +/- this difference
constexpr int32 MID_LEVEL_DIFF = 3; // 50% chance at +/- this difference
constexpr int32 LOW_LEVEL_DIFF = 2; // 75% chance at +/- this difference

// Cache duration before reconsidering attack decision, and old cache cleanup interval
constexpr uint32 ATTACK_DECISION_CACHE_DURATION = 2 * MINUTE;
constexpr uint32 ATTACK_DECISION_CACHE_CLEANUP_INTERVAL = 10 * MINUTE;

// Custom hash function for (botGUID, targetGUID) pairs
struct PairGuidHash
{
std::size_t operator()(const std::pair<ObjectGuid, ObjectGuid>& pair) const
{
return std::hash<uint64>()(pair.first.GetRawValue()) ^
(std::hash<uint64>()(pair.second.GetRawValue()) << 1);
}
};

// Cache for probability-based attack decisions (Per-bot: non-global)
// Map: (botGUID, targetGUID) -> (should attack decision, timestamp)
static std::unordered_map<std::pair<ObjectGuid, ObjectGuid>, std::pair<bool, time_t>, PairGuidHash> attackDecisionCache;
static std::mutex attackDecisionCacheMutex;

void PossibleTargetsValue::FindUnits(std::list<Unit*>& targets)
{
Expand All @@ -24,7 +52,121 @@ void PossibleTargetsValue::FindUnits(std::list<Unit*>& targets)
Cell::VisitObjects(bot, searcher, range);
}

bool PossibleTargetsValue::AcceptUnit(Unit* unit) { return AttackersValue::IsPossibleTarget(unit, bot, range); }
static void CleanupAttackDecisionCache()
{
// Mutex already held by caller in AcceptUnit
time_t currentTime = time(nullptr);
for (auto it = attackDecisionCache.begin(); it != attackDecisionCache.end();)
{
if (currentTime - it->second.second >= ATTACK_DECISION_CACHE_DURATION)
it = attackDecisionCache.erase(it);
else
++it;
}
}

bool PossibleTargetsValue::AcceptUnit(Unit* unit)
{
if (!AttackersValue::IsPossibleTarget(unit, bot, range))
return false;

// Level-based PvP restrictions
if (unit->IsPlayer())
{
// Self-defense - always allow fighting back
if (bot->IsInCombat() && bot->GetVictim() == unit)
return true; // Already fighting

Unit* botAttacker = bot->getAttackerForHelper();
if (botAttacker)
{
if (botAttacker == unit)
return true; // Enemy attacking

if (botAttacker->IsPet())
{
Unit* petOwner = botAttacker->GetOwner();
if (petOwner && petOwner == unit)
return true; // Enemy's pet attacking
}
}

// Skip restrictions in BG/Arena
if (bot->InBattleground() || bot->InArena())
return true;

// Skip restrictions if in duel with this player
if (bot->duel && bot->duel->Opponent == unit)
return true;

// Capital cities - no restrictions
uint32 zoneId = bot->GetZoneId();
bool inCapitalCity = (zoneId == AREA_STORMWIND_CITY ||
zoneId == AREA_IRONFORGE ||
zoneId == AREA_DARNASSUS ||
zoneId == AREA_THE_EXODAR ||
zoneId == AREA_ORGRIMMAR ||
zoneId == AREA_THUNDER_BLUFF ||
zoneId == AREA_UNDERCITY ||
zoneId == AREA_SILVERMOON_CITY);
Comment on lines +103 to +111
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AREA_* constants used in this capital city check (e.g. AREA_STORMWIND_CITY, AREA_ORGRIMMAR, etc.) are not defined anywhere in the current codebase, and the only reference to AreaDefines.h is this new include. This will cause compilation failures due to undefined identifiers unless AreaDefines.h and these macros are added elsewhere in the project. Either include an existing header that already defines these IDs, or switch to using AreaTableEntry flags (e.g. AREA_FLAG_CAPITAL) consistently with the rest of the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +111
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coding a list of capital city zone IDs here diverges from the established convention elsewhere in the codebase, where capital status is determined via AreaTableEntry flags (for example, see src/Ai/Base/Actions/GuildCreateActions.cpp:288-293 and 323-329, and src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp:878-883, 930-936, 1025-1030). To keep things maintainable and compatible with future content changes, consider reusing the same AREA_FLAG_CAPITAL-based check instead of maintaining a separate list of explicit zone IDs.

Copilot uses AI. Check for mistakes.

if (inCapitalCity)
return true;

// Level difference check
int32 levelDifference = unit->GetLevel() - bot->GetLevel();
int32 absLevelDifference = std::abs(levelDifference);

// Extreme difference - do not attack
if (levelDifference >= EXTREME_LEVEL_DIFF)
return false;

// Calculate attack chance based on level difference
uint32 attackChance = 100; // Default 100%: Bot and target's levels are very close

// There's a chance a bot might gank on an extremly low target
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the comment "There's a chance a bot might gank on an extremly low target" the word "extremly" is misspelled. It should be "extremely" for correct spelling.

Suggested change
// There's a chance a bot might gank on an extremly low target
// There's a chance a bot might gank on an extremely low target

Copilot uses AI. Check for mistakes.
if ((absLevelDifference < EXTREME_LEVEL_DIFF && absLevelDifference >= HIGH_LEVEL_DIFF) ||
levelDifference <= -EXTREME_LEVEL_DIFF)
attackChance = 25;

else if (absLevelDifference < HIGH_LEVEL_DIFF && absLevelDifference >= MID_LEVEL_DIFF)
attackChance = 50;

else if (absLevelDifference < MID_LEVEL_DIFF && absLevelDifference >= LOW_LEVEL_DIFF)
attackChance = 75;

// If probability check needed, use cache (with thread-safe access)
if (attackChance < 100)
{
std::pair<ObjectGuid, ObjectGuid> cacheKey = std::make_pair(bot->GetGUID(), unit->GetGUID());
time_t currentTime = time(nullptr);

// Lock for cache access
std::lock_guard<std::mutex> lock(attackDecisionCacheMutex);

// Cleanup cache while holding lock
static time_t lastCleanup = 0;
if (currentTime - lastCleanup > ATTACK_DECISION_CACHE_CLEANUP_INTERVAL)
{
CleanupAttackDecisionCache();
lastCleanup = currentTime;
}

auto it = attackDecisionCache.find(cacheKey);
if (it != attackDecisionCache.end())
{
if (currentTime - it->second.second < ATTACK_DECISION_CACHE_DURATION)
return it->second.first;
}

bool shouldAttack = (urand(1, 100) <= attackChance);
attackDecisionCache[cacheKey] = std::make_pair(shouldAttack, currentTime);
return shouldAttack;
}
}

return true;
}

void PossibleTriggersValue::FindUnits(std::list<Unit*>& targets)
{
Expand All @@ -36,9 +178,8 @@ void PossibleTriggersValue::FindUnits(std::list<Unit*>& targets)
bool PossibleTriggersValue::AcceptUnit(Unit* unit)
{
if (!unit->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE))
{
return false;
}

Unit::AuraEffectList const& aurasPeriodicTriggerSpell =
unit->GetAuraEffectsByType(SPELL_AURA_PERIODIC_TRIGGER_SPELL);
Unit::AuraEffectList const& aurasPeriodicTriggerWithValueSpell =
Expand All @@ -58,9 +199,7 @@ bool PossibleTriggersValue::AcceptUnit(Unit* unit)
for (int j = 0; j < MAX_SPELL_EFFECTS; j++)
{
if (triggerSpellInfo->Effects[j].Effect == SPELL_EFFECT_SCHOOL_DAMAGE)
{
return true;
}
}
}
}
Expand Down