Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/label_translation-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "Added translation"
69 changes: 50 additions & 19 deletions conf/playerbots.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ AiPlayerbot.AutoGearScoreLimit = 0
AiPlayerbot.BotCheats = "food,taxi,raid"

# List of attunement quests (comma-separated list of quest IDs) that are automatically completed for all bots.
# While mod-playerbots does not restore removed attunement requirements, although other mods, such as mod-individual-progression, may do so.
# While mod-playerbots does not restore removed attunement requirements, other mods, such as mod-individual-progression, may do so.
# This is meant to exclude bots from such requirements.
#
# Default:
Expand Down Expand Up @@ -865,34 +865,65 @@ AiPlayerbot.ExcludedHunterPetFamilies = ""
#
#
####################################################################################################


####################################################################################################
# ACTIVITIES
# ACTIVITY
#
# BotActiveAlone
# - Controls how many bots are active when no real players are nearby.
# - Think of it as a rough percentage: 10 means approximately 10% of bots will be active.
# Not exact — the actual number may vary slightly per rotation cycle.
# - The active bots rotate: every <DurationSeconds> a different set of bots takes a turn.
# - The real number of active bots will always be higher than this value, because bots in
# combat, dungeons, battlegrounds, LFG queue, groups with real players, etc. are always
# forced active on top of this (see force rules below).
# - Set to 100 (with SmartScale off) = all bots always active. Maximum server load.
# - Set to 0 = only bots that match a force rule below will be active.
#
# Specify percent of active bots
# The default is 100% but will be automatically adjusted if botActiveAloneSmartScale
# is enabled. Regardless, this value is only applied to inactive areas where no real players
# are detected. When real players are nearby, the value is always enforced to 100%
AiPlayerbot.BotActiveAlone = 100
# BotActiveAloneDurationSeconds
# - How often the active roster rotates (in seconds). A different group of bots wakes up
# and the previous group may go idle.
# - This is a minimum, not exact. If a bot is in combat or meets any force rule when the
# rotation happens, it stays active until those conditions end — it won't be cut off
# mid-fight just because its turn expired.
#
AiPlayerbot.BotActiveAlone = 10
AiPlayerbot.BotActiveAloneDurationSeconds = 30

# Force botActiveAlone when bot is within the specified distance of a real player
#
# Force-active rules (1 = on, 0 = off)
# These override the percentage above. If any of these conditions is true, the bot stays active.
#
# InRadius - A real player is within this many yards (set to 0 to disable).
# InZone - A real player is in the same zone (e.g. Elwynn Forest).
# InMap - A real player is on the same continent (e.g. Eastern Kingdoms).
# IsFriend - A real player has this bot on their friends list.
# InGuild - This bot is in a guild that has a real player in it.
#
# Bots are also always forced active (not configurable) when:
# in combat, inside a dungeon/raid/BG, in a BG or LFG queue,
# grouped with a real player, or controlled by a real player.
#
AiPlayerbot.BotActiveAloneForceWhenInRadius = 150
AiPlayerbot.BotActiveAloneForceWhenInZone = 1
AiPlayerbot.BotActiveAloneForceWhenInMap = 0
AiPlayerbot.BotActiveAloneForceWhenIsFriend = 1
AiPlayerbot.BotActiveAloneForceWhenIsFriend = 0
AiPlayerbot.BotActiveAloneForceWhenInGuild = 1

# SmartScale (automatic scaling of percentage of active bots based on latency)
# The default is 1. When enabled (smart) scales the 'BotActiveAlone' value.
# (The scaling will be overruled by the BotActiveAloneForceWhen...rules)
# SmartScale — automatically reduces active bots when the server is struggling.
# Monitors the server's update time (how long each server tick takes in milliseconds).
# When the server slows down, fewer bots are kept active to reduce load.
#
# Limitfloor - when DIFF (latency) is above floor, activity scaling begins
# LimitCeiling - when DIFF (latency) is above ceiling, activity is 0%
# Floor (default 50ms) - Below this, no reduction. Server is running fine.
# Ceiling (default 200ms) - At or above this, all non-forced bots are paused.
# Between floor and ceiling, activity scales down gradually.
# Example: BotActiveAlone=10, floor=50, ceiling=200
# Server at 50ms → ~10% active (no reduction)
# Server at 125ms → ~5% active (half reduction)
# Server at 200ms → 0% active (only forced bots remain)
#
# MinLevel - only apply scaling when level is above or equal to min(bot)Level
# MaxLevel - only apply scaling when level is lower or equal of max(bot)Level
# MinLevel/MaxLevel — only bots within this level range are affected by SmartScale.
# Bots outside the range always use the full BotActiveAlone value.
# Force rules always win over SmartScale.
#
AiPlayerbot.botActiveAloneSmartScale = 1
AiPlayerbot.botActiveAloneSmartScaleDiffLimitfloor = 50
Expand Down Expand Up @@ -1687,7 +1718,7 @@ AiPlayerbot.PremadeSpecLink.9.1.60 = -003203301135112530135201051
AiPlayerbot.PremadeSpecLink.9.1.70 = -003203301135112530135201051-55
AiPlayerbot.PremadeSpecLink.9.1.80 = -003203301135112530135221351-55000005
AiPlayerbot.PremadeSpecName.9.2 = destro pve
AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,45785
AiPlayerbot.PremadeSpecGlyph.9.2 = 45785,43390,42454,43394,43393,42453
AiPlayerbot.PremadeSpecLink.9.2.60 = --05203215200231051305031151
AiPlayerbot.PremadeSpecLink.9.2.80 = 23-0302-05203215220331051335231351
AiPlayerbot.PremadeSpecName.9.3 = affli pvp
Expand Down
33 changes: 32 additions & 1 deletion src/Ai/Base/Actions/GenericSpellActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ bool CastMeleeSpellAction::isUseful()
return CastSpellAction::isUseful();
}

CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime)
CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(
PlayerbotAI* botAI, std::string const spell, bool isOwner, float needLifeTime) :
CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime)
{
range = ATTACK_DISTANCE;
}
Expand Down Expand Up @@ -203,6 +205,35 @@ bool CastEnchantItemAction::isPossible()
return spellId && AI_VALUE2(Item*, "item for spell", spellId);
}

CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell)
: CastEnchantItemAction(botAI, spell) {}

bool CastEnchantItemMainHandAction::isPossible()
{
if (!CastEnchantItemAction::isPossible())
return false;

Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);
return item && !item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT) &&
item->GetTemplate()->Class == ITEM_CLASS_WEAPON;
}

CastEnchantItemOffHandAction::CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell)
: CastEnchantItemAction(botAI, spell) {}

bool CastEnchantItemOffHandAction::isPossible()
{
if (!CastEnchantItemAction::isPossible())
return false;

Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_OFFHAND);
if (!item || item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT))
return false;

uint32 invType = item->GetTemplate()->InventoryType;
return invType == INVTYPE_WEAPON || invType == INVTYPE_WEAPONOFFHAND;
}

CastHealingSpellAction::CastHealingSpellAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount,
HealingManaEfficiency manaEfficiency, bool isOwner)
: CastAuraSpellAction(botAI, spell, isOwner), estAmount(estAmount), manaEfficiency(manaEfficiency)
Expand Down
14 changes: 14 additions & 0 deletions src/Ai/Base/Actions/GenericSpellActions.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ class CastEnchantItemAction : public CastSpellAction
std::string const GetTargetName() override { return "self target"; }
};

class CastEnchantItemMainHandAction : public CastEnchantItemAction
{
public:
CastEnchantItemMainHandAction(PlayerbotAI* botAI, std::string const spell);
bool isPossible() override;
};

class CastEnchantItemOffHandAction : public CastEnchantItemAction
{
public:
CastEnchantItemOffHandAction(PlayerbotAI* botAI, std::string const spell);
bool isPossible() override;
};

class CastHealingSpellAction : public CastAuraSpellAction
{
public:
Expand Down
7 changes: 7 additions & 0 deletions src/Ai/Base/Trigger/GenericTriggers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,13 @@ bool FearCharmSleepTrigger::IsActive()
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP);
}

bool FearSleepSapTrigger::IsActive()
{
return bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SAPPED);
}

bool HasAuraStackTrigger::IsActive()
{
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);
Expand Down
8 changes: 8 additions & 0 deletions src/Ai/Base/Trigger/GenericTriggers.h
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,14 @@ class FearCharmSleepTrigger : public Trigger
bool IsActive() override;
};

class FearSleepSapTrigger : public Trigger
{
public:
FearSleepSapTrigger(PlayerbotAI* botAI) : Trigger(botAI, "fear sleep sap", 1) {}

bool IsActive() override;
};

class IsSwimmingTrigger : public Trigger
{
public:
Expand Down
2 changes: 1 addition & 1 deletion src/Ai/Base/Trigger/RtiTriggers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ bool NoRtiTrigger::IsActive()
return false;

Unit* target = AI_VALUE(Unit*, "rti target");
return target != nullptr;
return target == nullptr;
}
2 changes: 2 additions & 0 deletions src/Ai/Base/TriggerContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class TriggerContext : public NamedObjectContext<Trigger>
creators["generic boost"] = &TriggerContext::generic_boost;
creators["loss of control"] = &TriggerContext::loss_of_control;
creators["fear charm sleep"] = &TriggerContext::fear_charm_sleep;
creators["fear sleep sap"] = &TriggerContext::fear_sleep_sap;

creators["protect party member"] = &TriggerContext::protect_party_member;

