Compare commits

...

18 Commits

Author SHA1 Message Date
Keleborn
62ef4b63b1
Merge pull request #2432 from mod-playerbots/test-staging
Test staging
2026-06-05 08:42:15 -07:00
Keleborn
714bb6bca3
Shorten paths (#2396)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
This is designed to significantly shorten the overall path while
maintaining the more detailed structure. I tried to follow the principle
of removing any folder that had 1 or 2 files in it, shortenining
dungeon/raid names to acronyms, and removing instances when the base
name was in the file name (Like Raid) etc.


## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->



## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->

Plan and execute

<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [ ] Stability is not compromised.
- - [ ] Performance impact is understood, tested, and acceptable.
- - [ ] Added logic complexity is justified and explained.
- - [ ] Any new bot dialogue lines are translated.
- - [ ] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-31 18:38:01 +02:00
dillyns
585027fab7
Remove fire totem override from enhancement AOE strategy (#2270)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
Currently in AOE enchancement shamans are dropping magma totems. They
should not be overriding the set fire totem strategy.

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
Bring an enhancement shaman into medium aoe scenario (3 enemies)
They should no longer replace their fire totem with magma totem. They
should only drop the fire totem that is set in their strategies.

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## Messages to Translate
<!--
Bot messages have to be translatable, but you don't need to do the
translations here. You only need to make sure
the message is in a translatable format, and list in the table the
message_key and the default English message.
Search for GetBotTextOrDefault in the codebase for examples.
-->
- Does this change add bot messages to translate?
    - - [x] No
    - - [ ] Yes (**list messages in the table**)

| Message key  | Default message |
| --------------- | ------------------ |
|			 |			      |
|			 |			      |

## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
- Was AI assistance used while working on this change?
    - - [x] No
    - - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-31 06:41:13 -07:00
Crow
571735cd57
Fix crash from missing spellInfo check in TogglePetSpellAutoCastAction (#2431)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
I had repeated crashes upon login traced to line 89 of
TogglePetSpellAutoCastAction, where a spellInfo check is missing. I
confirmed that adding the check fixed my crashing.

I don't know why there was invalid spellInfo to create the crash, as
this missing check is not a new development and I never had a crash
before, but clearly this code should be fixed in any case.

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->



## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-30 20:10:33 -07:00
kadeshar
b430118124
Sunder Armor fixes (#2427)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->

Arms and Fury using Sunder Armor when there is not Protection Warrior in
group

## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->

Invite warrior/warriors to group and check how they apply Sunder Armor
on dummy target


## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [ ] No
    - - [x] Yes (**explain why**)

Yes, expained in Pull Request Description

- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->

Analyze orginal behavior

<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-30 14:14:54 -07:00
Keleborn
180be33d9d Remove stray folder. 2026-05-30 14:09:09 -07:00
Keleborn
9e251dc397 Fix conf merge error 2026-05-30 13:50:54 -07:00
Crow
92fa97c3aa
Rewrite Equipment-Randomization-Related Configs (#2409)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
The main focus on this PR is to clarify and eliminate overlap between
three very confusingly described yet important config options:
AutoUpgradeEquip, EquipmentPersistence, and IncrementalGearInit.

For context, after initialization, randombots generally get randomized
each time after logging in and periodically between
MinRandomBotRandomizeTime (configurable, default 2 hours) to
MaxRandomBotRandomizeTime (configurable, default 14 days). This
randomization happens only when the bot is idle.

Now let’s look at what those three config options currently say.
- AutoUpgradeEquip: Randombots automatically upgrade their equipment on
levelup.
- EquipmentPersistence: Enable/Disable bot equipment persistence (stop
random initialization) after certain level (EquipmentPersistenceLevel).
- IncrementalGearInit: If disabled, random bots can only upgrade
equipment through looting and quest

None of those descriptions are accurate.
- AutoUpgradeEquip determines if randombots, upon leveling up, refresh
ammo, reagents, food, consumables, potions, and, ONLY IF
IncrementalGearInit is also enabled and EquipmentPersistence is
disabled, upgrade equipment (yes, three config options required for this
one thing).
-	EquipmentPersistence affects both equipment and talents. 
- Disabling IncrementalGearInit does not prevent randombots from
changing their equipment through the login/periodic randomization
process unless EquipmentPersistence is enabled.

These config options shouldn’t overlap with or be dependent on each
other, and their names and descriptions should reflect what they
actually do. Thus, this PR does the following:
- AutoUpgradeEquip solely controls whether or not randombots
automatically upgrade their gear upon level up. No other config option
is involved for this purpose, and AutoUpgradeEquip no longer impacts
inventory items. This does mean that it is no longer possible to stop
randombots from being given ammo, potions, etc. when they level up. I
tend to think that randombots as they currently are cannot fully
function otherwise so I have no issue with the loss of this ability, but
if there is disagreement, then we need to introduce a new config option.
AutoUpgradeEquip is also now set to true in PlayerbotAIConfig to reflect
that the default is true in the .dist.
- I originally wanted to combine EquipmentPersistence and
EquipmentPersistenceLevel into a single config option that also included
talents in the name, but EquipmentPersistence is used in the level
brackets mod so I don’t want to make a breaking change there. I settled
for making EquipmentPersistence enabled by default and also reducing
EquipmentPersistenceLevel to 1 by default, in effect entirely disabling
periodic/login randomization of randombot talents and gear by default. I
only see people complain about these features so I think unless there is
some compelling performance or structural reason to the contrary, the
default should be that randombots do not continuously randomize their
talents and equipment.
- IncrementalGearInit is eliminated. There are two real functional
impacts: (1) as noted above, randombots cannot be blocked from receiving
standard inventory items upon level-up (though the way it is currently
being gated does not make sense anyway), and (2) you can no longer have
equipment persistence for equipment only but not talents (I cannot
imagine anybody would ever want to do that, and I don't think it was
even intended given the strange interaction between config options that
was needed to even accomplish that before).
- For all these settings, the config descriptions are updated to try to
be clear about what the player is actually configuring.

Beyond that, I made some clean-up changes to the config to fix some
typos and try to shorten it in places, and I also deleted a few config
options that do not appear in operative code anymore (and any
corresponding references in PlayerbotAIConfig). I also deleted some
comments and made some minor style changes in
AutoMaintenanceOnLevelupAction.cpp.


## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.

The main substance of the PR consists of changes to checks in
PlayerbotFactory and AutoMaintenanceOnLevelupAction.

## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->

- To test AutoUpgradeEquip, use the gm command .level 1 while targeting
an rndbot.
- To test EquipmentPersistence, the only way I could find to force an
incremental randomization was to use .playerbots rndbot level BotName.
This will do an incremental randomization and a level up. However, this
level does not go through the AutoMaintenanceOnLevelupAction path so
just disregard the +1 level aspect and consider it a pure test of
EquipmentPersistence.

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->
I had GPT-5.4 review my changes and come up with the testing method.


<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2026-05-30 11:13:18 -07:00
Mat
ff001afd46
Reset instance ID via existing cmd refresh=raid for alt bots (#2422)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
Alt bots can now use refresh=raid cmd if cfg is set to 1.
`AiPlayerbot.ResetInstanceIdForAltBots = 1`


## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
Add alt bot, use .playerbots bot refresh=raid and it should say ok after
reseting instance.


## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [ ] No
    - - [x] Yes (**explain why**)
Alt bots will now reset instance ID if cfg is set to 1.


- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] No
    - - [x] Yes (**explain below**)
It will check if cfg is set to 1 to enable alt bots to reset instance
id.


## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2026-05-30 11:12:54 -07:00
Crow
32d10080a4
Improve bot trinket usage and fix related bugs (#2425)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->

This PR makes three changes to UseTrinketAction:
1. It adds health and mana gating (based on existing config thresholds)
for bots to activate mana recovery, mana efficiency, and defensive
trinkets. The thresholds are mediumMana (default 40%) for mana recovery
trinkets, highMana (default 65%) for mana efficiency trinkets, and
lowHealth (default 45%) for defensive trinkets.
2. It removes the old overinclusive procflag/specproc gate introduced by
PR 1385, which prevents bots from using dozens of valid trinkets,
including some extremely powerful ones (such as Skull of Gul’dan, the
iconic TBC expansion BiS trinket for all casters), and replaces it with
a narrower exclusion that still addresses the original issue.

- Regarding PR 1385, focusing on liyunfan’s post specifically, I
interpet the issue to be relating to trinkets where the item data is
screwed up such that a passive effect was implemented as an on-use
spell. I had AI do a scan, and it seems that issue impacts only 2
trinkets in the game, Oracle Talisman of Ablution and Frenzyheart
Insignia of Fury, and specifically only the versions of those items that
are not legitimately obtainable (unclear why they exist at all). I’ve
excluded those trinkets from bots via the config now, and this PR
maintains the guards against those trinkets regardless but does so in a
narrower fashion. PR 1385's approach of using ProcFlags != 0 (i.e.,
excluding all on-use trinkets with non-zero ProcFlags) works only if
proc metadata can be used to distinguish between active/passive
trinkets, and that’s not even close to being the case. AI came up with
44 false positives, including many significant trinkets beyond Skull of
Gul’dan such as the ZG Hakkar quest trinkets, Badge of the Swarmguard,
Petrified Scarab, Scarab Brooch, Eye of the Dead, Essence of the Martyr,
Abacus of Violent Odds, Ribbon of Sacrifice, and Pendant of the Violet
Eye (and I’m not mentioning WotLK trinkets only because I don’t know
anything about what is relevant for that expansion).

3. It fixes an issue where bots were not respecting trinket cooldowns in
some cases. This resulted because trinkets with shared cooldown
categories (i.e., those that are not stackable) would substitute the
individual trinket cooldowns with shared category cooldowns, which let
bots spam usages of trinkets, by tracking per-item and per-category
trinket cooldowns locally. The root of this is based in AC; I don't know
if it should be considered a bug or not, but regardless it doesn't
impact players, presumably because cooldowns are enforced on the client
side.

- Here’s an illustration to explain the issue in practice. Skull of
Gul’dan has a 240s cooldown and Shifting Naaru Sliver has a 180s
cooldown, and they share a cooldown category so their usages cannot be
stacked. The shared cooldown matches the length of the on-use effect (so
20s for Skull and 15s for Sliver). The below is what can happen without
this PR (and is what I observed in testing).

- t = 0s: Shifting Naaru Sliver used, writes its 90s personal cooldown
and 15s shared category cooldown.
- t = 15s: Skull of Gul’dan used, writes its 120s personal cooldown and
20s shared category cooldown. Skull’s shared category cooldown
overwrites Sliver’s spell-cooldown entry, giving Sliver 20s left on its
personal cooldown instead of 75s.
- t = 35s: Sliver incorrectly appears ready and can be used again (55s
sooner than should be possible). Then Sliver’s shared category cooldown
in turn overwrites Skull’s longer personal cooldown.
- t = 50s: Skull incorrectly appears ready and can be used again (85s
sooner than should be possible).
  - Repeat.


## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.

The logic runs through the existing trinket use code. Specifically:

- Helpers are used to classify on-use trinket effects into mana
restoration, mana efficiency, and defensive/tank categories using
spell-effect checks
- Existing configured mana and health thresholds are applied to those
trinkets
- The old procflag gate is replaced with a one-time cached set of mixed
ON_USE/ON_EQUIP trinket spell ids
- Two small cooldown maps per bot are tracked in UseTrinketAction: one
for item cooldowns and one for shared category cooldowns


## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->

I did all of these things, but verification is always good. Overall, I
think it is worth running with this PR merged into test-staging if/when
that happens and keeping an eye on overall performance.
1. Equip a bot with a trinket referenced above, such as Skull of
Gul'dan, and confirm it is now used in combat.
2. Test mana-based classes with mana recovery and mana-efficiency
trinkets and confirm they are used only when below the applicable
configured mana threshold. An easy one to check is Glimmering Naaru
Sliver because it is a channel.
3. Test defensive on-use trinkets and confirm they are used only when
health is below the configured low-health threshold. Something like
Shadowmoon Insignia, which increases maximum health, is pretty obvious.
4. Confirm that the error versions of Oracle/Frenzyheart don’t stack
auras on bots (i.e., the bug addressed in PR 1385 has not returned). You
can do this by having a bot kill mobs and check .listauras, though I
checked through logging in the code because auras are noisy as hell.
5. Equip a bot with two trinkets that have shared cooldowns. I used
Skull of Gul’dan and Shifting Naaru Sliver. Go fight a mostly
tank-and-spank boss, such as Gruul. Use an add-on like Skada that tracks
buffs. You should see that before this PR, bots will use the trinkets
multiple times in one cooldown period, and after, they observe the
actual cooldowns.

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [ ] No, not at all
    - - [x] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)

Any additional impact is confined to UseTrinketAction. The exclusion of
the busted trinkets uses a cache that is built once per server process
followed by constant-time lookups. The per-bot trinket cooldown maps
also use constant-time lookups and store only a few timestamp entries
per bot.

- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] No
    - - [x] Yes (**explain below**)

Sort of--trinkets previously didn't have any consideration for effects
with respect to usage so that is new. I think it is necessary though to
have half-decent bot trinket usage, and there could be further
refinement for how bots decide to use trinkets based on this structure.

## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->

GPT-5.4 was used for investigation and root-cause analysis and
assistance with drafting. All resulting code and the PR rationale was
validated through in-game testing.

<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
Bots also suck at using DPS trinkets properly, but I don't think there's
a simple way to address that unlike with mana recovery or defensive
trinkets. So that's to consider another day.

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2026-05-30 11:12:34 -07:00
dillyns
1bbed177c8
Add Nefarian Fear Ward action and trigger, along with Wild Magic trigger (#2412)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
For Nefarian in BWL, this does the following:
Mages - Ice block if the mage class call "Wild Magic" happens
Priests - Buff the current target of Nefarian with Fear Ward

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
Go to Nefarian (id 11583) or his phase 1 form (id 10162) in BWL with a
mage and a priest bot.
During phase 2, the priest bot should cast fear ward on whoever nefarian
is targeting.
Wait for a mage class call. Once it happens the mage bot should cast ice
block (if its available) to remove the Wild Magic debuff.


## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->
AI assistance was used to explore how dungeon/raid strats are
implemented and to explore examples.
All code has been reviewed


<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-30 07:14:38 -07:00
Lichborne
a3ca438bef
Make .playerbots bot commands case-insensitive (#2419)
## Pull Request Description

\`.playerbots bot add jared\` fails with "Character 'jared' not found"
even when \`Jared\` exists, because the lookup is case-sensitive. Same
issue hits \`remove\`, \`init=<...>\`, \`refresh\`, \`addaccount\`, and
any other subcommand that takes a character name.

Fix at the parser layer in \`HandlePlayerbotCommand\`: every subcommand
that takes a charname flows through one shared name-lookup step.
Normalize the typed name to canonical form (first letter upper, rest
lower) at that step so \`jared\`, \`JARED\`, \`JaReD\` all resolve to
\`Jared\`. The \`addaccount\` path still tries the raw token as an
account name first (account names are case-insensitive on the auth
side), then falls back to a normalized character-name lookup.

No subcommand's behavior changes — only the name lookup that precedes
them. Subcommands with no charname (\`initself\`, \`list\`, \`reload\`,
\`tweak\`, \`self\`, \`lookup\`, \`addclass\`) are unaffected.


## Feature Evaluation

- Describe the **minimum logic** required to achieve the intended
behavior.

One \`normalizePlayerName(s)\` call on each comma-separated token before
the existing \`GetCharacterGuidByName(s)\` lookup. Uses the helper this
module already calls in four other places.

- Describe the **processing cost** when this logic executes across many
bots.

Runs only when a player types a \`.playerbots bot ...\` command, on the
typed tokens. Not in any bot-tick or per-bot path. Cost does not scale
with bot count.


## How to Test the Changes

\`\`\`
.playerbots bot add Jared           → adds Jared
.playerbots bot add jared           → adds Jared
.playerbots bot add JARED           → adds Jared
.playerbots bot add JaReD           → adds Jared
.playerbots bot remove Jared        → removes Jared
.playerbots bot remove jared        → removes Jared
.playerbots bot init=auto jared     → re-rolls Jared's gear
.playerbots bot levelup jared → re-rolls Jared at his current level
.playerbots bot refresh jared       → refreshes Jared (HP/mana restored)
.playerbots bot refresh=raid jared  → unbinds Jared from saved raid IDs
.playerbots bot add jAred,saLLy,BOB → adds all three
.playerbots bot add nonexistent     → still reports not found
.playerbots bot initself            → still works (untouched path)
\`\`\`

All cases verified in-game.


## Impact Assessment

- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - [x] No, not at all
    - [ ] Minimal impact (**explain below**)
    - [ ] Moderate impact (**explain below**)

- Does this change modify default bot behavior?
    - [x] No
    - [ ] Yes (**explain why**)

- Does this change add new decision branches or increase maintenance
complexity?
    - [x] No
    - [ ] Yes (**explain below**)


## AI Assistance

Was AI assistance used while working on this change?
- [ ] No
- [x] Yes (**explain below**)

Used Claude to help trace the call graph (confirming which subcommands
are affected and which are untouched) and to draft the patch. Reviewed
and tested in-game before pushing.


## Final Checklist

- [x] Stability is not compromised.
- [x] Performance impact is understood, tested, and acceptable.
- [x] Added logic complexity is justified and explained.
- [x] Any new bot dialogue lines are translated.
- [x] Documentation updated if needed (Conf comments, WiKi commands).


## Notes for Reviewers

Diff is small (~25 lines in one file). One minor cosmetic side effect:
"Character 'X' not found" errors now echo the normalized form (\`Bogus\`
instead of \`bogus\`).

---------

Co-authored-by: Lichborne-AC <lzeppelin112@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:03:22 -07:00
Keleborn
28ec9b34b8
add conf option for disabling send mail (#2411)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
Fixes exploit in multiplayer servers where players can ask bots to mail
them items.


## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
Conf.
- Describe the **processing cost** when this logic executes across many
bots.
Cheap bool checkl


## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->

Use send mail command to try to get a bot to send you mail. Should
follow bool setting.

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->
CLaude Make do. 
simple enough one even AI can do it. 


<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-30 06:52:54 -07:00
Mat
a1f9ff4542
fix bot leader handling (#2426)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
This pr fixes bots "freaking out" after leader change and bots will
promote player to party leader after give leader command
closes #2420 #2424 

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
1. Join rdf or make group
2. Promote bot to leader (rnd or alt), if bot was not leader
3. Type /p give leader
4. Bot will promote you to leader and it will follow you instead of
freaking out

## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [ ] No
    - - [x] Yes (**explain why**)

1. Bots now reset their AI on every SMSG_GROUP_SET_LEADER packet, so old
leader strategies get cleared after a leader change.
2. Random bots auto-bind their master to a real-player group member
during reset botAI, so commands like give leader and follow logic that
depend on HasActivePlayerMaster() start working for them.
3. On OnBotLogin, bots no longer steal leadership from a real player,
they only force the leader change if the current leader is a bot or
offline.

- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] No
    - - [x] Yes (**explain below**)

Adds two small guards: an "is current leader a real player" check on
login, and a "find first real-player member" loop inside ResetAiAction.
Both reuse existing patterns (IsRealPlayer(), the OnPlayerLogin
master-assign loop).


## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-30 06:48:29 -07:00
NoxMax
0afaf753c6
Clarifies BG bracket auto-join comments and configs (#2417)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
playerbots.conf.dist:
1. Updated and clarified comments. At best they were misleading, at
worst they were wrong.
2. Reordered brackets configs top to bottom, from Warsong to IOC.
3. No config values have actually been changed.

PlayerbotAIConfig.cpp:
1. Made sure the config values actually match the .dist file.
2. Reordered brackets configs top to bottom, from Warsong to IOC.

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->



## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
Bracket ranges are from `pvpdifficulty_dbc`
2026-05-30 06:48:16 -07:00
Mat
240bb2dfca
Autogear suffixes (#2415)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
Suffix scoring was already wired up in playerbots, bots could evaluate
suffix items they owned, but factory init scored candidates with
randomPropertyId = 0 and equipped via EquipNewItem which doesn't roll
suffixes, so suffix items always looked like junk and never got picked.

Added a cached item_enchantment_template pool in RandomItemMgr, a
PickBestRandomPropertyId helper that picks the best suffix per bot
class/spec, and added it on the equipped item via
SetItemRandomProperties.

Tested levels 20-80, suffixes match spec. Closes #2370.



## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.

For items with RandomProperty != 0 or RandomSuffix != 0, run the
item_enchantment_template pool once, score each candidate suffix against
the bot's existing class/spec stat weights, keep the highest, and stamp
that id on the item right after EquipNewItem. Items without a suffix
pool skip the helper entirely. Without this, every suffix template
scores as base stats only (often near zero) and never gets picked during
factory init.

- Describe the **processing cost** when this logic executes across many
bots.

Startup: one SELECT entry, ench FROM item_enchantment_template (few
hundred rows), parsed into an unordered_map<uint32, vector<uint32>>
once. No SQL afterwards.
Per bot during init: PickBestRandomPropertyId only runs on candidates
that actually have a suffix pool. Each call is a hash lookup plus a loop
over 5-15 enchantment ids, each doing DBC lookups already used by the
scoring code. Cost is mostly by the existing CalculateRandomProperty
work, not new logic.
Scales linearly with bot count, same as the rest of factory init. No new
per-tick work.


## How to Test the Changes

1. Generate a fresh random bot at level 20-79 and use autogear (cloth,
leather, mail, plate, lots of suffix gear at that range).
2. Inspect the bot's gear. Items with names like "of the Eagle", "of the
Monkey", "of Healing", etc... should be equipped, with the suffix stats
visible on the tooltip.
3. Generate a level 80 bot at high gear-score limit. Confirm it still
equips raid epics normally (epics have no suffix pool, so this path is
untouched).



## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [ ] No
    - - [x] Yes (**explain why**)
Bots now consider random-suffix items during factory init and equip them
with the best suffix for their class/spec. Before, suffix items were
effectively invisible to init autogear because they scored as base stats
only. This is the intended fix for #2370.


- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] No
    - - [x] Yes (**explain below**)
Added one new public method on StatsWeightCalculator
(PickBestRandomPropertyId) and one new cache in RandomItemMgr
(LoadEnchantmentPool + GetEnchantmentPool). The candidate list in
InitEquipment changed from vector<uint32> to vector<pair<uint32, int32>>
to carry the chosen suffix id alongside the item id. Logic mirrors
existing patterns in the same files (DBC lookups, SQL-backed caches,
signed randomPropertyId encoding), no new abstractions, no new wrappers.


## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)
Used AI to help me find the bug. It mapped the existing scoring pipeline
and pointed out that PlayerbotFactory::InitEquipment was scoring
candidates with randomPropertyId = 0 and equipping via EquipNewItem
(which doesn't roll suffixes), so the scoring side was already built,
just never fed real suffix ids during init. I reviewed and tested all
the code in-game across levels 20-80 and multiple classes/specs.




<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
2026-05-30 06:48:07 -07:00
dillyns
4a63ee37e2
Shadow Priest Vampiric Embrac (#2410)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND
PREDICTABILITY over behavioral realism.

Every action and decision executes PER BOT AND PER TRIGGER. Small
increases in logic complexity scale
poorly across thousands of bots and negatively affect all. We prioritize
a stable system over a smarter
one. Bots don't need to behave perfectly; believable behavior is the
goal, not human simulation.
Default behavior must be cheap in processing; expensive behavior must be
opt-in.

Before submitting, make sure your changes aligns with these principles.
-->

## Pull Request Description
<!-- Describe what this change does and why it is needed -->
Shadow priest's Vampiric Embrace was incorrectly set as a debuff, and
was in the combat strategy.
Fixed it to be a buff, and move it to the noncombat strategy with other
buffs

## Feature Evaluation
<!--
If your PR is very minimal (comment typo, wrong ID reference, etc), and
it is very obvious it will not have
any impact on performance, you may skip these question. If necessary, a
maintainer may ask you for them later.
-->

<!-- Please answer the following: -->
- Describe the **minimum logic** required to achieve the intended
behavior.
- Describe the **processing cost** when this logic executes across many
bots.



## How to Test the Changes
<!--
- Step-by-step instructions to test the change.
- Any required setup (e.g. multiple players, number of bots, specific
configuration).
- Expected behavior and how to verify it.
-->
Get a shadow priest bot. They should now buff themselves with Vampiric
Embrace.


## Impact Assessment
<!-- As a generic test, before and after measure of pmon (playerbot pmon
tick) can help you here. -->
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [x] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



- Does this change modify default bot behavior?
    - - [x] No
    - - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - - [x] No
    - - [ ] Yes (**explain below**)



## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- - [x] No
- - [ ] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->



<!--
TRANSLATIONS:
Anything new that the bots say in chat must be in a translatable format.
This is done using GetBotTextOrDefault,
which you can search for in the codebase to find examples. Your code
needs to have English as the default fallback,
while the full translations need to be in an SQL update file. The
languages in the file are the nine language
options supported by AzerothCore: English, Korean, French, German,
Chinese, Taiwanese, Spanish, Spanish Mexico, and
Russian. See
data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as
an example of a translation SQL
update, whose content are called within the codebase at
src/strategy/actions/FishingAction.cpp
-->

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-05-29 23:09:28 -07:00
Crow
9bba4b78dd
Overhaul party buff/greater blessing system (#2358)
## Pull Request Description

These changes I originally made for myself because as a person who
really likes to raid with bots, I felt like the current group buff
system is fundamentally broken, and I needed something more consistent
and optimal. I debated a lot whether to PR this because it's such an
extensive overhaul that was almost entirely reliant on AI, and I know
that wishmaster still has a PR open regarding the greater blessings. I
decided to after a couple of conversations so at least people can look
at it and see if it's something that they want.

The tl;dr version is that this PR overhauls buff handling in two related
areas:
1. It adds a dedicated greater blessing assignment system.
2. It generalizes party/raid reagent-buff handling for Paladins, Druids,
Mages, and Priests.

Under this PR, greater blessings are determined by assignments for the
current group, and those assignments are determined based on:

1. a hardcoded priority list of blessings for each spec;
2. the number of Paladins in the group; and 
3. whether any Paladins have talents for Blessing of Sanctuary, Improved
Blessing of Might, or Improved Blessing of Wisdom.

Assignment determinations are cached in a value to avoid constant
reevaluation.

The exact priority list is:

- All casters: Kings, Wisdom, Sanctuary, Might
- Physical-only DPS (Rogues, Warriors, DKs): Might, Kings, Sanctuary,
N/A
- Hybrid DPS (Enh, Ret, Hunters, Cats): Might, Kings, Wisdom, Sanctuary
- Druid tanks: Kings, Might, Sanctuary, Wisdom
- Warrior and DK tanks: Kings, Might, Sanctuary, N/A
- Paladin tank: Sanctuary, Might, Wisdom, Kings

Note that Sanctuary is preferred over Kings for Paladin tanks because of
the mana regen component but deprioritized for other tanks because Kings
provides Agility. The extra 3% damage reduction from Sanctuary does not
stack with Disc Priests’ Renewed Hope, which will have 100% uptime.

For group buffs, logic is centralized so that class triggers use the
same gating and upgrade rules for Gift of the Wild, Arcane Brilliance,
Prayer of Fortitude, Prayer of Spirit, and Prayer of Shadow Protection.
Also, Shadow Protection is now a default strategy for Priests (rshadow,
which existed before but wasn’t added by default).

I’ve added a config setting for the greater blessing system and adjusted
the current config setting for group buffs. In each case, you can pick
whether to disable the feature entirely, use it in all groups, or use it
only in raid groups. The default is raid only for greater blessings and
all groups for group buffs. Note that for group buffs, even if the
config is enabled, they will be used only if at least 3 group/raid
members on the same map are missing the buff family. This is mainly to
stop group buff spamming during wipe recovery as bots are revived
one-by-one.

I renamed the Paladin buff strategies to align them with the actual
blessing names:
- `bhealth` -> `bsanc`
- `bmana` -> `bwisdom`
- `bdps` -> `bmight`
- `bstats` -> `bkings`

This is an intentional breaking change for saved strategy strings. Bots
will need a one-time strategy reset after update.

I removed bots telling you when they are out of reagents for greater
blessings. If people like that though, I can add it back.

A small cleanup is also included in TankPaladinStrategy: Holy Shield was
subject to three overlapping health triggers with the same priority; I
removed the two lower health thresholds which have no purpose.

## Feature Evaluation

- Describe the **minimum logic** required to achieve the intended
behavior.

I’m going to let the AI answer this one.
> The minimum logic is:
> - a shared config-gated check for whether group/raid buff variants are
allowed
> - a shared way to treat single and group variants as equivalent aura
families
> - a shared upgrade path from single-target buff to group buff when the
group variant is appropriate
> - a Paladin-only cached assignment model that decides which blessing
family each Paladin should cover for the current group
> - trigger/action wiring that only attempts casts when a group member
is actually missing the assigned buff
>
> This avoids scattering separate per-class heuristics across many
triggers and actions.

- Describe the **processing cost** when this logic executes across many
bots.

Processing cost should be minimal but non-zero. The general party buff
changes are limited to existing buff trigger paths and mostly replace
duplicated checks with shared helpers. They do not add expensive default
per-tick behavior outside those existing trigger evaluations.

The Paladin greater blessing logic does add extra decision-making, but
it is limited to Paladins, gated by config and group eligibility,
subject to a delayed trigger evaluation of only once per 4s, and cached
per group assignment set instead of recomputing the full assignment
model on every action attempt.

This PR also increases the throttle duration for group buff triggers to
limit performance impact; I’m open to adjustments to these durations:
- Mark of the Wild triggers were increased from 4s to 8s
- Arcane Intellect triggers were increased from 4s to 8s
- Priest buff triggers were increased to 8s (previously, Fortitude was
6s, Spirit was 4s, and Shadow Protection had no throttle)
- There is now a 5s delay on buffing (greater blessings and group buffs)
after bots log in—I was getting bots spamming buffs as soon as they
logged in even when it was not necessary

I’ve tested with pmon, and the impact is minimal—these are very cheap
triggers even compared to standard bot rotational ability triggers.

## How to Test the Changes

1. Try different config settings to confirm that they work to
enable/disable greater blessings/group buffs in the configured scenarios

2. For greater blessing changes:
   - test with one Paladin in a party/raid
   - test with multiple Paladins in a party/raid
- confirm the Paladins divide blessing coverage instead of repeatedly
overwriting each other
- include at least one Paladin with Improved Blessing of Might and make
sure it casts Might over Paladins without the talent; check the same
with a Paladin with Improved Blessing of Wisdom
- do not include a Paladin that knows Sanctuary, confirm any Paladin
tank receives Kings instead (you’ll need a low-level Paladin for this
since Sanctuary is a prot talent)
- confirm bots cast blessings only when a member is actually missing the
relevant blessing family
   - confirm there is a 5s delay on buffing when bots log in

3. For group buff changes:
   - confirm there is a 5s delay on buffing when bots log in
- confirm that single buffs are used when there aren’t at least three
unbuffed members in the same map, even if group buffs are enabled in the
config

4. For all buffs, test with reagents missing to confirm fallback to
single-target buffs and single blessings

5. Confirm the Paladin buff strategy names are changed after resetting
AI

## Impact Assessment

- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - - [ ] No, not at all
    - - [x] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)

Discussed above in processing costs.

- Does this change modify default bot behavior?
    - - [ ] No
    - - [x] Yes (**explain why**)

Yes—that is the purpose of this PR, to change default buffing behavior.

- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] No
    - - [x] Yes (**explain below**)

Yes, but I think it’s inevitable to add complexity to get greater
blessings to function consistently, given the challenges brought by
their mechanic of applying across each class.

## AI Assistance

Was AI assistance used while working on this change?
- - [ ] No
- - [x] Yes (**explain below**)

I used GPT-5.4 extensively for this overhaul. It’s much more complicated
than I could handle on my own. I’ve done a lot of testing and have
reviewed the code and provided plenty of revisions, but I cannot say I
can perfectly explain each addition and how it works, not even close.

## Final Checklist

- - [x] Stability is not compromised.
- - [x] Performance impact is understood, tested, and acceptable.
- - [x] Added logic complexity is justified and explained.
- - [x] Any new bot dialogue lines are translated.
- - [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
2026-05-29 23:08:21 -07:00
450 changed files with 3800 additions and 2050 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
DELETE FROM ai_playerbot_texts WHERE name IN (
'send_mail_disabled'
);
DELETE FROM ai_playerbot_texts_chance WHERE name IN (
'send_mail_disabled'
);
INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES
(1899, 'send_mail_disabled', 'I cannot send mail', 0, 0, '우편을 보낼 수 없습니다', 'Je ne peux pas envoyer de courrier', 'Ich kann keine Post senden', '我不能寄送邮件', '我不能寄送郵件', 'No puedo enviar correo', 'No puedo enviar correo', 'Я не могу отправить почту');
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES
('send_mail_disabled', 100);

View File

@ -12,8 +12,8 @@ bool AutoMaintenanceOnLevelupAction::Execute(Event /*event*/)
{ {
AutoPickTalents(); AutoPickTalents();
AutoLearnSpell(); AutoLearnSpell();
AutoUpgradeEquip();
AutoTeleportForLevel(); AutoTeleportForLevel();
AutoUpgradeEquip();
return true; return true;
} }
@ -21,13 +21,11 @@ bool AutoMaintenanceOnLevelupAction::Execute(Event /*event*/)
void AutoMaintenanceOnLevelupAction::AutoTeleportForLevel() void AutoMaintenanceOnLevelupAction::AutoTeleportForLevel()
{ {
if (!sPlayerbotAIConfig.autoTeleportForLevel || !sRandomPlayerbotMgr.IsRandomBot(bot)) if (!sPlayerbotAIConfig.autoTeleportForLevel || !sRandomPlayerbotMgr.IsRandomBot(bot))
{
return; return;
}
if (botAI->HasRealPlayerMaster()) if (botAI->HasRealPlayerMaster())
{
return; return;
}
sRandomPlayerbotMgr.RandomTeleportForLevel(bot); sRandomPlayerbotMgr.RandomTeleportForLevel(bot);
return; return;
} }
@ -89,21 +87,17 @@ void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out)
{ {
Quest const* quest = i->second; Quest const* quest = i->second;
// only process class-specific quests to learn class-related spells, cuz if (!quest->GetRequiredClasses() || quest->IsRepeatable() || quest->GetMinLevel() < 10 ||
// we don't want all these bunch of entries to be handled! quest->GetMinLevel() > bot->GetLevel())
if (!quest->GetRequiredClasses()) {
continue; continue;
}
// skip quests that are repeatable, too low level, or above bots' level
if (quest->IsRepeatable() || quest->GetMinLevel() < 10 || quest->GetMinLevel() > bot->GetLevel())
continue;
// skip if bot doesnt satisfy class, race, or skill requirements
if (!bot->SatisfyQuestClass(quest, false) || !bot->SatisfyQuestRace(quest, false) || if (!bot->SatisfyQuestClass(quest, false) || !bot->SatisfyQuestRace(quest, false) ||
!bot->SatisfyQuestSkill(quest, false)) !bot->SatisfyQuestSkill(quest, false))
{
continue; continue;
}
// use the same logic and impl from Player::learnQuestRewardedSpells
int32 spellId = quest->GetRewSpellCast(); int32 spellId = quest->GetRewSpellCast();
if (!spellId) if (!spellId)
@ -113,31 +107,26 @@ void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out)
if (!spellInfo) if (!spellInfo)
continue; continue;
// xinef: find effect with learn spell and check if we have this spell
bool found = false; bool found = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i) for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{ {
if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && spellInfo->Effects[i].TriggerSpell && if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && spellInfo->Effects[i].TriggerSpell &&
!bot->HasSpell(spellInfo->Effects[i].TriggerSpell)) !bot->HasSpell(spellInfo->Effects[i].TriggerSpell))
{ {
// pusywizard: don't re-add profession specialties!
if (SpellInfo const* triggeredInfo = sSpellMgr->GetSpellInfo(spellInfo->Effects[i].TriggerSpell)) if (SpellInfo const* triggeredInfo = sSpellMgr->GetSpellInfo(spellInfo->Effects[i].TriggerSpell))
if (triggeredInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL) if (triggeredInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL)
break; // pussywizard: break and not cast the spell (found is false) break;
found = true; found = true;
break; break;
} }
} }
// xinef: we know the spell, continue
if (!found) if (!found)
continue; continue;
bot->CastSpell(bot, spellId, true); bot->CastSpell(bot, spellId, true);
// Check if RewardDisplaySpell is set to output the proper spell learned
// after processing quests. Output the original RewardSpell otherwise.
uint32 rewSpellId = quest->GetRewSpell(); uint32 rewSpellId = quest->GetRewSpell();
if (rewSpellId) if (rewSpellId)
{ {
@ -167,12 +156,11 @@ std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* s
void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip() void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
{ {
if (!sPlayerbotAIConfig.autoUpgradeEquip || !sRandomPlayerbotMgr.IsRandomBot(bot)) if (!sRandomPlayerbotMgr.IsRandomBot(bot))
return; return;
PlayerbotFactory factory(bot, bot->GetLevel()); PlayerbotFactory factory(bot, bot->GetLevel());
// Clean up old consumables before adding new ones
factory.CleanupConsumables(); factory.CleanupConsumables();
factory.InitAmmo(); factory.InitAmmo();
@ -181,9 +169,6 @@ void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
factory.InitConsumables(); factory.InitConsumables();
factory.InitPotions(); factory.InitPotions();
if (!sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel) if (sPlayerbotAIConfig.autoUpgradeEquip)
{
if (sPlayerbotAIConfig.incrementalGearInit)
factory.InitEquipment(true); factory.InitEquipment(true);
}
} }

View File

@ -154,9 +154,11 @@ void EquipAction::EquipItem(Item* item)
calculator.SetOverflowPenalty(false); calculator.SetOverflowPenalty(false);
// Calculate item scores once and store them // Calculate item scores once and store them
float newItemScore = calculator.CalculateItem(itemId); float newItemScore = calculator.CalculateItem(itemId, item->GetItemRandomPropertyId());
float mainHandScore = mainHandItem ? calculator.CalculateItem(mainHandItem->GetTemplate()->ItemId) : 0.0f; float mainHandScore = mainHandItem
float offHandScore = offHandItem ? calculator.CalculateItem(offHandItem->GetTemplate()->ItemId) : 0.0f; ? calculator.CalculateItem(mainHandItem->GetTemplate()->ItemId, mainHandItem->GetItemRandomPropertyId()) : 0.0f;
float offHandScore = offHandItem
? calculator.CalculateItem(offHandItem->GetTemplate()->ItemId, offHandItem->GetItemRandomPropertyId()) : 0.0f;
// Determine where this weapon can go // Determine where this weapon can go
bool canGoMain = (invType == INVTYPE_WEAPON || bool canGoMain = (invType == INVTYPE_WEAPON ||

View File

@ -86,7 +86,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event /*event*/)
uint32 spellId = itr->first; uint32 spellId = itr->first;
const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo->IsAutocastable()) if (!spellInfo || !spellInfo->IsAutocastable())
continue; continue;
bool shouldApply = true; bool shouldApply = true;

View File

@ -6,6 +6,7 @@
#include "GenericSpellActions.h" #include "GenericSpellActions.h"
#include <ctime> #include <ctime>
#include <unordered_set>
#include "Event.h" #include "Event.h"
#include "ItemTemplate.h" #include "ItemTemplate.h"
@ -23,11 +24,119 @@
using ai::buff::MakeAuraQualifierForBuff; using ai::buff::MakeAuraQualifierForBuff;
using ai::spell::HasSpellOrCategoryCooldown; using ai::spell::HasSpellOrCategoryCooldown;
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell) namespace
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell)
{ {
std::unordered_set<uint32> const& GetMixedTriggerTrinketSpellIds()
{
static std::unordered_set<uint32> const mixedTriggerSpellIds = []()
{
std::unordered_set<uint32> onUseSpellIds;
std::unordered_set<uint32> onEquipSpellIds;
std::unordered_set<uint32> mixedSpellIds;
auto const* itemTemplates = sObjectMgr->GetItemTemplateStore();
if (!itemTemplates)
return mixedSpellIds;
auto const markSpellId = [&](int32 spellId, uint8 spellTrigger)
{
if (spellId <= 0)
return;
if (spellTrigger == ITEM_SPELLTRIGGER_ON_USE)
{
if (onEquipSpellIds.find(spellId) != onEquipSpellIds.end())
mixedSpellIds.insert(spellId);
onUseSpellIds.insert(spellId);
}
else if (spellTrigger == ITEM_SPELLTRIGGER_ON_EQUIP)
{
if (onUseSpellIds.find(spellId) != onUseSpellIds.end())
mixedSpellIds.insert(spellId);
onEquipSpellIds.insert(spellId);
}
};
for (auto const& itr : *itemTemplates)
{
ItemTemplate const& proto = itr.second;
if (proto.InventoryType != INVTYPE_TRINKET)
continue;
for (uint8 spellIndex = 0; spellIndex < MAX_ITEM_PROTO_SPELLS; ++spellIndex)
{
auto const& spellData = proto.Spells[spellIndex];
markSpellId(spellData.SpellId, spellData.SpellTrigger);
}
}
return mixedSpellIds;
}();
return mixedTriggerSpellIds;
}
bool IsManaRestoreEffect(SpellEffectInfo const& effectInfo)
{
return (effectInfo.Effect == SPELL_EFFECT_ENERGIZE &&
effectInfo.MiscValue == POWER_MANA) ||
(effectInfo.Effect == SPELL_EFFECT_APPLY_AURA &&
effectInfo.ApplyAuraName == SPELL_AURA_PERIODIC_ENERGIZE &&
effectInfo.MiscValue == POWER_MANA);
}
bool IsManaEfficiencyEffect(SpellEffectInfo const& effectInfo)
{
return effectInfo.Effect == SPELL_EFFECT_APPLY_AURA &&
(((effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN ||
effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN_PERCENT) &&
effectInfo.MiscValue == POWER_MANA) ||
effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_COST_SCHOOL ||
effectInfo.ApplyAuraName == SPELL_AURA_MOD_POWER_COST_SCHOOL_PCT ||
effectInfo.ApplyAuraName == SPELL_AURA_MOD_MANA_REGEN_INTERRUPT);
}
bool IsDefensiveTankEffect(SpellEffectInfo const& effectInfo)
{
if (effectInfo.Effect != SPELL_EFFECT_APPLY_AURA)
return false;
uint32 const tankRatingsMask =
(1u << CR_DEFENSE_SKILL) |
(1u << CR_DODGE) |
(1u << CR_PARRY) |
(1u << CR_BLOCK) |
(1u << CR_HIT_TAKEN_MELEE) |
(1u << CR_HIT_TAKEN_RANGED) |
(1u << CR_HIT_TAKEN_SPELL) |
(1u << CR_CRIT_TAKEN_MELEE) |
(1u << CR_CRIT_TAKEN_RANGED) |
(1u << CR_CRIT_TAKEN_SPELL);
switch (effectInfo.ApplyAuraName)
{
case SPELL_AURA_MOD_RESISTANCE:
return (effectInfo.MiscValue & SPELL_SCHOOL_MASK_NORMAL) != 0;
case SPELL_AURA_MOD_RATING:
return (effectInfo.MiscValue & tankRatingsMask) != 0;
case SPELL_AURA_MOD_INCREASE_HEALTH:
case SPELL_AURA_MOD_INCREASE_HEALTH_PERCENT:
case SPELL_AURA_MOD_PARRY_PERCENT:
case SPELL_AURA_MOD_DODGE_PERCENT:
case SPELL_AURA_MOD_BLOCK_PERCENT:
case SPELL_AURA_MOD_DAMAGE_PERCENT_TAKEN:
return true;
default:
return false;
}
}
} }
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell)
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell) {}
bool CastSpellAction::Execute(Event /*event*/) bool CastSpellAction::Execute(Event /*event*/)
{ {
if (spell == "conjure food" || spell == "conjure water") if (spell == "conjure food" || spell == "conjure water")
@ -53,18 +162,12 @@ bool CastSpellAction::Execute(Event /*event*/)
wstrToLower(wnamepart); wstrToLower(wnamepart);
if (!Utf8FitTo(spell, wnamepart)) if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue;
if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue; continue;
uint32 itemId = spellInfo->Effects[0].ItemType; uint32 itemId = spellInfo->Effects[0].ItemType;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId); ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto) if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue;
if (bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue; continue;
if (spellInfo->Id > castId) if (spellInfo->Id > castId)
@ -92,10 +195,7 @@ bool CastSpellAction::isUseful()
} }
Unit* spellTarget = GetTarget(); Unit* spellTarget = GetTarget();
if (!spellTarget) if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
return false;
if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
return false; return false;
// float combatReach = bot->GetCombatReach() + target->GetCombatReach(); // float combatReach = bot->GetCombatReach() + target->GetCombatReach();
@ -143,10 +243,7 @@ CastMeleeSpellAction::CastMeleeSpellAction(
bool CastMeleeSpellAction::isUseful() bool CastMeleeSpellAction::isUseful()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target || !bot->IsWithinMeleeRange(target))
return false;
if (!bot->IsWithinMeleeRange(target))
return false; return false;
return CastSpellAction::isUseful(); return CastSpellAction::isUseful();
@ -162,10 +259,7 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(
bool CastMeleeDebuffSpellAction::isUseful() bool CastMeleeDebuffSpellAction::isUseful()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target || !bot->IsWithinMeleeRange(target))
return false;
if (!bot->IsWithinMeleeRange(target))
return false; return false;
return CastDebuffSpellAction::isUseful(); return CastDebuffSpellAction::isUseful();
@ -175,14 +269,55 @@ bool CastAuraSpellAction::isUseful()
{ {
if (!GetTarget() || !CastSpellAction::isUseful()) if (!GetTarget() || !CastSpellAction::isUseful())
return false; return false;
Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration); Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration);
if (!aura) if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
if (beforeDuration && aura->GetDuration() < beforeDuration)
return true; return true;
return false; return false;
} }
bool CastBuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !CastSpellAction::isUseful())
return false;
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
return !aura || (beforeDuration && aura->GetDuration() < beforeDuration);
}
bool CastBuffSpellAction::Execute(Event /*event*/)
{
return botAI->CastSpell(spell, GetTarget());
}
bool GroupBuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !CastSpellAction::isUseful())
return false;
if (ai::buff::IsGroupVariantEnabled(bot, spell))
{
std::string const groupVariant = ai::buff::GroupVariantFor(spell);
if (!groupVariant.empty() && botAI->HasAura(groupVariant, target, false, isOwner, -1, checkDuration))
return false;
}
Aura* aura = botAI->GetAura(spell, target, isOwner, checkDuration);
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
return false;
}
bool GroupBuffSpellAction::Execute(Event /*event*/)
{
std::string const castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, spell);
return botAI->CastSpell(castName, GetTarget());
}
CastEnchantItemMainHandAction::CastEnchantItemMainHandAction( CastEnchantItemMainHandAction::CastEnchantItemMainHandAction(
PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
@ -248,25 +383,16 @@ Value<Unit*>* CurePartyMemberAction::GetTargetValue()
return context->GetValue<Unit*>("party member to dispel", dispelType); return context->GetValue<Unit*>("party member to dispel", dispelType);
} }
// Make Bots Paladin, druid, mage use the greater buff rank spell
// TODO Priest doen't verify il he have components
Value<Unit*>* BuffOnPartyAction::GetTargetValue() Value<Unit*>* BuffOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member without aura", spell);
}
Value<Unit*>* GroupBuffOnPartyAction::GetTargetValue()
{ {
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell)); return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
} }
bool BuffOnPartyAction::Execute(Event /*event*/)
{
std::string castName = spell; // default = mono
auto SendGroupRP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(
bot, botAI, castName, /*announceOnMissing=*/true, SendGroupRP);
return botAI->CastSpell(castName, GetTarget());
}
// End greater buff fix
CastShootAction::CastShootAction( CastShootAction::CastShootAction(
PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0) PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
{ {
@ -365,16 +491,7 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
bool CastEveryManForHimselfAction::isPossible() bool CastEveryManForHimselfAction::isPossible()
{ {
uint32 spellId = AI_VALUE2(uint32, "spell id", spell); uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId) return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
} }
bool CastEveryManForHimselfAction::isUseful() bool CastEveryManForHimselfAction::isUseful()
@ -390,16 +507,7 @@ bool CastEveryManForHimselfAction::isUseful()
bool CastWillOfTheForsakenAction::isPossible() bool CastWillOfTheForsakenAction::isPossible()
{ {
uint32 spellId = AI_VALUE2(uint32, "spell id", spell); uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId) return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
} }
bool CastWillOfTheForsakenAction::isUseful() bool CastWillOfTheForsakenAction::isUseful()
@ -427,70 +535,141 @@ bool UseTrinketAction::Execute(Event /*event*/)
bool UseTrinketAction::UseTrinket(Item* item) bool UseTrinketAction::UseTrinket(Item* item)
{ {
if (bot->CanUseItem(item) != EQUIP_ERR_OK) if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true))
return false;
if (bot->IsNonMeleeSpellCast(true))
return false; return false;
uint8 bagIndex = item->GetBagSlot(); uint8 bagIndex = item->GetBagSlot();
uint8 slot = item->GetSlot(); uint8 slot = item->GetSlot();
// uint8 spell_index = 0; //not used, line marked for removal.
uint8 cast_count = 1; uint8 cast_count = 1;
ObjectGuid item_guid = item->GetGUID(); ObjectGuid item_guid = item->GetGUID();
uint32 glyphIndex = 0; uint32 glyphIndex = 0;
uint8 castFlags = 0; uint8 castFlags = 0;
uint32 targetFlag = TARGET_FLAG_NONE; uint32 targetFlag = TARGET_FLAG_NONE;
uint32 spellId = 0; uint32 spellId = 0;
int32 itemSpellCooldown = 0;
uint32 itemSpellCategory = 0;
int32 itemSpellCategoryCooldown = 0;
for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i) for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
{ {
if (item->GetTemplate()->Spells[i].SpellId > 0 && if (item->GetTemplate()->Spells[i].SpellId > 0 &&
item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE)
{ {
spellId = item->GetTemplate()->Spells[i].SpellId; spellId = item->GetTemplate()->Spells[i].SpellId;
const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId); itemSpellCooldown = item->GetTemplate()->Spells[i].SpellCooldown;
itemSpellCategory = item->GetTemplate()->Spells[i].SpellCategory;
itemSpellCategoryCooldown = item->GetTemplate()->Spells[i].SpellCategoryCooldown;
uint64 const itemCooldownKey = (static_cast<uint64>(item->GetEntry()) << 32) | spellId;
uint32 const now = getMSTime();
if (itemSpellCooldown > 0)
{
auto const itemCooldownItr = trinketItemCooldownExpiries.find(itemCooldownKey);
if (itemCooldownItr != trinketItemCooldownExpiries.end())
{
if (itemCooldownItr->second > now)
return false;
trinketItemCooldownExpiries.erase(itemCooldownItr);
}
}
if (itemSpellCategory && itemSpellCategoryCooldown > 0)
{
auto const categoryCooldownItr = trinketCategoryCooldownExpiries.find(itemSpellCategory);
if (categoryCooldownItr != trinketCategoryCooldownExpiries.end())
{
if (categoryCooldownItr->second > now)
return false;
trinketCategoryCooldownExpiries.erase(categoryCooldownItr);
}
}
const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo || !spellInfo->IsPositive()) if (!spellInfo || !spellInfo->IsPositive())
return false; return false;
bool applyAura = false; bool applyAura = false;
bool restoresMana = false;
bool improvesManaEfficiency = false;
bool defensiveTankEffect = false;
for (int i = 0; i < MAX_SPELL_EFFECTS; i++) for (int i = 0; i < MAX_SPELL_EFFECTS; i++)
{ {
const SpellEffectInfo& effectInfo = spellInfo->Effects[i]; const SpellEffectInfo& effectInfo = spellInfo->Effects[i];
if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA) if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA)
{
applyAura = true; applyAura = true;
break;
} restoresMana = restoresMana || IsManaRestoreEffect(effectInfo);
improvesManaEfficiency = improvesManaEfficiency || IsManaEfficiencyEffect(effectInfo);
defensiveTankEffect = defensiveTankEffect || IsDefensiveTankEffect(effectInfo);
} }
if (!applyAura) if (!applyAura && !restoresMana)
return false; return false;
uint32 spellProcFlag = spellInfo->ProcFlags; if (restoresMana || improvesManaEfficiency)
{
if (!AI_VALUE2(bool, "has mana", "self target"))
return false;
// Handle items with procflag "if you kill a target that grants honor or experience" uint8 const manaPct = AI_VALUE2(uint8, "mana", "self target");
// Bots will "learn" the trinket proc, so CanCastSpell() will be true if ((restoresMana && manaPct >= sPlayerbotAIConfig.mediumMana) ||
// e.g. on Item https://www.wowhead.com/wotlk/item=44074/oracle-talisman-of-ablution leading to manaPct >= sPlayerbotAIConfig.highMana)
// constant casting of the proc spell onto themselfes https://www.wowhead.com/wotlk/spell=59787/oracle-ablutions
// This will lead to multiple hundreds of entries in m_appliedAuras -> Once killing an enemy -> Big diff time spikes
if (spellProcFlag != 0) return false;
if (!botAI->CanCastSpell(spellId, bot, false))
{ {
return false; return false;
} }
}
if (defensiveTankEffect)
{
uint8 const healthPct = AI_VALUE2(uint8, "health", "self target");
if (healthPct > sPlayerbotAIConfig.lowHealth)
return false;
}
auto const& mixedTriggerTrinketSpellIds = GetMixedTriggerTrinketSpellIds();
// Exclude trinkets that expose the same spell as both ON_EQUIP and ON_USE across
// item templates. Those are equip/proc effects leaking into the active-use path,
// as seen with the error versions of Oracle Talisman of Ablution (44870) and
// Frenzyheart Insignia of Fury (44869).
if (mixedTriggerTrinketSpellIds.find(spellId) != mixedTriggerTrinketSpellIds.end())
return false;
if (!botAI->CanCastSpell(spellId, bot, false, nullptr, item))
return false;
break; break;
} }
} }
if (!spellId) if (!spellId)
return false; return false;
WorldPacket packet(CMSG_USE_ITEM); WorldPacket packet(CMSG_USE_ITEM);
packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags; packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags;
targetFlag = TARGET_FLAG_NONE; targetFlag = TARGET_FLAG_NONE;
packet << targetFlag << bot->GetPackGUID(); packet << targetFlag << bot->GetPackGUID();
bot->GetSession()->HandleUseItemOpcode(packet); bot->GetSession()->HandleUseItemOpcode(packet);
uint32 const now = getMSTime();
uint32 const cooldownDelay = bot->GetSpellCooldownDelay(spellId);
if (cooldownDelay > 0)
{
if (itemSpellCooldown > 0)
{
uint64 const itemCooldownKey = (static_cast<uint64>(item->GetEntry()) << 32) | spellId;
trinketItemCooldownExpiries[itemCooldownKey] = now + static_cast<uint32>(itemSpellCooldown);
}
if (itemSpellCategory && itemSpellCategoryCooldown > 0)
{
trinketCategoryCooldownExpiries[itemSpellCategory] = now + static_cast<uint32>(itemSpellCategoryCooldown);
}
}
return true; return true;
} }
@ -500,9 +679,8 @@ bool CastDebuffSpellAction::isUseful()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld()) if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false; return false;
}
return CastAuraSpellAction::isUseful() && return CastAuraSpellAction::isUseful() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
} }

View File

@ -69,9 +69,7 @@ class CastDebuffSpellAction : public CastAuraSpellAction
{ {
public: public:
CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f) CastDebuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = false, float needLifeTime = 8.0f)
: CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) : CastAuraSpellAction(botAI, spell, isOwner), needLifeTime(needLifeTime) {}
{
}
bool isUseful() override; bool isUseful() override;
private: private:
@ -90,9 +88,7 @@ class CastDebuffSpellOnAttackerAction : public CastDebuffSpellAction
public: public:
CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true, CastDebuffSpellOnAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
float needLifeTime = 8.0f) float needLifeTime = 8.0f)
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {}
{
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; } std::string const getName() override { return spell + " on attacker"; }
@ -104,9 +100,7 @@ class CastDebuffSpellOnMeleeAttackerAction : public CastDebuffSpellAction
public: public:
CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true, CastDebuffSpellOnMeleeAttackerAction(PlayerbotAI* botAI, std::string const spell, bool isOwner = true,
float needLifeTime = 8.0f) float needLifeTime = 8.0f)
: CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) : CastDebuffSpellAction(botAI, spell, isOwner, needLifeTime) {}
{
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; } std::string const getName() override { return spell + " on attacker"; }
@ -119,6 +113,19 @@ public:
CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0); CastBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false, uint32 beforeDuration = 0);
std::string const GetTargetName() override { return "self target"; } std::string const GetTargetName() override { return "self target"; }
bool isUseful() override;
bool Execute(Event event) override;
};
class GroupBuffSpellAction : public CastBuffSpellAction
{
public:
GroupBuffSpellAction(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = false,
uint32 beforeDuration = 0)
: CastBuffSpellAction(botAI, spell, checkIsOwner, beforeDuration) {}
bool isUseful() override;
bool Execute(Event event) override;
}; };
class CastEnchantItemMainHandAction : public CastSpellAction class CastEnchantItemMainHandAction : public CastSpellAction
@ -151,8 +158,6 @@ public:
// Yunfan: Mana efficiency tell the bot how to save mana. The higher the better. // Yunfan: Mana efficiency tell the bot how to save mana. The higher the better.
HealingManaEfficiency manaEfficiency; HealingManaEfficiency manaEfficiency;
uint8 estAmount; uint8 estAmount;
// protected:
}; };
class CastAoeHealSpellAction : public CastHealingSpellAction class CastAoeHealSpellAction : public CastHealingSpellAction
@ -192,9 +197,7 @@ class HealPartyMemberAction : public CastHealingSpellAction, public PartyMemberA
public: public:
HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f, HealPartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint8 estAmount = 15.0f,
HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true) HealingManaEfficiency manaEfficiency = HealingManaEfficiency::MEDIUM, bool isOwner = true)
: CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) : CastHealingSpellAction(botAI, spell, estAmount, manaEfficiency, isOwner), PartyMemberActionNameSupport(spell) {}
{
}
std::string const GetTargetName() override { return "party member to heal"; } std::string const GetTargetName() override { return "party member to heal"; }
std::string const getName() override { return PartyMemberActionNameSupport::getName(); } std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
@ -219,9 +222,7 @@ class CurePartyMemberAction : public CastSpellAction, public PartyMemberActionNa
{ {
public: public:
CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType) CurePartyMemberAction(PlayerbotAI* botAI, std::string const spell, uint32 dispelType)
: CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) : CastSpellAction(botAI, spell), PartyMemberActionNameSupport(spell), dispelType(dispelType) {}
{
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return PartyMemberActionNameSupport::getName(); } std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
@ -230,18 +231,25 @@ protected:
uint32 dispelType; uint32 dispelType;
}; };
// Make Bots Paladin, druid, mage use the greater buff rank spell
class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport class BuffOnPartyAction : public CastBuffSpellAction, public PartyMemberActionNameSupport
{ {
public: public:
BuffOnPartyAction(PlayerbotAI* botAI, std::string const spell) BuffOnPartyAction(PlayerbotAI* botAI, std::string const spell)
: CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) { } : CastBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
};
class GroupBuffOnPartyAction : public GroupBuffSpellAction, public PartyMemberActionNameSupport
{
public:
GroupBuffOnPartyAction(PlayerbotAI* botAI, std::string const spell)
: GroupBuffSpellAction(botAI, spell), PartyMemberActionNameSupport(spell) {}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
std::string const getName() override { return PartyMemberActionNameSupport::getName(); } std::string const getName() override { return PartyMemberActionNameSupport::getName(); }
}; };
// End Fix
class CastShootAction : public CastSpellAction class CastShootAction : public CastSpellAction
{ {
@ -323,8 +331,13 @@ class UseTrinketAction : public Action
public: public:
UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {} UseTrinketAction(PlayerbotAI* botAI) : Action(botAI, "use trinket") {}
bool Execute(Event event) override; bool Execute(Event event) override;
protected: protected:
bool UseTrinket(Item* trinket); bool UseTrinket(Item* trinket);
private:
std::unordered_map<uint64, uint32> trinketItemCooldownExpiries;
std::unordered_map<uint32, uint32> trinketCategoryCooldownExpiries;
}; };
class CastSpellOnEnemyHealerAction : public CastSpellAction class CastSpellOnEnemyHealerAction : public CastSpellAction
@ -461,12 +474,11 @@ class BuffOnMainTankAction : public CastBuffSpellAction, public MainTankActionNa
{ {
public: public:
BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false) BuffOnMainTankAction(PlayerbotAI* ai, std::string spell, bool checkIsOwner = false)
: CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) : CastBuffSpellAction(ai, spell, checkIsOwner), MainTankActionNameSupport(spell) {}
{
}
public: public:
virtual Value<Unit*>* GetTargetValue(); virtual Value<Unit*>* GetTargetValue();
virtual std::string const getName() { return MainTankActionNameSupport::getName(); } virtual std::string const getName() { return MainTankActionNameSupport::getName(); }
}; };
#endif #endif

View File

@ -44,6 +44,21 @@ bool ResetAiAction::Execute(Event event)
} }
} }
} }
if (Player* master = botAI->GetMaster())
{
Group* botGroup = bot->GetGroup();
Group* masterGroup = master->GetGroup();
if (botGroup && (!masterGroup || masterGroup != botGroup))
botAI->SetMaster(nullptr);
}
if (sRandomPlayerbotMgr.IsRandomBot(bot) && !bot->InBattleground())
{
if (bot->GetGroup() && (!botAI->GetMaster() || GET_PLAYERBOT_AI(botAI->GetMaster())))
{
if (Player* newMaster = botAI->FindNewMaster())
botAI->SetMaster(newMaster);
}
}
PlayerbotRepository::instance().Reset(botAI); PlayerbotRepository::instance().Reset(botAI);
botAI->ResetStrategies(false); botAI->ResetStrategies(false);
botAI->TellMaster("AI was reset to defaults"); botAI->TellMaster("AI was reset to defaults");

View File

@ -34,24 +34,23 @@ bool SendMailAction::Execute(Event event)
Player* receiver = GetMaster(); Player* receiver = GetMaster();
Player* tellTo = receiver; Player* tellTo = receiver;
std::vector<std::string> ss = split(text, ' ');
if (ss.size() > 1)
{
if (Player* p = ObjectAccessor::FindPlayer(ObjectGuid(uint64(ss[ss.size() - 1].c_str()))))
receiver = p;
}
if (!receiver) if (!receiver)
receiver = event.getOwner(); receiver = event.getOwner();
if (!receiver || receiver == bot) if (!receiver || receiver == bot)
{
return false; return false;
}
if (!tellTo) if (!tellTo)
tellTo = receiver; tellTo = receiver;
if (!sPlayerbotAIConfig.botSendMailEnabled)
{
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_disabled", "I cannot send mail", {}),
LANG_UNIVERSAL, tellTo);
return false;
}
if (!mailboxFound && !randomBot) if (!mailboxFound && !randomBot)
{ {
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault( bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(

View File

@ -16,7 +16,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
triggers.push_back( triggers.push_back(
new TriggerNode("uninvite guid", { NextAction("uninvite", relevance) })); new TriggerNode("uninvite guid", { NextAction("uninvite", relevance) }));
triggers.push_back( triggers.push_back(
new TriggerNode("group set leader", { /*NextAction("leader", relevance),*/ })); new TriggerNode("group set leader", { NextAction("reset botAI", relevance) }));
triggers.push_back(new TriggerNode( triggers.push_back(new TriggerNode(
"not enough money", { NextAction("tell not enough money", relevance) })); "not enough money", { NextAction("tell not enough money", relevance) }));
triggers.push_back( triggers.push_back(

View File

@ -7,6 +7,7 @@
#include <string> #include <string>
#include "GenericBuffUtils.h"
#include "CreatureAI.h" #include "CreatureAI.h"
#include "ItemVisitors.h" #include "ItemVisitors.h"
#include "LastSpellCastValue.h" #include "LastSpellCastValue.h"
@ -41,52 +42,50 @@ bool LowEnergyTrigger::IsActive()
bool NoPetTrigger::IsActive() bool NoPetTrigger::IsActive()
{ {
return (bot->GetMinionGUID().IsEmpty()) && (!AI_VALUE(Unit*, "pet target")) && (!bot->GetGuardianPet()) && return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() &&
(!bot->GetFirstControlled()) && (!AI_VALUE2(bool, "mounted", "self target")); !bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target");
} }
bool HasPetTrigger::IsActive() bool HasPetTrigger::IsActive()
{ {
return (AI_VALUE(Unit*, "pet target")) && !AI_VALUE2(bool, "mounted", "self target"); return AI_VALUE(Unit*, "pet target") && !AI_VALUE2(bool, "mounted", "self target");
;
} }
bool PetAttackTrigger::IsActive() bool PetAttackTrigger::IsActive()
{ {
Guardian* pet = bot->GetGuardianPet(); Guardian* pet = bot->GetGuardianPet();
if (!pet) if (!pet)
{
return false; return false;
}
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
if (!target) if (!target)
{
return false; return false;
}
if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack()) if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack())
{
return false; return false;
}
if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat()) if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat())
{
return false; return false;
}
return true; return true;
} }
bool HighManaTrigger::IsActive() bool HighManaTrigger::IsActive()
{ {
return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana; return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.highMana;
} }
bool AlmostFullManaTrigger::IsActive() bool AlmostFullManaTrigger::IsActive()
{ {
return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > 85; return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") > 85;
} }
bool EnoughManaTrigger::IsActive() bool EnoughManaTrigger::IsActive()
{ {
return AI_VALUE2(bool, "has mana", "self target") && AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana; return AI_VALUE2(bool, "has mana", "self target") &&
AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.highMana;
} }
bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; } bool RageAvailable::IsActive() { return AI_VALUE2(uint8, "rage", "self target") >= amount; }
@ -101,9 +100,8 @@ bool TargetWithComboPointsLowerHealTrigger::IsActive()
{ {
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
if (!target || !target->IsAlive() || !target->IsInWorld()) if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false; return false;
}
return ComboPointsAvailableTrigger::IsActive() && return ComboPointsAvailableTrigger::IsActive() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime; (target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime;
} }
@ -164,19 +162,27 @@ bool BuffTrigger::IsActive()
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
return false; return false;
if (!SpellTrigger::IsActive())
return false;
Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration); Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration);
if (!aura) if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
if (beforeDuration && aura->GetDuration() < beforeDuration)
return true; return true;
return false; return false;
} }
Value<Unit*>* BuffOnPartyTrigger::GetTargetValue() Value<Unit*>* BuffOnPartyTrigger::GetTargetValue()
{ {
return context->GetValue<Unit*>("party member without aura", spell); return context->GetValue<Unit*>(
"party member without aura", ai::buff::MakeAuraQualifierForBuff(spell));
}
bool BuffOnPartyTrigger::IsActive()
{
Unit* target = GetTarget();
if (ai::buff::ShouldDeferPartyBuffEvaluationForRecentLogin(bot, target, spell))
return false;
return BuffTrigger::IsActive();
} }
bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); } bool ProtectPartyMemberTrigger::IsActive() { return AI_VALUE(Unit*, "party member to protect"); }
@ -209,13 +215,14 @@ bool MediumThreatTrigger::IsActive()
{ {
if (!AI_VALUE(Unit*, "main tank")) if (!AI_VALUE(Unit*, "main tank"))
return false; return false;
return MyAttackerCountTrigger::IsActive(); return MyAttackerCountTrigger::IsActive();
} }
bool LowTankThreatTrigger::IsActive() bool LowTankThreatTrigger::IsActive()
{ {
Unit* mt = AI_VALUE(Unit*, "main tank"); Unit* mainTank = AI_VALUE(Unit*, "main tank");
if (!mt) if (!mainTank)
return false; return false;
Unit* current_target = AI_VALUE(Unit*, "current target"); Unit* current_target = AI_VALUE(Unit*, "current target");
@ -224,7 +231,7 @@ bool LowTankThreatTrigger::IsActive()
ThreatManager& mgr = current_target->GetThreatMgr(); ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot); float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mt); float tankThreat = mgr.GetThreat(mainTank);
return tankThreat == 0.0f || threat > tankThreat * 0.5f; return tankThreat == 0.0f || threat > tankThreat * 0.5f;
} }
@ -232,9 +239,8 @@ bool AoeTrigger::IsActive()
{ {
Unit* current_target = AI_VALUE(Unit*, "current target"); Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target) if (!current_target)
{
return false; return false;
}
GuidVector attackers = context->GetValue<GuidVector>("attackers")->Get(); GuidVector attackers = context->GetValue<GuidVector>("attackers")->Get();
int attackers_count = 0; int attackers_count = 0;
for (ObjectGuid const guid : attackers) for (ObjectGuid const guid : attackers)
@ -242,11 +248,10 @@ bool AoeTrigger::IsActive()
Unit* unit = botAI->GetUnit(guid); Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive()) if (!unit || !unit->IsAlive())
continue; continue;
if (unit->GetDistance(current_target->GetPosition()) <= range) if (unit->GetDistance(current_target->GetPosition()) <= range)
{
attackers_count++; attackers_count++;
} }
}
return attackers_count >= amount; return attackers_count >= amount;
} }
@ -274,20 +279,19 @@ bool DebuffTrigger::IsActive()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld()) if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false; return false;
}
return BuffTrigger::IsActive() && (target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime; return BuffTrigger::IsActive() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
} }
bool DebuffOnBossTrigger::IsActive() bool DebuffOnBossTrigger::IsActive()
{ {
if (!DebuffTrigger::IsActive()) if (!DebuffTrigger::IsActive())
{
return false; return false;
}
Creature* c = GetTarget()->ToCreature(); Creature* creature = GetTarget()->ToCreature();
return c && ((c->IsDungeonBoss()) || (c->isWorldBoss())); return creature && (creature->IsDungeonBoss() || creature->isWorldBoss());
} }
bool SpellTrigger::IsActive() { return GetTarget(); } bool SpellTrigger::IsActive() { return GetTarget(); }
@ -317,9 +321,7 @@ bool SpellCooldownTrigger::IsActive()
} }
RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability) RandomTrigger::RandomTrigger(PlayerbotAI* botAI, std::string const name, int32 probability)
: Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) : Trigger(botAI, name), probability(probability), lastCheck(getMSTime()) {}
{
}
bool RandomTrigger::IsActive() bool RandomTrigger::IsActive()
{ {
@ -330,6 +332,7 @@ bool RandomTrigger::IsActive()
int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier); int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier);
if (k < 1) if (k < 1)
k = 1; k = 1;
return (rand() % k) == 0; return (rand() % k) == 0;
} }
@ -368,9 +371,11 @@ bool BoostTrigger::IsActive()
{ {
if (!BuffTrigger::IsActive()) if (!BuffTrigger::IsActive())
return false; return false;
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->ToPlayer()) if (target && target->ToPlayer())
return true; return true;
return AI_VALUE(uint8, "balance") <= balance; return AI_VALUE(uint8, "balance") <= balance;
} }
@ -379,20 +384,19 @@ bool GenericBoostTrigger::IsActive()
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->ToPlayer()) if (target && target->ToPlayer())
return true; return true;
return AI_VALUE(uint8, "balance") <= balance; return AI_VALUE(uint8, "balance") <= balance;
} }
bool HealerShouldAttackTrigger::IsActive() bool HealerShouldAttackTrigger::IsActive()
{ {
// nobody can help me
if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1) if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1)
return true; return true;
if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth) if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth)
return false; return false;
// special check for resto druid (dont remove tree of life frequently) if (bot->GetAura(33891)) // Tree of Life
if (bot->GetAura(33891))
{ {
LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get(); LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get();
if (lastSpell.timer + 5 > time(nullptr)) if (lastSpell.timer + 5 > time(nullptr))
@ -401,7 +405,6 @@ bool HealerShouldAttackTrigger::IsActive()
int manaThreshold; int manaThreshold;
int balance = AI_VALUE(uint8, "balance"); int balance = AI_VALUE(uint8, "balance");
// higher threshold in higher pressure
if (balance <= 50) if (balance <= 50)
manaThreshold = 85; manaThreshold = 85;
else if (balance <= 100) else if (balance <= 100)
@ -425,13 +428,7 @@ bool InterruptSpellTrigger::IsActive()
bool DeflectSpellTrigger::IsActive() bool DeflectSpellTrigger::IsActive()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID())
return false;
if (!target->IsNonMeleeSpellCast(true))
return false;
if (target->GetTarget() != bot->GetGUID())
return false; return false;
uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get(); uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get();
@ -462,6 +459,7 @@ bool DeflectSpellTrigger::IsActive()
return true; return true;
} }
} }
return false; return false;
} }
@ -495,17 +493,16 @@ bool FearSleepSapTrigger::IsActive()
bool HasAuraStackTrigger::IsActive() bool HasAuraStackTrigger::IsActive()
{ {
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); return botAI->GetAura(getName(), GetTarget(), false, true, stack);
// sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "HasAuraStackTrigger::IsActive %s %d", getName(), aura ?
// aura->GetStackAmount() : -1);
return aura;
} }
bool TimerTrigger::IsActive() bool TimerTrigger::IsActive()
{ {
if (time(nullptr) != lastCheck) time_t now = time(nullptr);
if (now != lastCheck)
{ {
lastCheck = time(nullptr); lastCheck = now;
return true; return true;
} }
@ -552,9 +549,8 @@ bool IsBehindTargetTrigger::IsActive()
bool IsNotBehindTargetTrigger::IsActive() bool IsNotBehindTargetTrigger::IsActive()
{ {
if (botAI->HasStrategy("stay", botAI->GetState())) if (botAI->HasStrategy("stay", botAI->GetState()))
{
return false; return false;
}
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
return target && !AI_VALUE2(bool, "behind", "current target"); return target && !AI_VALUE2(bool, "behind", "current target");
} }
@ -562,9 +558,8 @@ bool IsNotBehindTargetTrigger::IsActive()
bool IsNotFacingTargetTrigger::IsActive() bool IsNotFacingTargetTrigger::IsActive()
{ {
if (botAI->HasStrategy("stay", botAI->GetState())) if (botAI->HasStrategy("stay", botAI->GetState()))
{
return false; return false;
}
return !AI_VALUE2(bool, "facing", "current target"); return !AI_VALUE2(bool, "facing", "current target");
} }
@ -581,12 +576,14 @@ bool NoPossibleTargetsTrigger::IsActive()
return !targets.size(); return !targets.size();
} }
bool PossibleAddsTrigger::IsActive() { return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target"); } bool PossibleAddsTrigger::IsActive()
{
return AI_VALUE(bool, "possible adds") && !AI_VALUE(ObjectGuid, "pull target");
}
bool NotDpsTargetActiveTrigger::IsActive() bool NotDpsTargetActiveTrigger::IsActive()
{ {
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
// do not switch if enemy target
if (target && target->IsAlive()) if (target && target->IsAlive())
{ {
Unit* enemy = AI_VALUE(Unit*, "enemy player target"); Unit* enemy = AI_VALUE(Unit*, "enemy player target");
@ -604,7 +601,6 @@ bool NotDpsAoeTargetActiveTrigger::IsActive()
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
Unit* enemy = AI_VALUE(Unit*, "enemy player target"); Unit* enemy = AI_VALUE(Unit*, "enemy player target");
// do not switch if enemy target
if (target && target == enemy && target->IsAlive()) if (target && target == enemy && target->IsAlive())
return false; return false;
@ -638,7 +634,10 @@ Value<Unit*>* InterruptEnemyHealerTrigger::GetTargetValue()
return context->GetValue<Unit*>("enemy healer target", spell); return context->GetValue<Unit*>("enemy healer target", spell);
} }
bool RandomBotUpdateTrigger::IsActive() { return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update"); } bool RandomBotUpdateTrigger::IsActive()
{
return RandomTrigger::IsActive() && AI_VALUE(bool, "random bot update");
}
bool NoNonBotPlayersAroundTrigger::IsActive() bool NoNonBotPlayersAroundTrigger::IsActive()
{ {
@ -718,43 +717,24 @@ bool AmmoCountTrigger::IsActive()
bool NewPetTrigger::IsActive() bool NewPetTrigger::IsActive()
{ {
// Get the bot player object from the AI
Player* bot = botAI->GetBot();
if (!bot)
return false;
// Try to get the current pet; initialize guardian and GUID to null/empty
Pet* pet = bot->GetPet();
Guardian* guardian = nullptr;
ObjectGuid currentPetGuid = ObjectGuid::Empty; ObjectGuid currentPetGuid = ObjectGuid::Empty;
// If bot has a pet, get its GUID if (Pet* pet = bot->GetPet())
if (pet)
{
currentPetGuid = pet->GetGUID(); currentPetGuid = pet->GetGUID();
} else if (Guardian* guardian = bot->GetGuardianPet())
else
{
// If no pet, try to get a guardian pet and its GUID
guardian = bot->GetGuardianPet();
if (guardian)
currentPetGuid = guardian->GetGUID(); currentPetGuid = guardian->GetGUID();
}
// If the current pet or guardian GUID has changed (including becoming empty), reset the trigger state
if (currentPetGuid != lastPetGuid) if (currentPetGuid != lastPetGuid)
{ {
triggered = false; triggered = false;
lastPetGuid = currentPetGuid; lastPetGuid = currentPetGuid;
} }
// If there's a valid current pet/guardian (non-empty GUID) and we haven't triggered yet, activate trigger
if (currentPetGuid != ObjectGuid::Empty && !triggered) if (currentPetGuid != ObjectGuid::Empty && !triggered)
{ {
triggered = true; triggered = true;
return true; return true;
} }
// Otherwise, do not activate
return false; return false;
} }

View File

@ -20,9 +20,7 @@ class StatAvailable : public Trigger
{ {
public: public:
StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available") StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available")
: Trigger(botAI, name), amount(amount) : Trigger(botAI, name), amount(amount) {}
{
}
protected: protected:
int32 amount; int32 amount;
@ -118,8 +116,8 @@ public:
class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger
{ {
public: public:
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* ai, int32 combo_point = 5, float lifeTime = 8.0f) TargetWithComboPointsLowerHealTrigger(PlayerbotAI* botAI, int32 combo_point = 5, float lifeTime = 8.0f)
: ComboPointsAvailableTrigger(ai, combo_point), lifeTime(lifeTime) : ComboPointsAvailableTrigger(botAI, combo_point), lifeTime(lifeTime)
{ {
} }
bool IsActive() override; bool IsActive() override;
@ -196,7 +194,6 @@ public:
bool IsActive() override; bool IsActive() override;
}; };
// TODO: check other targets
class InterruptSpellTrigger : public SpellTrigger class InterruptSpellTrigger : public SpellTrigger
{ {
public: public:
@ -217,9 +214,7 @@ class AttackerCountTrigger : public Trigger
{ {
public: public:
AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance) AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance)
: Trigger(botAI), amount(amount), distance(distance) : Trigger(botAI), amount(amount), distance(distance) {}
{
}
bool IsActive() override; bool IsActive() override;
std::string const getName() override { return "attacker count"; } std::string const getName() override { return "attacker count"; }
@ -269,9 +264,7 @@ class AoeTrigger : public AttackerCountTrigger
{ {
public: public:
AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f) AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f)
: AttackerCountTrigger(botAI, amount), range(range) : AttackerCountTrigger(botAI, amount), range(range) {}
{
}
bool IsActive() override; bool IsActive() override;
std::string const getName() override { return "aoe"; } std::string const getName() override { return "aoe"; }
@ -317,7 +310,8 @@ public:
class BuffTrigger : public SpellTrigger class BuffTrigger : public SpellTrigger
{ {
public: public:
BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0) BuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1,
bool checkIsOwner = false, bool checkDuration = false, uint32 beforeDuration = 0)
: SpellTrigger(botAI, spell, checkInterval) : SpellTrigger(botAI, spell, checkInterval)
{ {
this->checkIsOwner = checkIsOwner; this->checkIsOwner = checkIsOwner;
@ -339,11 +333,10 @@ class BuffOnPartyTrigger : public BuffTrigger
{ {
public: public:
BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1) BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
: BuffTrigger(botAI, spell, checkInterval) : BuffTrigger(botAI, spell, checkInterval) {}
{
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
bool IsActive() override;
std::string const getName() override { return spell + " on party"; } std::string const getName() override { return spell + " on party"; }
}; };
@ -393,9 +386,7 @@ class DebuffTrigger : public BuffTrigger
public: public:
DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false, DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false,
float needLifeTime = 8.0f, uint32 beforeDuration = 0) float needLifeTime = 8.0f, uint32 beforeDuration = 0)
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) : BuffTrigger(botAI, spell, checkInterval, checkIsOwner, false, beforeDuration), needLifeTime(needLifeTime) {}
{
}
std::string const GetTargetName() override { return "current target"; } std::string const GetTargetName() override { return "current target"; }
bool IsActive() override; bool IsActive() override;
@ -408,9 +399,7 @@ class DebuffOnBossTrigger : public DebuffTrigger
{ {
public: public:
DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false) DebuffOnBossTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false)
: DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) : DebuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
{
}
bool IsActive() override; bool IsActive() override;
}; };
@ -419,9 +408,7 @@ class DebuffOnAttackerTrigger : public DebuffTrigger
public: public:
DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true, DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
float needLifeTime = 8.0f) float needLifeTime = 8.0f)
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
{
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; } std::string const getName() override { return spell + " on attacker"; }
@ -432,9 +419,7 @@ class DebuffOnMeleeAttackerTrigger : public DebuffTrigger
public: public:
DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true, DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
float needLifeTime = 8.0f) float needLifeTime = 8.0f)
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) : DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
{
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; } std::string const getName() override { return spell + " on attacker"; }
@ -444,9 +429,7 @@ class BoostTrigger : public BuffTrigger
{ {
public: public:
BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f) BoostTrigger(PlayerbotAI* botAI, std::string const spell, float balance = 50.f)
: BuffTrigger(botAI, spell, 1), balance(balance) : BuffTrigger(botAI, spell, 1), balance(balance) {}
{
}
bool IsActive() override; bool IsActive() override;
@ -458,9 +441,7 @@ class GenericBoostTrigger : public Trigger
{ {
public: public:
GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f) GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f)
: Trigger(botAI, "generic boost", 1), balance(balance) : Trigger(botAI, "generic boost", 1), balance(balance) {}
{
}
bool IsActive() override; bool IsActive() override;
@ -472,9 +453,7 @@ class HealerShouldAttackTrigger : public Trigger
{ {
public: public:
HealerShouldAttackTrigger(PlayerbotAI* botAI) HealerShouldAttackTrigger(PlayerbotAI* botAI)
: Trigger(botAI, "healer should attack", 1) : Trigger(botAI, "healer should attack", 1) {}
{
}
bool IsActive() override; bool IsActive() override;
}; };
@ -580,7 +559,7 @@ public:
class HasPetTrigger : public Trigger class HasPetTrigger : public Trigger
{ {
public: public:
HasPetTrigger(PlayerbotAI* ai) : Trigger(ai, "has pet", 5 * 1000) {} HasPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "has pet", 5 * 1000) {}
virtual bool IsActive() override; virtual bool IsActive() override;
}; };
@ -588,7 +567,7 @@ public:
class PetAttackTrigger : public Trigger class PetAttackTrigger : public Trigger
{ {
public: public:
PetAttackTrigger(PlayerbotAI* ai) : Trigger(ai, "pet attack") {} PetAttackTrigger(PlayerbotAI* botAI) : Trigger(botAI, "pet attack") {}
virtual bool IsActive() override; virtual bool IsActive() override;
}; };
@ -597,9 +576,7 @@ class ItemCountTrigger : public Trigger
{ {
public: public:
ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000) ItemCountTrigger(PlayerbotAI* botAI, std::string const item, int32 count, int32 interval = 30 * 1000)
: Trigger(botAI, item, interval), item(item), count(count) : Trigger(botAI, item, interval), item(item), count(count) {}
{
}
bool IsActive() override; bool IsActive() override;
std::string const getName() override { return "item count"; } std::string const getName() override { return "item count"; }
@ -613,9 +590,7 @@ class AmmoCountTrigger : public ItemCountTrigger
{ {
public: public:
AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000) AmmoCountTrigger(PlayerbotAI* botAI, std::string const item, uint32 count = 1, int32 interval = 30 * 1000)
: ItemCountTrigger(botAI, item, count, interval) : ItemCountTrigger(botAI, item, count, interval) {}
{
}
bool IsActive() override; bool IsActive() override;
}; };
@ -623,9 +598,7 @@ class HasAuraTrigger : public Trigger
{ {
public: public:
HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1) HasAuraTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
: Trigger(botAI, spell, checkInterval) : Trigger(botAI, spell, checkInterval) {}
{
}
std::string const GetTargetName() override { return "self target"; } std::string const GetTargetName() override { return "self target"; }
bool IsActive() override; bool IsActive() override;
@ -634,10 +607,8 @@ public:
class HasAuraStackTrigger : public Trigger class HasAuraStackTrigger : public Trigger
{ {
public: public:
HasAuraStackTrigger(PlayerbotAI* ai, std::string spell, int stack, int checkInterval = 1) HasAuraStackTrigger(PlayerbotAI* botAI, std::string spell, int stack, int checkInterval = 1)
: Trigger(ai, spell, checkInterval), stack(stack) : Trigger(botAI, spell, checkInterval), stack(stack) {}
{
}
std::string const GetTargetName() override { return "self target"; } std::string const GetTargetName() override { return "self target"; }
bool IsActive() override; bool IsActive() override;
@ -858,9 +829,7 @@ class StayTimeTrigger : public Trigger
{ {
public: public:
StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name) StayTimeTrigger(PlayerbotAI* botAI, uint32 delay, std::string const name)
: Trigger(botAI, name, 5 * 1000), delay(delay) : Trigger(botAI, name, 5 * 1000), delay(delay) {}
{
}
bool IsActive() override; bool IsActive() override;
@ -877,7 +846,7 @@ public:
class ReturnToStayPositionTrigger : public Trigger class ReturnToStayPositionTrigger : public Trigger
{ {
public: public:
ReturnToStayPositionTrigger(PlayerbotAI* ai) : Trigger(ai, "return to stay position", 2) {} ReturnToStayPositionTrigger(PlayerbotAI* botAI) : Trigger(botAI, "return to stay position", 2) {}
virtual bool IsActive() override; virtual bool IsActive() override;
}; };
@ -892,9 +861,7 @@ class GiveItemTrigger : public Trigger
{ {
public: public:
GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item) GiveItemTrigger(PlayerbotAI* botAI, std::string const name, std::string const item)
: Trigger(botAI, name, 2 * 1000), item(item) : Trigger(botAI, name, 2 * 1000), item(item) {}
{
}
bool IsActive() override; bool IsActive() override;
@ -962,9 +929,7 @@ class BuffOnMainTankTrigger : public BuffTrigger
{ {
public: public:
BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1) BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1)
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner) : BuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
{
}
public: public:
virtual Value<Unit*>* GetTargetValue(); virtual Value<Unit*>* GetTargetValue();
@ -973,7 +938,7 @@ public:
class SelfResurrectTrigger : public Trigger class SelfResurrectTrigger : public Trigger
{ {
public: public:
SelfResurrectTrigger(PlayerbotAI* ai) : Trigger(ai, "can self resurrect") {} SelfResurrectTrigger(PlayerbotAI* botAI) : Trigger(botAI, "can self resurrect") {}
bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); } bool IsActive() override { return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL); }
}; };
@ -981,7 +946,7 @@ public:
class NewPetTrigger : public Trigger class NewPetTrigger : public Trigger
{ {
public: public:
NewPetTrigger(PlayerbotAI* ai) : Trigger(ai, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {} NewPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "new pet"), lastPetGuid(ObjectGuid::Empty), triggered(false) {}
bool IsActive() override; bool IsActive() override;

View File

@ -4,23 +4,89 @@
*/ */
#include "GenericBuffUtils.h" #include "GenericBuffUtils.h"
#include "PlayerbotAIConfig.h"
#include <map>
#include "Player.h"
#include "Group.h"
#include "SpellMgr.h"
#include "Chat.h"
#include "PlayerbotAI.h"
#include "ServerFacade.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"
#include "GameTime.h"
#include "Group.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include "SpellMgr.h"
#include "Unit.h"
#include "Value.h" #include "Value.h"
#include "Config.h"
#include "PlayerbotTextMgr.h"
namespace ai::buff namespace ai::buff
{ {
namespace
{
// Prevents bots from immediately casting already-present buffs upon logging in
constexpr uint32 POST_LOGIN_BUFF_GRACE_MS = 5 * IN_MILLISECONDS;
bool IsWithinPostLoginBuffGrace(Player* player)
{
if (!player)
return false;
return getMSTimeDiff(
player->GetInGameTime(), GameTime::GetGameTimeMS().count()) < POST_LOGIN_BUFF_GRACE_MS;
}
}
static bool HasEnoughSameMapMissingPlayersForGroupVariant(
Player* bot, PlayerbotAI* botAI, std::string const& baseName,
std::string const& groupName, uint32 requiredCount = 3)
{
Group* group = bot->GetGroup();
if (!group)
return false;
uint32 missingCount = 0;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member || !member->IsInWorld() || !member->IsAlive() ||
member->GetMap() != bot->GetMap())
{
continue;
}
if (botAI->HasAura(baseName, member) || botAI->HasAura(groupName, member))
continue;
if (++missingCount >= requiredCount)
return true;
}
return false;
}
static bool IsEligibleGroupForPartyBuffs(Group const* group)
{
if (!group)
return false;
switch (sPlayerbotAIConfig.autoPartyBuffs)
{
case AutoPartyBuffMode::RAID_ONLY:
return group->isRaidGroup();
case AutoPartyBuffMode::GROUP_OR_RAID:
return true;
case AutoPartyBuffMode::DISABLED:
return false;
}
return false;
}
bool IsGroupVariantEnabled(Player* bot, std::string const& name)
{
if (!IsEligibleGroupForPartyBuffs(bot->GetGroup()))
return false;
return !GroupVariantFor(name).empty();
}
std::string MakeAuraQualifierForBuff(std::string const& name) std::string MakeAuraQualifierForBuff(std::string const& name)
{ {
// Paladin // Paladin
@ -34,27 +100,89 @@ namespace ai::buff
if (name == "arcane intellect") return "arcane intellect,arcane brilliance"; if (name == "arcane intellect") return "arcane intellect,arcane brilliance";
// Priest // Priest
if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude"; if (name == "power word: fortitude") return "power word: fortitude,prayer of fortitude";
if (name == "divine spirit") return "divine spirit,prayer of spirit";
if (name == "shadow protection") return "shadow protection,prayer of shadow protection";
return name; return name;
} }
std::string GroupVariantFor(std::string const& name) std::string GroupVariantFor(std::string const& name)
{ {
// Paladin
if (name == "blessing of kings") return "greater blessing of kings";
if (name == "blessing of might") return "greater blessing of might";
if (name == "blessing of wisdom") return "greater blessing of wisdom";
if (name == "blessing of sanctuary") return "greater blessing of sanctuary";
// Druid // Druid
if (name == "mark of the wild") return "gift of the wild"; if (name == "mark of the wild") return "gift of the wild";
// Mage // Mage
if (name == "arcane intellect") return "arcane brilliance"; if (name == "arcane intellect") return "arcane brilliance";
// Priest // Priest
if (name == "power word: fortitude") return "prayer of fortitude"; if (name == "power word: fortitude") return "prayer of fortitude";
if (name == "divine spirit") return "prayer of spirit";
if (name == "shadow protection") return "prayer of shadow protection";
// Paladin blessings are intentionally not included here because they are
// coordinated by the auto greater blessing system instead.
return std::string(); return std::string();
} }
bool NeedsPostLoginBuffGrace(std::string const& name)
{
static char const* const trackedBuffs[] = {
"mark of the wild",
"arcane intellect",
"power word: fortitude",
"prayer of fortitude",
"divine spirit",
"prayer of spirit",
"shadow protection",
"prayer of shadow protection",
"blessing of kings",
"blessing of might",
"blessing of wisdom",
"blessing of sanctuary"
};
for (char const* trackedBuff : trackedBuffs)
{
if (name.find(trackedBuff) != std::string::npos)
return true;
}
return false;
}
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
Player* bot, Unit* target, std::string const& spell)
{
if (!NeedsPostLoginBuffGrace(spell))
return false;
if (IsWithinPostLoginBuffGrace(bot))
return true;
Player* playerTarget = target ? target->ToPlayer() : nullptr;
return IsWithinPostLoginBuffGrace(playerTarget);
}
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot)
{
if (IsWithinPostLoginBuffGrace(bot))
return true;
Group* group = bot->GetGroup();
if (!group)
return false;
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member || !member->IsInWorld())
continue;
if (IsWithinPostLoginBuffGrace(member))
return true;
}
return false;
}
bool HasRequiredReagents(Player* bot, uint32 spellId) bool HasRequiredReagents(Player* bot, uint32 spellId)
{ {
if (!spellId) if (!spellId)
@ -72,75 +200,33 @@ namespace ai::buff
return false; return false;
} }
} }
// No reagent required
return true; return true;
} }
return false; return false;
} }
std::string UpgradeToGroupIfAppropriate( std::string UpgradeToGroupIfAppropriate(
Player* bot, Player* bot, PlayerbotAI* botAI, std::string const& baseName)
PlayerbotAI* botAI,
std::string const& baseName,
bool announceOnMissing,
std::function<void(std::string const&)> announce)
{ {
std::string castName = baseName; if (!IsGroupVariantEnabled(bot, baseName))
Group* g = bot->GetGroup(); return baseName;
if (!g || g->GetMembersCount() < static_cast<uint32>(sPlayerbotAIConfig.minBotsForGreaterBuff))
return castName; // Group too small: stay in solo mode
if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty()) std::string const groupName = GroupVariantFor(baseName);
{ if (groupName.empty())
uint32 const groupVariantSpellId = botAI->GetAiObjectContext() return baseName;
// Prefer singles until at least three living, in-world group members on the bot's map
// are missing both the single-target buff and its group variant.
if (!HasEnoughSameMapMissingPlayersForGroupVariant(bot, botAI, baseName, groupName))
return baseName;
uint32 const groupSpellId = botAI->GetAiObjectContext()
->GetValue<uint32>("spell id", groupName)->Get(); ->GetValue<uint32>("spell id", groupName)->Get();
// We check usefulness on the **basic** buff (not the greater version), if (groupSpellId && HasRequiredReagents(bot, groupSpellId))
// because "spell cast useful" may return false for the greater variant.
bool const usefulBase = botAI->GetAiObjectContext()
->GetValue<bool>("spell cast useful", baseName)->Get();
if (groupVariantSpellId && HasRequiredReagents(bot, groupVariantSpellId))
{
// Learned + reagents OK -> switch to greater
return groupName; return groupName;
}
// Missing reagents -> announce if (a) greater is known, (b) base buff is useful, return baseName;
// (c) announce was requested, (d) a callback is provided.
if (announceOnMissing && groupVariantSpellId && usefulBase && announce)
{
static std::map<std::pair<uint32, std::string>, time_t> s_lastWarn; // par bot & par buff
time_t now = std::time(nullptr);
uint32 botLow = static_cast<uint32>(bot->GetGUID().GetCounter());
time_t& last = s_lastWarn[ std::make_pair(botLow, groupName) ];
if (!last || now - last >= sPlayerbotAIConfig.rpWarningCooldown) // Configurable anti-spam
{
// DB Key choice in regard of the buff
std::string key;
if (groupName.find("greater blessing") != std::string::npos)
key = "rp_missing_reagent_greater_blessing";
else if (groupName == "gift of the wild")
key = "rp_missing_reagent_gift_of_the_wild";
else if (groupName == "arcane brilliance")
key = "rp_missing_reagent_arcane_brilliance";
else
key = "rp_missing_reagent_generic";
// Placeholders
std::map<std::string, std::string> placeholders;
placeholders["%group_spell"] = groupName;
placeholders["%base_spell"] = baseName;
std::string announceText = PlayerbotTextMgr::instance().GetBotTextOrDefault(key,
"Out of components for %group_spell. Using %base_spell!", placeholders);
announce(announceText);
last = now;
}
}
}
return castName;
} }
} }

View File

@ -6,63 +6,40 @@
#pragma once #pragma once
#include <string> #include <string>
#include <functional>
#include "Common.h" #include "Common.h"
#include "Group.h"
#include "Chat.h"
#include "Language.h"
class Player; class Player;
class PlayerbotAI; class PlayerbotAI;
class Unit;
namespace ai::buff namespace ai::buff
{ {
// Build an aura qualifier "single + greater" to avoid double-buffing bool IsGroupVariantEnabled(Player* bot, std::string const& name);
std::string MakeAuraQualifierForBuff(std::string const& name); std::string MakeAuraQualifierForBuff(std::string const& name);
// Returns the group spell name for a given single-target buff.
// If no group equivalent exists, returns "".
std::string GroupVariantFor(std::string const& name); std::string GroupVariantFor(std::string const& name);
// Checks if the bot has the required reagents to cast a spell (by its spellId). bool NeedsPostLoginBuffGrace(std::string const& name);
// Returns false if the spellId is invalid.
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
Player* bot,
Unit* target,
std::string const& spell);
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot);
bool HasRequiredReagents(Player* bot, uint32 spellId); bool HasRequiredReagents(Player* bot, uint32 spellId);
// Applies the "switch to group buff" policy if: the bot is in a group of size x+,
// the group variant is known/useful, and reagents are available. Otherwise, returns baseName.
// If announceOnMissing == true and reagents are missing, calls the 'announce' callback
// (if provided) to notify the party/raid.
std::string UpgradeToGroupIfAppropriate( std::string UpgradeToGroupIfAppropriate(
Player* bot, Player* bot,
PlayerbotAI* botAI, PlayerbotAI* botAI,
std::string const& baseName, std::string const& baseName);
bool announceOnMissing = false,
std::function<void(std::string const&)> announce = {}
);
} }
namespace ai::spell namespace ai::spell
{ {
bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId); bool HasSpellOrCategoryCooldown(Player* bot, uint32 spellId);
} }
namespace ai::chat {
inline std::function<void(std::string const&)> MakeGroupAnnouncer(Player* me)
{
return [me](std::string const& msg)
{
if (Group* g = me->GetGroup())
{
WorldPacket data;
ChatMsg type = g->isRaidGroup() ? CHAT_MSG_RAID : CHAT_MSG_PARTY;
ChatHandler::BuildChatPacket(data, type, LANG_UNIVERSAL, me, /*receiver=*/nullptr, msg.c_str());
g->BroadcastPacket(&data, true, -1, me->GetGUID());
}
else
{
me->Say(msg, LANG_UNIVERSAL);
}
};
}
}

View File

@ -87,16 +87,16 @@ public:
bool isUseful() override; bool isUseful() override;
}; };
class CastMarkOfTheWildAction : public CastBuffSpellAction class CastMarkOfTheWildAction : public GroupBuffSpellAction
{ {
public: public:
CastMarkOfTheWildAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mark of the wild") {} CastMarkOfTheWildAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "mark of the wild") {}
}; };
class CastMarkOfTheWildOnPartyAction : public BuffOnPartyAction class CastMarkOfTheWildOnPartyAction : public GroupBuffOnPartyAction
{ {
public: public:
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "mark of the wild") {} CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "mark of the wild") {}
}; };
class CastSurvivalInstinctsAction : public CastBuffSpellAction class CastSurvivalInstinctsAction : public CastBuffSpellAction

View File

@ -9,11 +9,6 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "ServerFacade.h" #include "ServerFacade.h"
bool MarkOfTheWildOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
}
bool MarkOfTheWildTrigger::IsActive() bool MarkOfTheWildTrigger::IsActive()
{ {
return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget()); return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());

View File

@ -23,15 +23,13 @@ class PlayerbotAI;
class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 2 * 2000) {} MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 4 * 2000) {}
bool IsActive() override;
}; };
class MarkOfTheWildTrigger : public BuffTrigger class MarkOfTheWildTrigger : public BuffTrigger
{ {
public: public:
MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 2 * 2000) {} MarkOfTheWildTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "mark of the wild", 4 * 2000) {}
bool IsActive() override; bool IsActive() override;
}; };

View File

@ -40,16 +40,16 @@ public:
CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {} CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {}
}; };
class CastArcaneIntellectAction : public CastBuffSpellAction class CastArcaneIntellectAction : public GroupBuffSpellAction
{ {
public: public:
CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {} CastArcaneIntellectAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "arcane intellect") {}
}; };
class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction class CastArcaneIntellectOnPartyAction : public GroupBuffOnPartyAction
{ {
public: public:
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {} CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "arcane intellect") {}
}; };
class CastFocusMagicOnPartyAction : public CastSpellAction class CastFocusMagicOnPartyAction : public CastSpellAction

View File

@ -31,11 +31,6 @@ bool NoManaGemTrigger::IsActive()
return true; return true;
} }
bool ArcaneIntellectOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
}
bool ArcaneIntellectTrigger::IsActive() bool ArcaneIntellectTrigger::IsActive()
{ {
return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget()); return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());

View File

@ -19,14 +19,13 @@ class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI) ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "arcane intellect", 2 * 2000) {} : BuffOnPartyTrigger(botAI, "arcane intellect", 4 * 2000) {}
bool IsActive() override;
}; };
class ArcaneIntellectTrigger : public BuffTrigger class ArcaneIntellectTrigger : public BuffTrigger
{ {
public: public:
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 2 * 2000) {} ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 4 * 2000) {}
bool IsActive() override; bool IsActive() override;
}; };

View File

@ -1,529 +0,0 @@
/*
* 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 "PaladinActions.h"
#include "AiFactory.h"
#include "Event.h"
#include "PaladinHelper.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "SharedDefines.h"
#include "../../../../../src/server/scripts/Spells/spell_generic.cpp"
#include "Ai/Base/Util/GenericBuffUtils.h"
#include "Group.h"
#include "ObjectAccessor.h"
using ai::buff::MakeAuraQualifierForBuff;
// Helper : detect tank role on the target (player bot or not) return true if spec is tank or if the bot have tank strategies (bear/tank/tank face).
static inline bool IsTankRole(Player* p)
{
if (!p) return false;
if (p->HasTankSpec())
return true;
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(p))
{
if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("tank", BOT_STATE_COMBAT) ||
otherAI->HasStrategy("tank face", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("tank face", BOT_STATE_COMBAT) ||
otherAI->HasStrategy("bear", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("bear", BOT_STATE_COMBAT))
return true;
}
return false;
}
// Added for solo paladin patch : determine if he's the only paladin on party
static inline bool IsOnlyPaladinInGroup(Player* bot)
{
if (!bot) return false;
Group* g = bot->GetGroup();
if (!g) return true; // solo
uint32 pals = 0u;
for (GroupReference* r = g->GetFirstMember(); r; r = r->next())
{
Player* p = r->GetSource();
if (!p || !p->IsInWorld()) continue;
if (p->getClass() == CLASS_PALADIN) ++pals;
}
return pals == 1u;
}
inline std::string const GetActualBlessingOfMight(Unit* target)
{
if (!target->ToPlayer())
{
return "blessing of might";
}
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass())
{
case CLASS_MAGE:
case CLASS_PRIEST:
case CLASS_WARLOCK:
return "blessing of wisdom";
break;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION)
{
return "blessing of wisdom";
}
break;
case CLASS_DRUID:
if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE)
{
return "blessing of wisdom";
}
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_HOLY)
{
return "blessing of wisdom";
}
break;
}
return "blessing of might";
}
inline std::string const GetActualBlessingOfWisdom(Unit* target)
{
if (!target->ToPlayer())
{
return "blessing of might";
}
int tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass())
{
case CLASS_WARRIOR:
case CLASS_ROGUE:
case CLASS_DEATH_KNIGHT:
case CLASS_HUNTER:
return "blessing of might";
break;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ENHANCEMENT)
{
return "blessing of might";
}
break;
case CLASS_DRUID:
if (tab == DRUID_TAB_FERAL)
{
return "blessing of might";
}
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION)
{
return "blessing of might";
}
break;
}
return "blessing of wisdom";
}
inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot)
{
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY))
return "";
Player* tp = target->ToPlayer();
if (!tp)
return "";
if (auto* ai = GET_PLAYERBOT_AI(bot))
{
if (Unit* mt = ai->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
{
if (mt == target)
return "blessing of sanctuary";
}
}
if (tp->HasTankSpec())
return "blessing of sanctuary";
return "";
}
Value<Unit*>* CastBlessingOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member without aura", MakeAuraQualifierForBuff(spell));
}
bool CastBlessingOfMightAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfMight(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
Value<Unit*>* CastBlessingOfMightOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of might,greater blessing of might,blessing of wisdom,greater blessing of wisdom,blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfMight(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
bool CastBlessingOfWisdomAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfWisdom(target);
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
Player* targetPlayer = target->ToPlayer();
if (Group* g = bot->GetGroup())
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
return false;
if (botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT) &&
targetPlayer && IsTankRole(targetPlayer))
{
LOG_DEBUG("playerbots", "[Wisdom/bmana] Skip tank {} (Kings only)", target->GetName());
return false;
}
std::string castName = GetActualBlessingOfWisdom(target);
if (castName.empty())
return false;
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
return botAI->CastSpell(castName, target);
}
Value<Unit*>* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
{
if (!bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY))
return false;
Unit* target = GetTarget();
if (!target)
{
// Fallback: GetTarget() can be null if no one needs a buff.
// Keep a valid pointer for the checks/logs that follow.
target = bot;
}
Player* targetPlayer = target ? target->ToPlayer() : nullptr;
// Small helpers to check relevant auras
const auto HasKingsAura = [&](Unit* u) -> bool {
return botAI->HasAura("blessing of kings", u) || botAI->HasAura("greater blessing of kings", u);
};
const auto HasSanctAura = [&](Unit* u) -> bool {
return botAI->HasAura("blessing of sanctuary", u) || botAI->HasAura("greater blessing of sanctuary", u);
};
if (Group* g = bot->GetGroup())
{
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
{
LOG_DEBUG("playerbots", "[Sanct] Initial target not in group, ignoring");
target = bot;
targetPlayer = bot->ToPlayer();
}
}
if (Player* self = bot->ToPlayer())
{
bool selfHasSanct = HasSanctAura(self);
bool needSelf = IsTankRole(self) && !selfHasSanct;
LOG_DEBUG("playerbots", "[Sanct] {} isTank={} selfHasSanct={} needSelf={}",
bot->GetName(), IsTankRole(self), selfHasSanct, needSelf);
if (needSelf)
{
target = self;
targetPlayer = self;
}
}
// Try to re-target a valid tank in group if needed
bool targetOk = false;
if (targetPlayer)
{
bool hasSanct = HasSanctAura(targetPlayer);
targetOk = IsTankRole(targetPlayer) && !hasSanct;
}
if (!targetOk)
{
if (Group* g = bot->GetGroup())
{
for (GroupReference* gref = g->GetFirstMember(); gref; gref = gref->next())
{
Player* p = gref->GetSource();
if (!p) continue;
if (!p->IsInWorld() || !p->IsAlive()) continue;
if (!IsTankRole(p)) continue;
bool hasSanct = HasSanctAura(p);
if (!hasSanct)
{
target = p; // prioritize this tank
targetPlayer = p;
targetOk = true;
break;
}
}
}
}
{
bool hasKings = HasKingsAura(target);
bool hasSanct = HasSanctAura(target);
bool knowSanct = bot->HasSpell(SPELL_BLESSING_OF_SANCTUARY);
LOG_DEBUG("playerbots", "[Sanct] Final target={} hasKings={} hasSanct={} knowSanct={}",
target->GetName(), hasKings, hasSanct, knowSanct);
}
std::string castName = GetActualBlessingOfSanctuary(target, bot);
// If internal logic didn't recognize the tank (e.g., bear druid), force single-target Sanctuary
if (castName.empty())
{
if (targetPlayer)
{
if (IsTankRole(targetPlayer))
castName = "blessing of sanctuary"; // force single-target
else
return false;
}
else
return false;
}
if (targetPlayer && !IsTankRole(targetPlayer))
{
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
}
else
{
castName = "blessing of sanctuary";
}
bool ok = botAI->CastSpell(castName, target);
LOG_DEBUG("playerbots", "[Sanct] Cast {} on {} result={}", castName, target->GetName(), ok);
return ok;
}
Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of kings,greater blessing of kings,blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
Group* g = bot->GetGroup();
if (!g)
return false;
// Added for patch solo paladin, never buff itself to not remove his sanctuary buff
if (botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT) && IsOnlyPaladinInGroup(bot))
{
if (target->GetGUID() == bot->GetGUID())
{
LOG_DEBUG("playerbots", "[Kings/bstats-solo] Skip self to keep Sanctuary on {}", bot->GetName());
return false;
}
}
// End solo paladin patch
Player* targetPlayer = target->ToPlayer();
if (targetPlayer && !g->IsMember(targetPlayer->GetGUID()))
return false;
const bool hasBmana = botAI->HasStrategy("bmana", BOT_STATE_NON_COMBAT);
const bool hasBstats = botAI->HasStrategy("bstats", BOT_STATE_NON_COMBAT);
if (hasBmana)
{
if (!targetPlayer || !IsTankRole(targetPlayer))
{
LOG_DEBUG("playerbots", "[Kings/bmana] Skip non-tank {}", target->GetName());
return false;
}
}
if (targetPlayer)
{
const bool isTank = IsTankRole(targetPlayer);
const bool hasSanctFromMe =
target->HasAura(SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
target->HasAura(SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
const bool hasSanctAny =
botAI->HasAura("blessing of sanctuary", target) ||
botAI->HasAura("greater blessing of sanctuary", target);
if (isTank && hasSanctFromMe)
{
LOG_DEBUG("playerbots", "[Kings] Skip: {} has my Sanctuary and is a tank", target->GetName());
return false;
}
if (hasBstats && isTank && hasSanctAny)
{
LOG_DEBUG("playerbots", "[Kings] Skip (bstats): {} already has Sanctuary and is a tank", target->GetName());
return false;
}
}
std::string castName = "blessing of kings";
bool allowGreater = true;
if (hasBmana)
allowGreater = false;
if (allowGreater && hasBstats && targetPlayer)
{
switch (targetPlayer->getClass())
{
case CLASS_WARRIOR:
case CLASS_PALADIN:
case CLASS_DRUID:
case CLASS_DEATH_KNIGHT:
allowGreater = false;
break;
default:
break;
}
}
if (allowGreater)
{
auto RP = ai::chat::MakeGroupAnnouncer(bot);
castName = ai::buff::UpgradeToGroupIfAppropriate(bot, botAI, castName, /*announceOnMissing=*/true, RP);
}
return botAI->CastSpell(castName, target);
}
bool CastSealSpellAction::isUseful() { return AI_VALUE2(bool, "combat", "self target"); }
Value<Unit*>* CastTurnUndeadAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", getName()); }
Unit* CastHandOfFreedomOnPartyAction::GetTarget()
{
bool const selfImpaired = botAI->IsMovementImpaired(bot);
bool const hasSelfHand = selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
if (!bot->GetGroup())
{
if (selfImpaired && !hasSelfHand)
return bot;
return nullptr;
}
if (selfImpaired && !hasSelfHand)
return bot;
return CastBuffSpellAction::GetTarget();
}
Value<Unit*>* CastHandOfFreedomOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member snared target");
}
bool CastHandOfFreedomOnPartyAction::isUseful()
{
Unit* target = GetTarget();
if (!target)
return false;
return CastBuffSpellAction::isUseful() && !ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
}
Unit* CastRighteousDefenseAction::GetTarget()
{
Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target)
return nullptr;
return current_target->GetVictim();
}
bool CastDivineSacrificeAction::isUseful()
{
return GetTarget() && (GetTarget() != nullptr) && CastSpellAction::isUseful() &&
!botAI->HasAura("divine guardian", GetTarget(), false, false, -1, true);
}
bool CastCancelDivineSacrificeAction::Execute(Event /*event*/)
{
botAI->RemoveAura("divine sacrifice");
return true;
}
bool CastCancelDivineSacrificeAction::isUseful()
{
return botAI->HasAura("divine sacrifice", GetTarget(), false, true, -1, true);
}

View File

@ -0,0 +1,609 @@
/*
* 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 "PaladinActions.h"
#include "AiFactory.h"
#include "Event.h"
#include "GenericBuffUtils.h"
#include "PaladinGreaterBlessingAction.h"
#include "PaladinHelper.h"
#include "Playerbots.h"
#include "SharedDefines.h"
static bool IsBlessingTargetCandidate(Player* bot, Player* player)
{
if (!player || !player->IsAlive() || player->GetMapId() != bot->GetMapId())
return false;
if (player->IsGameMaster())
return false;
return bot->GetDistance(player) < sPlayerbotAIConfig.spellDistance * 2 &&
bot->IsWithinLOS(player->GetPositionX(), player->GetPositionY(),
player->GetPositionZ());
}
static bool HasBlessingAura(
PlayerbotAI* botAI, Unit* target, std::initializer_list<char const*> auraNames)
{
for (char const* auraName : auraNames)
{
if (botAI->HasAura(auraName, target))
return true;
}
return false;
}
static bool IsGreaterBlessingMode(Player* bot)
{
return ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup());
}
template <typename Predicate>
static Unit* FindBlessingTarget(
Player* bot, PlayerbotAI* botAI, Predicate&& predicate)
{
std::vector<Player*> masters;
std::vector<Player*> healers;
std::vector<Player*> tanks;
std::vector<Player*> others;
Player* master = botAI->GetMaster();
auto addPlayer = [&](Player* player)
{
if (!IsBlessingTargetCandidate(bot, player))
return;
if (player == master)
masters.push_back(player);
else if (botAI->IsHeal(player))
healers.push_back(player);
else if (botAI->IsTank(player))
tanks.push_back(player);
else
others.push_back(player);
};
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
addPlayer(ref->GetSource());
}
else
{
addPlayer(bot);
}
std::vector<std::vector<Player*>*> orderedLists = {
&masters, &healers, &tanks, &others };
for (std::vector<Player*>* players : orderedLists)
{
for (Player* player : *players)
{
if (predicate(player))
return player;
}
}
return nullptr;
}
static inline bool IsTankRole(Player* player)
{
if (!player)
return false;
if (player->HasTankSpec())
return true;
if (PlayerbotAI* otherAI = GET_PLAYERBOT_AI(player))
{
if (otherAI->HasStrategy("tank", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("tank", BOT_STATE_COMBAT) ||
otherAI->HasStrategy("tank face", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("tank face", BOT_STATE_COMBAT) ||
otherAI->HasStrategy("bear", BOT_STATE_NON_COMBAT) ||
otherAI->HasStrategy("bear", BOT_STATE_COMBAT))
return true;
}
return false;
}
static inline bool IsOnlyPaladinInGroup(Player* bot)
{
if (!bot)
return false;
Group* group = bot->GetGroup();
if (!group)
return true;
uint32 paladins = 0u;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* player = ref->GetSource();
if (!player || !player->IsInWorld()) continue;
if (player->getClass() == CLASS_PALADIN) ++paladins;
}
return paladins == 1u;
}
inline std::string const GetActualBlessingOfMight(Unit* target)
{
if (!target->ToPlayer())
return "blessing of might";
uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass())
{
case CLASS_MAGE:
case CLASS_PRIEST:
case CLASS_WARLOCK:
return "blessing of wisdom";
break;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ELEMENTAL || tab == SHAMAN_TAB_RESTORATION)
return "blessing of wisdom";
break;
case CLASS_DRUID:
if (tab == DRUID_TAB_RESTORATION || tab == DRUID_TAB_BALANCE)
return "blessing of wisdom";
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_HOLY)
return "blessing of wisdom";
break;
}
return "blessing of might";
}
inline std::string const GetActualBlessingOfWisdom(Unit* target)
{
if (!target->ToPlayer())
return "blessing of might";
uint8 tab = AiFactory::GetPlayerSpecTab(target->ToPlayer());
switch (target->getClass())
{
case CLASS_WARRIOR:
case CLASS_ROGUE:
case CLASS_DEATH_KNIGHT:
case CLASS_HUNTER:
return "blessing of might";
break;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ENHANCEMENT)
return "blessing of might";
break;
case CLASS_DRUID:
if (tab == DRUID_TAB_FERAL)
return "blessing of might";
break;
case CLASS_PALADIN:
if (tab == PALADIN_TAB_PROTECTION || tab == PALADIN_TAB_RETRIBUTION)
return "blessing of might";
break;
}
return "blessing of wisdom";
}
inline std::string const GetActualBlessingOfSanctuary(Unit* target, Player* bot)
{
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return "";
Player* targetPlayer = target->ToPlayer();
if (!targetPlayer)
return "";
if (auto* botAI = GET_PLAYERBOT_AI(bot))
{
if (Unit* mainTank =
botAI->GetAiObjectContext()->GetValue<Unit*>("main tank")->Get())
{
if (mainTank == target)
return "blessing of sanctuary";
}
}
if (targetPlayer->HasTankSpec())
return "blessing of sanctuary";
return "";
}
Unit* CastBlessingOfMightOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
return !HasBlessingAura(botAI, player,
{ "blessing of might", "greater blessing of might",
"blessing of wisdom", "greater blessing of wisdom",
"blessing of sanctuary", "greater blessing of sanctuary" });
});
}
bool CastBlessingOfMightAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfMight(target);
return botAI->CastSpell(castName, target);
}
Value<Unit*>* CastBlessingOfMightOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of might,greater blessing of might,blessing of wisdom,"
"greater blessing of wisdom,blessing of sanctuary,"
"greater blessing of sanctuary"
);
}
bool CastBlessingOfMightOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfMight(target);
return botAI->CastSpell(castName, target);
}
bool CastBlessingOfWisdomAction::Execute(Event /*event*/)
{
Unit* target = GetTarget();
if (!target)
return false;
std::string castName = GetActualBlessingOfWisdom(target);
return botAI->CastSpell(castName, target);
}
Unit* CastBlessingOfWisdomOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) && IsTankRole(player))
return false;
return !HasBlessingAura(botAI, player,
{ "blessing of might", "greater blessing of might",
"blessing of wisdom", "greater blessing of wisdom",
"blessing of sanctuary", "greater blessing of sanctuary" });
});
}
Value<Unit*>* CastBlessingOfWisdomOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of wisdom,greater blessing of wisdom,blessing of might,greater blessing of might,"
"blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfWisdomOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget();
if (!target)
return false;
Player* targetPlayer = target->ToPlayer();
if (Group* group = bot->GetGroup())
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
return false;
if (botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT) &&
targetPlayer && IsTankRole(targetPlayer))
return false;
std::string castName = GetActualBlessingOfWisdom(target);
if (castName.empty())
return false;
return botAI->CastSpell(castName, target);
}
Value<Unit*>* CastBlessingOfSanctuaryOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of sanctuary,greater blessing of sanctuary"
);
}
bool CastBlessingOfSanctuaryOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return false;
Unit* target = GetTarget();
if (!target)
target = bot;
Player* targetPlayer = target ? target->ToPlayer() : nullptr;
const auto HasKingsAura = [&](Unit* unit) -> bool {
return botAI->HasAura("blessing of kings", unit) ||
botAI->HasAura("greater blessing of kings", unit);
};
const auto HasSanctAura = [&](Unit* unit) -> bool {
return botAI->HasAura("blessing of sanctuary", unit) ||
botAI->HasAura("greater blessing of sanctuary", unit);
};
if (Group* group = bot->GetGroup())
{
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
{
target = bot;
targetPlayer = bot->ToPlayer();
}
}
if (Player* self = bot->ToPlayer())
{
bool selfHasSanct = HasSanctAura(self);
bool needSelf = IsTankRole(self) && !selfHasSanct;
if (needSelf)
{
target = self;
targetPlayer = self;
}
}
bool targetOk = false;
if (targetPlayer)
{
bool hasSanct = HasSanctAura(targetPlayer);
targetOk = IsTankRole(targetPlayer) && !hasSanct;
}
if (!targetOk)
{
if (Group* group = bot->GetGroup())
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* player = ref->GetSource();
if (!player) continue;
if (!player->IsInWorld() || !player->IsAlive()) continue;
if (!IsTankRole(player)) continue;
bool hasSanct = HasSanctAura(player);
if (!hasSanct)
{
target = player;
targetPlayer = player;
targetOk = true;
break;
}
}
}
}
if (GetActualBlessingOfSanctuary(target, bot).empty())
{
if (targetPlayer)
{
if (IsTankRole(targetPlayer))
return botAI->CastSpell("blessing of sanctuary", target);
else
return false;
}
else
return false;
}
return botAI->CastSpell("blessing of sanctuary", target);
}
Unit* CastBlessingOfSanctuaryOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
if (!bot->HasSpell(ai::paladin::SPELL_BLESSING_OF_SANCTUARY))
return nullptr;
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
return IsTankRole(player) &&
!HasBlessingAura(botAI, player,
{ "blessing of sanctuary", "greater blessing of sanctuary" });
});
}
Value<Unit*>* CastBlessingOfKingsOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>(
"party member without aura",
"blessing of kings,greater blessing of kings,"
"blessing of sanctuary,greater blessing of sanctuary"
);
}
Unit* CastBlessingOfKingsOnPartyAction::GetTarget()
{
if (IsGreaterBlessingMode(bot))
return nullptr;
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
const bool onlyPaladinInGroup = IsOnlyPaladinInGroup(bot);
return FindBlessingTarget(bot, botAI, [&](Player* player)
{
const bool isTank = IsTankRole(player);
const bool hasKingsOrSanct = HasBlessingAura(botAI, player,
{ "blessing of kings", "greater blessing of kings",
"blessing of sanctuary", "greater blessing of sanctuary" });
if (hasKingsOrSanct)
return false;
if (hasBwisdom)
return isTank;
if (hasBkings)
{
if (isTank)
return false;
if (onlyPaladinInGroup && player == bot)
return false;
}
return true;
});
}
bool CastBlessingOfKingsOnPartyAction::Execute(Event /*event*/)
{
if (IsGreaterBlessingMode(bot))
return false;
Unit* target = GetTarget();
if (!target)
return false;
Group* group = bot->GetGroup();
if (!group)
return false;
if (botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT) &&
IsOnlyPaladinInGroup(bot))
{
if (target->GetGUID() == bot->GetGUID())
return false;
}
Player* targetPlayer = target->ToPlayer();
if (targetPlayer && !group->IsMember(targetPlayer->GetGUID()))
return false;
const bool hasBwisdom = botAI->HasStrategy("bwisdom", BOT_STATE_NON_COMBAT);
const bool hasBkings = botAI->HasStrategy("bkings", BOT_STATE_NON_COMBAT);
if (hasBwisdom && (!targetPlayer || !IsTankRole(targetPlayer)))
return false;
if (targetPlayer)
{
const bool isTank = IsTankRole(targetPlayer);
const bool hasSanctFromMe =
target->HasAura(ai::paladin::SPELL_BLESSING_OF_SANCTUARY, bot->GetGUID()) ||
target->HasAura(ai::paladin::SPELL_GREATER_BLESSING_OF_SANCTUARY, bot->GetGUID());
const bool hasSanctAny =
botAI->HasAura("blessing of sanctuary", target) ||
botAI->HasAura("greater blessing of sanctuary", target);
if (isTank && hasSanctFromMe)
return false;
if (hasBkings && isTank && hasSanctAny)
return false;
}
return botAI->CastSpell("blessing of kings", target);
}
bool CastSealSpellAction::isUseful()
{
return AI_VALUE2(bool, "combat", "self target");
}
Value<Unit*>* CastTurnUndeadAction::GetTargetValue()
{
return context->GetValue<Unit*>("cc target", getName());
}
Unit* CastHandOfFreedomOnPartyAction::GetTarget()
{
bool const selfImpaired = botAI->IsMovementImpaired(bot);
bool const hasSelfHand =
selfImpaired && ai::paladin::HasAnyPaladinHandFromCaster(bot, bot);
if (!bot->GetGroup())
{
if (selfImpaired && !hasSelfHand)
return bot;
return nullptr;
}
if (selfImpaired && !hasSelfHand)
return bot;
return CastBuffSpellAction::GetTarget();
}
Value<Unit*>* CastHandOfFreedomOnPartyAction::GetTargetValue()
{
return context->GetValue<Unit*>("party member snared target");
}
bool CastHandOfFreedomOnPartyAction::isUseful()
{
Unit* target = GetTarget();
if (!target)
return false;
return CastBuffSpellAction::isUseful() &&
!ai::paladin::HasAnyPaladinHandFromCaster(target, bot);
}
Unit* CastRighteousDefenseAction::GetTarget()
{
Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target)
return nullptr;
return current_target->GetVictim();
}
bool CastDivineSacrificeAction::isUseful()
{
return GetTarget() && (GetTarget() != nullptr) && CastSpellAction::isUseful() &&
!botAI->HasAura("divine guardian", GetTarget(), false, false, -1, true);
}
bool CastCancelDivineSacrificeAction::Execute(Event /*event*/)
{
botAI->RemoveAura("divine sacrifice");
return true;
}
bool CastCancelDivineSacrificeAction::isUseful()
{
return botAI->HasAura("divine sacrifice", GetTarget(), false, true, -1, true);
}

View File

@ -8,10 +8,6 @@
#include "AiObject.h" #include "AiObject.h"
#include "GenericSpellActions.h" #include "GenericSpellActions.h"
#include "SharedDefines.h"
class PlayerbotAI;
class Unit;
// seals // seals
BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness"); BUFF_ACTION(CastSealOfRighteousnessAction, "seal of righteousness");
@ -88,24 +84,13 @@ public:
bool Execute(Event event) override; bool Execute(Event event) override;
}; };
class CastBlessingOnPartyAction : public BuffOnPartyAction
{
public:
CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name)
: BuffOnPartyAction(botAI, name), name(name) {}
Value<Unit*>* GetTargetValue() override;
private:
std::string name;
};
class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction class CastBlessingOfMightOnPartyAction : public BuffOnPartyAction
{ {
public: public:
CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {} CastBlessingOfMightOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of might") {}
std::string const getName() override { return "blessing of might on party"; } std::string const getName() override { return "blessing of might on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override; bool Execute(Event event) override;
}; };
@ -124,6 +109,7 @@ public:
CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {} CastBlessingOfWisdomOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of wisdom") {}
std::string const getName() override { return "blessing of wisdom on party"; } std::string const getName() override { return "blessing of wisdom on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override; bool Execute(Event event) override;
}; };
@ -134,12 +120,13 @@ public:
CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {} CastBlessingOfKingsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "blessing of kings") {}
}; };
class CastBlessingOfKingsOnPartyAction : public CastBlessingOnPartyAction class CastBlessingOfKingsOnPartyAction : public BuffOnPartyAction
{ {
public: public:
CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : CastBlessingOnPartyAction(botAI, "blessing of kings") {} CastBlessingOfKingsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of kings") {}
std::string const getName() override { return "blessing of kings on party"; } std::string const getName() override { return "blessing of kings on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override; // added for Sanctuary priority Value<Unit*>* GetTargetValue() override; // added for Sanctuary priority
bool Execute(Event event) override; // added for 2 paladins logic bool Execute(Event event) override; // added for 2 paladins logic
}; };
@ -156,6 +143,7 @@ public:
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {} CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
std::string const getName() override { return "blessing of sanctuary on party"; } std::string const getName() override { return "blessing of sanctuary on party"; }
Unit* GetTarget() override;
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override; bool Execute(Event event) override;
}; };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,267 @@
/*
* 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_PALADINGREATERBLESSINGACTION_H
#define _PLAYERBOT_PALADINGREATERBLESSINGACTION_H
#include <array>
#include <string>
#include <vector>
#include "Action.h"
#include "AiFactory.h"
#include "Playerbots.h"
#include "SharedDefines.h"
class UntypedValue;
namespace ai::gbless
{
enum RoleProfile : uint8
{
ROLE_CASTER = 0,
ROLE_PHYSICAL_DPS = 1,
ROLE_HYBRID_DPS = 2,
ROLE_DRUID_TANK = 3,
ROLE_WARRIOR_DK_TANK = 4,
ROLE_PALADIN_TANK = 5,
ROLE_PROFILE_COUNT = 6
};
enum BlessingType : uint8
{
BLESSING_NONE = 0,
BLESSING_MIGHT_SINGLE = 1,
BLESSING_MIGHT_GREATER = 2,
BLESSING_WISDOM_SINGLE = 3,
BLESSING_WISDOM_GREATER = 4,
BLESSING_KINGS_SINGLE = 5,
BLESSING_KINGS_GREATER = 6,
BLESSING_SANCTUARY_SINGLE = 7,
BLESSING_SANCTUARY_GREATER = 8
};
enum BaseBlessingCategory : uint8
{
BASE_NONE = 0,
BASE_MIGHT = 1,
BASE_WISDOM = 2,
BASE_KINGS = 3,
BASE_SANCTUARY = 4
};
inline constexpr BaseBlessingCategory BaseBlessingOf(BlessingType type)
{
switch (type)
{
case BLESSING_MIGHT_SINGLE:
case BLESSING_MIGHT_GREATER: return BASE_MIGHT;
case BLESSING_WISDOM_SINGLE:
case BLESSING_WISDOM_GREATER: return BASE_WISDOM;
case BLESSING_KINGS_SINGLE:
case BLESSING_KINGS_GREATER: return BASE_KINGS;
case BLESSING_SANCTUARY_SINGLE:
case BLESSING_SANCTUARY_GREATER: return BASE_SANCTUARY;
default: return BASE_NONE;
}
}
inline constexpr bool IsSingleVariant(BlessingType type)
{
return type == BLESSING_MIGHT_SINGLE || type == BLESSING_WISDOM_SINGLE ||
type == BLESSING_KINGS_SINGLE || type == BLESSING_SANCTUARY_SINGLE;
}
inline constexpr bool IsGreaterVariant(BlessingType type)
{
return type == BLESSING_MIGHT_GREATER || type == BLESSING_WISDOM_GREATER ||
type == BLESSING_KINGS_GREATER || type == BLESSING_SANCTUARY_GREATER;
}
inline constexpr BlessingType ToSingleVariant(BaseBlessingCategory category)
{
switch (category)
{
case BASE_MIGHT: return BLESSING_MIGHT_SINGLE;
case BASE_WISDOM: return BLESSING_WISDOM_SINGLE;
case BASE_KINGS: return BLESSING_KINGS_SINGLE;
case BASE_SANCTUARY: return BLESSING_SANCTUARY_SINGLE;
default: return BLESSING_NONE;
}
}
inline constexpr BlessingType ToSingleVariant(BlessingType type)
{
return ToSingleVariant(BaseBlessingOf(type));
}
inline constexpr BlessingType ToGreaterVariant(BaseBlessingCategory category)
{
switch (category)
{
case BASE_MIGHT: return BLESSING_MIGHT_GREATER;
case BASE_WISDOM: return BLESSING_WISDOM_GREATER;
case BASE_KINGS: return BLESSING_KINGS_GREATER;
case BASE_SANCTUARY: return BLESSING_SANCTUARY_GREATER;
default: return BLESSING_NONE;
}
}
inline constexpr BlessingType ToGreaterVariant(BlessingType type)
{
return ToGreaterVariant(BaseBlessingOf(type));
}
inline std::string BlessingSpellName(BlessingType type)
{
switch (type)
{
case BLESSING_MIGHT_SINGLE: return "blessing of might";
case BLESSING_MIGHT_GREATER: return "greater blessing of might";
case BLESSING_WISDOM_SINGLE: return "blessing of wisdom";
case BLESSING_WISDOM_GREATER: return "greater blessing of wisdom";
case BLESSING_KINGS_SINGLE: return "blessing of kings";
case BLESSING_KINGS_GREATER: return "greater blessing of kings";
case BLESSING_SANCTUARY_SINGLE: return "blessing of sanctuary";
case BLESSING_SANCTUARY_GREATER: return "greater blessing of sanctuary";
default: return "";
}
}
struct BaseBlessingPriorityEntry
{
BaseBlessingCategory priorities[4];
};
inline constexpr BaseBlessingPriorityEntry BASE_BLESSING_PRIORITIES[ROLE_PROFILE_COUNT] =
{
// All casters
{{ BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY, BASE_MIGHT }},
// Physical DPS (no mana)
{{ BASE_MIGHT, BASE_KINGS, BASE_SANCTUARY, BASE_NONE }},
// Hybrid DPS
{{ BASE_MIGHT, BASE_KINGS, BASE_WISDOM, BASE_SANCTUARY }},
// Druid tanks
{{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_WISDOM, }},
// Warrior and DK tanks
{{ BASE_KINGS, BASE_MIGHT, BASE_SANCTUARY, BASE_NONE }},
// Paladin tanks
{{ BASE_SANCTUARY, BASE_MIGHT, BASE_WISDOM, BASE_KINGS }},
};
constexpr uint32 SPELL_IMPROVED_MIGHT_R1 = 20042;
constexpr uint32 SPELL_IMPROVED_MIGHT_R2 = 20045;
constexpr uint32 SPELL_IMPROVED_WISDOM_R1 = 20244;
constexpr uint32 SPELL_IMPROVED_WISDOM_R2 = 20245;
inline RoleProfile ResolveRoleProfile(Player* player)
{
if (!player)
return ROLE_CASTER;
uint8 cls = player->getClass();
int tab = AiFactory::GetPlayerSpecTab(player);
bool isTank = PlayerbotAI::IsTank(player);
switch (cls)
{
case CLASS_WARRIOR:
if (isTank)
return ROLE_WARRIOR_DK_TANK;
return ROLE_PHYSICAL_DPS;
case CLASS_DEATH_KNIGHT:
if (isTank)
return ROLE_WARRIOR_DK_TANK;
return ROLE_PHYSICAL_DPS;
case CLASS_SHAMAN:
if (tab == SHAMAN_TAB_ENHANCEMENT)
return ROLE_HYBRID_DPS;
return ROLE_CASTER;
case CLASS_PALADIN:
if (isTank)
return ROLE_PALADIN_TANK;
if (tab == PALADIN_TAB_HOLY)
return ROLE_CASTER;
return ROLE_HYBRID_DPS;
case CLASS_DRUID:
if (tab == DRUID_TAB_FERAL)
return isTank ? ROLE_DRUID_TANK : ROLE_HYBRID_DPS;
return ROLE_CASTER;
case CLASS_ROGUE:
return ROLE_PHYSICAL_DPS;
case CLASS_HUNTER:
return ROLE_HYBRID_DPS;
case CLASS_MAGE:
return ROLE_CASTER;
case CLASS_WARLOCK:
return ROLE_CASTER;
case CLASS_PRIEST:
return ROLE_CASTER;
default:
return ROLE_CASTER;
}
}
struct GreaterBlessingPlayerAssignment
{
Player* player = nullptr;
BlessingType blessing = BLESSING_NONE;
};
struct CachedBlessingBucketAssignment
{
uint8 classId = 0;
RoleProfile role = ROLE_CASTER;
bool byRole = false;
BlessingType blessing = BLESSING_NONE;
};
struct CachedBlessingAssignments
{
uint32 groupKey = 0;
bool valid = false;
std::vector<CachedBlessingBucketAssignment> assignments;
};
struct CachedPendingBlessingAssignment
{
uint32 groupKey = 0;
bool valid = false;
GreaterBlessingPlayerAssignment assignment;
std::string spellName;
};
UntypedValue* greater_blessing_assignments_value(PlayerbotAI* botAI);
UntypedValue* greater_blessing_pending_assignment_value(PlayerbotAI* botAI);
bool IsEligibleGroupForAutoBlessings(Group const* group);
}
class CastGreaterBlessingAssignmentAction : public Action
{
public:
CastGreaterBlessingAssignmentAction(PlayerbotAI* botAI);
bool Execute(Event event) override;
bool isUseful() override;
bool HasPendingAssignment();
private:
bool FindPendingAssignment(
ai::gbless::GreaterBlessingPlayerAssignment& outAssignment,
std::string& outSpellName);
};
#endif

View File

@ -7,6 +7,7 @@
#include "DpsPaladinStrategy.h" #include "DpsPaladinStrategy.h"
#include "GenericPaladinNonCombatStrategy.h" #include "GenericPaladinNonCombatStrategy.h"
#include "PaladinGreaterBlessingAction.h"
#include "HealPaladinStrategy.h" #include "HealPaladinStrategy.h"
#include "NamedObjectContext.h" #include "NamedObjectContext.h"
#include "OffhealRetPaladinStrategy.h" #include "OffhealRetPaladinStrategy.h"
@ -70,17 +71,17 @@ class PaladinBuffStrategyFactoryInternal : public NamedObjectContext<Strategy>
public: public:
PaladinBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true) PaladinBuffStrategyFactoryInternal() : NamedObjectContext<Strategy>(false, true)
{ {
creators["bhealth"] = &PaladinBuffStrategyFactoryInternal::bhealth; creators["bsanc"] = &PaladinBuffStrategyFactoryInternal::bsanc;
creators["bmana"] = &PaladinBuffStrategyFactoryInternal::bmana; creators["bwisdom"] = &PaladinBuffStrategyFactoryInternal::bwisdom;
creators["bdps"] = &PaladinBuffStrategyFactoryInternal::bdps; creators["bmight"] = &PaladinBuffStrategyFactoryInternal::bmight;
creators["bstats"] = &PaladinBuffStrategyFactoryInternal::bstats; creators["bkings"] = &PaladinBuffStrategyFactoryInternal::bkings;
} }
private: private:
static Strategy* bhealth(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); } static Strategy* bsanc(PlayerbotAI* botAI) { return new PaladinBuffHealthStrategy(botAI); }
static Strategy* bmana(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); } static Strategy* bwisdom(PlayerbotAI* botAI) { return new PaladinBuffManaStrategy(botAI); }
static Strategy* bdps(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); } static Strategy* bmight(PlayerbotAI* botAI) { return new PaladinBuffDpsStrategy(botAI); }
static Strategy* bstats(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); } static Strategy* bkings(PlayerbotAI* botAI) { return new PaladinBuffStatsStrategy(botAI); }
}; };
class PaladinCombatStrategyFactoryInternal : public NamedObjectContext<Strategy> class PaladinCombatStrategyFactoryInternal : public NamedObjectContext<Strategy>
@ -154,6 +155,7 @@ public:
creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party; creators["blessing of sanctuary on party"] = &PaladinTriggerFactoryInternal::blessing_of_sanctuary_on_party;
creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath; creators["avenging wrath"] = &PaladinTriggerFactoryInternal::avenging_wrath;
creators["greater blessing needed"] = &PaladinTriggerFactoryInternal::greater_blessing_needed;
} }
private: private:
@ -211,8 +213,8 @@ private:
static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); } static Trigger* repentance_on_enemy_healer(PlayerbotAI* botAI) { return new RepentanceOnHealerTrigger(botAI); }
static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); } static Trigger* repentance_on_snare_target(PlayerbotAI* botAI) { return new RepentanceSnareTrigger(botAI); }
static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); } static Trigger* repentance_interrupt(PlayerbotAI* botAI) { return new RepentanceInterruptTrigger(botAI); }
static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new BeaconOfLightOnMainTankTrigger(ai); } static Trigger* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new BeaconOfLightOnMainTankTrigger(botAI); }
static Trigger* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new SacredShieldOnMainTankTrigger(ai); } static Trigger* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new SacredShieldOnMainTankTrigger(botAI); }
static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); } static Trigger* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new HandOfFreedomOnPartyTrigger(botAI); }
static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); } static Trigger* blessing_of_kings_on_party(PlayerbotAI* botAI) { return new BlessingOfKingsOnPartyTrigger(botAI); }
@ -227,6 +229,10 @@ private:
} }
static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); } static Trigger* avenging_wrath(PlayerbotAI* botAI) { return new AvengingWrathTrigger(botAI); }
static Trigger* greater_blessing_needed(PlayerbotAI* botAI)
{
return new GreaterBlessingNeededTrigger(botAI);
}
}; };
class PaladinAiObjectContextInternal : public NamedObjectContext<Action> class PaladinAiObjectContextInternal : public NamedObjectContext<Action>
@ -316,6 +322,8 @@ public:
creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice; creators["divine sacrifice"] = &PaladinAiObjectContextInternal::divine_sacrifice;
creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice; creators["cancel divine sacrifice"] = &PaladinAiObjectContextInternal::cancel_divine_sacrifice;
creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party; creators["hand of freedom on party"] = &PaladinAiObjectContextInternal::hand_of_freedom_on_party;
creators["cast greater blessing assignment"] =
&PaladinAiObjectContextInternal::cast_greater_blessing_assignment;
} }
private: private:
@ -414,15 +422,41 @@ private:
static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); } static Action* sanctity_aura(PlayerbotAI* botAI) { return new CastSanctityAuraAction(botAI); }
static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); } static Action* holy_shock(PlayerbotAI* botAI) { return new CastHolyShockAction(botAI); }
static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); } static Action* holy_shock_on_party(PlayerbotAI* botAI) { return new CastHolyShockOnPartyAction(botAI); }
static Action* divine_plea(PlayerbotAI* ai) { return new CastDivinePleaAction(ai); } static Action* divine_plea(PlayerbotAI* botAI) { return new CastDivinePleaAction(botAI); }
static Action* shield_of_righteousness(PlayerbotAI* ai) { return new ShieldOfRighteousnessAction(ai); } static Action* shield_of_righteousness(PlayerbotAI* botAI) { return new ShieldOfRighteousnessAction(botAI); }
static Action* beacon_of_light_on_main_tank(PlayerbotAI* ai) { return new CastBeaconOfLightOnMainTankAction(ai); } static Action* beacon_of_light_on_main_tank(PlayerbotAI* botAI) { return new CastBeaconOfLightOnMainTankAction(botAI); }
static Action* sacred_shield_on_main_tank(PlayerbotAI* ai) { return new CastSacredShieldOnMainTankAction(ai); } static Action* sacred_shield_on_main_tank(PlayerbotAI* botAI) { return new CastSacredShieldOnMainTankAction(botAI); }
static Action* avenging_wrath(PlayerbotAI* ai) { return new CastAvengingWrathAction(ai); } static Action* avenging_wrath(PlayerbotAI* botAI) { return new CastAvengingWrathAction(botAI); }
static Action* divine_illumination(PlayerbotAI* ai) { return new CastDivineIlluminationAction(ai); } static Action* divine_illumination(PlayerbotAI* botAI) { return new CastDivineIlluminationAction(botAI); }
static Action* divine_sacrifice(PlayerbotAI* ai) { return new CastDivineSacrificeAction(ai); } static Action* divine_sacrifice(PlayerbotAI* botAI) { return new CastDivineSacrificeAction(botAI); }
static Action* cancel_divine_sacrifice(PlayerbotAI* ai) { return new CastCancelDivineSacrificeAction(ai); } static Action* cancel_divine_sacrifice(PlayerbotAI* botAI) { return new CastCancelDivineSacrificeAction(botAI); }
static Action* hand_of_freedom_on_party(PlayerbotAI* ai) { return new CastHandOfFreedomOnPartyAction(ai); } static Action* hand_of_freedom_on_party(PlayerbotAI* botAI) { return new CastHandOfFreedomOnPartyAction(botAI); }
static Action* cast_greater_blessing_assignment(PlayerbotAI* botAI)
{
return new CastGreaterBlessingAssignmentAction(botAI);
}
};
class PaladinValueContextInternal : public NamedObjectContext<UntypedValue>
{
public:
PaladinValueContextInternal()
{
creators["greater blessing assignments"] = &PaladinValueContextInternal::greater_blessing_assignments;
creators["greater blessing pending assignment"] =
&PaladinValueContextInternal::greater_blessing_pending_assignment;
}
private:
static UntypedValue* greater_blessing_assignments(PlayerbotAI* botAI)
{
return ai::gbless::greater_blessing_assignments_value(botAI);
}
static UntypedValue* greater_blessing_pending_assignment(PlayerbotAI* botAI)
{
return ai::gbless::greater_blessing_pending_assignment_value(botAI);
}
}; };
SharedNamedObjectContextList<Strategy> PaladinAiObjectContext::sharedStrategyContexts; SharedNamedObjectContextList<Strategy> PaladinAiObjectContext::sharedStrategyContexts;
@ -467,4 +501,5 @@ void PaladinAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContext
void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& valueContexts) void PaladinAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& valueContexts)
{ {
AiObjectContext::BuildSharedValueContexts(valueContexts); AiObjectContext::BuildSharedValueContexts(valueContexts);
valueContexts.Add(new PaladinValueContextInternal());
} }

View File

@ -18,6 +18,8 @@ static constexpr uint32 SPELL_HAND_OF_PROTECTION = 1022;
static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038; static constexpr uint32 SPELL_HAND_OF_SALVATION = 1038;
static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044; static constexpr uint32 SPELL_HAND_OF_FREEDOM = 1044;
static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940; static constexpr uint32 SPELL_HAND_OF_SACRIFICE = 6940;
static constexpr uint32 SPELL_BLESSING_OF_SANCTUARY = 20911;
static constexpr uint32 SPELL_GREATER_BLESSING_OF_SANCTUARY = 25899;
inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list<uint32> spellIds) inline bool HasHandFromCaster(Unit* target, Player* caster, std::initializer_list<uint32> spellIds)
{ {

View File

@ -5,10 +5,11 @@
#include "PaladinTriggers.h" #include "PaladinTriggers.h"
#include "GenericBuffUtils.h"
#include "PaladinGreaterBlessingAction.h"
#include "PaladinActions.h" #include "PaladinActions.h"
#include "PlayerbotAIConfig.h"
#include "Playerbots.h"
#include "PaladinHelper.h" #include "PaladinHelper.h"
#include "Playerbots.h"
bool SealTrigger::IsActive() bool SealTrigger::IsActive()
{ {
@ -28,7 +29,8 @@ bool CrusaderAuraTrigger::IsActive()
bool BlessingTrigger::IsActive() bool BlessingTrigger::IsActive()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", return SpellTrigger::IsActive() &&
!botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr); "blessing of kings", "blessing of sanctuary", nullptr);
} }
@ -62,7 +64,8 @@ bool HandOfFreedomOnPartyTrigger::IsActive()
if (!target) if (!target)
return false; return false;
if (target != bot && bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f) if (target != bot &&
bot->GetExactDist2dSq(target->GetPositionX(), target->GetPositionY()) > 30.0f * 30.0f)
return false; return false;
if (!botAI->CanCastSpell("hand of freedom", target)) if (!botAI->CanCastSpell("hand of freedom", target))
@ -75,3 +78,29 @@ bool NotSensingUndeadTrigger::IsActive()
{ {
return !botAI->HasAura("sense undead", bot); return !botAI->HasAura("sense undead", bot);
} }
bool GreaterBlessingNeededTrigger::IsActive()
{
if (!ai::gbless::IsEligibleGroupForAutoBlessings(bot->GetGroup()))
return false;
if (ai::buff::ShouldDeferGreaterBlessingAssignmentForRecentLogin(bot))
return false;
Group* group = bot->GetGroup();
uint32 const groupKey = group ? group->GetLeaderGUID().GetCounter() : 0;
Value<ai::gbless::CachedPendingBlessingAssignment>* pendingValue =
context->GetValue<ai::gbless::CachedPendingBlessingAssignment>("greater blessing pending assignment");
if (!pendingValue)
return false;
ai::gbless::CachedPendingBlessingAssignment pendingAssignment = pendingValue->Get();
if (pendingAssignment.groupKey != groupKey)
{
pendingValue->Reset();
pendingAssignment = pendingValue->Get();
}
return pendingAssignment.valid && pendingAssignment.groupKey == groupKey;
}

View File

@ -13,32 +13,6 @@
class PlayerbotAI; class PlayerbotAI;
inline std::string const GetActualBlessingOfMight(Unit* target)
{
switch (target->getClass())
{
case CLASS_MAGE:
case CLASS_PRIEST:
case CLASS_WARLOCK:
return "blessing of wisdom";
}
return "blessing of might";
}
inline std::string const GetActualBlessingOfWisdom(Unit* target)
{
switch (target->getClass())
{
case CLASS_WARRIOR:
case CLASS_ROGUE:
case CLASS_DEATH_KNIGHT:
return "blessing of might";
}
return "blessing of wisdom";
}
BUFF_TRIGGER(HolyShieldTrigger, "holy shield"); BUFF_TRIGGER(HolyShieldTrigger, "holy shield");
BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury"); BUFF_TRIGGER(RighteousFuryTrigger, "righteous fury");
@ -212,42 +186,55 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
{ {
public: public:
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) BeaconOfLightOnMainTankTrigger(PlayerbotAI* botAI)
: BuffOnMainTankTrigger(ai, "beacon of light", true) {} : BuffOnMainTankTrigger(botAI, "beacon of light", true) {}
}; };
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
{ {
public: public:
SacredShieldOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "sacred shield", false) {} SacredShieldOnMainTankTrigger(PlayerbotAI* botAI)
: BuffOnMainTankTrigger(botAI, "sacred shield", false) {}
}; };
class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfKingsOnPartyTrigger : public BlessingOnPartyTrigger
{ {
public: public:
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} : BlessingOnPartyTrigger(botAI)
{
spell = "blessing of kings";
}
}; };
class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfWisdomOnPartyTrigger : public BlessingOnPartyTrigger
{ {
public: public:
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} : BlessingOnPartyTrigger(botAI)
{
spell = "blessing of might,blessing of wisdom";
}
}; };
class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfMightOnPartyTrigger : public BlessingOnPartyTrigger
{ {
public: public:
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} : BlessingOnPartyTrigger(botAI)
{
spell = "blessing of might,blessing of wisdom";
}
}; };
class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfSanctuaryOnPartyTrigger : public BlessingOnPartyTrigger
{ {
public: public:
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} : BlessingOnPartyTrigger(botAI)
{
spell = "blessing of sanctuary";
}
}; };
class HandOfFreedomOnPartyTrigger : public Trigger class HandOfFreedomOnPartyTrigger : public Trigger
@ -266,4 +253,13 @@ public:
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
}; };
class GreaterBlessingNeededTrigger : public Trigger
{
public:
GreaterBlessingNeededTrigger(PlayerbotAI* botAI)
: Trigger(botAI, "greater blessing needed", 4) {}
bool IsActive() override;
};
#endif #endif

View File

@ -30,4 +30,7 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) })); triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION) if (specTab == PALADIN_TAB_PROTECTION || specTab == PALADIN_TAB_RETRIBUTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) })); triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) }));
triggers.push_back(new TriggerNode("greater blessing needed",
{ NextAction("cast greater blessing assignment", ACTION_NORMAL) }));
} }

View File

@ -16,7 +16,7 @@ public:
PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} PaladinBuffManaStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override; void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bmana"; } std::string const getName() override { return "bwisdom"; }
}; };
class PaladinBuffHealthStrategy : public Strategy class PaladinBuffHealthStrategy : public Strategy
@ -25,7 +25,7 @@ public:
PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} PaladinBuffHealthStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override; void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bhealth"; } std::string const getName() override { return "bsanc"; }
}; };
class PaladinBuffDpsStrategy : public Strategy class PaladinBuffDpsStrategy : public Strategy
@ -34,7 +34,7 @@ public:
PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} PaladinBuffDpsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override; void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bdps"; } std::string const getName() override { return "bmight"; }
}; };
class PaladinBuffArmorStrategy : public Strategy class PaladinBuffArmorStrategy : public Strategy
@ -88,7 +88,7 @@ public:
PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {} PaladinBuffStatsStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override; void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bstats"; } std::string const getName() override { return "bkings"; }
}; };
class PaladinShadowResistanceStrategy : public Strategy class PaladinShadowResistanceStrategy : public Strategy

View File

@ -95,13 +95,14 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
} }
) )
); );
triggers.push_back(new TriggerNode( triggers.push_back(
new TriggerNode(
"light aoe", "light aoe",
{ {
NextAction("avenger's shield", ACTION_HIGH + 5) NextAction("avenger's shield", ACTION_HIGH + 5)
} }
) )
); );
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"medium aoe", "medium aoe",
@ -122,21 +123,6 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"medium health", "medium health",
{ NextAction("holy shield", ACTION_HIGH + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"low health",
{
NextAction("holy shield", ACTION_HIGH + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"critical health",
{ {
NextAction("holy shield", ACTION_HIGH + 4) NextAction("holy shield", ACTION_HIGH + 4)
} }
@ -149,7 +135,7 @@ void TankPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
NextAction("avenging wrath", ACTION_HIGH + 2) NextAction("avenging wrath", ACTION_HIGH + 2)
} }
) )
); );
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"target critical health", "target critical health",

View File

@ -13,9 +13,19 @@
class PlayerbotAI; class PlayerbotAI;
// disc // disc
BUFF_ACTION(CastPowerWordFortitudeAction, "power word: fortitude"); class CastPowerWordFortitudeAction : public GroupBuffSpellAction
BUFF_PARTY_ACTION(CastPowerWordFortitudeOnPartyAction, "power word: fortitude"); {
BUFF_PARTY_ACTION(CastPrayerOfFortitudeOnPartyAction, "prayer of fortitude"); public:
CastPowerWordFortitudeAction(PlayerbotAI* botAI)
: GroupBuffSpellAction(botAI, "power word: fortitude") {}
};
class CastPowerWordFortitudeOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastPowerWordFortitudeOnPartyAction(PlayerbotAI* botAI)
: GroupBuffOnPartyAction(botAI, "power word: fortitude") {}
};
BUFF_ACTION(CastPowerWordShieldAction, "power word: shield"); BUFF_ACTION(CastPowerWordShieldAction, "power word: shield");
BUFF_ACTION(CastInnerFireAction, "inner fire"); BUFF_ACTION(CastInnerFireAction, "inner fire");
@ -26,9 +36,19 @@ CC_ACTION(CastShackleUndeadAction, "shackle undead");
SPELL_ACTION_U(CastManaBurnAction, "mana burn", SPELL_ACTION_U(CastManaBurnAction, "mana burn",
AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20); AI_VALUE2(uint8, "mana", "self target") < 50 && AI_VALUE2(uint8, "mana", "current target") >= 20);
BUFF_ACTION(CastLevitateAction, "levitate"); BUFF_ACTION(CastLevitateAction, "levitate");
BUFF_ACTION(CastDivineSpiritAction, "divine spirit"); class CastDivineSpiritAction : public GroupBuffSpellAction
BUFF_PARTY_ACTION(CastDivineSpiritOnPartyAction, "divine spirit"); {
BUFF_PARTY_ACTION(CastPrayerOfSpiritOnPartyAction, "prayer of spirit"); public:
CastDivineSpiritAction(PlayerbotAI* botAI)
: GroupBuffSpellAction(botAI, "divine spirit") {}
};
class CastDivineSpiritOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastDivineSpiritOnPartyAction(PlayerbotAI* botAI)
: GroupBuffOnPartyAction(botAI, "divine spirit") {}
};
// disc 2.4.3 // disc 2.4.3
SPELL_ACTION(CastMassDispelAction, "mass dispel"); SPELL_ACTION(CastMassDispelAction, "mass dispel");
@ -103,13 +123,23 @@ SPELL_ACTION(CastMindBlastAction, "mind blast");
SPELL_ACTION(CastPsychicScreamAction, "psychic scream"); SPELL_ACTION(CastPsychicScreamAction, "psychic scream");
DEBUFF_ACTION(CastMindSootheAction, "mind soothe"); DEBUFF_ACTION(CastMindSootheAction, "mind soothe");
BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup()); BUFF_ACTION_U(CastFadeAction, "fade", bot->GetGroup());
BUFF_ACTION(CastShadowProtectionAction, "shadow protection"); class CastShadowProtectionAction : public GroupBuffSpellAction
BUFF_PARTY_ACTION(CastShadowProtectionOnPartyAction, "shadow protection"); {
BUFF_PARTY_ACTION(CastPrayerOfShadowProtectionAction, "prayer of shadow protection"); public:
CastShadowProtectionAction(PlayerbotAI* botAI)
: GroupBuffSpellAction(botAI, "shadow protection") {}
};
class CastShadowProtectionOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastShadowProtectionOnPartyAction(PlayerbotAI* botAI)
: GroupBuffOnPartyAction(botAI, "shadow protection") {}
};
// shadow talents // shadow talents
SPELL_ACTION(CastMindFlayAction, "mind flay"); SPELL_ACTION(CastMindFlayAction, "mind flay");
DEBUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace"); BUFF_ACTION(CastVampiricEmbraceAction, "vampiric embrace");
BUFF_ACTION(CastShadowformAction, "shadowform"); BUFF_ACTION(CastShadowformAction, "shadowform");
SPELL_ACTION(CastSilenceAction, "silence"); SPELL_ACTION(CastSilenceAction, "silence");
ENEMY_HEALER_ACTION(CastSilenceOnEnemyHealerAction, "silence"); ENEMY_HEALER_ACTION(CastSilenceOnEnemyHealerAction, "silence");

View File

@ -92,8 +92,6 @@ public:
creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection; creators["shadow protection"] = &PriestTriggerFactoryInternal::shadow_protection;
creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party; creators["shadow protection on party"] = &PriestTriggerFactoryInternal::shadow_protection_on_party;
creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead; creators["shackle undead"] = &PriestTriggerFactoryInternal::shackle_undead;
creators["prayer of fortitude on party"] = &PriestTriggerFactoryInternal::prayer_of_fortitude_on_party;
creators["prayer of spirit on party"] = &PriestTriggerFactoryInternal::prayer_of_spirit_on_party;
creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire; creators["holy fire"] = &PriestTriggerFactoryInternal::holy_fire;
creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness; creators["touch of weakness"] = &PriestTriggerFactoryInternal::touch_of_weakness;
creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness; creators["hex of weakness"] = &PriestTriggerFactoryInternal::hex_of_weakness;
@ -136,8 +134,6 @@ private:
static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); } static Trigger* shadow_protection_on_party(PlayerbotAI* botAI) { return new ShadowProtectionOnPartyTrigger(botAI); }
static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); } static Trigger* shadow_protection(PlayerbotAI* botAI) { return new ShadowProtectionTrigger(botAI); }
static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); } static Trigger* shackle_undead(PlayerbotAI* botAI) { return new ShackleUndeadTrigger(botAI); }
static Trigger* prayer_of_fortitude_on_party(PlayerbotAI* botAI) { return new PrayerOfFortitudeTrigger(botAI); }
static Trigger* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new PrayerOfSpiritTrigger(botAI); }
static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); } static Trigger* feedback(PlayerbotAI* botAI) { return new FeedbackTrigger(botAI); }
static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); } static Trigger* fear_ward(PlayerbotAI* botAI) { return new FearWardTrigger(botAI); }
static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); } static Trigger* shadowguard(PlayerbotAI* botAI) { return new ShadowguardTrigger(botAI); }
@ -207,8 +203,6 @@ public:
creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection; creators["shadow protection"] = &PriestAiObjectContextInternal::shadow_protection;
creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party; creators["shadow protection on party"] = &PriestAiObjectContextInternal::shadow_protection_on_party;
creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead; creators["shackle undead"] = &PriestAiObjectContextInternal::shackle_undead;
creators["prayer of fortitude on party"] = &PriestAiObjectContextInternal::prayer_of_fortitude_on_party;
creators["prayer of spirit on party"] = &PriestAiObjectContextInternal::prayer_of_spirit_on_party;
creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party; creators["power infusion on party"] = &PriestAiObjectContextInternal::power_infusion_on_party;
creators["silence"] = &PriestAiObjectContextInternal::silence; creators["silence"] = &PriestAiObjectContextInternal::silence;
creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer; creators["silence on enemy healer"] = &PriestAiObjectContextInternal::silence_on_enemy_healer;
@ -311,11 +305,6 @@ private:
static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); } static Action* fade(PlayerbotAI* botAI) { return new CastFadeAction(botAI); }
static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); } static Action* inner_fire(PlayerbotAI* botAI) { return new CastInnerFireAction(botAI); }
static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); } static Action* shackle_undead(PlayerbotAI* botAI) { return new CastShackleUndeadAction(botAI); }
static Action* prayer_of_spirit_on_party(PlayerbotAI* botAI) { return new CastPrayerOfSpiritOnPartyAction(botAI); }
static Action* prayer_of_fortitude_on_party(PlayerbotAI* botAI)
{
return new CastPrayerOfFortitudeOnPartyAction(botAI);
}
static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); } static Action* feedback(PlayerbotAI* botAI) { return new CastFeedbackAction(botAI); }
static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); } static Action* elunes_grace(PlayerbotAI* botAI) { return new CastElunesGraceAction(botAI); }
static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); } static Action* starshards(PlayerbotAI* botAI) { return new CastStarshardsAction(botAI); }

View File

@ -8,10 +8,9 @@
#include "Player.h" #include "Player.h"
#include "Playerbots.h" #include "Playerbots.h"
bool PowerWordFortitudeOnPartyTrigger::IsActive() bool ShadowProtectionTrigger::IsActive()
{ {
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("power word : fortitude", GetTarget()) && return BuffTrigger::IsActive() && !botAI->HasAura("prayer of shadow protection", GetTarget());
!botAI->HasAura("prayer of fortitude", GetTarget());
} }
bool PowerWordFortitudeTrigger::IsActive() bool PowerWordFortitudeTrigger::IsActive()
@ -20,43 +19,12 @@ bool PowerWordFortitudeTrigger::IsActive()
!botAI->HasAura("prayer of fortitude", GetTarget()); !botAI->HasAura("prayer of fortitude", GetTarget());
} }
bool DivineSpiritOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
!botAI->HasAura("prayer of spirit", GetTarget());
}
bool DivineSpiritTrigger::IsActive() bool DivineSpiritTrigger::IsActive()
{ {
return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) && return BuffTrigger::IsActive() && !botAI->HasAura("divine spirit", GetTarget()) &&
!botAI->HasAura("prayer of spirit", GetTarget()); !botAI->HasAura("prayer of spirit", GetTarget());
} }
bool PrayerOfFortitudeTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsPlayer())
return false;
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of fortitude", GetTarget()) &&
botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) &&
botAI->GetBuffedCount((Player*)GetTarget(), "prayer of fortitude") < 4 &&
!botAI->GetBuffedCount((Player*)GetTarget(), "power word: fortitude");
}
bool PrayerOfSpiritTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsPlayer())
return false;
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("prayer of spirit", GetTarget()) &&
botAI->GetBot()->IsInSameGroupWith((Player*)GetTarget()) &&
// botAI->GetManaPercent() > 50 &&
botAI->GetBuffedCount((Player*)GetTarget(), "prayer of spirit") < 4 &&
!botAI->GetBuffedCount((Player*)GetTarget(), "divine spirit");
}
bool InnerFireTrigger::IsActive() bool InnerFireTrigger::IsActive()
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();

View File

@ -27,8 +27,6 @@ BUFF_TRIGGER_A(InnerFireTrigger, "inner fire");
BUFF_TRIGGER_A(ShadowformTrigger, "shadowform"); BUFF_TRIGGER_A(ShadowformTrigger, "shadowform");
BOOST_TRIGGER(PowerInfusionTrigger, "power infusion"); BOOST_TRIGGER(PowerInfusionTrigger, "power infusion");
BUFF_TRIGGER(InnerFocusTrigger, "inner focus"); BUFF_TRIGGER(InnerFocusTrigger, "inner focus");
BUFF_TRIGGER(ShadowProtectionTrigger, "shadow protection");
BUFF_PARTY_TRIGGER(ShadowProtectionOnPartyTrigger, "shadow protection");
CC_TRIGGER(ShackleUndeadTrigger, "shackle undead"); CC_TRIGGER(ShackleUndeadTrigger, "shackle undead");
INTERRUPT_TRIGGER(SilenceTrigger, "silence"); INTERRUPT_TRIGGER(SilenceTrigger, "silence");
INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence"); INTERRUPT_HEALER_TRIGGER(SilenceEnemyHealerTrigger, "silence");
@ -44,20 +42,34 @@ SNARE_TRIGGER(ChastiseTrigger, "chastise");
BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend"); BOOST_TRIGGER_A(ShadowfiendTrigger, "shadowfiend");
class ShadowProtectionTrigger : public BuffTrigger
{
public:
ShadowProtectionTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "shadow protection", 4 * 2000) {}
bool IsActive() override;
};
class ShadowProtectionOnPartyTrigger : public BuffOnPartyTrigger
{
public:
ShadowProtectionOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "shadow protection", 4 * 2000) {}
};
class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger class PowerWordFortitudeOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) PowerWordFortitudeOnPartyTrigger(PlayerbotAI* botAI)
{ : BuffOnPartyTrigger(botAI, "power word: fortitude", 4 * 2000) {}
}
bool IsActive() override;
}; };
class PowerWordFortitudeTrigger : public BuffTrigger class PowerWordFortitudeTrigger : public BuffTrigger
{ {
public: public:
PowerWordFortitudeTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {} PowerWordFortitudeTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "power word: fortitude", 4 * 2000) {}
bool IsActive() override; bool IsActive() override;
}; };
@ -65,31 +77,15 @@ public:
class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger class DivineSpiritOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
DivineSpiritOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {} DivineSpiritOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "divine spirit", 4 * 2000) {}
bool IsActive() override;
}; };
class DivineSpiritTrigger : public BuffTrigger class DivineSpiritTrigger : public BuffTrigger
{ {
public: public:
DivineSpiritTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine spirit", 4 * 2000) {} DivineSpiritTrigger(PlayerbotAI* botAI)
: BuffTrigger(botAI, "divine spirit", 4 * 2000) {}
bool IsActive() override;
};
class PrayerOfFortitudeTrigger : public BuffOnPartyTrigger
{
public:
PrayerOfFortitudeTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of fortitude", 3 * 2000) {}
bool IsActive() override;
};
class PrayerOfSpiritTrigger : public BuffOnPartyTrigger
{
public:
PrayerOfSpiritTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "prayer of spirit", 2 * 2000) {}
bool IsActive() override; bool IsActive() override;
}; };
@ -106,9 +102,7 @@ class MindSearChannelCheckTrigger : public Trigger
{ {
public: public:
MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2) MindSearChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2)
: Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) : Trigger(botAI, "mind sear channel check"), minEnemies(minEnemies) {}
{
}
bool IsActive() override; bool IsActive() override;

View File

@ -19,6 +19,8 @@ void PriestNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back( triggers.push_back(
new TriggerNode("inner fire",{ NextAction("inner fire", 10.0f) })); new TriggerNode("inner fire",{ NextAction("inner fire", 10.0f) }));
triggers.push_back(
new TriggerNode("vampiric embrace", { NextAction("vampiric embrace", 16.0f) }));
triggers.push_back(new TriggerNode( triggers.push_back(new TriggerNode(
"party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11), "party member dead",{ NextAction("remove shadowform", ACTION_CRITICAL_HEAL + 11),
NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) })); NextAction("resurrection", ACTION_CRITICAL_HEAL + 10) }));
@ -54,12 +56,6 @@ void PriestBuffStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{ {
NonCombatStrategy::InitTriggers(triggers); NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode("prayer of fortitude on party",
{ NextAction("prayer of fortitude on party", 12.0f) }));
triggers.push_back(
new TriggerNode("prayer of spirit on party",
{ NextAction("prayer of spirit on party", 14.0f) }));
triggers.push_back( triggers.push_back(
new TriggerNode("power word: fortitude on party", new TriggerNode("power word: fortitude on party",
{ NextAction("power word: fortitude on party", 11.0f) })); { NextAction("power word: fortitude on party", 11.0f) }));

View File

@ -30,8 +30,6 @@ public:
creators["flash heal"] = &flash_heal; creators["flash heal"] = &flash_heal;
creators["flash heal on party"] = &flash_heal_on_party; creators["flash heal on party"] = &flash_heal_on_party;
creators["circle of healing on party"] = &circle_of_healing; creators["circle of healing on party"] = &circle_of_healing;
creators["prayer of fortitude on party"] = &prayer_of_fortitude_on_party;
creators["prayer of spirit on party"] = &prayer_of_spirit_on_party;
} }
private: private:
@ -134,20 +132,6 @@ private:
/*A*/ {}, /*A*/ {},
/*C*/ {}); /*C*/ {});
} }
static ActionNode* prayer_of_fortitude_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("prayer of fortitude on party",
/*P*/ { NextAction("remove shadowform") },
/*A*/ { NextAction("power word: fortitude on party") },
/*C*/ {});
}
static ActionNode* prayer_of_spirit_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("prayer of spirit on party",
/*P*/ { NextAction("remove shadowform") },
/*A*/ { NextAction("divine spirit on party") },
/*C*/ {});
}
}; };
#endif #endif

View File

@ -51,14 +51,6 @@ void ShadowPriestStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
} }
) )
); );
triggers.push_back(
new TriggerNode(
"vampiric embrace",
{
NextAction("vampiric embrace", 16.0f)
}
)
);
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"silence", "silence",

View File

@ -161,9 +161,7 @@ void ShamanAoeStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
} }
else if (tab == SHAMAN_TAB_ENHANCEMENT) else if (tab == SHAMAN_TAB_ENHANCEMENT)
{ {
triggers.push_back(new TriggerNode("medium aoe",{ NextAction("magma totem", 24.0f), triggers.push_back(new TriggerNode("medium aoe",{ NextAction("fire nova", 23.0f), }));
NextAction("fire nova", 23.0f), }));
triggers.push_back(new TriggerNode("maelstrom weapon 5 and medium aoe", { NextAction("chain lightning", 22.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 5 and medium aoe", { NextAction("chain lightning", 22.0f), }));
triggers.push_back(new TriggerNode("maelstrom weapon 4 and medium aoe", { NextAction("chain lightning", 21.0f), })); triggers.push_back(new TriggerNode("maelstrom weapon 4 and medium aoe", { NextAction("chain lightning", 21.0f), }));
triggers.push_back(new TriggerNode("enemy within melee", { NextAction("fire nova", 5.1f), })); triggers.push_back(new TriggerNode("enemy within melee", { NextAction("fire nova", 5.1f), }));

View File

@ -112,6 +112,7 @@ std::vector<NextAction> ArmsWarriorStrategy::getDefaultActions()
return { return {
NextAction("bladestorm", ACTION_DEFAULT + 0.2f), NextAction("bladestorm", ACTION_DEFAULT + 0.2f),
NextAction("mortal strike", ACTION_DEFAULT + 0.1f), NextAction("mortal strike", ACTION_DEFAULT + 0.1f),
NextAction("sunder armor", ACTION_DEFAULT + 0.05f),
NextAction("melee", ACTION_DEFAULT) NextAction("melee", ACTION_DEFAULT)
}; };
} }

View File

@ -5,6 +5,7 @@
#include "WarriorActions.h" #include "WarriorActions.h"
#include "AiFactory.h"
#include "Playerbots.h" #include "Playerbots.h"
bool CastBerserkerRageAction::isPossible() bool CastBerserkerRageAction::isPossible()
@ -36,6 +37,27 @@ bool CastBerserkerRageAction::isUseful()
bool CastSunderArmorAction::isUseful() bool CastSunderArmorAction::isUseful()
{ {
Group* group = bot->GetGroup();
if (!group)
return false;
if (!botAI->IsTank(bot, false))
{
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!member || member == bot || !member->IsAlive() || !member->IsInWorld() ||
member->GetMapId() != bot->GetMapId())
{
continue;
}
if (member->getClass() == CLASS_WARRIOR &&
botAI->IsTank(member, false))
return false;
}
}
Aura* aura = botAI->GetAura("sunder armor", GetTarget(), false, true); Aura* aura = botAI->GetAura("sunder armor", GetTarget(), false, true);
return !aura || aura->GetStackAmount() < 5 || aura->GetDuration() <= 6000; return !aura || aura->GetStackAmount() < 5 || aura->GetDuration() <= 6000;
} }

View File

@ -3,7 +3,7 @@
#include "AiObjectContext.h" #include "AiObjectContext.h"
#include "Action.h" #include "Action.h"
#include "AuchenaiCryptsActions.h" #include "ACActions.h"
class TbcDungeonAuchenaiCryptsActionContext : public NamedObjectContext<Action> class TbcDungeonAuchenaiCryptsActionContext : public NamedObjectContext<Action>
{ {

View File

@ -1,7 +1,7 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "AiFactory.h" #include "AiFactory.h"
#include "AuchenaiCryptsTriggers.h" #include "ACTriggers.h"
#include "AuchenaiCryptsActions.h" #include "ACActions.h"
// Shirrak the Dead Watcher // Shirrak the Dead Watcher

View File

@ -3,7 +3,7 @@
#include "AttackAction.h" #include "AttackAction.h"
#include "MovementActions.h" #include "MovementActions.h"
#include "AuchenaiCryptsTriggers.h" #include "ACTriggers.h"
// Shirrak the Dead Watcher // Shirrak the Dead Watcher

View File

@ -1,6 +1,6 @@
#include "AuchenaiCryptsMultipliers.h" #include "ACMultipliers.h"
#include "AuchenaiCryptsActions.h" #include "ACActions.h"
#include "AuchenaiCryptsTriggers.h" #include "ACTriggers.h"
#include "MovementActions.h" #include "MovementActions.h"
#include "ReachTargetActions.h" #include "ReachTargetActions.h"
#include "FollowActions.h" #include "FollowActions.h"

View File

@ -1,6 +1,6 @@
#include "AuchenaiCryptsTriggers.h" #include "ACTriggers.h"
#include "AuchenaiCryptsStrategy.h" #include "ACStrategy.h"
#include "AuchenaiCryptsMultipliers.h" #include "ACMultipliers.h"
void TbcDungeonAuchenaiCryptsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers) void TbcDungeonAuchenaiCryptsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{ {

View File

@ -3,7 +3,7 @@
#include "AiObjectContext.h" #include "AiObjectContext.h"
#include "TriggerContext.h" #include "TriggerContext.h"
#include "AuchenaiCryptsTriggers.h" #include "ACTriggers.h"
class TbcDungeonAuchenaiCryptsTriggerContext : public NamedObjectContext<Trigger> class TbcDungeonAuchenaiCryptsTriggerContext : public NamedObjectContext<Trigger>
{ {

View File

@ -1,5 +1,5 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "AuchenaiCryptsTriggers.h" #include "ACTriggers.h"
#include "AiObject.h" #include "AiObject.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"

View File

@ -3,7 +3,7 @@
#include "Action.h" #include "Action.h"
#include "NamedObjectContext.h" #include "NamedObjectContext.h"
#include "OldKingdomActions.h" #include "AKActions.h"
class WotlkDungeonOKActionContext : public NamedObjectContext<Action> class WotlkDungeonOKActionContext : public NamedObjectContext<Action>
{ {

View File

@ -1,5 +1,5 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "OldKingdomActions.h" #include "AKActions.h"
bool AttackNadoxGuardianAction::Execute(Event /*event*/) bool AttackNadoxGuardianAction::Execute(Event /*event*/)
{ {

View File

@ -5,7 +5,7 @@
#include "AttackAction.h" #include "AttackAction.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "OldKingdomTriggers.h" #include "AKTriggers.h"
class AttackNadoxGuardianAction : public AttackAction class AttackNadoxGuardianAction : public AttackAction
{ {

View File

@ -1,9 +1,9 @@
#include "OldKingdomMultipliers.h" #include "AKMultipliers.h"
#include "OldKingdomActions.h" #include "AKActions.h"
#include "GenericSpellActions.h" #include "GenericSpellActions.h"
#include "ChooseTargetActions.h" #include "ChooseTargetActions.h"
#include "MovementActions.h" #include "MovementActions.h"
#include "OldKingdomTriggers.h" #include "AKTriggers.h"
#include "Action.h" #include "Action.h"
float ElderNadoxMultiplier::GetValue(Action* action) float ElderNadoxMultiplier::GetValue(Action* action)

View File

@ -1,5 +1,5 @@
#include "OldKingdomStrategy.h" #include "AKStrategy.h"
#include "OldKingdomMultipliers.h" #include "AKMultipliers.h"
void WotlkDungeonOKStrategy::InitTriggers(std::vector<TriggerNode*> &triggers) void WotlkDungeonOKStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{ {

View File

@ -3,7 +3,7 @@
#include "NamedObjectContext.h" #include "NamedObjectContext.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"
#include "OldKingdomTriggers.h" #include "AKTriggers.h"
class WotlkDungeonOKTriggerContext : public NamedObjectContext<Trigger> class WotlkDungeonOKTriggerContext : public NamedObjectContext<Trigger>
{ {

View File

@ -1,5 +1,5 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "OldKingdomTriggers.h" #include "AKTriggers.h"
#include "AiObject.h" #include "AiObject.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"

View File

@ -3,7 +3,7 @@
#include "Action.h" #include "Action.h"
#include "NamedObjectContext.h" #include "NamedObjectContext.h"
#include "AzjolNerubActions.h" #include "ANActions.h"
class WotlkDungeonANActionContext : public NamedObjectContext<Action> class WotlkDungeonANActionContext : public NamedObjectContext<Action>
{ {

View File

@ -1,5 +1,5 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "AzjolNerubActions.h" #include "ANActions.h"
bool AttackWebWrapAction::isUseful() { return !botAI->IsHeal(bot); } bool AttackWebWrapAction::isUseful() { return !botAI->IsHeal(bot); }
bool AttackWebWrapAction::Execute(Event /*event*/) bool AttackWebWrapAction::Execute(Event /*event*/)

View File

@ -5,7 +5,7 @@
#include "AttackAction.h" #include "AttackAction.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "AzjolNerubTriggers.h" #include "ANTriggers.h"
class AttackWebWrapAction : public AttackAction class AttackWebWrapAction : public AttackAction
{ {

View File

@ -1,9 +1,9 @@
#include "AzjolNerubMultipliers.h" #include "ANMultipliers.h"
#include "AzjolNerubActions.h" #include "ANActions.h"
#include "GenericSpellActions.h" #include "GenericSpellActions.h"
#include "ChooseTargetActions.h" #include "ChooseTargetActions.h"
#include "MovementActions.h" #include "MovementActions.h"
#include "AzjolNerubTriggers.h" #include "ANTriggers.h"
#include "Action.h" #include "Action.h"
float KrikthirMultiplier::GetValue(Action* action) float KrikthirMultiplier::GetValue(Action* action)

View File

@ -1,5 +1,5 @@
#include "AzjolNerubStrategy.h" #include "ANStrategy.h"
#include "AzjolNerubMultipliers.h" #include "ANMultipliers.h"
void WotlkDungeonANStrategy::InitTriggers(std::vector<TriggerNode*> &triggers) void WotlkDungeonANStrategy::InitTriggers(std::vector<TriggerNode*> &triggers)
{ {

View File

@ -3,7 +3,7 @@
#include "NamedObjectContext.h" #include "NamedObjectContext.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"
#include "AzjolNerubTriggers.h" #include "ANTriggers.h"
class WotlkDungeonANTriggerContext : public NamedObjectContext<Trigger> class WotlkDungeonANTriggerContext : public NamedObjectContext<Trigger>
{ {

View File

@ -1,5 +1,5 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "AzjolNerubTriggers.h" #include "ANTriggers.h"
#include "AiObject.h" #include "AiObject.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"

View File

@ -3,7 +3,7 @@
#include "Action.h" #include "Action.h"
#include "NamedObjectContext.h" #include "NamedObjectContext.h"
#include "CullingOfStratholmeActions.h" #include "CoSActions.h"
class WotlkDungeonCoSActionContext : public NamedObjectContext<Action> class WotlkDungeonCoSActionContext : public NamedObjectContext<Action>
{ {

View File

@ -1,5 +1,5 @@
#include "Playerbots.h" #include "Playerbots.h"
#include "CullingOfStratholmeActions.h" #include "CoSActions.h"
bool ExplodeGhoulSpreadAction::Execute(Event /*event*/) bool ExplodeGhoulSpreadAction::Execute(Event /*event*/)
{ {

View File

@ -6,7 +6,7 @@
#include "GenericSpellActions.h" #include "GenericSpellActions.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "CullingOfStratholmeTriggers.h" #include "CoSTriggers.h"
class ExplodeGhoulSpreadAction : public MovementAction class ExplodeGhoulSpreadAction : public MovementAction
{ {

View File

@ -1,9 +1,9 @@
#include "CullingOfStratholmeMultipliers.h" #include "CoSMultipliers.h"
#include "CullingOfStratholmeActions.h" #include "CoSActions.h"
#include "GenericSpellActions.h" #include "GenericSpellActions.h"
#include "ChooseTargetActions.h" #include "ChooseTargetActions.h"
#include "MovementActions.h" #include "MovementActions.h"
#include "CullingOfStratholmeTriggers.h" #include "CoSTriggers.h"
#include "Action.h" #include "Action.h"
float EpochMultiplier::GetValue(Action* action) float EpochMultiplier::GetValue(Action* action)

Some files were not shown because too many files have changed in this diff Show More