Compare commits

...

3 Commits

Author SHA1 Message Date
NoxMax
02e8465a3b Fix/Feat: Stop bots in party from PVP when master isn't, and PVP probablity system (#1914)
There are two related PVP components in this PR. First is the simple yet
fundamental change to bot behaviour when they are in party. Right now
bots with a master will go into PVP when there's a nearby PVP target,
even if master is not in PVP. This absolutely should not happen. Bots
should not consider PVP at all if master is not in PVP. The fix is only
3 lines in EnemyPlayerValue

The second component is introducing PVP probabilities, to make decisions
more realistic. Right now even a level 1 bot will 100% go into PVP if it
sees a level 80 PVP target. They can't help themselves. So the change
here addresses that insanity. Several thresholds (subject to community
review) are introduced:

1. Bots will not fight a target 5 or more levels higher than them
2. Bots have a 25% chance starting a fight with a target +/- 4 levels
from them.
3. Bots have a 50% chance starting a fight with a target +/- 3 levels
from them.
4. Bots have a 75% chance starting a fight with a target +/- 2 levels
from them.
5. Bots have a 100% chance starting a fight with a target +/- 1 level
from them.
6. Bots have a 25% chance starting a fight with a target 5 or more
levels below them (ganking. thought it would be funny, and technically
realistic of player behaviour)

Exception of course exist for BG/Arena/Duel, and in capitals where bots
will always PVP. Also bots will always defend themselves if attacked.

Few notes: 
1. The if/ else if logic can be further simplified, but only if we use
thresholds that are different by one. So current logic allows for
flexibility of using values like 10/7/5/3 instead of 5/4/3/2.
2. The caching system is per-bot basis. So for some target X, if some
bot decides to attack it, another bot will make its own decision. At
first I used a simplified global system (thinking there might be
performance concerns) where if one bot decides to attack a target then
they all do, but when I switched to the more realistic per-bot basis, I
didn't see an effect on performance.
3. Variables are obviously not configurable right now. I'm starting to
see Bash's POV that maybe we have too many configs 😬 Still,
they can be easily exposed in the future, and if someone is reading this
then, remember to change constexpr to const.
2026-01-08 22:44:09 +01:00
Crow
f53a8704eb Remove InitSpells() (#1983)
InitSpells() is never called, and even if it were to be, it would do
nothing but call InitAvailableSpells().
2026-01-08 11:46:36 -08:00
bashermens
e5525958c8 [BUG FIX] Movement flag update called for bots while rooted (#1991)
Fix for the error "Attempted sending heartbeat with root flag for guid"

The core does not allow movement flag updates when unit/player has the
MOVEMENTFLAG_ROOT flag. Change scope bots alone.
2026-01-08 20:38:49 +01:00
8 changed files with 162 additions and 12 deletions

View File

@@ -1426,8 +1426,8 @@ void PlayerbotAI::DoNextAction(bool min)
return;
}
// Change engine if just ressed
if (currentEngine == engines[BOT_STATE_DEAD] && isBotAlive)
// Change engine if just ressed (no movement update when rooted)
if (currentEngine == engines[BOT_STATE_DEAD] && isBotAlive && !bot->IsRooted())
{
bot->SendMovementFlagUpdate();

View File

@@ -1654,6 +1654,10 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
if (bot->IsBeingTeleported() || !bot->IsInWorld())
return;
// no teleport / movement update when rooted.
if (bot->IsRooted())
return;
// ignore when in queue for battle grounds.
if (bot->InBattlegroundQueue())
return;

View File

@@ -41,13 +41,17 @@ bool ServerFacade::IsDistanceLessOrEqualThan(float dist1, float dist2) { return
void ServerFacade::SetFacingTo(Player* bot, WorldObject* wo, bool force)
{
if (!bot)
return;
float angle = bot->GetAngle(wo);
// if (!force && bot->isMoving())
// bot->SetFacingTo(bot->GetAngle(wo));
// else
// {
bot->SetOrientation(angle);
bot->SendMovementFlagUpdate();
if (!bot->IsRooted())
bot->SendMovementFlagUpdate();
// }
}

View File

@@ -1103,8 +1103,6 @@ void PlayerbotFactory::ResetQuests()
}
}
void PlayerbotFactory::InitSpells() { InitAvailableSpells(); }
void PlayerbotFactory::InitTalentsTree(bool increment /*false*/, bool use_template /*true*/, bool reset /*false*/)
{
uint32 specTab;

View File

@@ -95,7 +95,6 @@ private:
void InitTradeSkills();
void UpdateTradeSkills();
void SetRandomSkill(uint16 id);
void InitSpells();
void ClearSpells();
void ClearSkills();
void InitTalents(uint32 specNo);

View File

@@ -417,7 +417,8 @@ bool FishingAction::Execute(Event event)
{
float angle = bot->GetAngle(pos.GetPositionX(), pos.GetPositionY());
bot->SetOrientation(angle);
bot->SendMovementFlagUpdate();
if (!bot->IsRooted())
bot->SendMovementFlagUpdate();
}
EquipFishingPoleAction equipAction(botAI);

View File

@@ -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() &&
@@ -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;
}

View File

@@ -16,6 +16,32 @@
#include "SpellAuraEffects.h"
#include "SpellMgr.h"
#include "Unit.h"
#include "AreaDefines.h"
#include <unordered_map>
// 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;
void PossibleTargetsValue::FindUnits(std::list<Unit*>& targets)
{
@@ -24,7 +50,117 @@ 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()
{
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)
{
// attackDecisionCache cleanup
static time_t lastCleanup = 0;
time_t currentTime = time(nullptr);
if (currentTime - lastCleanup > ATTACK_DECISION_CACHE_CLEANUP_INTERVAL)
{
CleanupAttackDecisionCache();
lastCleanup = currentTime;
}
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);
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
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
if (attackChance < 100)
{
std::pair<ObjectGuid, ObjectGuid> cacheKey = std::make_pair(bot->GetGUID(), unit->GetGUID());
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)
{
@@ -36,9 +172,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 =
@@ -58,9 +193,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;
}
}
}
}