Expand Down Expand Up @@ -369,6 +370,7 @@ class TriggerContext : public NamedObjectContext<Trigger>
static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); }
static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); }
static Trigger* fear_charm_sleep(PlayerbotAI* botAI) { return new FearCharmSleepTrigger(botAI); }
static Trigger* fear_sleep_sap(PlayerbotAI* botAI) { return new FearSleepSapTrigger(botAI); }
static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI)
{
return new PartyMemberCriticalHealthTrigger(botAI);
Expand Down
64 changes: 64 additions & 0 deletions src/Ai/Base/Value/PartyMemberSnaredTargetValue.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/

#include "PartyMemberSnaredTargetValue.h"

#include <limits>

#include "PlayerbotAIAware.h"
#include "Playerbots.h"

class PartyMemberSnaredTargetPredicate : public FindPlayerPredicate, public PlayerbotAIAware
{
public:
PartyMemberSnaredTargetPredicate(PlayerbotAI* botAI)
: PlayerbotAIAware(botAI)
{
}

bool Check(Unit* unit) override
{
if (!unit || !unit->IsAlive() || !unit->IsInWorld() || unit == botAI->GetBot())
return false;

if (unit->GetMapId() != botAI->GetBot()->GetMapId())
return false;

if (!botAI->GetBot()->IsWithinLOSInMap(unit))
return false;

return botAI->IsMovementImpaired(unit);
}
};

Unit* PartyMemberSnaredTargetValue::Calculate()
{
Group* group = bot->GetGroup();
if (!group)
return nullptr;

PartyMemberSnaredTargetPredicate predicate(botAI);
Player* bestTarget = nullptr;
float closestDistanceSq = std::numeric_limits<float>::max();

for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member)
continue;

if (!predicate.Check(member))
continue;

float const distanceSq = bot->GetExactDist2dSq(member->GetPositionX(), member->GetPositionY());
if (distanceSq < closestDistanceSq)
{
closestDistanceSq = distanceSq;
bestTarget = member;
}
}

return bestTarget;
}
24 changes: 24 additions & 0 deletions src/Ai/Base/Value/PartyMemberSnaredTargetValue.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/

#ifndef _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H
#define _PLAYERBOT_PARTYMEMBERSNAREDTARGETVALUE_H

#include "NamedObjectContext.h"
#include "PartyMemberValue.h"

class PartyMemberSnaredTargetValue : public PartyMemberValue
{
public:
PartyMemberSnaredTargetValue(PlayerbotAI* botAI, std::string const name = "party member snared target")
: PartyMemberValue(botAI, name)
{
}

protected:
Unit* Calculate() override;
};

#endif
3 changes: 3 additions & 0 deletions src/Ai/Base/ValueContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
#include "PartyMemberToDispel.h"
#include "PartyMemberToHeal.h"
#include "PartyMemberToResurrect.h"
#include "PartyMemberSnaredTargetValue.h"
#include "PartyMemberWithoutAuraValue.h"
#include "PartyMemberWithoutItemValue.h"
#include "PetTargetValue.h"
Expand Down Expand Up @@ -152,6 +153,7 @@ class ValueContext : public NamedObjectContext<UntypedValue>
creators["duel target"] = &ValueContext::duel_target;
creators["party member to dispel"] = &ValueContext::party_member_to_dispel;
creators["party member to protect"] = &ValueContext::party_member_to_protect;
creators["party member snared target"] = &ValueContext::party_member_snared_target;
creators["health"] = &ValueContext::health;
creators["rage"] = &ValueContext::rage;
creators["energy"] = &ValueContext::energy;
Expand Down Expand Up @@ -450,6 +452,7 @@ class ValueContext : public NamedObjectContext<UntypedValue>
static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); }
static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); }
static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); }
static UntypedValue* party_member_snared_target(PlayerbotAI* botAI) { return new PartyMemberSnaredTargetValue(botAI); }
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); }
Expand Down
52 changes: 52 additions & 0 deletions src/Ai/Class/Dk/Action/DKActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,55 @@ bool CastRaiseDeadAction::Execute(Event event)

return true;
}

Unit* CastHysteriaAction::GetTarget()
{
Group* group = bot->GetGroup();
if (!group)
{
if (!bot->HasAura(49016))
return bot;
return nullptr;
}

Unit* rangedDps = nullptr;
Unit* tank = nullptr;

for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!member || !member->IsAlive())
continue;

if (member->GetMap() != bot->GetMap() || bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance)
continue;

// Skip if already has hysteria
if (member->HasAura(49016))
continue;

// Priority 1: Melee DPS
if (botAI->IsMelee(member) && botAI->IsDps(member))
return member;

// Priority 2: Ranged DPS (physical, not casters)
if (!rangedDps && botAI->IsRanged(member) && botAI->IsDps(member) && !botAI->IsCaster(member))
rangedDps = member;

// Priority 3: Tank
if (!tank && botAI->IsTank(member))
tank = member;
}

if (rangedDps)
return rangedDps;

if (tank)
return tank;

// Fallback to self if no hysteria
if (!bot->HasAura(49016))
return bot;

return nullptr;
}
Loading
Loading