Compare commits

..

51 Commits

Author SHA1 Message Date
bash
344358989f core filter isnt working yet 2026-06-05 10:02:09 +02:00
bash
716feab676 refactor(Core/Travel): Drop redundant NAV_GROUND_STEEP excludes (core handles via IsBot) 2026-06-05 10:02:09 +02:00
bash
839132825a fix(Core/Travel): Exclude NAV_GROUND_STEEP at all bot PathGenerator sites 2026-06-05 10:02:09 +02:00
bash
f93d3a1ad2 feat(Core/Travel): Align MoveFarTo and probe pipeline with cmangos 2026-06-05 10:02:09 +02:00
bash
b3145d151d feat(Core/Travel): Cap bots at 50° via NAV_GROUND_STEEP exclude 2026-06-05 10:02:09 +02:00
bash
249395f14f feat(Core/Debug): Trace movement entry points and visualize travel nodes 2026-06-05 10:02:09 +02:00
bash
5139255856 feat(Core/RPG): MoveFarTo flow, quest-pursuit at POI, MoveRandomNear retries 2026-06-05 10:02:09 +02:00
bash
4455829366 feat(Core/Travel): Travel-node graph routing for long-distance pathing 2026-06-05 10:02:09 +02:00
bash
345b1d6775 feat(Core/Loot): Quest GO loot, bag-make-room, item-pursuit 2026-06-05 10:02:09 +02:00
bash
40b8cc92fe chore(Tools): Add mmap/vmap client-data extraction script 2026-06-05 10:02:08 +02:00
bash
cd00841d96 feat(DB/Travel): Import cmangos travel-node graph 2026-06-05 10:02:08 +02: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
Keleborn
82a92f6296 Modify Illidan distbeyondtrap 2026-05-26 09:01:56 -07:00
ThePenguinMan96
8ca6e42f10
Druid Overhaul (#2392)
<img width="1274" height="952" alt="druids"
src="https://github.com/user-attachments/assets/7390d7e5-ed99-4eb3-8802-8c2f457d7c86"
/>


Hello playerbots community!! After my pvp gear update, I was itching to
get back to class strategies. I was testing raids, and noticed that
druids... Well, they kinda sucked.

They had glaring issues, would randomly die from bugs, and overall felt
bad. Looking at issues discussed on github/discord, I decided to start
working on a druid update. I started with the boomkin, and made a
boomkin PR, but there were some technical issues with the storing of the
eclipse mapping not clearing, and it was not good...

I closed that PR, and went back to the drawing board, with one goal in
mind: Make the druid class function as best as possible WHILST keeping
the code consistent with what already exists. There is a TON of _yoink
and twist_ (copy and paste with or without slight edits), and anything
that is custom/new is discussed in the section below. I am very proud
and excited to release this though - after 45 days of coding and
testing, the druid finally feels good to have in the group.

Disclaimer - this PR aims to address bugs, utility, and overall
performance of Druids. It will not magically make them top DPS. I have
done hours of testing, and druids can occasionally top the charts - but
inconsistently. Boomkins are inconsistent due to Eclipse (until later
gear phases), and Feral Cats are incredibly dependent on positioning,
timing, combo points, energy, clearcasting procs... The stars have to
align for things to go right for the Cat (It doesn't help that all
movement, targeting, and actions are ran through the same engine, so
cats really suffer in boss fights with scripted movement) But the
changes I have made help those situations occur a fair bit more
frequently. Let's dive in!

<!--
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 -->

A druid overhaul across all four specs — new Cat stealth and CC systems,
Eclipse tracking and rotation fixes for Balance, a Bear threat rotation
rework, a Resto healing priority overhaul, and a restructured CC and AoE
strategy architecture.

---

## Talent & Glyph Config

- **Balance:** Moved points out of Improved Moonfire and into Nature's
Reach for threat reduction and extended cast range.
- **Bear:** Moved points out of King of the Jungle (15% enrage damage)
and into Feral Instinct for increased Swipe damage and AoE threat.
- **Cat:** Swapped the Glyph of Typhoon (useless for feral) for Glyph of
Dash, which benefits the cat a ton in prowl.
- **Resto:** Swapped Glyph of Rejuvenation for Glyph of Nourish (better
HPS for difficult content), and Glyph of Typhoon (useless for resto) for
Glyph of Dash. Moved one point from Nature's Bounty to Empowered Touch.

---

## Balance

- **New skill - Typhoon:** Added Typhoon, triggered by "enemy within
melee," using cone targeting logic ported from Cone of Cold. Typhoon is
from the "balance pvp" spec only, and is housed in the shared AoE
strategy.

- **New skill - Cyclone:** Added Cyclone targeting the RTI CC-marked
target (default moon). Incapacitates for 6 seconds with full damage
immunity — cannot be broken by AoE. Priority 24.0f > Hibernate (23.0f) >
Entangling Roots (22.0f). *(See CC Implementation in the code notes
below.)*

- **Bug fix - Eclipse cooldown tracking:** Eclipse procs referenced a
cooldown not registered in the database, so boomkins immediately
reverted to the wrong filler after an Eclipse buff fell off. Fixed with
manual timestamp tracking. *(See Eclipse Cooldown Tracking in the code
notes below.)*

- **Bug fix — Starfall no longer pulls out-of-combat hostile enemies:**
Previously fired on cooldown regardless of surroundings — its 36-yard
radius (the largest AoE in the game) could silently pull entire
unengaged packs. Now suppressed if any non-combat hostile NPC is within
40 yards. *(See Starfall Pull Safety in the code notes below.)*

- **Filler changed to Starfire:** Starfire is now the filler (priority
5.4) over Wrath (5.3). Starfire has a 100% spellpower coefficient vs.
Wrath's 12%, is more mana-efficient per point of damage, and has a 100%
eclipse proc chance on crit vs. Wrath's 40%. In practice this
significantly reduced mana consumption, especially before level 40 when
Moonkin Form's mana-on-crit passive isn't available.

- **Moonfire / Insect Swarm on Attacker rework:** Previously tied to the
light AoE trigger (2+ enemies, 13.0f), causing low-level boomkins to
multi-dot instead of casting fillers — mana-inefficient at low levels
where targets rarely live long enough to tick the full DoT. Re-added as
dedicated on-attacker triggers at lower priority than the fillers, so
they fire only as a movement fallback. The triggers have been changed to
override the TTL check (time to life), similar to the warlocks DoTs.

- **Hurricane channel check rework:** Previously cancelled based on
distance from the bot — enemies could leave the AoE but remain within 30
yards, keeping the channel alive. Now reads the Hurricane
DynamicObject's actual radius and counts only attackers physically
inside it. *(See Hurricane Channel Cancel in the code notes below.)*

---

## Cat

- **Prowl (Stealth):** Implemented using the same logic as the existing
Rogue stealth system. The bot enters Prowl when out of combat and a
target is within range. Engagement distances are:
  - 30 yards baseline
  - −10 yards if the target already has a victim (engaged in combat)
  - −10 more yards if the target is also moving (minimum 10 yards)
  - +15 yards in Battlegrounds or Arenas
- Enemy player targets take priority over grind/DPS targets when
evaluating distance.

- **Prowl openers:** The bot approaches the target in Prowl and opens
based on approach angle and level:
- **From behind:** Ravage (learned at level 32). Before Ravage is
learned, Shred is used as the opener.
- **From the front:** Pounce (stun + bleed, learned at level 36). Before
Pounce is learned, Claw is used as the opener.

- **New skill - Maim:** Added Maim as a 5 second stun-finisher at 5
combo points, but only against player targets. It will only fire when
Rip and Savage Roar are already active.

- **Innervate on healer:** Cats now cast Innervate when a healer drops
below the low mana threshold (`AiPlayerbot.LowMana`, default 15%). *(See
Healer Low Mana Framework in the code notes below for the shared
value/trigger infrastructure backing this.)*

- **Predator's Swiftness with CC spells:** Added a twotrigger pairing
Predator's Swiftness (the instant-cast proc from finishing moves) with
the existing CC triggers (Cyclone, Hibernate, Entangling Roots). Feral
cats can now instant-cast CC the RTI CC-marked target (default moon)
after a finisher using the Predator's Swiftness proc.

- **Predator's Swiftness with Rebirth:** Added a twotrigger pairing
Predator's Swiftness with the combat resurrection trigger. Cats can now
use a Predator's Swiftness proc to instantly cast Rebirth on a dead
party member.

- **Bug fix - Autoattack no longer breaks prowl:**
`MeleeAction::isUseful()` now returns false while the bot has the Prowl
aura, preventing autoattack from breaking stealth before an opener spell
fires. The code comment notes this pattern should be reused for a future
Rogue autoattack in stealth fix.

- **Bug fix - Non-prowl skills no longer break prowl:** `isUseful()`
overrides were added to Feral Charge (Cat), Mangle (Cat), Swipe (Cat),
Rake to return false while Prowl is active, preventing accidental prowl
breaks before the opener fires.

- **Bug fix - Clearcasting proc with energy spells:** Added dedicated
`ClearcastingTrigger` / action pairings to ensure Clearcasting procs are
consumed immediately. On single target, Shred is used; on AoE, Swipe
(Cat) is used. This prevents the cat from using it's valuable
clearcasting proc on a low energy spell (rake, feral charge - cat,
cower, etc). This also fixes a bug where Clearcasting would sometimes
linger for 4+ seconds without being used, as energy-based spells do not
recognize the free cast and would not fire until their energy condition
was met.

- **Tiger's Fury rework:** TF previously fired on cooldown as a default
action, meaning the bot would use it at full or near-full energy and
gain no benefit from the energy it generates. The trigger now requires
energy to be below 30 before firing, ensuring the bot recovers the full
60 energy granted by the King of the Jungle talent.

- **Faerie Fire (Feral) rework for cat:** With Omen of Clarity, spams on
cooldown to fish for Clearcasting procs. Without it, applies as a normal
debuff and does not reapply while active. *(See Faerie Fire (Feral)
Trigger in the code notes below.)*

- **Feral Charge toggleable strategy:** Feral Charge (Cat) is now housed
in a dedicated `feral charge` strategy, enabled by default for Cat
druids. It can be disabled with `co -feral charge` and re-enabled with
`co +feral charge`. Disabling this strategy can be useful for encounters
where charging in would be unfavorable.

- **Rake on Melee Attackers removed:** The Rake on Melee Attackers
action was removed from the Cat AoE strategy. Applying Rake to multiple
attackers and spreading out combo points produced far lower AoE DPS than
simply continuing the single-target rotation.

- **Antiquated Omen of Clarity framework removed:**
`OmenOfClarityTrigger` and `CastOmenOfClarityAction` were not functional
and have been removed. The `ClearcastingTrigger` appropriately tracks
Omen of Clarity procs. I believe this was from TBC when Omen of Clarity
was a spell.

---

## Bear

- **Berserk tracking for Mangle:** A `berserk active` trigger fires
Mangle (Bear) at priority 25.0f while Berserk is up. Previously, Mangle
sat at 5.5f in the default actions while Swipe (Bear) sat at 25.0f on
the light AoE trigger — meaning in any 2+ enemy encounter, Swipe would
always win regardless of Berserk. Now Mangle (25.0f) sits above the AoE
triggers (24.5f), so it takes priority during Berserk. It's pretty cool
to see a Bear's dps on pull!

- **Faerie Fire (Feral) rework:** Previously applied once and stopped.
Now spams on cooldown for continuous threat generation (~3.5k threat per
cast at level 80), and serves as a ranged soft-taunt fallback on the
`lose aggro` trigger when Growl is on cooldown. *(See Faerie Fire
(Feral) Trigger in the code notes below.)*

- **Lacerate rework:** Previously, lacerate was a low priority default
action and bears had no duration awareness. They would occasionally let
5 stacks of Lacerate fall off, resulting in pretty significant threat
loss. Now, `LacerateTrigger` fires when the target has no Lacerate
debuff, the stack count is below 5, or the remaining duration is ≤ 6
seconds.

- **Demoralizing Roar on single target:** Previously, Demoralizing Roar
only fired on the medium AoE trigger (3+ enemies, skipped on bosses). A
dedicated trigger now applies it in any encounter, provided Vindication,
Demoralizing Shout, or Curse of Weakness are not already present, as
they don't stack with Demoralizing Roar.

- **Bug fix - Rebirth on bears:** Bears no longer attempt to cast
Rebirth in combat. The generic combat resurrection trigger was firing
for all druid specs, causing bears to shift out of Dire Bear Form
mid-fight to cast Rebirth — dropping their armor and HP while still
holding aggro. Lots of sudden tank deaths...

- **Feral Charge toggleable strategy:** Feral Charge (Bear) is now
housed in a dedicated `feral charge` strategy, enabled by default for
Bear druids. It can be disabled with `co -feral charge` and re-enabled
with `co +feral charge`. Disabling this strategy can be useful for
encounters where charging in would be unfavorable.

---

## Resto

- **Blanketing strategy:** Added a `blanketing` strategy (enabled by
default, `co -blanketing` to disable) that pre-HoTs group members with
Wild Growth and Rejuvenation regardless of current health, prioritizing
tanks → melee → ranged to maximize Revitalize uptime. *(See Blanketing
Strategy in the code notes below.)*

- **Nature's Swiftness → instant Healing Touch combo:** Nature's
Swiftness was previously included in the boost strategy, where it would
fire proactively on cooldown regardless of context. This wasted the proc
on situations where it provided no benefit. It is now exclusively
reactive — triggered at high priority (56.0f) when a party member hits
critical health. A paired `nature's swiftness active` trigger then
immediately fires Healing Touch (55.0f) on the lowest-health party
member, consuming the proc as an instant-cast emergency heal.

- **Lifebloom priority lowered (29.0f → 13.0f):** Lifebloom on the main
tank is cast on Omen of Clarity procs. At 29.0f it previously
outprioritised all low health reactive healing (21.4f), meaning a
Clearcasting proc while a party member was at 25–44% HP would cause the
bot to cast Lifebloom on the tank instead of Swiftmend or Nourish on the
injured target. Lowered to 13.0f so it fires only when no reactive
healing is queued.

- **Healing spell priority order reworked:** All three reactive
categories (critical, low, medium) now follow the same sequence:
Swiftmend → Wild Growth → Nourish → Regrowth → Healing Touch.

- **Tranquility toggleable strategy:** Tranquility is now housed in a
dedicated `tranquility` strategy, enabled by default for Resto druids.
It can be disabled with `co -tranquility`. In raids, Tranquility only
heals the druid's own group — not the full raid — making it
situationally poor during raid-wide damage or heavy movement phases.
Disabling this strategy lets players suppress the cast on those
encounters without affecting the rest of the healing rotation.

---

## CC & Strategy

- **Boost strategy:** The boost strategy is assigned to all druid specs
by default. It is spec-gated internally — Balance druids use it for
Force of Nature (treants on cooldown), and Feral druids (both cat and
bear) use it for Berserk.

- **CC strategy enabled by default:** The CC strategy was previously not
assigned to druids by default. It is now enabled for Balance and Feral
Cat, with behavior gated by spec:
- Balance receives the full RTI CC trigger set (Cyclone, Hibernate,
Entangling Roots).
- Feral cats only receive RTI CC triggers when Predator's Swiftness is
active (see above).

- **AoE strategy reorganized:** All AoE spells — Hurricane, Starfall,
Typhoon, Swipe (Cat), and DoTs on attackers (Moonfire, Insect Swarm) —
are now handled by the shared AoE strategy, consistent with how the rest
of the playerbot project structures AoE logic. This does not affect Bear
druids.

- **Aquatic Form while submerged:** The non-combat strategy now shifts
into Aquatic Form when the bot is fully submerged out of combat
(`LIQUID_MAP_UNDER_WATER`). If the bot is in another shapeshift (Bear,
Cat, Moonkin, Tree), it first shifts to caster form as a prerequisite
before entering Aquatic Form. The trigger intentionally does not fire at
the water surface (`LIQUID_MAP_IN_WATER`), or use the "swimming"
trigger, because it caused the druid to loop caster form and aquatic
form endlessly while surfaced.

---

## Code Consolidation & Refactoring

- The Bear, Cat, Heal, and Caster strategy files have been renamed to
Bear, Cat, Balance, and Resto respectively, to match the naming
conventions used elsewhere in the project.
- The Melee and Offheal strategy files have been deleted. The offheal
healing logic is preserved in full — it now lives as an optional
strategy (`CatOffhealStrategy`) inside `CatDruidStrategy`, sharing the
same action node factories as the base cat strategy rather than
duplicating them.
- All action relevance values across the druid strategies have been
converted from named constants (e.g. `ACTION_NORMAL`, `ACTION_HIGH + 4`)
to explicit numerical floats (e.g. `10.0f`, `24.0f`). This makes
priority ordering immediately visible in the source without needing to
cross-reference the constant definitions.

---

## New Code & Project References

### Eclipse Cooldown Tracking (`DruidActions.cpp`)

The previous implementation tracked the Eclipse cooldown using
`EclipseSolarCooldownTrigger` and `EclipseLunarCooldownTrigger`, both of
which extended `SpellCooldownTrigger` and called
`bot->HasSpellCooldown(48517/48518)`. `SpellCooldownTrigger` is the
standard project pattern for this — it works correctly for spells whose
cooldowns are registered in the database. However, Eclipse (Solar) and
Eclipse (Lunar) both have **Cooldown: n/a** in the DB.
`HasSpellCooldown` always returned false, so boomkins never respected
the cooldown and would revert to the wrong filler immediately after an
Eclipse buff fell off.

Since the cooldown can't be read from the DB, the fix tracks it
manually. When `CastWrathAction::isUseful()` or
`CastStarfireAction::isUseful()` detects that the corresponding Eclipse
aura has become active, it records the current timestamp and suppresses
the opposing filler for 30 seconds — the actual in-game cooldown
duration.

The timestamps are stored using `ManualSetValue<time_t>`, the same
pattern as `LastSpellCastTimeValue`
(`src/Ai/Base/Value/LastSpellCastTimeValue.h`), which is already used
throughout the project to record when spells were last cast. Two new
value classes — `EclipseSolarProcTimeValue` and
`EclipseLunarProcTimeValue` — are registered in a new
`DruidValueContextInternal` factory inside `DruidAiObjectContext.cpp`,
following the same factory pattern as `DruidTriggerFactoryInternal` and
`DruidAiObjectContextInternal` in the same file. Because these values
live in the per-bot `AiObjectContext`, they are automatically destroyed
when the bot logs out — no manual cleanup needed, and no shared state
between bots.

---

### Healer Low Mana Framework (`PartyMemberToHeal.h/.cpp`,
`HealthTriggers.h/.cpp`, `ValueContext.h`, `TriggerContext.h`)

`HealerLowMana` and `HealerLowManaTrigger` are added to the shared base
framework rather than the druid-specific code. Currently used by the Cat
Innervate trigger; designed so Mana Tide Totem, Hymn of Hope, and
similar spells from other classes can hook into the same trigger without
duplicating the group-scanning logic.

The pair follows the same pattern as the existing `PartyMemberToHeal` /
`PartyMemberLowHealthTrigger` — the project's standard design for "scan
the group for the most in-need member, then trigger when that member
crosses a threshold."

**Value** (`HealerLowMana : PartyMemberValue`): `Calculate()` walks the
group reference list, skips non-healers via the existing `IsHeal()`
check, and uses `MinValueCalculator` to return the lowest-mana healer as
a `Unit*`. Registered in `ValueContext.h` under the key `"healer low
mana"`.

**Trigger** (`HealerLowManaTrigger : Trigger`): `GetTargetName()`
returns `"healer low mana"`, which the base `Trigger::GetTarget()`
resolves against the value context — exactly how
`PartyMemberLowHealthTrigger::GetTargetName()` returns `"party member to
heal"`. `IsActive()` calls `GetTarget()` and checks
`GetPowerPct(POWER_MANA) < sPlayerbotAIConfig.lowMana`.

The trigger doesn't extend `HealthInRangeTrigger` because that class is
specifically for health (it reads the `"health"` value). Mana requires a
direct `GetPowerPct(POWER_MANA)` call, so a plain `Trigger` with a
custom `IsActive()` is used instead.

Both are registered in the global `ValueContext.h` and
`TriggerContext.h` rather than a class-specific factory, consistent with
how all other `PartyMemberValue` subclasses are registered in the
project.

---

### Blanketing Strategy (`RestoDruidStrategy.h/.cpp`,
`DruidActions.h/.cpp`, `DruidAiObjectContext.cpp`)

**Strategy structure:** `DruidBlanketStrategy` is a standalone
`Strategy` overlay, not embedded inside `RestoDruidStrategy`. This is
the same pattern as `DruidTranquilityStrategy`, `DruidBoostStrategy`,
and `DruidCcStrategy` — additive overlays that layer behavior on top of
the base strategy and can be toggled independently via `co
+/-blanketing`.

**Triggers:** Both `"wild growth blanket"` and `"rejuvenation blanket"`
are instantiated as `BuffOnPartyTrigger(ai, spellName)` — the project's
existing class from `GenericTriggers.h` for party-wide buff maintenance,
used throughout the codebase for things like Blessings and Mark of the
Wild. `BuffOnPartyTrigger` extends `BuffTrigger` and fires when any
party member is missing the named aura. No custom trigger class was
needed.

**Actions:** Both actions inherit from a shared `CastBlanketHotAction`
base that itself extends `CastSpellAction`. Inheriting from
`CastSpellAction` means `isPossible()` is handled for free — spell
known, off cooldown, target reachable, resources available. The
constructor sets `range = botAI->GetRange("heal")` to use the standard
healing range.

**`GetBlanketTarget(auraName)`:** The custom part of the implementation.
Walks the group in three prioritized passes — tanks first, then melee
non-tanks, then ranged — returning the first eligible member found.
Eligible is defined as: alive, not a GM, within `spellDistance`, and
`!botAI->HasAura(auraName, member, false, true)` (not already carrying
the HoT). Returns nullptr if every member is already covered.

**`isUseful()`:** On both actions simply returns `GetTarget() !=
nullptr` — fires as long as `GetBlanketTarget` finds someone without the
HoT, and suppresses itself the moment all targets are covered.

---

### CC Implementation — Cyclone, Hibernate, Entangling Roots
(`DruidTriggers.h/.cpp`, `DruidActions.h/.cpp`,
`GenericDruidStrategy.cpp`)

The druid CC spells use the project's strict RTI-only pattern rather
than the fallback "best candidate" pattern used by other classes (e.g.,
Mage Polymorph).

**`"rti cc target"` value:** A direct raid icon lookup. Reads the `"rti
cc"` string value (default `"moon"`, configurable per-bot via `rti cc
<icon>`), converts it to a raid icon index, and returns the live `Unit*`
for that GUID. If no CC icon is set, it returns `nullptr`. There is no
fallback to a best-candidate scan.

**Triggers** (`CycloneTrigger`, `HibernateTrigger`,
`EntanglingRootsTrigger`): All three extend `HasCcTargetTrigger` and
override `IsActive()`. The first check is always `"rti cc target"` — if
it returns `nullptr`, the trigger is immediately silent. If an icon is
set, it checks `"cc target"` (with the spell name as a qualifier) to
verify the RTI target matches and delegates to
`HasCcTargetTrigger::IsActive()`, which handles the "don't re-cast while
already CC'd" check.

**Actions** (`CastCycloneCcAction`, `CastHibernateCcAction`,
`CastEntanglingRootsCcAction`): All three extend
`CastCrowdControlSpellAction` rather than plain `CastSpellAction`. The
action names are `"cyclone on cc"`, `"hibernate on cc"`, `"entangling
roots on cc"` — not the raw spell names. This matters because
`CastSpellAction` stores its constructor argument as both the action
name and the spell name, and `isPossible()` calls `CanCastSpell(spell,
target)` using that string. Passing `"cyclone on cc"` to
`CastSpellAction` would resolve to spell ID 0 and silently return false
forever. `CastCrowdControlSpellAction` keeps the spell name separate
from the action name, avoiding this. `GetTargetValue()` on all three
returns `context->GetValue<Unit*>("rti cc target")` directly.

**Form prerequisite:** The action nodes for `"cyclone on cc"` and
`"hibernate on cc"` have `NextAction("caster form")` as a prerequisite,
so the bot automatically shifts out of Bear, Cat, or Moonkin form before
casting. Entangling Roots has the same prerequisite.

**Priority order:** Cyclone (24.0f) > Hibernate (23.0f) > Entangling
Roots (22.0f). Cyclone is preferred because it works on any target type
and the target is immune to all damage and healing while cycloned — it
cannot be broken by AoE. Hibernate is beast/dragonkin only. Entangling
Roots can be broken by damage.

**Feral Cat CC:** Wired through `TwoTrigger` pairings with `"predator's
swiftness"` (see Cat section above). Because the Predator's Swiftness
proc makes the spell instant-cast, no form shift is needed — the cat
casts directly from Cat Form after a finisher.

---

### Ferocious Bite Execute (`DruidTriggers.h`, `DruidCatActions.h`,
`CatDruidStrategy.cpp`)

Two separate triggers fire the same `CastFerociousBiteAction`, which is
a plain `CastMeleeSpellAction` with no custom logic — all the
intelligence lives in the triggers.

**`FerociousBiteTimeTrigger`** ("ferocious bite time", 22.5f) — the
normal rotation path. Requires 5 combo points, Savage Roar active with
>10 seconds remaining, and Rip active on the target with >10 seconds
remaining. The duration checks prevent spending combo points on
Ferocious Bite when either buff is about to fall off and needs to be
refreshed first.

**`FerociousBiteExecuteTrigger`** ("ferocious bite execute", 24.0f) —
the execute window, higher priority than the time trigger. Requires only
1 combo point, and fires when the target is below **both** 25% HP and
20,000 absolute HP. The dual condition is the key design detail: the 25%
threshold alone would trigger on a raid boss at 25% health — which could
still be millions of HP remaining. The 20,000 HP cap ensures the execute
behavior only activates when the target is genuinely close to death, at
which point dumping even a partial combo point buildup into Ferocious
Bite is better than continuing a normal builder-spender cycle.

---

### Faerie Fire (Feral) Trigger (`DruidTriggers.h`)

A single `FaerieFireFeralTrigger` class handles both Bear and Cat with
spec-branched behavior inside `IsActive()`. It extends `DebuffTrigger` —
the project's standard class for debuff maintenance on the current
target — but overrides `IsActive()` to produce three distinct behaviors
depending on form and talent state:

**Bear:** Bypasses `DebuffTrigger::IsActive()` entirely. Returns true
whenever the target is alive and in world, regardless of whether the
debuff is already present. Every cast generates immediate threat and
damage, so there is no reason to wait for it to fall off before
recasting.

**Cat with Omen of Clarity (talent aura 16864):** Same bypass — spams on
cooldown to fish for Clearcasting procs. Faerie Fire (Feral) has no
energy cost, making it a free input that can proc Omen of Clarity on any
hit.

**Cat without Omen of Clarity:** Falls through to
`DebuffTrigger::IsActive()` — the standard base class behavior, which
checks: target alive and in world, debuff not already present
(`!botAI->HasAura("faerie fire (feral)", target)`), and estimated
remaining lifetime of the target is at least `needLifeTime` seconds
(default 8.0f — no point applying a 30-second debuff to something about
to die). Applied as a normal debuff; does not reapply while active.

Both spam paths additionally guard against Prowl — `IsActive()` returns
false while the bot has the Prowl aura to prevent casting from breaking
stealth.

**Strategy wiring:**
- Bear: standard rotation slot at 17.0f, plus wired into the `"lose
aggro"` trigger at 25.5f as a soft-taunt fallback when Growl is on
cooldown.
- Cat: low-priority filler at 5.0f.

---

### Starfall Pull Safety (`DruidActions.cpp`)

Starfall's 36-yard AoE radius is the largest in the game. A single cast
near an unengaged patrol or mob pack would silently pull everything in
that area. The previous implementation fired on cooldown with no
awareness of the surrounding area.

`CastStarfallAction::isUseful()` now applies two guards before allowing
the cast:

**CC safety check** (standard project pattern): reads `"current cc
target"` and `"aoe position"`; suppresses the cast if the CC'd target is
within `aoeRadius` of the bot's AoE position.

**Unengaged hostile NPC scan (custom)**: reads `"nearest hostile npcs"`
(`NearestHostileNpcsValue`), which uses the project's standard
`Acore::AnyUnitInObjectRangeCheck` + `Cell::VisitObjects` grid searcher
at `sightDistance` (~50 yards). The value pre-filters via
`AcceptUnit()`: non-players only, and `unit->IsHostileTo(bot)` must be
true — this excludes neutral-faction trigger creatures, dummies, and
invisible spawns that would otherwise appear in a raw range scan. The
loop then applies four additional filters:

- Skip null / dead / out-of-world units (standard guard).
- Skip the current target — it is the reason we're in combat; its
in-combat flag is already covered.
- Skip `!bot->IsValidAttackTarget(unit)` — safety net for
hostile-faction trigger creatures carrying `UNIT_FLAG_NON_ATTACKABLE`
that `IsHostileTo` alone doesn't filter.
- Skip units beyond 40 yards — Starfall's listed radius is 36; 40 adds a
small buffer for patrols about to enter range.

If any remaining unit is `!unit->IsInCombat()`, the cast is suppressed —
that mob is unengaged and would be pulled.

**Why `"nearest hostile npcs"` and not `"attackers"`:** `attackers` only
contains units currently targeting the bot. We need to scan all hostile
units in the area, not just those already aggro'd.

---

### Hurricane Channel Cancel (`DruidTriggers.h/.cpp`,
`GenericDruidStrategy.cpp`)

The previous cancel condition checked whether fewer than 3 enemies were
within 30 yards of the bot. This is a poor proxy — enemies could scatter
laterally but still sit within that radius, keeping the channel alive
while none of them were taking damage.

The replacement is `HurricaneChannelCheckTrigger`, which locates the
actual Hurricane `DynamicObject` on the field and measures from it
directly.

**`IsActive()` logic:**

1. Checks `bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL)` — if the bot
isn't channeling at all, returns false immediately. If it is channeling
but the spell isn't a Hurricane rank, also returns false. This check is
necessary because `CURRENT_CHANNELED_SPELL` is a slot, not a specific
spell — the same cancel action is reused for other channeled spells in
the codebase, so the trigger must verify it's specifically Hurricane
before acting.

2. Iterates through `HURRICANE_SPELL_IDS` (all five ranks: 16914, 17401,
17402, 27012, 48467) calling `bot->GetDynObject(spellId)` until a
non-null result is found. Hurricane places a `DynamicObject` on the
field that the server uses as the actual AoE cylinder — each damage tick
queries which units are inside it. The DynamicObject is keyed by spell
ID, so the trigger must try each rank to find whichever one the bot
currently has learned and placed.

3. Reads `dynObj->GetRadius()` — the actual radius stored on the
DynamicObject itself rather than a hardcoded constant. This matches
exactly what the server uses to calculate damage, so the trigger's
cancel condition is spatially identical to the server's hit detection.

4. Walks the `"attackers"` GuidVector and counts how many live attackers
are within `dynObj->GetRadius()` of the DynamicObject's position using
`unit->GetDistance(dynObj->GetPosition()) <= radius`.

5. Returns `count < minEnemies` (default 3). The trigger fires —
cancelling the channel — when fewer than 3 attackers are physically
inside the Hurricane AoE.

**Why `"attackers"` and not a full area scan:** Hurricane only deals
damage to units that are attacking the bot (or in its threat list).
Scanning all nearby hostile units would cause premature cancellation if
non-aggro'd enemies happened to be standing outside the AoE. Attackers
is the right scope.

**Strategy wiring:** The trigger is paired with `NextAction("cancel
channel", 22.0f)` in the AoE strategy for both Balance and Resto druids.
The cancel priority (22.0f) sits below the Hurricane cast priority
(23.0f), so if the medium AoE trigger re-activates on the same tick the
cancel fires — meaning enemies came back into range — the new cast wins
over the cancel.

---

## 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.

Most new triggers are simple aura or cooldown checks. The heavier ones
are the group scans (for the blanketing HoTs and the healer mana check),
but these are identical in cost to group scans already running
throughout the project (all of the party member health checks). The
Starfall safety check is the only genuinely new scan — it looks for
nearby hostile NPCs before allowing a cast, using the same grid search
the project already uses elsewhere. That being said, it's loaded on the
end of the trigger/action pairing - so in the StarfallNoCDTrigger, the
bot has to already have learned starfall, already be in combat, and have
Starfall off of cooldown and ready to use. The Hurricane cancel check
only runs while the bot is actively channeling, so it's tightly gated.

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

Negligible for almost everything in this PR. The vast majority of new
logic is aura/buff/debuff lookups and cooldown checks that cost nothing
at scale. The group scans for blanketing and healer mana follow the same
pattern as existing party scans that already run on every healer bot
every tick. No new unbounded operations, no shared state between 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.
-->

All druids perform a bit better now - I'd say test the branch out with
the druids y'all currently use. JUST REMEMBER TO DO reset botAI or
talents spec "x" again, since there have been some strategies changed!!
The big one being the blanketing strategy for resto druids. They heal so
much better now. Also being able to control when they pre-hot is really
great.

## 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**)

Tested before an after with the same performance logs. I tested it with
a 25 man group of only druids versus my normal 25 man group on several
raid bosses - no difference in pmon.

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

Druid bots currently have several bugs/issues with them. This doesn't
exactly change the skills they were already using - just refines the
scenarios in which they should be used. For example, a boomkin won't use
starfall when there is a pack within range but not aggro'd. You can turn
off feral charge for cat druids now, so they don't fly into a bosses aoe
(locust swarm on anub, overload on iron council). Bear druids don't
battle rez anymore. They just feel less clunky and heal/hold aggro
better.

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

There are only 2 changes to files outside of the druid strategy, which
is the healer low mana framework and the modification to autoattack not
being used while in prowl.

## 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 was used heavily in the process to make this PR. First in the
research necessary into how systems work, to the initial code
implementation, to the testing results (explaining the outcome/why it
sucks), to the fix, and then to the review of the code at the end. I
will say that after I started researching how to use AI, use .md files
for context, clearing sessions, I got a lot better results.

I'll be the first to admit that it is 10 times easier to introduce a bug
with AI than it is to solve one or implement something new. That is why
every time it proposed a change, I asked it if the code was consistent
with the project (Already present somewhere else) and if it wasn't, it
was heavily scrutinized.

It was written with Claude Code with Sonnet 4.6 (high), and peer
reviewed by Github copilot. AI also made the description part of the PR,
in which I modified myself.

<!--
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).

**Wiki commands**

@Dreathean 

While this PR does add strategies, they are all enabled by default:

co +feral charge (feral druids, both cat and bear) - enabled by default,
allows/prevents the use of feral charge
co +tranquility (resto druids) - enabled by default, allows/prevents the
use of tranquility
co +blanketing (resto druids) - enabled by default, allows the druid to
pre-hot with wild growth and rejuvenation

But it would be worth a mention on the wiki - there are scenarios where
having these strategies disabled would be beneficial.

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

@kadeshar 
@Celandriel 
@brighton-chi 

Thank you for taking the time to look this over. There is a lot of
copied code, a bit of new code (which is explained in the code
explanation part of it, but please still ask questions), and a lot of
refactoring. Please remember to reset the bot strategies before/after
you test this branch, due to the several changes (blanketing, feral
charge strategies). Reset with reset botAI or "talents spec balance pve"
for any testers out there that didn't know. If/When this PR goes to the
master branch, it will need to be noted to the people this same thing
about resetting strategies.
2026-05-23 11:42:08 -07:00
dillyns
6294199343
DKs should use weapon stones (#2407)
<!--
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 -->
Death Knights were not using weapon stones. This PR lets them use and
initialize with weapon stones.


## 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.
-->
Create an rndbot DK. They should be init with weaponstones and use them
on their weapon.
Or give a weapon stone to an altbot. They should use it on their weapon.


## 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-22 21:57:20 -07:00
dillyns
1443668694
Make arcane barrage the alternate for arcane blast for level 60 mages (#2401)
<!--
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 -->
Adds arcane barrage as the alternate for arcane blast so level 60 arcane
mages who dont have arcane blast yet will use arcane barrage whenever
available.
For level 60 arcane mages, its actually better dps to alternate between
arcane barrage and arcane missiles to fish for Missile Barrage procs.

Before:
<img width="576" height="403" alt="Screenshot_20260518_024601"
src="https://github.com/user-attachments/assets/567f198f-3655-4449-86e6-2cfffd19a529"
/>

After:
<img width="579" height="381" alt="Screenshot_20260518_025040"
src="https://github.com/user-attachments/assets/803f5380-58d3-490a-a3b1-e2941e2f0048"
/>


## 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.
-->

Have a level 60 arcane mage dps a dummy. To compare to the old
"rotation" of just using arcane missiles you can "ss Arcane Barrage"

## 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.
-->

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2026-05-22 21:47:38 -07:00
kadeshar
55c5d29e2d
Replace hardcoded bot texts (#2408)
<!--
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 -->

Replaced hardcoded bot text with translationable.

Related with: #1295 


## 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. Invite bots
2. Check that text after "follow" and "stay" return texts "Following"
and "Staying"

## 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.
-->

Summary hardcoded text and checking that they already exists to reuse.

<!--
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-22 21:41:24 -07:00
Crow
3a7e3e2719
Fix Mages' Armor Strategies & Light Refactor (#2390)
<!--
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 -->
Mages have a "bdps" strategy to use Molten Armor (default for Fire and
Arcane) and "bmana" strategy to use Mage Armor (default for Frost). The
existing code uses a series of alternatives for armor
(Molten->Mage->Ice->Frost), which is needed for Mages that have not
learned Molten or Mage. However, I was noticing that sometimes my Fire
and Arcane Mages would end up with Mage Armor, presumably because there
could be a situation in which the casting of Molten Armor failed and the
fallback to Mage Armor kicked in (for example, due to there being not
enough mana to cast Molten Armor). This PR makes bdps always mean Molten
Armor, if it is learned, and bmana always mean Mage Armor, if it is
learned, by gating through triggers.

Other changes:

- Added bdps and bmana to default Mage combat strategies (still bdps for
Fire and Arcane and bmana for Frost) so that Mages will reapply armor if
it expires in combat.
- Deleted Arcane Explosion strategy--it was not fully implemented
because there was no associated action (such as through a
CastArcaneExplosionAction class). I debated implementing it, but there
isn't a suitable targeting mechanism that exists in the code from what I
can tell. Arcane Mages generally just use Blizzard for AoE (and
Flamestrike with PoM); Arcane Explosion is useful to use if (1) a player
is moving or (2) mobs are almost dead in an AoE situation. Scenario (1)
is irrelevant for bots since they cannot cast while moving. With respect
to scenario (2), the existing AoE triggers in fact look for highest HP
mobs so to implement Arcane Explosion in a useful manner would probably
require a new Value, and that is not worth it for what would be
miniscule benefit anyway.
- General cleanups of Mage code (e.g., deleted empty ActionNodes). These
were based on a quick review; I did not do any sort of detailed or
comprehensive review and have no desire to with this PR. Note: I know
that FrostMageStrategy.cpp had a Fireball alternative for Frostfire
Bolt, but I deleted it anyway because the same ActionNode is already in
GenericMageStrategy.cpp.
- General cleanups of AiFactory default combat/noncombat strategies
(e.g., removal of deprecated bdps and bmana strategies for Shamans).


## 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.

Added getAlternatives for the armor strategies; this approach already
exists for Druids. Added one new trigger for Molten Armor that is
throttled by 10s like the existing Mage Armor trigger.

## 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.
-->
Give a Mage the "bdps" strategy. They should cast Molten Armor. Make
them cast a different armor. After the trigger throttle period (10s),
they should reapply Molten Armor. Same goes for bmana and Mage Armor.
Try this in combat, and it should work too.


## 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**)

The point of this PR is to fix the default Mage armor buffing behavior.


- 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 present a couple of possibilities to fix the issue of Mage
Armor being cast with bdps, and from there I settled on the
getAlternatives approach. I did everything else.


<!--
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.
-->
@Dreathean Perhaps the wiki can be updated to document the bdps and
bmana strategies for Mages?
2026-05-22 19:25:48 -07:00
Crow
cd2fe2f9a1
Use Stones for Prot Paladins & Reduce Oils (#2405)
<!--
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 -->

Back when I did the Stones/Oils PR, I gave Wizard Oil to Prot Paladins.
I was operating based on their mechanics in TBC and have since realized
that their damage is much more physical-based in WotLK as opposed to
spell-based in TBC. So I've changed them to now get stones upon
maintenance and apply stones.

I also reduced weapon oils given during maintenance from 4 to 2 stacks.
I did 4 originally because oils stack to 5 so it would align with the
amount given of stones and poisons (which stack to 20). But 4 inventory
slots taken up by oils is excessive, and 2 still last far longer than
buffing reagents in a raid context. So this is just to reduce inventory
clutter.

Other stuff is just little code stuff like reformatting or reordering
things to be more efficient.


## 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.

There's nothing new--essentially with respect to consumables, prot
paladins now are lumped with ret.


## 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.
-->

Create a prot paladin, turn on selfbot. Use maintenance and the paladin
should be given stones and no oils. The paladin should then apply the
stone to its weapon (if it is equipping a level-eligible weapon).

Create a caster, turn on selfbot. Use maintenance and they should be
given 2 stacks of oils.

## 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**)

Prot paladins will now use stones in their inventory and will not use
oils in their inventories. This is better for their mechanics.

- 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-22 19:25:07 -07:00
Crow
92081c9f1a
Expand PWS Usage by Disc Priests (#2403)
<!--
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 -->
Disc Priests currently do not use Power Word: Shield until a party
member is at "medium health" (configurable, default 65%). That is the
second-level healing threshold; at the highest level, "almost full
health" (configurable, default 85%), Disc Priests will use only Prayer
of Mending and Renew. This PR adds PWS as the highest priority healing
action starting at "almost full health."


## 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.
Added new action "power word: shield on party" to "party member almost
full health" trigger in HealPriestStrategy.cpp (the "heal" strategy,
which is the default strategy for Disc Priests, as Holy Priests use the
"holy heal" strategy).


## 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.
-->
Group up with a Disc Priest and take damage; confirm that PWS is cast
when you drop below your configured almost full health threshold.


## 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**)

PWS is the signature ability of Disc. Priests, and delaying it to 65% is
significantly limiting their effectiveness in group content,
particularly with raid bosses that deal raidwide damage and in heroic
dungeons, where waiting until the tank hits 65% is a death wish. This
single change makes an enormous difference in their effectiveness.

See below for before/after change for Eredar Twins (focus on the
activity).

Before:

<img width="693" height="397" alt="Screenshot 2026-05-18 201949"
src="https://github.com/user-attachments/assets/18ad84c7-9ad8-4c39-94e7-650c9bb5f36e"
/>

After:

<img width="746" height="409" alt="image"
src="https://github.com/user-attachments/assets/6240f118-e12f-44c7-a1af-373e94985957"
/>


- 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-22 19:24:47 -07:00
kadeshar
34f34ef13d
Fix for ru translation (#2400)
<!--
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 -->

Fixed bug with finding russian texts (loc8)

Related with:
https://github.com/mod-playerbots/mod-playerbots/issues/1884

## 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. Use ru client
2. Invite bot
3. Use command `focus heal ?"

## 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.
-->

Find and fix wrong places checking texts

<!--
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-22 19:24:15 -07:00
HennyWilly
ab196cb532
Refactoring of BWL strategy (#2397)
<!--
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 refactors the Blackwing Lair strategy to align with newer raid
strategy implementations.
It extracts repeated Blackwing Lair spell IDs, game object IDs, and
suppression device checks into a shared helper, so the action and
trigger code use the same centralized definitions instead of duplicating
literals and condition chains.
No gameplay behavior changes are expected.

This PR is intended as a base for follow-up PRs that introduce
additional boss-specific strategy logic.

## 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.
- Move existing BWL-specific constants and the suppression device
condition into a shared helper.
- Keep all existing trigger names, action names, spell IDs, game object
IDs, and condition checks unchanged.
- Update the affected action/trigger implementations to call the helper
instead of duplicating the same logic inline.
- Describe the **processing cost** when this logic executes across many
bots.
- No meaningful increase. The logic executed per bot remains effectively
the same.
- This refactor only replaces duplicated inline checks with a shared
helper function and named constants.
- There are no additional scans, branches, state tracking, or new
decision loops.

## 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.
-->
Run the Blackwing Lair raid and check if the already implemented
features still work:
- _Onyxia Scale Cloak handling_: Confirm the bot still applies/checks
the same aura as before.
- _Suppression device handling_: Confirm bots still detect nearby active
suppression devices and turn them off under the same conditions as
before.
- _Chromaggus bronze affliction handling_: Confirm bots still use
Hourglass Sand if they have the bronze affliction.

## 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**)

A small helper call was introduced for the suppression device check, so
there is technically a minor additional call overhead compared to the
previous inline condition. In practice, the executed logic, iteration
scope, and decision flow remain unchanged, so the impact is negligible
and does not introduce meaningful scaling risk.

- 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 was used for review support and drafting the pull request text.
The refactoring itself was done manually.

<!--
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.
-->
This PR is intended to be behavior-preserving only.
The main change is consolidation of repeated Blackwing Lair constants
and suppression device logic into a shared helper used by both actions
and triggers. Review can focus on verifying that the extracted helper
matches the previous conditions.
2026-05-22 19:23:49 -07:00
Mat
c7b4b9aa80
ICC V2, Autogear BiS cmd (#2363)
## Pull Request Description
<!-- Describe what this change does and why it is needed -->

Big thanks to @kadeshar for providing the bis list for many raids and
ilvls :D

Video demo for ICC 25HC:
https://studio.youtube.com/video/nACyjn817iQ/edit

Video demo for autogear bis chat command:
https://www.youtube.com/watch?v=2YqyVBaSb2g

split main IccActions.cpp into sperate per boss .cpp files
changed style to be more aligned with
https://www.azerothcore.org/wiki/cpp-code-standards (WIP)
added bisicc chat command for bots to gear with ICC bis gear if autogear
and bisicc is enabled in cfg
https://gist.github.com/metal0/0bb094bf65d27e17044308ad0646cae1 bis list
used

LM

Added multiple spike marking and focus for faster spike clearing, each
spike will get its own kill group, tank spike will never get melee bots
(only assist tank and ranged dps)
Added coldflame detection so that melee bots dont go for spikes that are
in flames
During bonestorm assist tank will go far away spot so that once
bonestorm is fixed, LM will bounce back and forth from MT to AT (atm it
targets randomly, it should always pick furthest target)
Coldflame avoidance is handled by avoid AOE, important to keep it on in
cfg

Tested on ALL diffs

LDW

Improved skull marking of adds, add handling by tanks and dps
Changed 1st position for ranged bots for easier adds handling in HC and
NM
Improved tanking logic for tanks, assist tank will focus on collecting
adds and bring them near boss
Real players will also get cyclone aura when mind controlled
Improved ranged position during 2nd phase, they should not get stuck in
corners/walls anymore
Tanks will remove LDW ToI aura in HC (really hard to tank with it since
many things are happening at once)
Added Cheat for LDW fight to help tanks with agro in 2nd phase of heroic
modes
Changed tank position in phase 2 closer to pillars opposed to stairs
(bots love to fall thru floor and run thru walls if near them) this
fixed the issue
Fixed edge case for escaping from shades, it could happen that multiple
shades would target bot, and it was running from 1st one he found, now
it will run form all that are targeting it
Hunters will cast viper sting now, to increase shield draining speed

Tested on ALL diffs
Edit 19.5. :
Tested 25hc with autogear bis gear, playebots cfg ICC cheats off, world
cfg ICC buff on max (30%)
In short cleared LDW without ICC cheats with bis gear but unoptimized
enchants, talents, gems. I still recommend using ICC cheats for better
and fun experience.

GS

Changed triggers and actions to enable cross faction play
Assist tank will now actually tank adds on friendly ship
Dps will properly jump to attack mage and go back to their ship, if
stuck on enemy ship /p reset, /p summon or /p follow
fixed trigger for cannons, if cannons are frozen bots wont try to mount
them anymore which prevented them from attacking mage properly
bots will use rocket packs to jump to and from enemy ship instead of
teleporting
Main tank will now jump 1st. tank enemy boss and wait until all bots
have jumped back before he jumps back
All bots will wait for main tank to engage enemy captain before jumping
to enemy ship
Cannons will focus rockets 1st, then other adds now (for when gs gets
scripted)
Rdps will focus nearby adds on enemy ship and mark with star rti icon
when there is no deep freeze

todo: remove tanking bypass when core fixes enemy ship boss threat
reseting
Tested crossfaction on horde with single ally bot, ally bot did
everything right, need to test more.
note horde side is heavily bugged due to threat issue of adds, tanks
cant take threat, on ally its somewhat ok, on horde rip. Horde is
doable, but annoying cus of threat issue.


Tested on ALL diffs

DBS

Remade tank taunt logic, tanks should now properly taunt boss and let
other tank taunt it if they get rune of blood
Tanks will tank adds better now, no loose adds anymore

Tested on ALL diffs

Dogs

Remade tank taunt logic, tanks should now properly taunt boss and let
other tank taunt it if they get 8 mortal wounds stacks
Tanks will tank adds better now, no loose adds anymore

Tested on ALL diffs


Festergut

Hunters sometimes populated row 0 which would make them in melee range
of the boss (bad for dps). They should pick correct rows now
Healers will populate row 0 1st then other rows for optimal healing
position
Ranged bots should properly choose unique spots to avoid stacking when
there is no spore present
Remade tank taunt logic, tanks should now properly taunt boss and let
other tank taunt it if they get 6 gastric bloat stacks
Changed ranged spore position closer to boss
Spore bots should be able to attack/do non movement action when they
have spore and are in position
Solved malleable goo detection via direct boss hooks to detect boss
targets!

Tested on ALL diffs

Rotface

Tanks should not fight over big ooze anymore
Improved big ooze kiting
Improved small ooze stacking logic (when no big ooze present, stack at
small ooze position, when big ooze is present move to it)
Fixed edge cases when main and assist tank get small ooze (they used to
move to big ooze, that was really bad since main tank would start to
tank big ooze and get hit by big ooze, assist tank would stop kiting and
get hit by big ooze)
stopped mutated plague from dispelling instantly (as fight goes on,
rotface cast mutated plague more and more, thus making it impossible to
pass due to sheer numbers of small oozes and big oozes on the map, this
will delay their spawning and give enough time for bots to handle them
properly)
Fixed edge case of multiple small oozes and big oozes being alive at
same time (bots would detect wrong oozes and wipe raid or get stuck)
Improved flood avoidance
Improved ranged positioning in heroic mode, instead of letting them
choose positions (which is a nightmare on dynamic fight as rotface, they
now will choose 1 spot of 22 premade ones and populate them based on
guid and adopt spot based on flood position)
Improved Explosion avoidance by making bots remember their starting
position so that they can return to it after big ooze explode, their
movement is not chaotic anymore, and improved timers, they will wait 2
sec at new position before returning to starting position so that they
can avoid explosion projectiles properly, they should also avoid moving
to other bots starting positions.
These changes ensure minimal movements so that bot can do maximum dps
possible.

Tested on ALL diffs

PP

Fixed many logic conflicts that caused bots to freeze, do bad dps to
ooze/clouds
Fixed triggers and multipliers
Improved Gas Cloud avoidance, bloated bot will now remember its previous
position to avoid backtracking/getting stuck in corners
Added boss hooks to finally detect malleable goo, it is not an npc,
object or creature and PP doesn't target anyone, bots will flee from it
now
Boss stacking now only in last phase
Added cheats for players also (if enabled in cfg) only bots used to get
auras
Fixed tank switching in last phase, atm PP doesn't apply aura, but it
should work, since same logic works for dogs, festergut and dbs
Assist tank will now become abo if there is no abo before first puddle
appears
Abo will during puddles, slow oozes, slash boss & oozes
In last phase assist will return to normal

Tested on ALL diffs
Edit 19.5. :
Tested 25hc with autogear bis gear, playebots cfg ICC cheats off, world
cfg ICC buff on max (30%)
Tanks switched in last phase flawlesly and shared stacks as they should
(mutated plague got fixed in core)
In short cleared PP without ICC cheats with bis gear but unoptimized
enchants, talents, gems. I still recommend using ICC cheats for better
and fun experience.

BPC

Added center position to prevent bots from pulling BQL or other adds
when they glitch thru walls/floor and thus resetting raid back to icc
entrance teleporter
Added additional z axis resetting since bots like to "fly" up in the air
when attacking kinetic bombs.
using cheat bypass for ball of inferno flames (atm bugged, doesn't
shrink), bots will simply kill them when they spawn.
Improved tanking for main tanks, improved collection of dark nuclei for
assist tank
Improved kinetics bomb handling
Improved shock vortex spreading
Improved valaran spreading for ranged
Added shock vortex (non empowered) detection to avoid it while moving
into safe positions
Fixed jittery movement

Tested on ALL diffs

BQL

Removed center position block so that bots can spread our easier in 25
mode, not ideal but makes 25hc easier
Replaced repulsion based spreading, now each bot will have its own spot
and move if needed to new spot
Improved air phase spreading
Fixed assist tank taking 1st bite

Tested on ALL diffs

VDW

Due to recent core changes bots got bugged in portals if no real player
entered and changed Z axis, if there was no z axis change bots would
chill under the cloud on the ground and do nothing. I could not figure
out how to fix this (thus breaking immersion) without force teleporting
them to the clouds.
Bots that go into portals will now teleport at the same time to clouds
instead of following leader bot.
Added feature that if players enter the portal, player with lowest guid
will become bot "leader" and they will follow that player so that there
is at least a little bit of immersion left.
Fixed cloud collection for Heroic Mode, bots will now time clouds more
precisely to avoid loosing stack due to not picking them up
Improved RTI marking
Improved group splitting
Improved zombie kiting and avoiding explosion

Tested on ALL diffs

Sindragosa

Bots will mark tomb positions with red smoke bomb in air phase so that
real player know where to go with when beacon on them
in last phase they will mark with blue smoke tomb position
Fixed tank positioning
Fixed wrong tomb choice and positioning
Fixed tomb marking
In last phase healers will stack with melee to allow boss healing
In last phase when waiting for mystic debuff to pass, bots will damage
tomb like in air phase to speed up the kill

todo: tank switch to reset mystic buffet stacks
Tested on ALL diffs
Edit 19.5. :
Tested 25hc with autogear bis gear, playebots cfg ICC cheats off, world
cfg ICC buff on max (30%)
In short cleared LDW without ICC cheats with bis gear but unoptimized
enchants, talents, gems. I still recommend using ICC cheats for better
and fun experience.

LK

Changed add gathering logic for 1st phase and winter phase, instead of
tank moving to shamblings, he will keep taunting until they agro him.
necrotic plague is easy now, ditched complex timing logic for a simple
logic ( move to shambling, wait until dispeled, go back. Healers dont
dispel until defile ally is near shambling )
Fixed winter phase gathering logic, assist tank will now properly move
to raging spirits asap and bring them to main tank, melee dps will no
properly move behind/flank spirits and shamblings to avoid instant
death. Rdps will now properly focus frost orbs and adds, Transition
should also be smoother now, but still needs /p reset if they get stuck.
Other phases are ok, LK fight is now even better than before, but player
still need to know tactics and use multibot addon to help out bots when
needed, especially during defile phase since its random and position
matter for valkyrs and future defiles
Non winter phase AT will collect raging spirits and move them to main
tank, ranged bots will keep distance, melee bots will flank them to
avoid aoe
Defile, ditched complex spreading which was mostly gamble with boss
hooks to detect defile victim. If bot, bot will move away from raid, if
real player main tank will yell Player name move away defile.
bots will stay in center now if safe from defile, raging spirits or vile
spirits
Vile spirits soaking by assist tank. Assist tank will stand between
spirits and raid and chase spirits. healers are allowed to move from
position to heal assist tank. one hunter if alive will be at center
position to place traps to slow down spirits


HC

Real players will also get buffs if cheats are enabled now
Assist tank will now never move towards the raid to gather adds, instead
it will taunt them instead so that they come to it
Assist tank will rotate shamblings at all times away from raid
Assist tank will stun shamblings before transition to avoid shockwave
wipe
Winter phase ice sphere location changed, ranged will focus sphere
faster and better now
Fixed jittery movement and low dps during winter phase
Fixed most of the bots getting stuck during winter phase
Valkyrs will be properly marked now, one by one, in hc bots will now
ignore low hp valkyrs and focus on grabbing valkyrs or boss
After winter raging spirit will have top priority for killing
After winter ranged bots will 1st handle ice spheres then skull targets
Spirit bomb avoidance improved, main tank should not back track into
unsafe positions anymore


Since real player is leader its crucial that player know the tactics,
bots can not handle edge cases during the fight alone,
they need some of reset, follow, summon here and there since its a long
fight and things can go wrong.

Tested on ALL diffs
NOTE: If server crash, bots will sometimes drop ICC strategy even though
they are in ICC, simply re enter or write /p nc +ICC to re enable.
NOTE: addons that mark icons during fight could break bots, since icons
are used for RTI by bots
NOTE: I did not use any raiding addons besides unbot and multibot to
control bots
NOTE: In theory everything should work wihout ICC buff from world cfg,
and ICC cheats from playerbots cfg, didnt test it, didnt try, its too
hard core for hc mode to go raw, but it should be possible good luck :)
NOTE: For normal about 5k gs should be enough to do most bosses. For HC
T10 set + ICC 25 nm or HC gear + gems + enchants + buffs from cfg for
fun experience.
NOTE: As player its good to know every strategy for Bosses, so that you
can spot and help out with reset, follow, summon if bots seem stuck or
are doing something strange, a lot of stuff is happening on most fights
so expect some intervention with reset, summon, follow.

10 MAN 2-3 Healers, 2 Tanks, at least 1 hunter, at least one druid for
bress (its not set in stone, but most success with this setup)
25 MAN 6-7 Healers, 2 tanks, at least 1 hunter, at least 3-4 druids for
bress (its not set in stone, but most success with this setup)

GL & HF, happy raiding :D

Closes #1421 #2120
Fixes #1219 NOTE: Not all of them, I have updated affected changes in
#1219. Trash, quest, cheats are still nice to haves, but I don't see
working on that in near future.

Before posting bugs check #1219 and write there. As I said, I dont plan
to implement certain things in near future, but I am more than willing
to fix bugs that crash server if they happen ASAP.

<!--
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.
-->





## 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.
Enter ICC
Test Bosses

bis ICC command
type bisicc into party chat or whisper and bots will reply and equip
gear

- Any required setup (e.g. multiple players, number of bots, specific
configuration).
NOTE: If server crash, bots will sometimes drop ICC strategy even though
they are in ICC, simply re enter or write /p nc +ICC to re enable.
NOTE: addons that mark icons during fight could break bots, since icons
are used for RTI by bots
NOTE: I did not use any raiding addons besides unbot and multibot to
control bots
NOTE: In theory everything should work wihout ICC buff from world cfg,
and ICC cheats from playerbots cfg, didnt test it, didnt try, its too
hard core for hc mode to go raw, but it should be possible good luck :)
NOTE: For normal about 5k gs should be enough to do most bosses. For HC
T10 set + ICC 25 nm or HC gear + gems + enchants + buffs from cfg for
fun experience.
NOTE: As player its good to know every strategy for Bosses, so that you
can spot and help out with reset, follow, summon if bots seem stuck or
are doing something strange, a lot of stuff is happening on most fights
so expect some intervention with reset, summon, follow.

10 MAN 2-3 Healers, 2 Tanks, at least 1 hunter, at least one druid for
bress (its not set in stone, but most success with this setup)
25 MAN 6-7 Healers, 2 tanks, at least 1 hunter, at least 3-4 druids for
bress (its not set in stone, but most success with this setup)
- Expected behavior and how to verify it.
If requirements are met, bots should not struggle with killing bosses
Compare to https://www.youtube.com/watch?v=nACyjn817iQ&t=460s
-->



## 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**)
In theory it should not impact, didnt test with hi bot count or large
player count
    - - [ ] 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?
    - - [ ] No
    - - [X ] Yes (**explain below**)

Impacts in raid, new actions, triggers
Impacts with new bisicc cmd that will gear bots
Everything should make it easier for maintenance since each boss is in
seperate file now


## 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 was used for analyzing code for ac code standard violations, edits
were made by me. It was used for fixing bugs, brainstorming and code
generation (for complex math problems, such as dynamicaly kiting oozes
around, assiging positions during multiple complex situations in rotface
encouter. Everything was checked and tested multiple times until it was
polished (to my abilites and understanding). It helped me to solve
Malleable goo detection, defile, by hooking directly to boss in order to
detect it, since it was detectable only by split second since it was not
npc, spell or object.


<!--
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.
-->
I have not tested with multiple players, or large servers or with 3k+
bots

---------

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-22 19:23:35 -07:00
Keleborn
2973083dda
RBAC sync (#2355)
<!--
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 -->
Implement RBAC Permission system in checks.

Claude flagged the following
PlayerbotMgr.cpp:751	<= SEC_PLAYER 
SecurityCheckAction.cpp:27	== SEC_PLAYER 

In these two cases a moderator level account has access to these
commands. This was preserved in PR. The question is whether mods should
maintain the override.


## 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.
-->



<!--
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-22 19:23:18 -07:00
Alex Dcnh
d0ba99f381
Updates the Windows CI workflow to build AzerothCore and mod-playerbots reliably with Ninja instead of the Visual Studio/MSBuild generator (#2383)
Added a Windows CI fix to build with Ninja and avoid Windows
command-line length failures.

<!--
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
This PR updates the Windows CI workflow to build AzerothCore +
mod-playerbots reliably with Ninja instead of the Visual Studio/MSBuild
generator.

The previous Windows build could fail during the final `worldserver`
build steps because MSBuild/RC generated command lines that exceeded
Windows command-line limits. Enabling long-path support was not enough,
because the failure was caused by tool command length rather than only
filesystem path length.

This workflow now:
- installs and uses Ninja as the CMake generator;
- creates an AzerothCore `conf/config.sh` for CI;
- forces the compiler to MSVC `cl`;
- enables CMake/Ninja response files to reduce command-line length;
- moves the source tree to a short path (`C:\ac`) before configuring and
building.

This keeps the Windows build on MSVC while avoiding the MSBuild/RC
command-line length issue.

**Pros:**
- Fixes Windows CI command-line length failures;
- avoids unreliable `rc.exe` / `MSB6003` errors from the MSBuild
generator;
- keeps the build on MSVC;
- makes the Windows CI build more predictable;
- does not affect runtime code or bot behavior.

**Cons:**
- Visual Studio project files (`.vcxproj`) are no longer generated in
CI;
- the workflow now copies the source tree to a short path before
building.

## Feature Evaluation
- Minimum logic: CI-only workflow changes to use Ninja, force MSVC,
enable response files, and build from a short path.
- Processing cost: None at runtime. This only affects GitHub Actions
build tooling.

## How to Test the Changes
- Run the Windows CI workflow on GitHub Actions.
- Confirm that CMake reports `-- Building for: Ninja`.
- Confirm that CMake detects MSVC as the C and C++ compiler.
- Confirm that the Windows build completes successfully.
- Confirm that no MSBuild `MSB6003`, `rc.exe`, or Ninja `CreateProcess`
command-line length errors occur.

## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
    - [x] No, not at all

- Does this change modify default bot behavior?
    - [x] No

- Does this change add new decision branches or increase maintenance
complexity?
    - [x] No

## AI Assistance
Was AI assistance used while working on this change?
- [x] No
Maybe if should so i'd made less commits...

## 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
This change is CI-only and does not affect gameplay, bot logic, database
logic, or runtime performance.

The Windows workflow was failing because generated build commands became
too long for Windows process creation. Switching the CI build to Ninja,
forcing MSVC, enabling response files, and building from `C:\ac` fixes
the issue while keeping the compiler/toolchain consistent with the
Windows target.

---------

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-15 23:36:59 -07:00
dillyns
9c9c386af7
Add ELEMENTAL_SHARPENING_STONE to prioritized weight stone IDs (#2395)
<!--
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 -->
Adds elemental sharpening stone to the list of weightstones for weapon
buff application.
Elemental sharpening stones can be used on any melee weapon, not just
sharp ones.


## 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.
-->
Give a bot that has a blunt weapon equipped (staff or mace) an elemental
sharpening stone (ID: 18262)
They should now use the stone on a blunt weapon.


## 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-15 23:28:04 -07:00
Crow
9d787ca0b4
Make Vashj Strategy Compatible with Acore Bug & Use AI Instance for Targeting (#2391)
<!--
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 -->
Lady Vashj has been screwed up in Acore for a month or two. In Phase 2,
only one generator activates (instead of four). As a result, disabling
one generator brings Vashj into Phase 3. The strategy uses Vashj's HP +
unit state to determine phase--with the Acore bug, Vashj enters Phase 3
at <65% HP (instead of the correct <50% HP, as deactivating a generator
damages her by 5%) so under the current strategy, bots won't assume
Phase 3 strategies at the right moment. I tried looking into the issue
with Vashj in Acore and cannot figure it out, and I've had an issue open
with Acore and don't know if/when it will be fixed. In the meantime, I'm
modifying the strategy to use the shield barrier aura check on Vashj
instead so it will work regardless of Vashj's HP at the time the
generators are disabled. If/when Vashj is fixed, the strategy will still
work.

I also changed bot-side targeting checks throughout the strategies I've
written from GetVictim() and GetTarget() to using the AI's target state
via "current target." This approach I believe is the best way to
actually check for the target because a GetVictim() check can lock up
Elemental Shamans and Balance Druids due to them not being able to start
an attack to pass that check when outside of spell range, and a
GetTarget() check can occasionally not align with the target that the
bot is actually attacking.

- In effect, this should reduce cases where the bot AI thinks it is on
the right raid target but encounter logic disagrees because the live
victim or client selection is lagging or different.
- Exception: multipliers for blocking TankAssistAction still use a
GetVictim() check. This is because "current target" processes before
issuing an attack, and the targeting check for TankAssistAction is not
to suppress it if the bot hasn't actually issued an attack.

There is a lot more than could be refactored with past strategies to
make them better, but that's for another day. This PR is intended to be
limited and simple to review.

## 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.
Logic is described above. There should be no relevant impact on
processing cost.


## 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.
-->

Attempt Lady Vashj with the current bugged version. After disabling the
lone generator, bots should enter Phase 3 strategies and the encounter
should be completable.

Otherwise, generally run raids, and confirm there are no issues with
bots switching targets or starting attacking.


## 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-15 23:27:42 -07:00
kadeshar
0e0d9fbde2
Hand of Freedom fix for Stealth (#2388)
<!--
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 -->

Paladin no longer using Hand of Freedom on Rogue with Stealth
Related with: #2385 

## 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. Invite paladin and rogue to group
2. Start fight
3. Rogue should use Stealth on fight beginning, Paladin should cast Hand
of Freedom
4. Apply some snare effect to Rogue bot (for example .aura 1715)
5. Paladin bot should use hand of freedom

## 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-15 23:27:30 -07:00
Crow
d4f86764bb
Fix raid group condition in SayToRaid (#2386)
<!--
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 -->
PlayerbotAI::SayToRaid is not used currently but I may use it for
Sunwell for bot speech during Kalecgos due to tank handoff mechanics.
The function currently erroneously returns false for a bot in a raid.
There's no impact on bot behavior or performance from this PR.


## 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-15 23:27:15 -07:00
Crow
05e8f4d82c
Implement Black Temple Strategies (#2381)
<!--
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 implements strategies for all bosses in the Black Temple. As
always, I’ve written these with the intent that all bosses be
completable with appropriate gear and 50/50 IP nerfs and boss HP
returned to TBC levels. Illidan is difficult at those parameters, but he
is certainly doable. You just won’t be able to roll a bunch of meme
specs or shitty compositions. Probably.


## 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.

My goal with raid strategies is the same—I’m not just trying to make
encounters doable but am trying to allow people to experience them in a
way that feels like they are part of a coordinated raid of real players.
To me, being able to achieve that is the minimum, and that requires
getting bots to respond to all major mechanics, even if they can
technically just be powered through.


## 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.
-->
Run the Black Temple raid and see if my descriptions of strategies in
the next post check 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?
    - - [ ] No, not at all
    - - [x] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)
 
The performance impact exists only when the “blacktemple” strategy is
on. I’ve tested with pmon and done my best to properly gate checks to
limit the performance impact even during the instance and boss
encounters.


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

The triggers and multipliers will be evaluated as long as the
"blacktemple" strategy is active, and it will be applied automatically
in the instance (and removed automatically when changing maps outside
the instance).


- 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.
-->
GPT-5.4, mainly for calculations and things that are more intermediate
concepts that I’m not proficient with like lambdas.


<!--
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-15 23:26:55 -07:00
kadeshar
61dbae19dc
Fix for lfg command (#2379)
<!--
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 -->

Fix for `lfg` to include healers in invitations and requester role.



## 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 command `/1 lfg 40` on server which contains correct amount of bots
possible to be invited. Such group should be create.

## 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.
-->

To identify reason.

<!--
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-15 23:26:34 -07:00
kadeshar
05aebfea46
Outfit database persist (#2378)
<!--
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 -->

Added storing outfit in database.
Related with: #2239 

## 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. Invite altbot
2. Use command `outfit testcollection +[itemlink]` to add outfit
collection and item
3. Check that is added `outfit ?`
4. Logout bot for example via multibot
5. Login bot
6. Check that is still added `outfit ?`



## 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.
-->

To identify places to change.

<!--
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.
-->

Result after bot relogin:
<img width="701" height="206" alt="obraz"
src="https://github.com/user-attachments/assets/ea6c875a-65dd-4a01-9d35-381c8b374abd"
/>
2026-05-15 23:26:22 -07:00
kadeshar
f1a2736a31
Polymorph spam fix (#2373)
<!--
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 -->

Fix for polymorph spam in combat

## 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. Invite mage bot which have polymorph (check `spells polymorph`)
2. Enter combat and mage should not spam polymorph
3. Enter another combat best with multiple mobs and one of this mobs
mark moon
4. Mage should cast polymorph on marked mob

## 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-15 23:26:03 -07:00
NoxMax
dbea1203b4
Refactor: Clean up triggers and reorganize supported commands (#2327)
<!--
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 -->
Removed duplicate and dead trigger-action mappings in
ChatCommandHandlerStrategy. Noticed while reviewing the recent edits to
test-staging. Also when trigger name == action name, that belongs to
supported in supported, so `wipe` and `roll` were moved there.
Beyond that, `InitTriggers` commands were changed to be in a single
line. Really it's easier to read that way, except multi-action lines,
where I gave each action a line of its own.


## 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.
Logic has been further minimized to eliminate redundancies.


## 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.
-->
Test the affected commands. They should work same as before.
Removed duplicates from `InitTriggers` that are already in `supported`:
1. open items
2. unlock items
3. unlock traded item
4. tame
5. glyphs
6. glyph equip
7. pet
8. pet attack
9. emblems

Dead entries that were in `supported` but already worked through
`InitTriggers`:
1. qi
2. focus heal

Triggers that were moved from `InitTriggers` to `supported`:
1. wipe
2. roll



## 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-15 23:19:58 -07:00
Fiery
eb3c101959
update to level 80 pve specs (#2366)
<!--
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 -->

Some of the specs needed improvement.


## 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.
- - [ ] 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-15 23:19:18 -07:00
571 changed files with 42964 additions and 15350 deletions

View File

@ -1,4 +1,5 @@
name: windows-build
on:
push:
branches: [ "master", "test-staging" ]
@ -6,7 +7,7 @@ on:
branches: [ "master", "test-staging" ]
concurrency:
group: "windows-build-${{ github.event.pull_request.number }}"
group: "windows-build-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs:
@ -15,35 +16,108 @@ jobs:
fail-fast: false
matrix:
os: [windows-latest]
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }}
env:
BOOST_ROOT: C:\local\boost_1_82_0
BOOST_ROOT: C:\local\boost_1_87_0
CMAKE_GENERATOR: Ninja
CTOOLS_BUILD: all
steps:
- name: Checkout AzerothCore
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: 'mod-playerbots/azerothcore-wotlk'
ref: ${{ (github.base_ref || github.ref_name) == 'test-staging' && 'test-staging' || 'Playerbot' }}
path: 'ac'
path: a
- name: Checkout Playerbot Module
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: 'mod-playerbots/mod-playerbots'
#path: 'modules/mod-playerbots'
path: ac/modules/mod-playerbots
path: a/modules/mod-playerbots
- name: Move source tree to short path
shell: powershell
run: |
if (Test-Path C:\ac) {
Remove-Item C:\ac -Recurse -Force
}
New-Item -ItemType Directory -Path C:\ac | Out-Null
robocopy "${{ github.workspace }}\a" "C:\ac" /MIR
if ($LASTEXITCODE -le 7) {
exit 0
}
exit $LASTEXITCODE
- name: Install Ninja
shell: powershell
run: |
choco install ninja -y
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.13
- name: Configure OS
shell: bash
working-directory: ac
working-directory: C:\ac
env:
CONTINUOUS_INTEGRATION: true
run: |
./acore.sh install-deps
- name: Create AzerothCore CI config
shell: bash
working-directory: C:\ac
run: |
cat > conf/config.sh <<'EOF'
CCOMPILERC="cl"
CCOMPILERCXX="cl"
CTYPE="Release"
CSCRIPTS="static"
CMODULES="static"
CTOOLS_BUILD="all"
CCUSTOMOPTIONS="-DCMAKE_RC_COMPILER=rc -DCMAKE_NINJA_FORCE_RESPONSE_FILE=ON -DCMAKE_NINJA_CMCLDEPS_RC=OFF -DCMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS=ON -DCMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS=ON -DCMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES=ON -DCMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES=ON -DCMAKE_C_USE_RESPONSE_FILE_FOR_LIBRARIES=ON -DCMAKE_CXX_USE_RESPONSE_FILE_FOR_LIBRARIES=ON"
EOF
cat conf/config.sh
- name: Setup MSVC
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
with:
arch: x64
- name: Build
shell: bash
working-directory: ac
working-directory: C:\ac
run: |
export CTOOLS_BUILD=all
export CMAKE_GENERATOR=Ninja
export CC=cl
export CXX=cl
export RC=rc
cmake --version
ninja --version
echo "CMAKE_GENERATOR=$CMAKE_GENERATOR"
echo "CC=$CC"
echo "CXX=$CXX"
which cl || true
which rc || true
cl || true
rc || true
rm -rf var/build/obj
./acore.sh compiler build

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
-- #########################################################
-- Playerbots - Add /p autogear bis command texts
-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN,
-- zhTW, esES, esMX, ruRU)
-- #########################################################
DELETE FROM ai_playerbot_texts WHERE name IN (
'bis_autogear_unavailable_error',
'bis_no_rows_fallback',
'bis_command_unavailable_error',
'bis_altbot_refused_error',
'bis_quality_floor_error',
'bis_pvp_refused_error',
'bis_invalid_arg_error',
'bis_arg_above_limit_error',
'bis_no_rows_autogear_msg',
'bis_closest_match_msg',
'bis_applying_msg',
'bis_applied_msg'
);
DELETE FROM ai_playerbot_texts_chance WHERE name IN (
'bis_autogear_unavailable_error',
'bis_no_rows_fallback',
'bis_command_unavailable_error',
'bis_altbot_refused_error',
'bis_quality_floor_error',
'bis_pvp_refused_error',
'bis_invalid_arg_error',
'bis_arg_above_limit_error',
'bis_no_rows_autogear_msg',
'bis_closest_match_msg',
'bis_applying_msg',
'bis_applied_msg'
);
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
(1767, 'bis_autogear_unavailable_error',
'autogear command is not allowed, please check the configuration.', 0, 0,
'자동 장비 명령이 허용되지 않습니다. 설정을 확인하세요.',
'La commande autogear n''est pas autorisée, veuillez vérifier la configuration.',
'Der autogear-Befehl ist nicht erlaubt, bitte überprüfe die Konfiguration.',
'自动装备命令未启用,请检查配置。',
'自動裝備指令未啟用,請檢查設定。',
'El comando autogear no está permitido, por favor revisa la configuración.',
'El comando autogear no está permitido, por favor revisa la configuración.',
'Команда autogear не разрешена, проверьте конфигурацию.'),
(1768, 'bis_no_rows_fallback',
'No BiS for your tier/spec/level, check cfg, running autogear instead', 0, 0,
'해당 등급/전문화/레벨에 BiS 목록이 없습니다. 설정을 확인하세요. 대신 자동 장비를 실행합니다.',
'Pas de BiS pour votre tier/spé/niveau, vérifiez la config, exécution d''autogear à la place.',
'Kein BiS für deinen Tier/Spec/Level, prüfe die Config, führe stattdessen autogear aus.',
'您的等级/天赋/级别没有BiS数据请检查配置改为运行autogear。',
'你的等級/天賦/級別沒有BiS資料請檢查設定改為執行autogear。',
'No hay BiS para tu tier/spec/nivel, revisa la configuración, ejecutando autogear en su lugar.',
'No hay BiS para tu tier/spec/nivel, revisa la configuración, ejecutando autogear en su lugar.',
'Нет BiS для вашего тира/спека/уровня, проверьте конфиг, запускаю autogear.'),
(1769, 'bis_command_unavailable_error',
'bis command is not allowed, please check the configuration.', 0, 0,
'bis 명령이 허용되지 않습니다. 설정을 확인하세요.',
'La commande bis n''est pas autorisée, veuillez vérifier la configuration.',
'Der bis-Befehl ist nicht erlaubt, bitte überprüfe die Konfiguration.',
'bis命令未启用请检查配置。',
'bis指令未啟用請檢查設定。',
'El comando bis no está permitido, por favor revisa la configuración.',
'El comando bis no está permitido, por favor revisa la configuración.',
'Команда bis не разрешена, проверьте конфигурацию.'),
(1770, 'bis_altbot_refused_error',
'You cannot use bis on alt bots.', 0, 0,
'부캐 봇에는 bis를 사용할 수 없습니다.',
'Vous ne pouvez pas utiliser bis sur des bots alternatifs.',
'Du kannst bis nicht auf Zweitbots verwenden.',
'你不能在副号机器人上使用bis。',
'你不能在副號機器人上使用bis。',
'No puedes usar bis en bots alternativos.',
'No puedes usar bis en bots alternativos.',
'Вы не можете использовать bis на дополнительных ботах.'),
(1771, 'bis_quality_floor_error',
'AutoGearQualityLimit must be 4 for BiS.', 0, 0,
'BiS를 사용하려면 AutoGearQualityLimit이 4여야 합니다.',
'AutoGearQualityLimit doit être à 4 pour utiliser BiS.',
'AutoGearQualityLimit muss für BiS auf 4 stehen.',
'BiS要求AutoGearQualityLimit设置为4。',
'BiS要求AutoGearQualityLimit設定為4。',
'AutoGearQualityLimit debe ser 4 para BiS.',
'AutoGearQualityLimit debe ser 4 para BiS.',
'AutoGearQualityLimit должен быть 4 для BiS.'),
(1772, 'bis_pvp_refused_error',
'bis is PvE only, bot is configured as PvP.', 0, 0,
'bis는 PvE 전용이며, 이 봇은 PvP로 설정되어 있습니다.',
'bis est uniquement pour le JcE, ce bot est configuré en JcJ.',
'bis ist nur für PvE, dieser Bot ist als PvP konfiguriert.',
'bis仅适用于PvE此机器人配置为PvP。',
'bis僅適用於PvE此機器人設定為PvP。',
'bis es solo para PvE, el bot está configurado como PvP.',
'bis es solo para PvE, el bot está configurado como PvP.',
'bis только для PvE, бот настроен на PvP.'),
(1773, 'bis_invalid_arg_error',
'Invalid BiS ilvl argument ''%param''. Use a positive integer.', 0, 0,
'잘못된 BiS 아이템 레벨 인수 ''%param''. 양의 정수를 사용하세요.',
'Argument iLvl BiS invalide ''%param''. Utilisez un entier positif.',
'Ungültiges BiS-iLvl-Argument ''%param''. Verwende eine positive ganze Zahl.',
'无效的BiS物品等级参数“%param”。请使用正整数。',
'無效的BiS物品等級參數「%param」。請使用正整數。',
'Argumento iLvl BiS inválido ''%param''. Usa un entero positivo.',
'Argumento iLvl BiS inválido ''%param''. Usa un entero positivo.',
'Неверный аргумент iLvl BiS ''%param''. Используйте положительное целое число.'),
(1774, 'bis_arg_above_limit_error',
'BiS ilvl %requested exceeds AutoGearScoreLimit %limit, refusing', 0, 0,
'BiS 아이템 레벨 %requested이(가) AutoGearScoreLimit %limit을(를) 초과합니다. 거부합니다.',
'iLvl BiS %requested dépasse AutoGearScoreLimit %limit, refusé.',
'BiS-iLvl %requested überschreitet AutoGearScoreLimit %limit, abgelehnt.',
'BiS物品等级%requested超过AutoGearScoreLimit %limit已拒绝。',
'BiS物品等級%requested超過AutoGearScoreLimit %limit已拒絕。',
'iLvl BiS %requested supera AutoGearScoreLimit %limit, rechazado.',
'iLvl BiS %requested supera AutoGearScoreLimit %limit, rechazado.',
'BiS iLvl %requested превышает AutoGearScoreLimit %limit, отказано.'),
(1775, 'bis_no_rows_autogear_msg',
'No BiS at ilvl %ilvl, using Autogear %ilvl instead', 0, 0,
'아이템 레벨 %ilvl의 BiS가 없어 대신 Autogear %ilvl을(를) 사용합니다.',
'Pas de BiS à l''iLvl %ilvl, utilisation d''Autogear %ilvl à la place.',
'Kein BiS auf iLvl %ilvl, verwende stattdessen Autogear %ilvl.',
'物品等级%ilvl没有BiS改用Autogear %ilvl。',
'物品等級%ilvl沒有BiS改用Autogear %ilvl。',
'No hay BiS en iLvl %ilvl, usando Autogear %ilvl en su lugar.',
'No hay BiS en iLvl %ilvl, usando Autogear %ilvl en su lugar.',
'Нет BiS на iLvl %ilvl, использую Autogear %ilvl.'),
(1776, 'bis_closest_match_msg',
'No BiS at ilvl %requested, using closest match at ilvl %resolved', 0, 0,
'아이템 레벨 %requested의 BiS가 없어 가장 가까운 아이템 레벨 %resolved을(를) 사용합니다.',
'Pas de BiS à l''iLvl %requested, utilisation de la correspondance la plus proche à l''iLvl %resolved.',
'Kein BiS auf iLvl %requested, verwende nächstliegende Übereinstimmung auf iLvl %resolved.',
'物品等级%requested没有BiS使用最接近的物品等级%resolved。',
'物品等級%requested沒有BiS使用最接近的物品等級%resolved。',
'No hay BiS en iLvl %requested, usando coincidencia más cercana en iLvl %resolved.',
'No hay BiS en iLvl %requested, usando coincidencia más cercana en iLvl %resolved.',
'Нет BiS на iLvl %requested, использую ближайшее совпадение на iLvl %resolved.'),
(1777, 'bis_applying_msg', 'Applying BiS gear', 0, 0,
'BiS 장비를 적용합니다.',
'Application de l''équipement BiS.',
'Wende BiS-Ausrüstung an.',
'正在装备BiS装备。',
'正在裝備BiS裝備。',
'Aplicando equipo BiS.',
'Aplicando equipo BiS.',
'Применяю BiS-снаряжение.'),
(1778, 'bis_applied_msg', 'BiS applied', 0, 0,
'BiS 장비가 적용되었습니다.',
'Équipement BiS appliqué.',
'BiS-Ausrüstung angewendet.',
'BiS装备已应用。',
'BiS裝備已套用。',
'Equipo BiS aplicado.',
'Equipo BiS aplicado.',
'BiS-снаряжение применено.');
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES
('bis_autogear_unavailable_error', 100),
('bis_no_rows_fallback', 100),
('bis_command_unavailable_error', 100),
('bis_altbot_refused_error', 100),
('bis_quality_floor_error', 100),
('bis_pvp_refused_error', 100),
('bis_invalid_arg_error', 100),
('bis_arg_above_limit_error', 100),
('bis_no_rows_autogear_msg', 100),
('bis_closest_match_msg', 100),
('bis_applying_msg', 100),
('bis_applied_msg', 100);

View File

@ -0,0 +1,489 @@
DELETE FROM ai_playerbot_texts WHERE name IN (
'quest_accept_debug',
'quest_already_have_error',
'quest_cant_take_error',
'arena_team_already_in_team',
'arena_team_thanks_for_invite',
'area_trigger_follow_too_far_error',
'area_trigger_wait_for_me',
'attack_no_target_error',
'attack_target_not_in_world_error',
'attack_in_flight_error',
'attack_pvp_prohibited_error',
'attack_target_friendly_error',
'attack_target_dead_error',
'attack_target_not_in_sight_error',
'attack_already_attacking_error',
'attack_invalid_target_error',
'bank_no_banker_nearby_error',
'move_from_group',
'running_away',
'clean_quest_log_started',
'quest_trivial_will_remove',
'quest_has_been_removed',
'quest_not_trivial_kept',
'quest_removed_debug',
'quest_removed_with_name',
'guild_accept_inviter_not_in_guild',
'guild_accept_already_in_guild',
'guild_accept_declined',
'outfit_usage_add',
'outfit_usage_remove',
'outfit_usage_equip',
'outfit_set_as',
'outfit_equipping',
'outfit_replace_current',
'outfit_resetting',
'outfit_updating_current',
'outfit_item_removed_from',
'outfit_item_added_to',
'release_spirit_not_dead_wait',
'release_spirit_already_spirit',
'release_spirit_releasing',
'release_spirit_meet_graveyard',
'send_mail_no_mailbox_nearby',
'send_mail_one_item_only',
'send_mail_cannot_send_money',
'send_mail_not_enough_money',
'send_mail_sending_to',
'send_mail_cannot_send_item',
'send_mail_item_not_for_sale',
'send_mail_sent_to',
'craft_reset',
'craft_usage',
'craft_cannot_craft',
'craft_summary',
'set_home_success',
'set_home_no_innkeeper_error',
'quest_shared',
'tame_invalid_id_error',
'tame_usage_error',
'tame_pet_changed',
'tame_pet_changed_initialized',
'tame_exotic_requires_beast_mastery',
'tame_no_pet_by_name',
'tame_no_pet_by_id',
'tame_no_pet_by_family',
'tame_no_pet_to_rename',
'tame_pet_name_length_error',
'tame_pet_name_alpha_error',
'tame_pet_name_forbidden_error',
'tame_pet_renamed',
'tame_pet_rename_refresh_hint',
'tame_only_hunters_level_10',
'tame_creature_template_not_found',
'tame_create_pet_failed',
'tame_pet_abandoned',
'tame_no_hunter_pet_to_abandon',
'taxi_ready_next_flight',
'taxi_cant_fly_with_you',
'taxi_no_flightmaster_nearby',
'trade_busy_now',
'trade_disabled',
'trade_thank_you_player',
'trade_selling_disabled',
'trade_buying_disabled',
'trade_item_not_for_sale',
'trade_item_not_needed',
'trade_no_items_error',
'trade_discount_buy_only',
'trade_success_pleasure',
'trade_success_fair_trade',
'trade_success_thanks',
'trade_success_off_with_you',
'trade_want_money_for_this',
'use_item_none_available',
'use_gameobject',
'socket_does_not_fit',
'use_item_on_target',
'use_item',
'socketing_item_with_gem',
'meeting_stone_in_combat',
'meeting_stone_welcome',
'meeting_stone_none_nearby',
'meeting_stone_none_near_you',
'meeting_stone_no_hearthstone_self',
'meeting_stone_no_hearthstone_you',
'meeting_stone_hearthstone_not_ready_self',
'meeting_stone_hearthstone_not_ready_you',
'meeting_stone_no_innkeepers_nearby',
'meeting_stone_no_innkeepers_near_you',
'meeting_stone_cannot_summon_vehicle',
'meeting_stone_cannot_summon_master_in_combat',
'meeting_stone_cannot_summon_master_dead',
'meeting_stone_cannot_summon_bot_dead',
'meeting_stone_revived',
'meeting_stone_not_enough_space',
'new_rpg_quest_accepted',
'new_rpg_quest_rewarded',
'new_rpg_quest_dropped',
'rpg_item_better_for_player',
'rpg_start_trade_with_player'
);
DELETE FROM ai_playerbot_texts_chance WHERE name IN (
'quest_accept_debug',
'quest_already_have_error',
'quest_cant_take_error',
'arena_team_already_in_team',
'arena_team_thanks_for_invite',
'area_trigger_follow_too_far_error',
'area_trigger_wait_for_me',
'attack_no_target_error',
'attack_target_not_in_world_error',
'attack_in_flight_error',
'attack_pvp_prohibited_error',
'attack_target_friendly_error',
'attack_target_dead_error',
'attack_target_not_in_sight_error',
'attack_already_attacking_error',
'attack_invalid_target_error',
'bank_no_banker_nearby_error',
'move_from_group',
'running_away',
'clean_quest_log_started',
'quest_trivial_will_remove',
'quest_has_been_removed',
'quest_not_trivial_kept',
'quest_removed_debug',
'quest_removed_with_name',
'guild_accept_inviter_not_in_guild',
'guild_accept_already_in_guild',
'guild_accept_declined',
'outfit_usage_add',
'outfit_usage_remove',
'outfit_usage_equip',
'outfit_set_as',
'outfit_equipping',
'outfit_replace_current',
'outfit_resetting',
'outfit_updating_current',
'outfit_item_removed_from',
'outfit_item_added_to',
'release_spirit_not_dead_wait',
'release_spirit_already_spirit',
'release_spirit_releasing',
'release_spirit_meet_graveyard',
'send_mail_no_mailbox_nearby',
'send_mail_one_item_only',
'send_mail_cannot_send_money',
'send_mail_not_enough_money',
'send_mail_sending_to',
'send_mail_cannot_send_item',
'send_mail_item_not_for_sale',
'send_mail_sent_to',
'craft_reset',
'craft_usage',
'craft_cannot_craft',
'craft_summary',
'set_home_success',
'set_home_no_innkeeper_error',
'quest_shared',
'tame_invalid_id_error',
'tame_usage_error',
'tame_pet_changed',
'tame_pet_changed_initialized',
'tame_exotic_requires_beast_mastery',
'tame_no_pet_by_name',
'tame_no_pet_by_id',
'tame_no_pet_by_family',
'tame_no_pet_to_rename',
'tame_pet_name_length_error',
'tame_pet_name_alpha_error',
'tame_pet_name_forbidden_error',
'tame_pet_renamed',
'tame_pet_rename_refresh_hint',
'tame_only_hunters_level_10',
'tame_creature_template_not_found',
'tame_create_pet_failed',
'tame_pet_abandoned',
'tame_no_hunter_pet_to_abandon',
'taxi_ready_next_flight',
'taxi_cant_fly_with_you',
'taxi_no_flightmaster_nearby',
'trade_busy_now',
'trade_disabled',
'trade_thank_you_player',
'trade_selling_disabled',
'trade_buying_disabled',
'trade_item_not_for_sale',
'trade_item_not_needed',
'trade_no_items_error',
'trade_discount_buy_only',
'trade_success_pleasure',
'trade_success_fair_trade',
'trade_success_thanks',
'trade_success_off_with_you',
'trade_want_money_for_this',
'use_item_none_available',
'use_gameobject',
'socket_does_not_fit',
'use_item_on_target',
'use_item',
'socketing_item_with_gem',
'meeting_stone_in_combat',
'meeting_stone_welcome',
'meeting_stone_none_nearby',
'meeting_stone_none_near_you',
'meeting_stone_no_hearthstone_self',
'meeting_stone_no_hearthstone_you',
'meeting_stone_hearthstone_not_ready_self',
'meeting_stone_hearthstone_not_ready_you',
'meeting_stone_no_innkeepers_nearby',
'meeting_stone_no_innkeepers_near_you',
'meeting_stone_cannot_summon_vehicle',
'meeting_stone_cannot_summon_master_in_combat',
'meeting_stone_cannot_summon_master_dead',
'meeting_stone_cannot_summon_bot_dead',
'meeting_stone_revived',
'meeting_stone_not_enough_space',
'new_rpg_quest_accepted',
'new_rpg_quest_rewarded',
'new_rpg_quest_dropped',
'rpg_item_better_for_player',
'rpg_start_trade_with_player'
);
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
(1779, 'quest_accept_debug', 'Quest [%quest] accepted', 0, 0, '퀘스트 [%quest] 수락', 'Quête [%quest] acceptée', 'Quest [%quest] angenommen', '任务 [%quest] 已接受', '任務 [%quest] 已接受', 'Misión [%quest] aceptada', 'Misión [%quest] aceptada', 'Задание [%quest] принято'),
(1780, 'quest_already_have_error', 'I have this quest', 0, 0, '이미 이 퀘스트를 가지고 있습니다', 'J''ai déjà cette quête', 'Ich habe diese Quest bereits', '我已经有这个任务了', '我已經有這個任務了', 'Ya tengo esta misión', 'Ya tengo esta misión', 'У меня уже есть это задание'),
(1781, 'quest_cant_take_error', 'I can''t take this quest', 0, 0, '이 퀘스트를 받을 수 없습니다', 'Je ne peux pas prendre cette quête', 'Ich kann diese Quest nicht annehmen', '我无法接受这个任务', '我無法接受這個任務', 'No puedo aceptar esta misión', 'No puedo aceptar esta misión', 'Я не могу взять это задание'),
(1782, 'arena_team_already_in_team', 'Sorry, I am already in such team', 0, 0, '죄송하지만 이미 그런 팀에 속해 있습니다', 'Désolé, je suis déjà dans une telle équipe', 'Entschuldigung, ich bin bereits in so einem Team', '抱歉,我已经在这样的队伍里了', '抱歉,我已經在這樣的隊伍裡了', 'Lo siento, ya estoy en un equipo así', 'Lo siento, ya estoy en un equipo así', 'Извините, я уже состою в такой команде'),
(1783, 'arena_team_thanks_for_invite', 'Thanks for the invite!', 0, 0, '초대해 주셔서 감사합니다!', 'Merci pour l''invitation !', 'Danke für die Einladung!', '谢谢你的邀请!', '謝謝你的邀請!', 'Gracias por la invitación!', 'Gracias por la invitación!', 'Спасибо за приглашение!'),
(1784, 'area_trigger_follow_too_far_error', 'I won''t follow: too far away', 0, 0, '너무 멀어서 따라가지 않겠습니다', 'Je ne suivrai pas : c''est trop loin', 'Ich werde nicht folgen: zu weit entfernt', '我不会跟随:距离太远了', '我不會跟隨:距離太遠了', 'No te seguiré: estás demasiado lejos', 'No te seguiré: estás demasiado lejos', 'Я не пойду следом: слишком далеко'),
(1785, 'area_trigger_wait_for_me', 'Wait for me', 0, 0, '잠깐만 기다려 주세요', 'Attendez-moi', 'Warte auf mich', '等等我', '等等我', 'Espérame', 'Espérame', 'Подожди меня'),
(1786, 'attack_no_target_error', 'I have no target', 0, 0, '대상이 없습니다', 'Je n''ai pas de cible', 'Ich habe kein Ziel', '我没有目标', '我沒有目標', 'No tengo objetivo', 'No tengo objetivo', 'У меня нет цели'),
(1787, 'attack_target_not_in_world_error', '%target is no longer in the world.', 0, 0, '%target은(는) 더 이상 월드에 없습니다.', '%target n''est plus dans le monde.', '%target ist nicht mehr in der Welt.', '%target 已不在世界中。', '%target 已不在世界中。', '%target ya no está en el mundo.', '%target ya no está en el mundo.', '%target больше не находится в мире.'),
(1788, 'attack_in_flight_error', 'I cannot attack in flight', 0, 0, '비행 중에는 공격할 수 없습니다', 'Je ne peux pas attaquer en vol', 'Ich kann im Flug nicht angreifen', '飞行中无法攻击', '飛行中無法攻擊', 'No puedo atacar en vuelo', 'No puedo atacar en vuelo', 'Я не могу атаковать в полёте'),
(1789, 'attack_pvp_prohibited_error', 'I cannot attack other players in PvP prohibited areas.', 0, 0, 'PvP 금지 지역에서는 다른 플레이어를 공격할 수 없습니다.', 'Je ne peux pas attaquer d''autres joueurs dans les zones où le PvP est interdit.', 'Ich kann andere Spieler in PvP-verbotenen Gebieten nicht angreifen.', '我不能在禁止 PvP 的区域攻击其他玩家。', '我不能在禁止 PvP 的區域攻擊其他玩家。', 'No puedo atacar a otros jugadores en zonas donde el JcJ está prohibido.', 'No puedo atacar a otros jugadores en zonas donde el JcJ está prohibido.', 'Я не могу атаковать других игроков в зонах, где PvP запрещено.'),
(1790, 'attack_target_friendly_error', '%target is friendly to me.', 0, 0, '%target은(는) 우호적인 대상입니다.', '%target m''est amical.', '%target ist mir gegenüber freundlich.', '%target 对我是友方目标。', '%target 對我是友方目標。', '%target es amistoso conmigo.', '%target es amistoso conmigo.', '%target дружелюбен ко мне.'),
(1791, 'attack_target_dead_error', '%target is dead.', 0, 0, '%target은(는) 죽었습니다.', '%target est mort.', '%target ist tot.', '%target 已经死了。', '%target 已經死了。', '%target está muerto.', '%target está muerto.', '%target мёртв.'),
(1792, 'attack_target_not_in_sight_error', '%target is not in my sight.', 0, 0, '%target이(가) 시야에 없습니다.', '%target n''est pas dans mon champ de vision.', '%target ist nicht in Sichtweite.', '%target 不在我的视野中。', '%target 不在我的視野中。', '%target no está a mi vista.', '%target no está a mi vista.', '%target вне поля моего зрения.'),
(1793, 'attack_already_attacking_error', 'I am already attacking %target.', 0, 0, '이미 %target을(를) 공격하고 있습니다.', 'J''attaque déjà %target.', 'Ich greife %target bereits an.', '我已经在攻击 %target。', '我已經在攻擊 %target。', 'Ya estoy atacando a %target.', 'Ya estoy atacando a %target.', 'Я уже атакую %target.'),
(1794, 'attack_invalid_target_error', 'I cannot attack an invalid target.', 0, 0, '유효하지 않은 대상은 공격할 수 없습니다.', 'Je ne peux pas attaquer une cible invalide.', 'Ich kann kein ungültiges Ziel angreifen.', '我不能攻击无效目标。', '我不能攻擊無效目標。', 'No puedo atacar un objetivo no válido.', 'No puedo atacar un objetivo no válido.', 'Я не могу атаковать недопустимую цель.'),
(1795, 'bank_no_banker_nearby_error', 'Cannot find banker nearby', 0, 0, '근처에 은행원이 없습니다', 'Impossible de trouver un banquier à proximité', 'Kein Bankier in der Nähe gefunden', '附近找不到银行职员', '附近找不到銀行職員', 'No encuentro un banquero cerca', 'No encuentro un banquero cerca', 'Поблизости нет банкира'),
(1796, 'move_from_group', 'Moving away from group', 0, 0, '파티에서 멀어지는 중입니다', 'Je m''éloigne du groupe', 'Ich entferne mich von der Gruppe', '正在远离队伍', '正在遠離隊伍', 'Me estoy alejando del grupo', 'Me estoy alejando del grupo', 'Отхожу от группы'),
(1797, 'running_away', 'Running away', 0, 0, '도망치는 중입니다', 'Je m''enfuis', 'Ich laufe weg', '正在逃跑', '正在逃跑', 'Estoy huyendo', 'Estoy huyendo', 'Убегаю'),
(1798, 'clean_quest_log_started', 'Clean Quest Log command received, removing grey/trivial quests...', 0, 0, '퀘스트 로그 정리 명령을 받았습니다. 회색/사소한 퀘스트를 제거하는 중입니다...', 'Commande de nettoyage du journal des quêtes reçue, suppression des quêtes grises/triviales...', 'Befehl zum Bereinigen des Questlogs erhalten, graue/triviale Quests werden entfernt...', '已收到清理任务日志命令,正在移除灰色/琐碎任务...', '已收到清理任務日誌命令,正在移除灰色/瑣碎任務...', 'Comando para limpiar el registro de misiones recibido, eliminando misiones grises/triviales...', 'Comando para limpiar el registro de misiones recibido, eliminando misiones grises/triviales...', 'Получена команда очистки журнала заданий, удаляю серые/простые задания...'),
(1799, 'quest_trivial_will_remove', 'Quest [%title] will be removed because it is trivial (grey).', 0, 0, '퀘스트 [%title]은(는) 사소한(회색) 퀘스트이므로 제거됩니다.', 'La quête [%title] sera supprimée car elle est triviale (grise).', 'Quest [%title] wird entfernt, weil sie trivial (grau) ist.', '任务 [%title] 将被移除,因为它是琐碎的(灰色)任务。', '任務 [%title] 將被移除,因為它是瑣碎的(灰色)任務。', 'La misión [%title] se eliminará porque es trivial (gris).', 'La misión [%title] se eliminará porque es trivial (gris).', 'Задание [%title] будет удалено, так как оно простое (серое).'),
(1800, 'quest_has_been_removed', 'Quest [%title] has been removed.', 0, 0, '퀘스트 [%title]이(가) 제거되었습니다.', 'La quête [%title] a été supprimée.', 'Quest [%title] wurde entfernt.', '任务 [%title] 已被移除。', '任務 [%title] 已被移除。', 'La misión [%title] ha sido eliminada.', 'La misión [%title] ha sido eliminada.', 'Задание [%title] было удалено.'),
(1801, 'quest_not_trivial_kept', 'Quest [%title] is not trivial and will be kept.', 0, 0, '퀘스트 [%title]은(는) 사소하지 않으므로 유지됩니다.', 'La quête [%title] n''est pas triviale et sera conservée.', 'Quest [%title] ist nicht trivial und wird behalten.', '任务 [%title] 并不琐碎,将被保留。', '任務 [%title] 並不瑣碎,將被保留。', 'La misión [%title] no es trivial y se conservará.', 'La misión [%title] no es trivial y se conservará.', 'Задание [%title] не является простым и будет сохранено.'),
(1802, 'quest_removed_debug', 'Quest [%quest] removed', 0, 0, '퀘스트 [%quest] 제거됨', 'Quête [%quest] supprimée', 'Quest [%quest] entfernt', '任务 [%quest] 已移除', '任務 [%quest] 已移除', 'Misión [%quest] eliminada', 'Misión [%quest] eliminada', 'Задание [%quest] удалено'),
(1803, 'quest_removed_with_name', 'Quest removed %quest', 0, 0, '퀘스트 제거됨 %quest', 'Quête supprimée %quest', 'Quest entfernt %quest', '任务已移除 %quest', '任務已移除 %quest', 'Misión eliminada %quest', 'Misión eliminada %quest', 'Задание удалено %quest'),
(1804, 'guild_accept_inviter_not_in_guild', 'You are not in a guild!', 0, 0, '당신은 길드에 속해 있지 않습니다!', 'Vous n''êtes pas dans une guilde !', 'Du bist in keiner Gilde!', '你不在公会中!', '你不在公會中!', 'No estás en un gremio!', 'No estás en un gremio!', 'Вы не состоите в гильдии!'),
(1805, 'guild_accept_already_in_guild', 'Sorry, I am in a guild already', 0, 0, '죄송하지만 이미 길드에 속해 있습니다', 'Désolé, je suis déjà dans une guilde', 'Entschuldigung, ich bin bereits in einer Gilde', '抱歉,我已经在公会里了', '抱歉,我已經在公會裡了', 'Lo siento, ya estoy en un gremio', 'Lo siento, ya estoy en un gremio', 'Извините, я уже в гильдии'),
(1806, 'guild_accept_declined', 'Sorry, I don''t want to join your guild :(', 0, 0, '죄송하지만 당신의 길드에 가입하고 싶지 않습니다 :(', 'Désolé, je ne veux pas rejoindre votre guilde :(', 'Entschuldigung, ich möchte deiner Gilde nicht beitreten :(', '抱歉,我不想加入你的公会 :(', '抱歉,我不想加入你的公會 :(', 'Lo siento, no quiero unirme a tu gremio :(', 'Lo siento, no quiero unirme a tu gremio :(', 'Извините, я не хочу вступать в вашу гильдию :('),
(1807, 'outfit_usage_add', 'outfit <name> +[item] to add items', 0, 0, '아이템을 추가하려면 outfit <name> +[item]', 'outfit <name> +[item] pour ajouter des objets', 'outfit <name> +[item], um Gegenstände hinzuzufügen', '使用 outfit <name> +[item] 添加物品', '使用 outfit <name> +[item] 添加物品', 'outfit <name> +[item] para agregar objetos', 'outfit <name> +[item] para agregar objetos', 'outfit <name> +[item], чтобы добавить предметы'),
(1808, 'outfit_usage_remove', 'outfit <name> -[item] to remove items', 0, 0, '아이템을 제거하려면 outfit <name> -[item]', 'outfit <name> -[item] pour retirer des objets', 'outfit <name> -[item], um Gegenstände zu entfernen', '使用 outfit <name> -[item] 移除物品', '使用 outfit <name> -[item] 移除物品', 'outfit <name> -[item] para eliminar objetos', 'outfit <name> -[item] para eliminar objetos', 'outfit <name> -[item], чтобы убрать предметы'),
(1809, 'outfit_usage_equip', 'outfit <name> equip/replace to equip items', 0, 0, '아이템을 장착하려면 outfit <name> equip/replace', 'outfit <name> equip/replace pour équiper des objets', 'outfit <name> equip/replace, um Gegenstände anzulegen', '使用 outfit <name> equip/replace 装备物品', '使用 outfit <name> equip/replace 裝備物品', 'outfit <name> equip/replace para equipar objetos', 'outfit <name> equip/replace para equipar objetos', 'outfit <name> equip/replace, чтобы экипировать предметы'),
(1810, 'outfit_set_as', 'Setting outfit %name as %param', 0, 0, '복장 %name을(를) %param(으)로 설정하는 중입니다', 'Définition de la tenue %name comme %param', 'Setze Outfit %name als %param', '正在将装备方案 %name 设置为 %param', '正在將裝備方案 %name 設定為 %param', 'Estableciendo el atuendo %name como %param', 'Estableciendo el atuendo %name como %param', 'Устанавливаю комплект %name как %param'),
(1811, 'outfit_equipping', 'Equipping outfit %name', 0, 0, '복장 %name을(를) 장착하는 중입니다', 'Équipement de la tenue %name', 'Outfit %name wird angelegt', '正在装备方案 %name', '正在裝備方案 %name', 'Equipando el atuendo %name', 'Equipando el atuendo %name', 'Экипирую комплект %name'),
(1812, 'outfit_replace_current', 'Replacing current equip with outfit %name', 0, 0, '현재 장비를 복장 %name으로 교체하는 중입니다', 'Remplacement de l''équipement actuel par la tenue %name', 'Aktuelle Ausrüstung wird durch Outfit %name ersetzt', '正在用装备方案 %name 替换当前装备', '正在用裝備方案 %name 替換目前裝備', 'Reemplazando el equipo actual con el atuendo %name', 'Reemplazando el equipo actual con el atuendo %name', 'Заменяю текущую экипировку комплектом %name'),
(1813, 'outfit_resetting', 'Resetting outfit %name', 0, 0, '복장 %name을(를) 초기화하는 중입니다', 'Réinitialisation de la tenue %name', 'Outfit %name wird zurückgesetzt', '正在重置装备方案 %name', '正在重置裝備方案 %name', 'Restableciendo el atuendo %name', 'Restableciendo el atuendo %name', 'Сбрасываю комплект %name'),
(1814, 'outfit_updating_current', 'Updating with current items outfit %name', 0, 0, '현재 아이템으로 복장 %name을(를) 업데이트하는 중입니다', 'Mise à jour de la tenue %name avec les objets actuels', 'Outfit %name wird mit der aktuellen Ausrüstung aktualisiert', '正在用当前物品更新装备方案 %name', '正在用目前物品更新裝備方案 %name', 'Actualizando el atuendo %name con los objetos actuales', 'Actualizando el atuendo %name con los objetos actuales', 'Обновляю комплект %name текущими предметами'),
(1815, 'outfit_item_removed_from', '%item removed from %name', 0, 0, '%name에서 %item이(가) 제거되었습니다', '%item retiré de %name', '%item aus %name entfernt', '已从 %name 中移除 %item', '已從 %name 中移除 %item', '%item eliminado de %name', '%item eliminado de %name', '%item удалён из %name'),
(1816, 'outfit_item_added_to', '%item added to %name', 0, 0, '%item이(가) %name에 추가되었습니다', '%item ajouté à %name', '%item zu %name hinzugefügt', '已将 %item 添加到 %name', '已將 %item 添加到 %name', '%item agregado a %name', '%item agregado a %name', '%item добавлен в %name'),
(1817, 'release_spirit_not_dead_wait', 'I am not dead, will wait here', 0, 0, '저는 죽지 않았습니다. 여기서 기다리겠습니다', 'Je ne suis pas mort, j''attendrai ici', 'Ich bin nicht tot und werde hier warten', '我还没死,会在这里等', '我還沒死,會在這裡等', 'No estoy muerto, esperaré aquí', 'No estoy muerto, esperaré aquí', 'Я не мёртв, буду ждать здесь'),
(1818, 'release_spirit_already_spirit', 'I am already a spirit', 0, 0, '저는 이미 유령입니다', 'Je suis déjà un esprit', 'Ich bin bereits ein Geist', '我已经是灵魂状态了', '我已經是靈魂狀態了', 'Ya soy un espíritu', 'Ya soy un espíritu', 'Я уже дух'),
(1819, 'release_spirit_releasing', 'Releasing...', 0, 0, '해방 중...', 'Je libère mon esprit...', 'Geist wird freigesetzt...', '正在释放灵魂...', '正在釋放靈魂...', 'Liberando espíritu...', 'Liberando espíritu...', 'Освобождаю дух...'),
(1820, 'release_spirit_meet_graveyard', 'Meet me at the graveyard', 0, 0, '묘지에서 만나요', 'Retrouvez-moi au cimetière', 'Triff mich auf dem Friedhof', '墓地见', '墓地見', 'Encuéntrame en el cementerio', 'Encuéntrame en el cementerio', 'Встречаемся на кладбище'),
(1821, 'send_mail_no_mailbox_nearby', 'There is no mailbox nearby', 0, 0, '근처에 우체통이 없습니다', 'Il n''y a pas de boîte aux lettres à proximité', 'Kein Briefkasten in der Nähe', '附近没有邮箱', '附近沒有郵箱', 'No hay un buzón cerca', 'No hay un buzón cerca', 'Поблизости нет почтового ящика'),
(1822, 'send_mail_one_item_only', 'You can not request more than one item', 0, 0, '둘 이상의 아이템은 요청할 수 없습니다', 'Vous ne pouvez pas demander plus d''un objet', 'Du kannst nicht mehr als einen Gegenstand anfordern', '你不能请求多于一件物品', '你不能要求超過一件物品', 'No puedes solicitar más de un objeto', 'No puedes solicitar más de un objeto', 'Нельзя запрашивать больше одного предмета'),
(1823, 'send_mail_cannot_send_money', 'I cannot send money', 0, 0, '돈은 보낼 수 없습니다', 'Je ne peux pas envoyer d''argent', 'Ich kann kein Geld senden', '我不能寄送钱', '我不能寄送金錢', 'No puedo enviar dinero', 'No puedo enviar dinero', 'Я не могу отправить деньги'),
(1824, 'send_mail_not_enough_money', 'I don''t have enough money', 0, 0, '돈이 충분하지 않습니다', 'Je n''ai pas assez d''argent', 'Ich habe nicht genug Geld', '我没有足够的钱', '我沒有足夠的錢', 'No tengo suficiente dinero', 'No tengo suficiente dinero', 'У меня недостаточно денег'),
(1825, 'send_mail_sending_to', 'Sending mail to %receiver', 0, 0, '%receiver에게 우편을 보내는 중입니다', 'Envoi du courrier à %receiver', 'Sende Post an %receiver', '正在向 %receiver 发送邮件', '正在向 %receiver 發送郵件', 'Enviando correo a %receiver', 'Enviando correo a %receiver', 'Отправляю почту %receiver'),
(1826, 'send_mail_cannot_send_item', 'Cannot send %item', 0, 0, '%item을(를) 보낼 수 없습니다', 'Impossible d''envoyer %item', '%item kann nicht gesendet werden', '无法发送 %item', '無法發送 %item', 'No se puede enviar %item', 'No se puede enviar %item', 'Невозможно отправить %item'),
(1827, 'send_mail_item_not_for_sale', '%item: it is not for sale', 0, 0, '%item: 판매 대상이 아닙니다', '%item : ce n''est pas à vendre', '%item: Das steht nicht zum Verkauf', '%item这不是出售物品', '%item這不是出售物品', '%item: esto no está en venta', '%item: esto no está en venta', '%item: это не продаётся'),
(1828, 'send_mail_sent_to', 'Sent mail to %receiver', 0, 0, '%receiver에게 우편을 보냈습니다', 'Courrier envoyé à %receiver', 'Post an %receiver gesendet', '已向 %receiver 发送邮件', '已向 %receiver 發送郵件', 'Correo enviado a %receiver', 'Correo enviado a %receiver', 'Почта отправлена %receiver'),
(1829, 'craft_reset', 'I will not craft anything', 0, 0, '아무것도 제작하지 않겠습니다', 'Je ne fabriquerai rien', 'Ich werde nichts herstellen', '我不会制作任何东西', '我不會製作任何東西', 'No fabricaré nada', 'No fabricaré nada', 'Я ничего не буду создавать'),
(1830, 'craft_usage', 'Usage: ''craft [itemId]'' or ''craft reset''', 0, 0, '사용법: ''craft [itemId]'' 또는 ''craft reset''', 'Utilisation : ''craft [itemId]'' ou ''craft reset''', 'Verwendung: ''craft [itemId]'' oder ''craft reset''', '用法:''craft [itemId]''''craft reset''', '用法:''craft [itemId]''''craft reset''', 'Uso: ''craft [itemId]'' o ''craft reset''', 'Uso: ''craft [itemId]'' o ''craft reset''', 'Использование: ''craft [itemId]'' или ''craft reset'''),
(1831, 'craft_cannot_craft', 'I cannot craft this', 0, 0, '이것은 제작할 수 없습니다', 'Je ne peux pas fabriquer ceci', 'Ich kann das nicht herstellen', '我无法制作这个', '我無法製作這個', 'No puedo fabricar esto', 'No puedo fabricar esto', 'Я не могу это создать'),
(1832, 'craft_summary', 'I will craft %item using reagents: %reagents (craft fee: %money)', 0, 0, '재료 %reagents을(를) 사용해 %item을(를) 제작하겠습니다 (제작 수수료: %money)', 'Je fabriquerai %item en utilisant les composants : %reagents (frais de fabrication : %money)', 'Ich werde %item mit folgenden Reagenzien herstellen: %reagents (Herstellungsgebühr: %money)', '我将使用材料 %reagents 制作 %item制作费用%money', '我將使用材料 %reagents 製作 %item製作費用%money', 'Fabricaré %item usando los materiales: %reagents (tarifa de fabricación: %money)', 'Fabricaré %item usando los materiales: %reagents (tarifa de fabricación: %money)', 'Я изготовлю %item, используя реагенты: %reagents (плата за изготовление: %money)'),
(1833, 'set_home_success', 'This inn is my new home', 0, 0, '이 여관을 새로운 집으로 설정했습니다', 'Cette auberge est ma nouvelle maison', 'Dieses Gasthaus ist mein neues Zuhause', '这家旅店是我的新家', '這間旅店是我的新家', 'Esta posada es mi nuevo hogar', 'Esta posada es mi nuevo hogar', 'Этот трактир теперь мой новый дом'),
(1834, 'set_home_no_innkeeper_error', 'Can''t find any innkeeper around', 0, 0, '주변에서 여관주인을 찾을 수 없습니다', 'Impossible de trouver un aubergiste aux alentours', 'Kein Gastwirt in der Nähe gefunden', '附近找不到旅店老板', '附近找不到旅店老闆', 'No encuentro ningún posadero cerca', 'No encuentro ningún posadero cerca', 'Не удалось найти поблизости трактирщика'),
(1835, 'quest_shared', 'Quest shared', 0, 0, '퀘스트 공유됨', 'Quête partagée', 'Quest geteilt', '任务已共享', '任務已共享', 'Misión compartida', 'Misión compartida', 'Задание поделено'),
(1836, 'tame_invalid_id_error', 'Invalid tame id.', 0, 0, '잘못된 길들이기 ID입니다.', 'Identifiant de dressage invalide.', 'Ungültige Zähm-ID.', '无效的驯服 ID。', '無效的馴服 ID。', 'ID de domesticación no válido.', 'ID de domesticación no válido.', 'Недопустимый ID приручения.'),
(1837, 'tame_usage_error', 'Usage: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', 0, 0, '사용법: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', 'Utilisation : tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', 'Verwendung: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', '用法tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', '用法tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', 'Uso: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', 'Uso: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon', 'Использование: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon'),
(1838, 'tame_pet_changed', 'Pet changed to %name, ID: %id.', 0, 0, '소환수가 %name, ID: %id(으)로 변경되었습니다.', 'Familier changé en %name, ID : %id.', 'Begleiter geändert zu %name, ID: %id.', '宠物已更改为 %nameID%id。', '寵物已更改為 %nameID%id。', 'Mascota cambiada a %name, ID: %id.', 'Mascota cambiada a %name, ID: %id.', 'Питомец изменён на %name, ID: %id.'),
(1839, 'tame_pet_changed_initialized', 'Pet changed and initialized!', 0, 0, '소환수가 변경되고 초기화되었습니다!', 'Familier changé et initialisé !', 'Begleiter geändert und initialisiert!', '宠物已更改并初始化!', '寵物已更改並初始化!', 'La mascota ha sido cambiada e inicializada!', 'La mascota ha sido cambiada e inicializada!', 'Питомец изменён и инициализирован!'),
(1840, 'tame_exotic_requires_beast_mastery', 'I cannot use exotic pets unless I have the Beast Mastery talent.', 0, 0, '야수 지배 특성이 없으면 특수 야수 소환수를 사용할 수 없습니다.', 'Je ne peux pas utiliser de familiers exotiques sans le talent Maîtrise des bêtes.', 'Ich kann ohne das Talent Tierherrschaft keine exotischen Begleiter benutzen.', '如果没有野兽掌握天赋,我无法使用异种宠物。', '如果沒有野獸控制天賦,我無法使用異種寵物。', 'No puedo usar mascotas exóticas a menos que tenga el talento Dominio de bestias.', 'No puedo usar mascotas exóticas a menos que tenga el talento Dominio de bestias.', 'Я не могу использовать экзотических питомцев без таланта Повелитель зверей.'),
(1841, 'tame_no_pet_by_name', 'No tameable pet found with name: %name', 0, 0, '이름이 %name인 길들일 수 있는 소환수를 찾을 수 없습니다', 'Aucun familier apprivoisable trouvé avec le nom : %name', 'Kein zähmbares Tier mit dem Namen %name gefunden', '未找到名为 %name 的可驯服宠物', '未找到名為 %name 的可馴服寵物', 'No se encontró ninguna mascota domesticable con el nombre: %name', 'No se encontró ninguna mascota domesticable con el nombre: %name', 'Не найден приручаемый питомец с именем: %name'),
(1842, 'tame_no_pet_by_id', 'No tameable pet found with id: %id', 0, 0, 'ID가 %id인 길들일 수 있는 소환수를 찾을 수 없습니다', 'Aucun familier apprivoisable trouvé avec l''id : %id', 'Kein zähmbares Tier mit der ID %id gefunden', '未找到 ID 为 %id 的可驯服宠物', '未找到 ID 為 %id 的可馴服寵物', 'No se encontró ninguna mascota domesticable con el id: %id', 'No se encontró ninguna mascota domesticable con el id: %id', 'Не найден приручаемый питомец с id: %id'),
(1843, 'tame_no_pet_by_family', 'No tameable pet found with family: %family', 0, 0, '계열이 %family인 길들일 수 있는 소환수를 찾을 수 없습니다', 'Aucun familier apprivoisable trouvé avec la famille : %family', 'Kein zähmbares Tier mit der Familie %family gefunden', '未找到家族为 %family 的可驯服宠物', '未找到家族為 %family 的可馴服寵物', 'No se encontró ninguna mascota domesticable con la familia: %family', 'No se encontró ninguna mascota domesticable con la familia: %family', 'Не найден приручаемый питомец семейства: %family'),
(1844, 'tame_no_pet_to_rename', 'You have no pet to rename.', 0, 0, '이름을 바꿀 소환수가 없습니다.', 'Vous n''avez pas de familier à renommer.', 'Du hast kein Tier zum Umbenennen.', '你没有可重命名的宠物。', '你沒有可重新命名的寵物。', 'No tienes ninguna mascota para renombrar.', 'No tienes ninguna mascota para renombrar.', 'У вас нет питомца для переименования.'),
(1845, 'tame_pet_name_length_error', 'Pet name must be between 1 and 12 alphabetic characters.', 0, 0, '소환수 이름은 1자에서 12자의 알파벳 문자여야 합니다.', 'Le nom du familier doit contenir entre 1 et 12 caractères alphabétiques.', 'Der Name des Begleiters muss zwischen 1 und 12 alphabetischen Zeichen lang sein.', '宠物名字必须为 1 到 12 个字母字符。', '寵物名字必須為 1 到 12 個字母字元。', 'El nombre de la mascota debe tener entre 1 y 12 caracteres alfabéticos.', 'El nombre de la mascota debe tener entre 1 y 12 caracteres alfabéticos.', 'Имя питомца должно содержать от 1 до 12 буквенных символов.'),
(1846, 'tame_pet_name_alpha_error', 'Pet name must only contain alphabetic characters (A-Z, a-z).', 0, 0, '소환수 이름에는 알파벳 문자(A-Z, a-z)만 포함될 수 있습니다.', 'Le nom du familier ne doit contenir que des caractères alphabétiques (A-Z, a-z).', 'Der Name des Begleiters darf nur alphabetische Zeichen (A-Z, a-z) enthalten.', '宠物名字只能包含字母字符A-Z, a-z', '寵物名字只能包含字母字元A-Z, a-z', 'El nombre de la mascota solo puede contener caracteres alfabéticos (A-Z, a-z).', 'El nombre de la mascota solo puede contener caracteres alfabéticos (A-Z, a-z).', 'Имя питомца должно содержать только буквенные символы (A-Z, a-z).'),
(1847, 'tame_pet_name_forbidden_error', 'That pet name is forbidden. Please choose another name.', 0, 0, '그 소환수 이름은 사용할 수 없습니다. 다른 이름을 선택해 주세요.', 'Ce nom de familier est interdit. Veuillez en choisir un autre.', 'Dieser Name für den Begleiter ist verboten. Bitte wähle einen anderen Namen.', '该宠物名称被禁止使用。请选择其他名字。', '該寵物名稱被禁止使用。請選擇其他名字。', 'Ese nombre de mascota está prohibido. Por favor, elige otro nombre.', 'Ese nombre de mascota está prohibido. Por favor, elige otro nombre.', 'Это имя для питомца запрещено. Пожалуйста, выберите другое имя.'),
(1848, 'tame_pet_renamed', 'Your pet has been renamed to %name!', 0, 0, '당신의 소환수 이름이 %name(으)로 변경되었습니다!', 'Votre familier a été renommé en %name !', 'Dein Begleiter wurde in %name umbenannt!', '你的宠物已重命名为 %name', '你的寵物已重新命名為 %name', 'Tu mascota ha sido renombrada a %name!', 'Tu mascota ha sido renombrada a %name!', 'Ваш питомец был переименован в %name!'),
(1849, 'tame_pet_rename_refresh_hint', 'If you do not see the new name, please dismiss and recall your pet.', 0, 0, '새 이름이 보이지 않으면 소환수를 해제했다가 다시 소환해 주세요.', 'Si vous ne voyez pas le nouveau nom, veuillez renvoyer puis rappeler votre familier.', 'Wenn du den neuen Namen nicht siehst, schicke deinen Begleiter weg und rufe ihn erneut.', '如果你看不到新名字,请先解散再召回你的宠物。', '如果你看不到新名字,請先解散再召回你的寵物。', 'Si no ves el nuevo nombre, por favor retira y vuelve a invocar a tu mascota.', 'Si no ves el nuevo nombre, por favor retira y vuelve a invocar a tu mascota.', 'Если вы не видите новое имя, отпустите и снова призовите питомца.'),
(1850, 'tame_only_hunters_level_10', 'Only level 10+ hunters can have pets.', 0, 0, '10레벨 이상의 사냥꾼만 소환수를 가질 수 있습니다.', 'Seuls les chasseurs de niveau 10+ peuvent avoir des familiers.', 'Nur Jäger ab Stufe 10 können Begleiter haben.', '只有 10 级以上的猎人才能拥有宠物。', '只有 10 級以上的獵人才能擁有寵物。', 'Solo los cazadores de nivel 10 o superior pueden tener mascotas.', 'Solo los cazadores de nivel 10 o superior pueden tener mascotas.', 'Только охотники 10 уровня и выше могут иметь питомцев.'),
(1851, 'tame_creature_template_not_found', 'Creature template not found.', 0, 0, '생물 템플릿을 찾을 수 없습니다.', 'Modèle de créature introuvable.', 'Kreaturvorlage nicht gefunden.', '未找到生物模板。', '未找到生物範本。', 'No se encontró la plantilla de criatura.', 'No se encontró la plantilla de criatura.', 'Шаблон существа не найден.'),
(1852, 'tame_create_pet_failed', 'Failed to create pet.', 0, 0, '소환수 생성에 실패했습니다.', 'Échec de la création du familier.', 'Erstellen des Begleiters fehlgeschlagen.', '创建宠物失败。', '建立寵物失敗。', 'No se pudo crear la mascota.', 'No se pudo crear la mascota.', 'Не удалось создать питомца.'),
(1853, 'tame_pet_abandoned', 'Your pet has been abandoned.', 0, 0, '당신의 소환수를 버렸습니다.', 'Votre familier a été abandonné.', 'Dein Begleiter wurde freigelassen.', '你的宠物已被放弃。', '你的寵物已被放棄。', 'Tu mascota ha sido abandonada.', 'Tu mascota ha sido abandonada.', 'Ваш питомец был брошен.'),
(1854, 'tame_no_hunter_pet_to_abandon', 'You have no hunter pet to abandon.', 0, 0, '버릴 사냥꾼 소환수가 없습니다.', 'Vous n''avez pas de familier de chasseur à abandonner.', 'Du hast kein Jägertier zum Freilassen.', '你没有可放弃的猎人宠物。', '你沒有可放棄的獵人寵物。', 'No tienes ninguna mascota de cazador que abandonar.', 'No tienes ninguna mascota de cazador que abandonar.', 'У вас нет питомца охотника, которого можно бросить.'),
(1855, 'taxi_ready_next_flight', 'I am ready for the next flight', 0, 0, '다음 비행을 탈 준비가 되었습니다', 'Je suis prêt pour le prochain vol', 'Ich bin bereit für den nächsten Flug', '我已准备好进行下一次飞行', '我已準備好進行下一次飛行', 'Estoy listo para el siguiente vuelo', 'Estoy listo para el siguiente vuelo', 'Я готов к следующему перелёту'),
(1856, 'taxi_cant_fly_with_you', 'I can''t fly with you', 0, 0, '당신과 함께 날 수 없습니다', 'Je ne peux pas voler avec vous', 'Ich kann nicht mit dir fliegen', '我不能和你一起飞', '我不能和你一起飛', 'No puedo volar contigo', 'No puedo volar contigo', 'Я не могу лететь с вами'),
(1857, 'taxi_no_flightmaster_nearby', 'Cannot find any flightmaster to talk', 0, 0, '대화할 비행 조련사를 찾을 수 없습니다', 'Impossible de trouver un maître de vol à qui parler', 'Keinen Flugmeister zum Ansprechen gefunden', '找不到可以交谈的飞行管理员', '找不到可以交談的飛行管理員', 'No encuentro ningún maestro de vuelo con quien hablar', 'No encuentro ningún maestro de vuelo con quien hablar', 'Не могу найти распорядителя полётов, с которым можно поговорить'),
(1858, 'trade_busy_now', 'I''m kind of busy now', 0, 0, '지금은 좀 바쁩니다', 'Je suis un peu occupé en ce moment', 'Ich bin gerade etwas beschäftigt', '我现在有点忙', '我現在有點忙', 'Ahora estoy un poco ocupado', 'Ahora estoy un poco ocupado', 'Сейчас я немного занят'),
(1859, 'trade_disabled', 'Trading is disabled', 0, 0, '거래가 비활성화되어 있습니다', 'L''échange est désactivé', 'Handel ist deaktiviert', '交易已禁用', '交易已停用', 'El comercio está deshabilitado', 'El comercio está deshabilitado', 'Торговля отключена'),
(1860, 'trade_thank_you_player', 'Thank you %player', 0, 0, '고마워요 %player', 'Merci %player', 'Danke %player', '谢谢你 %player', '謝謝你 %player', 'Gracias %player', 'Gracias %player', 'Спасибо, %player'),
(1861, 'trade_selling_disabled', 'Selling is disabled.', 0, 0, '판매가 비활성화되어 있습니다.', 'La vente est désactivée.', 'Verkaufen ist deaktiviert.', '出售已禁用。', '出售已停用。', 'La venta está deshabilitada.', 'La venta está deshabilitada.', 'Продажа отключена.'),
(1862, 'trade_buying_disabled', 'Buying is disabled.', 0, 0, '구매가 비활성화되어 있습니다.', 'L''achat est désactivé.', 'Kaufen ist deaktiviert.', '购买已禁用。', '購買已停用。', 'La compra está deshabilitada.', 'La compra está deshabilitada.', 'Покупка отключена.'),
(1863, 'trade_item_not_for_sale', '%item - This is not for sale', 0, 0, '%item - 이것은 판매용이 아닙니다', '%item - Ceci n''est pas à vendre', '%item - Das steht nicht zum Verkauf', '%item - 这不是出售物品', '%item - 這不是出售物品', '%item - Esto no está en venta', '%item - Esto no está en venta', '%item - это не продаётся'),
(1864, 'trade_item_not_needed', '%item - I don''t need this', 0, 0, '%item - 저는 이것이 필요 없습니다', '%item - Je n''en ai pas besoin', '%item - Das brauche ich nicht', '%item - 我不需要这个', '%item - 我不需要這個', '%item - No necesito esto', '%item - No necesito esto', '%item - мне это не нужно'),
(1865, 'trade_no_items_error', 'There are no items to trade', 0, 0, '거래할 아이템이 없습니다', 'Il n''y a aucun objet à échanger', 'Es gibt keine Gegenstände zum Handeln', '没有可交易的物品', '沒有可交易的物品', 'No hay objetos para comerciar', 'No hay objetos para comerciar', 'Нет предметов для обмена'),
(1866, 'trade_discount_buy_only', 'You can use discount to buy items only', 0, 0, '할인은 아이템 구매에만 사용할 수 있습니다', 'Vous ne pouvez utiliser la réduction que pour acheter des objets', 'Rabatte können nur zum Kauf von Gegenständen verwendet werden', '折扣只能用于购买物品', '折扣只能用於購買物品', 'Solo puedes usar el descuento para comprar objetos', 'Solo puedes usar el descuento para comprar objetos', 'Скидку можно использовать только для покупки предметов'),
(1867, 'trade_success_pleasure', 'A pleasure doing business with you', 0, 0, '당신과 거래해서 즐거웠습니다', 'Un plaisir de faire affaire avec vous', 'Es war mir ein Vergnügen, mit dir Geschäfte zu machen', '很高兴和你做生意', '很高興和你做生意', 'Un placer hacer negocios contigo', 'Un placer hacer negocios contigo', 'Приятно было иметь с вами дело'),
(1868, 'trade_success_fair_trade', 'Fair trade', 0, 0, '공정한 거래입니다', 'Échange équitable', 'Fairer Handel', '公平交易', '公平交易', 'Intercambio justo', 'Intercambio justo', 'Честная сделка'),
(1869, 'trade_success_thanks', 'Thanks', 0, 0, '감사합니다', 'Merci', 'Danke', '谢谢', '謝謝', 'Gracias', 'Gracias', 'Спасибо'),
(1870, 'trade_success_off_with_you', 'Off with you', 0, 0, '이제 가보세요', 'Allez-vous-en maintenant', 'Nun geh deiner Wege', '走吧你', '走吧你', 'Ahora vete', 'Ahora vete', 'Иди уже'),
(1871, 'trade_want_money_for_this', 'I want %money for this', 0, 0, '이것에 대해 %money을(를) 원합니다', 'Je veux %money pour ceci', 'Ich möchte %money dafür', '这个我想要 %money', '這個我想要 %money', 'Quiero %money por esto', 'Quiero %money por esto', 'Я хочу %money за это'),
(1872, 'use_item_none_available', 'No items (or game objects) available', 0, 0, '사용할 수 있는 아이템(또는 게임 오브젝트)이 없습니다', 'Aucun objet (ou objet interactif) disponible', 'Keine Gegenstände (oder Spielobjekte) verfügbar', '没有可用的物品(或游戏物体)', '沒有可用的物品(或遊戲物件)', 'No hay objetos (ni objetos del juego) disponibles', 'No hay objetos (ni objetos del juego) disponibles', 'Нет доступных предметов (или игровых объектов)'),
(1873, 'use_gameobject', 'Using %gameobject', 0, 0, '%gameobject 사용 중', 'Utilisation de %gameobject', 'Benutze %gameobject', '正在使用 %gameobject', '正在使用 %gameobject', 'Usando %gameobject', 'Usando %gameobject', 'Использую %gameobject'),
(1874, 'socket_does_not_fit', 'Socket does not fit', 0, 0, '소켓이 맞지 않습니다', 'La châsse ne correspond pas', 'Sockel passt nicht', '插槽不匹配', '插槽不匹配', 'La ranura no encaja', 'La ranura no encaja', 'Гнездо не подходит'),
(1875, 'use_item_on_target', 'Using %item on %target', 0, 0, '%target에게 %item을(를) 사용하는 중', 'Utilisation de %item sur %target', 'Benutze %item auf %target', '正在对 %target 使用 %item', '正在對 %target 使用 %item', 'Usando %item en %target', 'Usando %item en %target', 'Использую %item на %target'),
(1876, 'use_item', 'Using %item', 0, 0, '%item 사용 중', 'Utilisation de %item', 'Benutze %item', '正在使用 %item', '正在使用 %item', 'Usando %item', 'Usando %item', 'Использую %item'),
(1877, 'socketing_item_with_gem', 'Socketing %item with %gem', 0, 0, '%item에 %gem 보석을 장착하는 중', 'Sertissage de %item avec %gem', 'Sockele %item mit %gem', '正在将 %gem 镶嵌到 %item 上', '正在將 %gem 鑲嵌到 %item 上', 'Engarzando %item con %gem', 'Engarzando %item con %gem', 'Вставляю %gem в %item'),
(1878, 'meeting_stone_in_combat', 'I am in combat', 0, 0, '전투 중입니다', 'Je suis en combat', 'Ich bin im Kampf', '我在战斗中', '我在戰鬥中', 'Estoy en combate', 'Estoy en combate', 'Я в бою'),
(1879, 'meeting_stone_welcome', 'Welcome!', 0, 0, '환영합니다!', 'Bienvenue !', 'Willkommen!', '欢迎!', '歡迎!', 'Bienvenido!', 'Bienvenido!', 'Добро пожаловать!'),
(1880, 'meeting_stone_none_nearby', 'There is no meeting stone nearby', 0, 0, '근처에 집결의 돌이 없습니다', 'Il n''y a pas de pierre de rencontre à proximité', 'Es gibt keinen Beschwörungsstein in der Nähe', '附近没有集合石', '附近沒有集合石', 'No hay ninguna piedra de encuentro cerca', 'No hay ninguna piedra de encuentro cerca', 'Поблизости нет камня встреч'),
(1881, 'meeting_stone_none_near_you', 'There is no meeting stone near you', 0, 0, '당신 근처에 집결의 돌이 없습니다', 'Il n''y a pas de pierre de rencontre près de vous', 'Es gibt keinen Beschwörungsstein in deiner Nähe', '你附近没有集合石', '你附近沒有集合石', 'No hay ninguna piedra de encuentro cerca de ti', 'No hay ninguna piedra de encuentro cerca de ti', 'Рядом с вами нет камня встреч'),
(1882, 'meeting_stone_no_hearthstone_self', 'I have no hearthstone', 0, 0, '귀환석이 없습니다', 'Je n''ai pas de pierre de foyer', 'Ich habe keinen Ruhestein', '我没有炉石', '我沒有爐石', 'No tengo piedra de hogar', 'No tengo piedra de hogar', 'У меня нет камня возвращения'),
(1883, 'meeting_stone_no_hearthstone_you', 'You have no hearthstone', 0, 0, '당신에게 귀환석이 없습니다', 'Vous n''avez pas de pierre de foyer', 'Du hast keinen Ruhestein', '你没有炉石', '你沒有爐石', 'No tienes piedra de hogar', 'No tienes piedra de hogar', 'У вас нет камня возвращения'),
(1884, 'meeting_stone_hearthstone_not_ready_self', 'My hearthstone is not ready', 0, 0, '제 귀환석은 아직 준비되지 않았습니다', 'Ma pierre de foyer n''est pas prête', 'Mein Ruhestein ist nicht bereit', '我的炉石还没准备好', '我的爐石還沒準備好', 'Mi piedra de hogar no está lista', 'Mi piedra de hogar no está lista', 'Мой камень возвращения ещё не готов'),
(1885, 'meeting_stone_hearthstone_not_ready_you', 'Your hearthstone is not ready', 0, 0, '당신의 귀환석은 아직 준비되지 않았습니다', 'Votre pierre de foyer n''est pas prête', 'Dein Ruhestein ist nicht bereit', '你的炉石还没准备好', '你的爐石還沒準備好', 'Tu piedra de hogar no está lista', 'Tu piedra de hogar no está lista', 'Ваш камень возвращения ещё не готов'),
(1886, 'meeting_stone_no_innkeepers_nearby', 'There are no innkeepers nearby', 0, 0, '근처에 여관주인이 없습니다', 'Il n''y a pas d''aubergistes à proximité', 'Es gibt keine Gastwirte in der Nähe', '附近没有旅店老板', '附近沒有旅店老闆', 'No hay posaderos cerca', 'No hay posaderos cerca', 'Поблизости нет трактирщиков'),
(1887, 'meeting_stone_no_innkeepers_near_you', 'There are no innkeepers near you', 0, 0, '당신 근처에 여관주인이 없습니다', 'Il n''y a pas d''aubergistes près de vous', 'Es gibt keine Gastwirte in deiner Nähe', '你附近没有旅店老板', '你附近沒有旅店老闆', 'No hay posaderos cerca de ti', 'No hay posaderos cerca de ti', 'Рядом с вами нет трактирщиков'),
(1888, 'meeting_stone_cannot_summon_vehicle', 'You cannot summon me while I''m on a vehicle', 0, 0, '제가 탈것/차량에 타고 있는 동안에는 소환할 수 없습니다', 'Vous ne pouvez pas m''invoquer tant que je suis sur un véhicule', 'Du kannst mich nicht beschwören, solange ich auf einem Fahrzeug bin', '当我在载具上时,你不能召唤我', '當我在載具上時,你不能召喚我', 'No puedes invocarme mientras esté en un vehículo', 'No puedes invocarme mientras esté en un vehículo', 'Вы не можете призвать меня, пока я на транспорте'),
(1889, 'meeting_stone_cannot_summon_master_in_combat', 'You cannot summon me while you''re in combat', 0, 0, '당신이 전투 중일 때는 저를 소환할 수 없습니다', 'Vous ne pouvez pas m''invoquer pendant que vous êtes en combat', 'Du kannst mich nicht beschwören, solange du im Kampf bist', '当你在战斗中时,不能召唤我', '當你在戰鬥中時,不能召喚我', 'No puedes invocarme mientras estés en combate', 'No puedes invocarme mientras estés en combate', 'Вы не можете призвать меня, пока вы в бою'),
(1890, 'meeting_stone_cannot_summon_master_dead', 'You cannot summon me while you''re dead', 0, 0, '당신이 죽어 있는 동안에는 저를 소환할 수 없습니다', 'Vous ne pouvez pas m''invoquer pendant que vous êtes mort', 'Du kannst mich nicht beschwören, solange du tot bist', '当你死亡时,不能召唤我', '當你死亡時,不能召喚我', 'No puedes invocarme mientras estés muerto', 'No puedes invocarme mientras estés muerto', 'Вы не можете призвать меня, пока вы мертвы'),
(1891, 'meeting_stone_cannot_summon_bot_dead', 'You cannot summon me while I''m dead, you need to release my spirit first', 0, 0, '제가 죽어 있는 동안에는 저를 소환할 수 없습니다. 먼저 제 영혼을 해방해야 합니다', 'Vous ne pouvez pas m''invoquer tant que je suis mort, vous devez d''abord libérer mon esprit', 'Du kannst mich nicht beschwören, solange ich tot bin; du musst zuerst meinen Geist freisetzen', '当我死亡时,你不能召唤我,你需要先释放我的灵魂', '當我死亡時,你不能召喚我,你需要先釋放我的靈魂', 'No puedes invocarme mientras esté muerto, primero debes liberar mi espíritu', 'No puedes invocarme mientras esté muerto, primero debes liberar mi espíritu', 'Вы не можете призвать меня, пока я мёртв, сначала нужно освободить мой дух'),
(1892, 'meeting_stone_revived', 'I live, again!', 0, 0, '다시 살아났습니다!', 'Je vis à nouveau !', 'Ich lebe wieder!', '我又活了!', '我又活了!', 'Vivo de nuevo!', 'Vivo de nuevo!', 'Я снова жив!'),
(1893, 'meeting_stone_not_enough_space', 'Not enough place to summon', 0, 0, '소환할 공간이 부족합니다', 'Pas assez de place pour invoquer', 'Nicht genug Platz zum Beschwören', '没有足够的空间进行召唤', '沒有足夠的空間進行召喚', 'No hay suficiente espacio para invocar', 'No hay suficiente espacio para invocar', 'Недостаточно места для призыва'),
(1894, 'new_rpg_quest_accepted', 'Quest accepted %quest', 0, 0, '퀘스트 수락 %quest', 'Quête acceptée %quest', 'Quest angenommen %quest', '任务已接受 %quest', '任務已接受 %quest', 'Misión aceptada %quest', 'Misión aceptada %quest', 'Задание принято %quest'),
(1895, 'new_rpg_quest_rewarded', 'Quest rewarded %quest', 0, 0, '퀘스트 보상 받음 %quest', 'Quête récompensée %quest', 'Quest abgeschlossen %quest', '任务已奖励 %quest', '任務已獎勵 %quest', 'Misión completada %quest', 'Misión completada %quest', 'Награда за задание получена %quest'),
(1896, 'new_rpg_quest_dropped', 'Quest dropped %quest', 0, 0, '퀘스트 포기 %quest', 'Quête abandonnée %quest', 'Quest abgebrochen %quest', '任务已放弃 %quest', '任務已放棄 %quest', 'Misión abandonada %quest', 'Misión abandonada %quest', 'Задание отменено %quest'),
(1897, 'rpg_item_better_for_player', 'You can use this %item better than me, %player.', 0, 0, '%player님, 이 %item은(는) 저보다 당신에게 더 잘 맞습니다.', 'Vous pouvez mieux utiliser cet objet %item que moi, %player.', 'Du kannst diesen %item besser gebrauchen als ich, %player.', '%player这个 %item 比我更适合你使用。', '%player這個 %item 比我更適合你使用。', 'Tú puedes usar este %item mejor que yo, %player.', 'Tú puedes usar este %item mejor que yo, %player.', '%player, ты можешь использовать %item лучше, чем я.'),
(1898, 'rpg_start_trade_with_player', 'Start trade with %player', 0, 0, '%player와(과) 거래를 시작합니다', 'Début de l''échange avec %player', 'Beginne Handel mit %player', '开始与 %player 交易', '開始與 %player 交易', 'Iniciando intercambio con %player', 'Iniciando intercambio con %player', 'Начинаю обмен с %player');
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES
('quest_accept_debug', 100),
('quest_already_have_error', 100),
('quest_cant_take_error', 100),
('arena_team_already_in_team', 100),
('arena_team_thanks_for_invite', 100),
('area_trigger_follow_too_far_error', 100),
('area_trigger_wait_for_me', 100),
('attack_no_target_error', 100),
('attack_target_not_in_world_error', 100),
('attack_in_flight_error', 100),
('attack_pvp_prohibited_error', 100),
('attack_target_friendly_error', 100),
('attack_target_dead_error', 100),
('attack_target_not_in_sight_error', 100),
('attack_already_attacking_error', 100),
('attack_invalid_target_error', 100),
('bank_no_banker_nearby_error', 100),
('move_from_group', 100),
('running_away', 100),
('clean_quest_log_started', 100),
('quest_trivial_will_remove', 100),
('quest_has_been_removed', 100),
('quest_not_trivial_kept', 100),
('quest_removed_debug', 100),
('quest_removed_with_name', 100),
('guild_accept_inviter_not_in_guild', 100),
('guild_accept_already_in_guild', 100),
('guild_accept_declined', 100),
('outfit_usage_add', 100),
('outfit_usage_remove', 100),
('outfit_usage_equip', 100),
('outfit_set_as', 100),
('outfit_equipping', 100),
('outfit_replace_current', 100),
('outfit_resetting', 100),
('outfit_updating_current', 100),
('outfit_item_removed_from', 100),
('outfit_item_added_to', 100),
('release_spirit_not_dead_wait', 100),
('release_spirit_already_spirit', 100),
('release_spirit_releasing', 100),
('release_spirit_meet_graveyard', 100),
('send_mail_no_mailbox_nearby', 100),
('send_mail_one_item_only', 100),
('send_mail_cannot_send_money', 100),
('send_mail_not_enough_money', 100),
('send_mail_sending_to', 100),
('send_mail_cannot_send_item', 100),
('send_mail_item_not_for_sale', 100),
('send_mail_sent_to', 100),
('craft_reset', 100),
('craft_usage', 100),
('craft_cannot_craft', 100),
('craft_summary', 100),
('set_home_success', 100),
('set_home_no_innkeeper_error', 100),
('quest_shared', 100),
('tame_invalid_id_error', 100),
('tame_usage_error', 100),
('tame_pet_changed', 100),
('tame_pet_changed_initialized', 100),
('tame_exotic_requires_beast_mastery', 100),
('tame_no_pet_by_name', 100),
('tame_no_pet_by_id', 100),
('tame_no_pet_by_family', 100),
('tame_no_pet_to_rename', 100),
('tame_pet_name_length_error', 100),
('tame_pet_name_alpha_error', 100),
('tame_pet_name_forbidden_error', 100),
('tame_pet_renamed', 100),
('tame_pet_rename_refresh_hint', 100),
('tame_only_hunters_level_10', 100),
('tame_creature_template_not_found', 100),
('tame_create_pet_failed', 100),
('tame_pet_abandoned', 100),
('tame_no_hunter_pet_to_abandon', 100),
('taxi_ready_next_flight', 100),
('taxi_cant_fly_with_you', 100),
('taxi_no_flightmaster_nearby', 100),
('trade_busy_now', 100),
('trade_disabled', 100),
('trade_thank_you_player', 100),
('trade_selling_disabled', 100),
('trade_buying_disabled', 100),
('trade_item_not_for_sale', 100),
('trade_item_not_needed', 100),
('trade_no_items_error', 100),
('trade_discount_buy_only', 100),
('trade_success_pleasure', 100),
('trade_success_fair_trade', 100),
('trade_success_thanks', 100),
('trade_success_off_with_you', 100),
('trade_want_money_for_this', 100),
('use_item_none_available', 100),
('use_gameobject', 100),
('socket_does_not_fit', 100),
('use_item_on_target', 100),
('use_item', 100),
('socketing_item_with_gem', 100),
('meeting_stone_in_combat', 100),
('meeting_stone_welcome', 100),
('meeting_stone_none_nearby', 100),
('meeting_stone_none_near_you', 100),
('meeting_stone_no_hearthstone_self', 100),
('meeting_stone_no_hearthstone_you', 100),
('meeting_stone_hearthstone_not_ready_self', 100),
('meeting_stone_hearthstone_not_ready_you', 100),
('meeting_stone_no_innkeepers_nearby', 100),
('meeting_stone_no_innkeepers_near_you', 100),
('meeting_stone_cannot_summon_vehicle', 100),
('meeting_stone_cannot_summon_master_in_combat', 100),
('meeting_stone_cannot_summon_master_dead', 100),
('meeting_stone_cannot_summon_bot_dead', 100),
('meeting_stone_revived', 100),
('meeting_stone_not_enough_space', 100),
('new_rpg_quest_accepted', 100),
('new_rpg_quest_rewarded', 100),
('new_rpg_quest_dropped', 100),
('rpg_item_better_for_player', 100),
('rpg_start_trade_with_player', 100);

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

@ -9,6 +9,7 @@
#include "ObjectAccessor.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotSecurity.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "WorldPacket.h"
@ -55,7 +56,7 @@ bool AcceptInvitationAction::Execute(Event event)
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
botAI->Reset();
botAI->TellMaster("Hello");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault("hello", "Hello", {}));
if (sPlayerbotAIConfig.summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig.sightDistance)
{

View File

@ -6,6 +6,7 @@
#include "AcceptQuestAction.h"
#include "Event.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool AcceptAllQuestsAction::ProcessQuest(Quest const* quest, Object* questGiver)
@ -18,7 +19,11 @@ bool AcceptAllQuestsAction::ProcessQuest(Quest const* quest, Object* questGiver)
if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
{
LOG_INFO("playerbots", "{} => Quest [{}] accepted", bot->GetName(), quest->GetTitle());
bot->Say("Quest [" + text_quest + "] accepted", LANG_UNIVERSAL);
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_accept_debug",
"Quest [%quest] accepted",
{{"%quest", text_quest}});
bot->Say(text, LANG_UNIVERSAL);
}
return true;
@ -113,7 +118,8 @@ bool AcceptQuestShareAction::Execute(Event event)
if (bot->HasQuest(quest))
{
bot->SetDivider(ObjectGuid::Empty);
botAI->TellError("I have this quest");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_already_have_error", "I have this quest", {}));
return false;
}
@ -121,7 +127,8 @@ bool AcceptQuestShareAction::Execute(Event event)
{
// can't take quest
bot->SetDivider(ObjectGuid::Empty);
botAI->TellError("I can't take this quest");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_cant_take_error", "I can't take this quest", {}));
return false;
}
@ -149,7 +156,8 @@ bool AcceptQuestShareAction::Execute(Event event)
bot->CastSpell(bot, qInfo->GetSrcSpell(), true);
}
botAI->TellMaster("Quest accepted");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_accept", "Quest accepted", {}));
return true;
}

View File

@ -7,6 +7,7 @@
#include "Event.h"
#include "LastMovementValue.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "Transport.h"
@ -36,7 +37,8 @@ bool ReachAreaTriggerAction::Execute(Event event)
if (bot->GetMapId() != at->map)
{
botAI->TellError("I won't follow: too far away");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"area_trigger_follow_too_far_error", "I won't follow: too far away", {}));
return true;
}
@ -51,7 +53,8 @@ bool ReachAreaTriggerAction::Execute(Event event)
float distance = bot->GetDistance(at->x, at->y, at->z);
float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay;
botAI->TellError("Wait for me");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"area_trigger_wait_for_me", "Wait for me", {}));
botAI->SetNextCheckDelay(delay);
context->GetValue<LastMovement&>("last area trigger")->Get().lastAreaTrigger = triggerId;
@ -76,6 +79,6 @@ bool AreaTriggerAction::Execute(Event /*event*/)
p.rpos(0);
bot->GetSession()->HandleAreaTriggerOpcode(p);
botAI->TellMaster("Hello");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault("hello", "Hello", {}));
return true;
}

View File

@ -6,6 +6,7 @@
#include "ArenaTeamActions.h"
#include "ArenaTeamMgr.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool ArenaTeamAcceptAction::Execute(Event event)
@ -31,7 +32,9 @@ bool ArenaTeamAcceptAction::Execute(Event event)
if (bot->GetArenaTeamId(at->GetSlot()))
{
// bot is already in an arena team
bot->Say("Sorry, I am already in such team", LANG_UNIVERSAL);
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"arena_team_already_in_team", "Sorry, I am already in such team", {});
bot->Say(text, LANG_UNIVERSAL);
accept = false;
}
@ -39,7 +42,9 @@ bool ArenaTeamAcceptAction::Execute(Event event)
{
WorldPacket data(CMSG_ARENA_TEAM_ACCEPT);
bot->GetSession()->HandleArenaTeamAcceptOpcode(data);
bot->Say("Thanks for the invite!", LANG_UNIVERSAL);
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"arena_team_thanks_for_invite", "Thanks for the invite!", {});
bot->Say(text, LANG_UNIVERSAL);
LOG_INFO("playerbots", "Bot {} <{}> accepts Arena Team invite", bot->GetGUID().ToString().c_str(),
bot->GetName().c_str());
return true;

View File

@ -10,6 +10,7 @@
#include "LastMovementValue.h"
#include "LootObjectStack.h"
#include "PlayerbotAI.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "ServerFacade.h"
#include "SharedDefines.h"
@ -38,7 +39,8 @@ bool AttackMyTargetAction::Execute(Event /*event*/)
if (!guid)
{
if (verbose)
botAI->TellError("You have no target");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"pull_no_target_error", "You have no target", {}));
return false;
}
@ -56,7 +58,8 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (!target)
{
if (verbose)
botAI->TellError("I have no target");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_no_target_error", "I have no target", {}));
return false;
}
@ -64,7 +67,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (!target->IsInWorld())
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is no longer in the world.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_target_not_in_world_error",
"%target is no longer in the world.",
{{"%target", target->GetName()}}));
return false;
}
@ -73,7 +79,8 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
{
if (verbose)
botAI->TellError("I cannot attack in flight");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_in_flight_error", "I cannot attack in flight", {}));
return false;
}
@ -85,7 +92,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
sPlayerbotAIConfig.IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
{
if (verbose)
botAI->TellError("I cannot attack other players in PvP prohibited areas.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_pvp_prohibited_error",
"I cannot attack other players in PvP prohibited areas.",
{}));
return false;
}
@ -93,7 +103,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (bot->IsFriendlyTo(target))
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is friendly to me.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_target_friendly_error",
"%target is friendly to me.",
{{"%target", target->GetName()}}));
return false;
}
@ -101,7 +114,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (target->isDead())
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is dead.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_target_dead_error",
"%target is dead.",
{{"%target", target->GetName()}}));
return false;
}
@ -109,7 +125,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (!bot->IsWithinLOSInMap(target))
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is not in my sight.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_target_not_in_sight_error",
"%target is not in my sight.",
{{"%target", target->GetName()}}));
return false;
}
@ -129,7 +148,10 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (sameTarget && inCombat && sameAttackMode)
{
if (verbose)
botAI->TellError("I am already attacking " + std::string(target->GetName()) + ".");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_already_attacking_error",
"I am already attacking %target.",
{{"%target", target->GetName()}}));
return false;
}
@ -137,7 +159,8 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
if (!bot->IsValidAttackTarget(target))
{
if (verbose)
botAI->TellError("I cannot attack an invalid target.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attack_invalid_target_error", "I cannot attack an invalid target.", {}));
return false;
}

View File

@ -12,8 +12,8 @@ bool AutoMaintenanceOnLevelupAction::Execute(Event /*event*/)
{
AutoPickTalents();
AutoLearnSpell();
AutoUpgradeEquip();
AutoTeleportForLevel();
AutoUpgradeEquip();
return true;
}
@ -21,13 +21,11 @@ bool AutoMaintenanceOnLevelupAction::Execute(Event /*event*/)
void AutoMaintenanceOnLevelupAction::AutoTeleportForLevel()
{
if (!sPlayerbotAIConfig.autoTeleportForLevel || !sRandomPlayerbotMgr.IsRandomBot(bot))
{
return;
}
if (botAI->HasRealPlayerMaster())
{
return;
}
sRandomPlayerbotMgr.RandomTeleportForLevel(bot);
return;
}
@ -89,21 +87,17 @@ void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out)
{
Quest const* quest = i->second;
// only process class-specific quests to learn class-related spells, cuz
// we don't want all these bunch of entries to be handled!
if (!quest->GetRequiredClasses())
if (!quest->GetRequiredClasses() || quest->IsRepeatable() || quest->GetMinLevel() < 10 ||
quest->GetMinLevel() > bot->GetLevel())
{
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) ||
!bot->SatisfyQuestSkill(quest, false))
{
continue;
// use the same logic and impl from Player::learnQuestRewardedSpells
}
int32 spellId = quest->GetRewSpellCast();
if (!spellId)
@ -113,31 +107,26 @@ void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out)
if (!spellInfo)
continue;
// xinef: find effect with learn spell and check if we have this spell
bool found = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{
if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && 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 (triggeredInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL)
break; // pussywizard: break and not cast the spell (found is false)
break;
found = true;
break;
}
}
// xinef: we know the spell, continue
if (!found)
continue;
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();
if (rewSpellId)
{
@ -167,12 +156,11 @@ std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* s
void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
{
if (!sPlayerbotAIConfig.autoUpgradeEquip || !sRandomPlayerbotMgr.IsRandomBot(bot))
if (!sRandomPlayerbotMgr.IsRandomBot(bot))
return;
PlayerbotFactory factory(bot, bot->GetLevel());
// Clean up old consumables before adding new ones
factory.CleanupConsumables();
factory.InitAmmo();
@ -181,9 +169,6 @@ void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
factory.InitConsumables();
factory.InitPotions();
if (!sPlayerbotAIConfig.equipmentPersistence || bot->GetLevel() < sPlayerbotAIConfig.equipmentPersistenceLevel)
{
if (sPlayerbotAIConfig.incrementalGearInit)
factory.InitEquipment(true);
}
if (sPlayerbotAIConfig.autoUpgradeEquip)
factory.InitEquipment(true);
}

View File

@ -7,6 +7,7 @@
#include "Event.h"
#include "ItemCountValue.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool BankAction::Execute(Event event)
@ -23,7 +24,8 @@ bool BankAction::Execute(Event event)
return ExecuteBank(text, npc);
}
botAI->TellError("Cannot find banker nearby");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bank_no_banker_nearby_error", "Cannot find banker nearby", {}));
return false;
}

View File

@ -1299,7 +1299,7 @@ std::string const BGTactics::HandleConsoleCommandPrivate(WorldSession* session,
Player* player = session->GetPlayer();
if (!player)
return "Error - session player not found";
if (player->GetSession()->GetSecurity() < SEC_GAMEMASTER)
if (!player->CanBeGameMaster())
return "Command can only be used by a GM";
Battleground* bg = player->GetBattleground();
if (!bg)

View File

@ -49,7 +49,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event)
uint32 account = bot->GetSession()->GetAccountId();
if (sPlayerbotAIConfig.IsInRandomAccountList(account) && botAI->GetMaster() &&
botAI->GetMaster()->GetSession()->GetSecurity() < SEC_GAMEMASTER)
!botAI->GetMaster()->CanBeGameMaster())
{
if (text.find("loot") != std::string::npos || text.find("gather") != std::string::npos)
{

View File

@ -7,6 +7,7 @@
#include "Event.h"
#include "Formations.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "PositionValue.h"
@ -85,7 +86,8 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
if (moved)
{
botAI->TellMaster("Following");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"following", "Following", {}));
return true;
}
}
@ -108,7 +110,8 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
}
*/
botAI->TellMaster("Following");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"following", "Following", {}));
return true;
}
@ -125,7 +128,8 @@ bool StayChatShortcutAction::Execute(Event /*event*/)
SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
botAI->TellMaster("Staying");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"staying", "Staying", {}));
return true;
}
@ -140,7 +144,8 @@ bool MoveFromGroupChatShortcutAction::Execute(Event /*event*/)
botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT);
botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT);
botAI->TellMaster("Moving away from group");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"move_from_group", "Moving away from group", {}));
return true;
}
@ -159,11 +164,13 @@ bool FleeChatShortcutAction::Execute(Event /*event*/)
if (bot->GetMapId() != master->GetMapId() || bot->GetDistance(master) > sPlayerbotAIConfig.sightDistance)
{
botAI->TellError("I will not flee with you - too far away");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"fleeing_far", "I will not flee with you - too far away", {}));
return true;
}
botAI->TellMaster("Fleeing");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"fleeing", "Fleeing", {}));
return true;
}
@ -180,7 +187,8 @@ bool GoawayChatShortcutAction::Execute(Event /*event*/)
ResetReturnPosition();
ResetStayPosition();
botAI->TellMaster("Running away");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"running_away", "Running away", {}));
return true;
}
@ -196,7 +204,8 @@ bool GrindChatShortcutAction::Execute(Event /*event*/)
ResetReturnPosition();
ResetStayPosition();
botAI->TellMaster("Grinding");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"grinding", "Grinding", {}));
return true;
}
@ -216,7 +225,8 @@ bool TankAttackChatShortcutAction::Execute(Event /*event*/)
ResetReturnPosition();
ResetStayPosition();
botAI->TellMaster("Attacking");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"attacking", "Attacking", {}));
return true;
}

View File

@ -7,6 +7,7 @@
#include "ChatHelper.h"
#include "Event.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool DropQuestAction::Execute(Event event)
@ -51,10 +52,15 @@ bool DropQuestAction::Execute(Event event)
const Quest* pQuest = sObjectMgr->GetQuestTemplate(entry);
const std::string text_quest = ChatHelper::FormatQuest(pQuest);
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), pQuest->GetTitle());
bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL);
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_removed_debug",
"Quest [%quest] removed",
{{"%quest", text_quest}});
bot->Say(text, LANG_UNIVERSAL);
}
botAI->TellMaster("Quest removed");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_remove", "Quest removed", {}));
return true;
}
@ -69,7 +75,10 @@ bool CleanQuestLogAction::Execute(Event event)
// Only output this message if "debug rpg" strategy is enabled
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
botAI->TellMaster("Clean Quest Log command received, removing grey/trivial quests...");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"clean_quest_log_started",
"Clean Quest Log command received, removing grey/trivial quests...",
{}));
uint8 botLevel = bot->GetLevel(); // Get bot's level
@ -103,7 +112,10 @@ bool CleanQuestLogAction::Execute(Event event)
{
// Output only if "debug rpg" strategy is enabled
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey).");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_trivial_will_remove",
"Quest [%title] will be removed because it is trivial (grey).",
{{"%title", quest->GetTitle()}}));
// Remove quest
botAI->rpgStatistic.questDropped++;
@ -116,17 +128,27 @@ bool CleanQuestLogAction::Execute(Event event)
{
const std::string text_quest = ChatHelper::FormatQuest(quest);
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle());
bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL);
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_removed_debug",
"Quest [%quest] removed",
{{"%quest", text_quest}});
bot->Say(text, LANG_UNIVERSAL);
}
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] has been removed.");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_has_been_removed",
"Quest [%title] has been removed.",
{{"%title", quest->GetTitle()}}));
}
else
{
// Only output if "debug rpg" strategy is enabled
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] is not trivial and will be kept.");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_not_trivial_kept",
"Quest [%title] is not trivial and will be kept.",
{{"%title", quest->GetTitle()}}));
}
}
@ -204,9 +226,16 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG
{
const std::string text_quest = ChatHelper::FormatQuest(quest);
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle());
bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL);
std::string text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_removed_debug",
"Quest [%quest] removed",
{{"%quest", text_quest}});
bot->Say(text, LANG_UNIVERSAL);
}
botAI->TellMaster("Quest removed" + chat->FormatQuest(quest));
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_removed_with_name",
"Quest removed %quest",
{{"%quest", chat->FormatQuest(quest)}}));
}
}

View File

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

View File

@ -8,6 +8,7 @@
#include "Player.h"
#include "Pet.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotTextMgr.h"
#include "CreatureAI.h"
#include "Playerbots.h"
#include "CharmInfo.h"
@ -49,7 +50,10 @@ bool MeleeAction::isUseful()
if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true))
return false;
return true;
// Do not start autoattack while prowled — let opener spells break stealth intentionally.
// Future rogue stealth implementation should use this instead:
// return !(botAI->HasAura("stealth", bot) || botAI->HasAura("prowl", bot));
return !botAI->HasAura("prowl", bot);
}
bool TogglePetSpellAutoCastAction::Execute(Event /*event*/)
@ -82,7 +86,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event /*event*/)
uint32 spellId = itr->first;
const SpellInfo* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo->IsAutocastable())
if (!spellInfo || !spellInfo->IsAutocastable())
continue;
bool shouldApply = true;
@ -178,7 +182,8 @@ bool SetPetStanceAction::Execute(Event /*event*/)
// If there are no controlled pets or guardians, notify the player and exit
if (targets.empty())
{
botAI->TellError("You have no pet or guardian pet.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"pet_no_pet_error", "You have no pet or guardian pet.", {}));
return false;
}

View File

@ -6,6 +6,7 @@
#include "GenericSpellActions.h"
#include <ctime>
#include <unordered_set>
#include "Event.h"
#include "ItemTemplate.h"
@ -23,11 +24,119 @@
using ai::buff::MakeAuraQualifierForBuff;
using ai::spell::HasSpellOrCategoryCooldown;
CastSpellAction::CastSpellAction(PlayerbotAI* botAI, std::string const spell)
: Action(botAI, spell), range(botAI->GetRange("spell")), spell(spell)
namespace
{
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*/)
{
if (spell == "conjure food" || spell == "conjure water")
@ -53,18 +162,12 @@ bool CastSpellAction::Execute(Event /*event*/)
wstrToLower(wnamepart);
if (!Utf8FitTo(spell, wnamepart))
continue;
if (spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
if (!Utf8FitTo(spell, wnamepart) || spellInfo->Effects[0].Effect != SPELL_EFFECT_CREATE_ITEM)
continue;
uint32 itemId = spellInfo->Effects[0].ItemType;
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
continue;
if (bot->CanUseItem(proto) != EQUIP_ERR_OK)
if (!proto || bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue;
if (spellInfo->Id > castId)
@ -92,10 +195,7 @@ bool CastSpellAction::isUseful()
}
Unit* spellTarget = GetTarget();
if (!spellTarget)
return false;
if (!spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
if (!spellTarget || !spellTarget->IsInWorld() || spellTarget->GetMapId() != bot->GetMapId())
return false;
// float combatReach = bot->GetCombatReach() + target->GetCombatReach();
@ -143,10 +243,7 @@ CastMeleeSpellAction::CastMeleeSpellAction(
bool CastMeleeSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target)
return false;
if (!bot->IsWithinMeleeRange(target))
if (!target || !bot->IsWithinMeleeRange(target))
return false;
return CastSpellAction::isUseful();
@ -162,10 +259,7 @@ CastMeleeDebuffSpellAction::CastMeleeDebuffSpellAction(
bool CastMeleeDebuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target)
return false;
if (!bot->IsWithinMeleeRange(target))
if (!target || !bot->IsWithinMeleeRange(target))
return false;
return CastDebuffSpellAction::isUseful();
@ -175,14 +269,55 @@ bool CastAuraSpellAction::isUseful()
{
if (!GetTarget() || !CastSpellAction::isUseful())
return false;
Aura* aura = botAI->GetAura(spell, GetTarget(), isOwner, checkDuration);
if (!aura)
return true;
if (beforeDuration && aura->GetDuration() < beforeDuration)
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
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(
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);
}
// Make Bots Paladin, druid, mage use the greater buff rank spell
// TODO Priest doen't verify il he have components
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));
}
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(
PlayerbotAI* botAI) : CastSpellAction(botAI, "shoot"), shootSpellId(0)
{
@ -365,50 +491,32 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
bool CastEveryManForHimselfAction::isPossible()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId)
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
}
bool CastEveryManForHimselfAction::isUseful()
{
return (bot->HasAuraType(SPELL_AURA_MOD_STUN) ||
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM))
&& CastSpellAction::isUseful();
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM))
&& CastSpellAction::isUseful();
}
bool CastWillOfTheForsakenAction::isPossible()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId)
return false;
if (!bot->HasSpell(spellId))
return false;
if (HasSpellOrCategoryCooldown(bot, spellId))
return false;
return true;
return spellId && bot->HasSpell(spellId) && !HasSpellOrCategoryCooldown(bot, spellId);
}
bool CastWillOfTheForsakenAction::isUseful()
{
return (bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM) ||
bot->HasAuraType(SPELL_AURA_AOE_CHARM) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP))
&& CastSpellAction::isUseful();
bot->HasAuraType(SPELL_AURA_MOD_CHARM) ||
bot->HasAuraType(SPELL_AURA_AOE_CHARM) ||
bot->HasAuraWithMechanic(1 << MECHANIC_SLEEP))
&& CastSpellAction::isUseful();
}
bool UseTrinketAction::Execute(Event /*event*/)
@ -427,70 +535,141 @@ bool UseTrinketAction::Execute(Event /*event*/)
bool UseTrinketAction::UseTrinket(Item* item)
{
if (bot->CanUseItem(item) != EQUIP_ERR_OK)
return false;
if (bot->IsNonMeleeSpellCast(true))
if (bot->CanUseItem(item) != EQUIP_ERR_OK || bot->IsNonMeleeSpellCast(true))
return false;
uint8 bagIndex = item->GetBagSlot();
uint8 slot = item->GetSlot();
// uint8 spell_index = 0; //not used, line marked for removal.
uint8 cast_count = 1;
ObjectGuid item_guid = item->GetGUID();
uint32 glyphIndex = 0;
uint8 castFlags = 0;
uint32 targetFlag = TARGET_FLAG_NONE;
uint32 spellId = 0;
int32 itemSpellCooldown = 0;
uint32 itemSpellCategory = 0;
int32 itemSpellCategoryCooldown = 0;
for (uint8 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
{
if (item->GetTemplate()->Spells[i].SpellId > 0 &&
item->GetTemplate()->Spells[i].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE)
{
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())
return false;
bool applyAura = false;
bool restoresMana = false;
bool improvesManaEfficiency = false;
bool defensiveTankEffect = false;
for (int i = 0; i < MAX_SPELL_EFFECTS; i++)
{
const SpellEffectInfo& effectInfo = spellInfo->Effects[i];
if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA)
{
applyAura = true;
break;
restoresMana = restoresMana || IsManaRestoreEffect(effectInfo);
improvesManaEfficiency = improvesManaEfficiency || IsManaEfficiencyEffect(effectInfo);
defensiveTankEffect = defensiveTankEffect || IsDefensiveTankEffect(effectInfo);
}
if (!applyAura && !restoresMana)
return false;
if (restoresMana || improvesManaEfficiency)
{
if (!AI_VALUE2(bool, "has mana", "self target"))
return false;
uint8 const manaPct = AI_VALUE2(uint8, "mana", "self target");
if ((restoresMana && manaPct >= sPlayerbotAIConfig.mediumMana) ||
manaPct >= sPlayerbotAIConfig.highMana)
{
return false;
}
}
if (!applyAura)
return false;
uint32 spellProcFlag = spellInfo->ProcFlags;
// Handle items with procflag "if you kill a target that grants honor or experience"
// Bots will "learn" the trinket proc, so CanCastSpell() will be true
// e.g. on Item https://www.wowhead.com/wotlk/item=44074/oracle-talisman-of-ablution leading to
// 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))
if (defensiveTankEffect)
{
return false;
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;
}
}
if (!spellId)
return false;
WorldPacket packet(CMSG_USE_ITEM);
packet << bagIndex << slot << cast_count << spellId << item_guid << glyphIndex << castFlags;
targetFlag = TARGET_FLAG_NONE;
packet << targetFlag << bot->GetPackGUID();
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;
}
@ -500,9 +679,8 @@ bool CastDebuffSpellAction::isUseful()
{
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false;
}
return CastAuraSpellAction::isUseful() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) >= needLifeTime;
}

View File

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

View File

@ -8,6 +8,7 @@
#include "Event.h"
#include "GuildPackets.h"
#include "PlayerbotSecurity.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool GuildAcceptAction::Execute(Event event)
@ -28,17 +29,20 @@ bool GuildAcceptAction::Execute(Event event)
uint32 guildId = inviter->GetGuildId();
if (!guildId)
{
botAI->TellError("You are not in a guild!");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"guild_accept_inviter_not_in_guild", "You are not in a guild!", {}));
accept = false;
}
else if (bot->GetGuildId())
{
botAI->TellError("Sorry, I am in a guild already");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"guild_accept_already_in_guild", "Sorry, I am in a guild already", {}));
accept = false;
}
else if (!botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, inviter, true))
{
botAI->TellError("Sorry, I don't want to join your guild :(");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"guild_accept_declined", "Sorry, I don't want to join your guild :(", {}));
accept = false;
}

View File

@ -308,9 +308,8 @@ bool LfgAction::Execute(Event event)
allowedRoles[BOT_ROLE_HEALER] = 1;
allowedRoles[BOT_ROLE_DPS] = 3;
BotRoles role = botAI->IsTank(requester, false)
? BOT_ROLE_TANK
: (botAI->IsHeal(requester, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
BotRoles role = botAI->IsTank(requester, true) ? BOT_ROLE_TANK
: (botAI->IsHeal(requester, true) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
Classes cls = (Classes)requester->getClass();
if (group)
@ -383,8 +382,8 @@ bool LfgAction::Execute(Event event)
if (!botAI->IsSafe(player))
return false;
role = botAI->IsTank(player, false) ? BOT_ROLE_TANK
: (botAI->IsHeal(player, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
role = botAI->IsTank(player, true) ? BOT_ROLE_TANK
: (botAI->IsHeal(player, true) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
cls = (Classes)player->getClass();
if (allowedRoles[role] > 0)
@ -403,7 +402,7 @@ bool LfgAction::Execute(Event event)
allowedClassNr[cls][role]--;
}
role = botAI->IsTank(bot, false) ? BOT_ROLE_TANK : (botAI->IsHeal(bot, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
role = botAI->IsTank(bot, true) ? BOT_ROLE_TANK : (botAI->IsHeal(bot, true) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
cls = (Classes)bot->getClass();
if (allowedRoles[role] == 0)

View File

@ -7,6 +7,7 @@
#include "Event.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool LeaveGroupAction::Execute(Event event)
@ -86,7 +87,9 @@ bool LeaveGroupAction::Leave()
Player* master = botAI -> GetMaster();
if (master)
botAI->TellMaster("Goodbye!", PLAYERBOT_SECURITY_TALK);
botAI->TellMaster(
PlayerbotTextMgr::instance().GetBotTextOrDefault("goodbye", "Goodbye!", {}),
PLAYERBOT_SECURITY_TALK);
botAI->LeaveOrDisbandGroup();
return true;

View File

@ -7,6 +7,8 @@
#include "Event.h"
#include "ItemVisitors.h"
#include "PlayerbotTextMgr.h"
#include "PlayerbotRepository.h"
#include "Playerbots.h"
#include "ItemPackets.h"
@ -17,9 +19,12 @@ bool OutfitAction::Execute(Event event)
if (param == "?")
{
List();
botAI->TellMaster("outfit <name> +[item] to add items");
botAI->TellMaster("outfit <name> -[item] to remove items");
botAI->TellMaster("outfit <name> equip/replace to equip items");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_usage_add", "outfit <name> +[item] to add items", {}));
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_usage_remove", "outfit <name> -[item] to remove items", {}));
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_usage_equip", "outfit <name> equip/replace to equip items", {}));
}
else
{
@ -28,10 +33,13 @@ bool OutfitAction::Execute(Event event)
if (!name.empty())
{
Save(name, items);
PlayerbotRepository::instance().Save(botAI);
std::ostringstream out;
out << "Setting outfit " << name << " as " << param;
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_set_as",
"Setting outfit %name as %param",
{{"%name", name}, {"%param", param}}));
return true;
}
@ -47,18 +55,20 @@ bool OutfitAction::Execute(Event event)
std::string const command = param.substr(space + 1);
if (command == "equip")
{
std::ostringstream out;
out << "Equipping outfit " << name;
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_equipping",
"Equipping outfit %name",
{{"%name", name}}));
EquipItems(outfit);
return true;
}
else if (command == "replace")
{
std::ostringstream out;
out << "Replacing current equip with outfit " << name;
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_replace_current",
"Replacing current equip with outfit %name",
{{"%name", name}}));
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++)
{
@ -81,20 +91,24 @@ bool OutfitAction::Execute(Event event)
}
else if (command == "reset")
{
std::ostringstream out;
out << "Resetting outfit " << name;
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_resetting",
"Resetting outfit %name",
{{"%name", name}}));
Save(name, ItemIds());
PlayerbotRepository::instance().Save(botAI);
return true;
}
else if (command == "update")
{
std::ostringstream out;
out << "Updating with current items outfit " << name;
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_updating_current",
"Updating with current items outfit %name",
{{"%name", name}}));
Update(name);
PlayerbotRepository::instance().Save(botAI);
return true;
}
@ -103,27 +117,29 @@ bool OutfitAction::Execute(Event event)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemid);
std::ostringstream out;
out << chat->FormatItem(proto);
if (remove)
{
std::set<uint32>::iterator j = outfit.find(itemid);
if (j != outfit.end())
outfit.erase(j);
out << " removed from ";
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_item_removed_from",
"%item removed from %name",
{{"%item", chat->FormatItem(proto)}, {"%name", name}}));
}
else
{
outfit.insert(itemid);
out << " added to ";
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"outfit_item_added_to",
"%item added to %name",
{{"%item", chat->FormatItem(proto)}, {"%name", name}}));
}
out << name;
botAI->TellMaster(out.str());
}
Save(name, outfit);
PlayerbotRepository::instance().Save(botAI);
}
return true;

View File

@ -10,6 +10,7 @@
#include "NearestNpcsValue.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "ServerFacade.h"
#include "Corpse.h"
@ -22,7 +23,8 @@ bool ReleaseSpiritAction::Execute(Event event)
{
if (!bot->InBattleground())
{
botAI->TellMasterNoFacing("I am not dead, will wait here");
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"release_spirit_not_dead_wait", "I am not dead, will wait here", {}));
// -follow in bg is overwriten each tick with +follow
// +stay in bg causes stuttering effect as bot is cycled between +stay and +follow each tick
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
@ -33,14 +35,15 @@ bool ReleaseSpiritAction::Execute(Event event)
if (bot->GetCorpse() && bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
{
botAI->TellMasterNoFacing("I am already a spirit");
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"release_spirit_already_spirit", "I am already a spirit", {}));
return false;
}
const WorldPacket& packet = event.getPacket();
const std::string message = !packet.empty() && packet.GetOpcode() == CMSG_REPOP_REQUEST
? "Releasing..."
: "Meet me at the graveyard";
? PlayerbotTextMgr::instance().GetBotTextOrDefault("release_spirit_releasing", "Releasing...", {})
: PlayerbotTextMgr::instance().GetBotTextOrDefault("release_spirit_meet_graveyard", "Meet me at the graveyard", {});
botAI->TellMasterNoFacing(message);
IncrementDeathCount();

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);
botAI->ResetStrategies(false);
botAI->TellMaster("AI was reset to defaults");

View File

@ -9,6 +9,7 @@
#include "FleeManager.h"
#include "GameGraveyard.h"
#include "MapMgr.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "RandomPlayerbotMgr.h"
#include "ServerFacade.h"
@ -322,7 +323,7 @@ bool SpiritHealerAction::Execute(Event /*event*/)
bot->SpawnCorpseBones();
context->GetValue<Unit*>("current target")->Set(nullptr);
bot->SetTarget();
botAI->TellMaster("Hello");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault("hello", "Hello", {}));
if (dCount > 20)
context->GetValue<uint32>("death count")->Set(0);

View File

@ -12,7 +12,7 @@ bool SecurityCheckAction::isUseful()
{
return RandomPlayerbotMgr::instance().IsRandomBot(bot)
&& botAI->GetMaster()
&& botAI->GetMaster()->GetSession()->GetSecurity() < SEC_GAMEMASTER
&& !botAI->GetMaster()->CanBeGameMaster()
&& !GET_PLAYERBOT_AI(botAI->GetMaster());
}

View File

@ -9,6 +9,7 @@
#include "Event.h"
#include "ItemVisitors.h"
#include "Mail.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool SendMailAction::Execute(Event event)
@ -33,34 +34,37 @@ bool SendMailAction::Execute(Event event)
Player* receiver = GetMaster();
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)
receiver = event.getOwner();
if (!receiver || receiver == bot)
{
return false;
}
if (!tellTo)
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)
{
bot->Whisper("There is no mailbox nearby", LANG_UNIVERSAL, tellTo);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_no_mailbox_nearby", "There is no mailbox nearby", {}),
LANG_UNIVERSAL, tellTo);
return false;
}
ItemIds ids = chat->parseItems(text);
if (ids.size() > 1)
{
bot->Whisper("You can not request more than one item", LANG_UNIVERSAL, tellTo);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_one_item_only", "You can not request more than one item", {}),
LANG_UNIVERSAL, tellTo);
return false;
}
@ -72,13 +76,16 @@ bool SendMailAction::Execute(Event event)
if (randomBot)
{
bot->Whisper("I cannot send money", LANG_UNIVERSAL, tellTo);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_cannot_send_money", "I cannot send money", {}),
LANG_UNIVERSAL, tellTo);
return false;
}
if (bot->GetMoney() < money)
{
botAI->TellError("I don't have enough money");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_not_enough_money", "I don't have enough money", {}));
return false;
}
@ -100,8 +107,10 @@ bool SendMailAction::Execute(Event event)
CharacterDatabase.CommitTransaction(trans);
std::ostringstream out;
out << "Sending mail to " << receiver->GetName();
botAI->TellMaster(out.str());
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_sending_to",
"Sending mail to %receiver",
{{"%receiver", receiver->GetName()}}));
return true;
}
@ -125,7 +134,10 @@ bool SendMailAction::Execute(Event event)
if (item->IsSoulBound() || item->IsConjuredConsumable())
{
std::ostringstream out;
out << "Cannot send " << ChatHelper::FormatItem(item->GetTemplate());
out << PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_cannot_send_item",
"Cannot send %item",
{{"%item", ChatHelper::FormatItem(item->GetTemplate())}});
bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo);
continue;
}
@ -140,7 +152,10 @@ bool SendMailAction::Execute(Event event)
if (!price)
{
std::ostringstream out;
out << ChatHelper::FormatItem(item->GetTemplate()) << ": it is not for sale";
out << PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_item_not_for_sale",
"%item: it is not for sale",
{{"%item", ChatHelper::FormatItem(item->GetTemplate())}});
bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo);
return false;
}
@ -160,7 +175,10 @@ bool SendMailAction::Execute(Event event)
CharacterDatabase.CommitTransaction(trans);
std::ostringstream out;
out << "Sent mail to " << receiver->GetName();
out << PlayerbotTextMgr::instance().GetBotTextOrDefault(
"send_mail_sent_to",
"Sent mail to %receiver",
{{"%receiver", receiver->GetName()}});
bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo);
return true;
}

View File

@ -8,6 +8,7 @@
#include "ChatHelper.h"
#include "CraftValue.h"
#include "Event.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
std::map<uint32, SkillLineAbilityEntry const*> SetCraftAction::skillSpells;
@ -24,7 +25,8 @@ bool SetCraftAction::Execute(Event event)
if (link == "reset")
{
data.Reset();
botAI->TellMaster("I will not craft anything");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"craft_reset", "I will not craft anything", {}));
return true;
}
@ -37,7 +39,8 @@ bool SetCraftAction::Execute(Event event)
ItemIds itemIds = chat->parseItems(link);
if (itemIds.empty())
{
botAI->TellMaster("Usage: 'craft [itemId]' or 'craft reset'");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"craft_usage", "Usage: 'craft [itemId]' or 'craft reset'", {}));
return false;
}
@ -94,7 +97,8 @@ bool SetCraftAction::Execute(Event event)
if (data.required.empty())
{
botAI->TellMaster("I cannot craft this");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"craft_cannot_craft", "I cannot craft this", {}));
return false;
}
@ -109,7 +113,8 @@ void SetCraftAction::TellCraft()
CraftData& data = AI_VALUE(CraftData&, "craft");
if (data.IsEmpty())
{
botAI->TellMaster("I will not craft anything");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"craft_reset", "I will not craft anything", {}));
return;
}
@ -117,8 +122,7 @@ void SetCraftAction::TellCraft()
if (!proto)
return;
std::ostringstream out;
out << "I will craft " << chat->FormatItem(proto) << " using reagents: ";
std::ostringstream reagentsOut;
bool first = true;
for (std::map<uint32, uint32>::iterator i = data.required.begin(); i != data.required.end(); ++i)
@ -130,20 +134,23 @@ void SetCraftAction::TellCraft()
{
if (first)
first = false;
else
out << ", ";
reagentsOut << ", ";
out << chat->FormatItem(reagent, required);
reagentsOut << chat->FormatItem(reagent, required);
uint32 given = data.obtained[item];
if (given)
out << "|cffffff00(x" << given << " given)|r ";
reagentsOut << "|cffffff00(x" << given << " given)|r ";
}
}
out << " (craft fee: " << chat->formatMoney(GetCraftFee(data)) << ")";
botAI->TellMaster(out.str());
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"craft_summary",
"I will craft %item using reagents: %reagents (craft fee: %money)",
{{"%item", chat->FormatItem(proto)},
{"%reagents", reagentsOut.str()},
{"%money", chat->formatMoney(GetCraftFee(data))}}));
}
uint32 SetCraftAction::GetCraftFee(CraftData& data)

View File

@ -6,6 +6,7 @@
#include "SetHomeAction.h"
#include "Event.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool SetHomeAction::Execute(Event /*event*/)
@ -28,7 +29,8 @@ bool SetHomeAction::Execute(Event /*event*/)
{
Creature* creature = botAI->GetCreature(selection);
bot->GetSession()->SendBindPoint(creature);
botAI->TellMaster("This inn is my new home");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"set_home_success", "This inn is my new home", {}));
return true;
}
@ -40,10 +42,12 @@ bool SetHomeAction::Execute(Event /*event*/)
continue;
bot->GetSession()->SendBindPoint(unit);
botAI->TellMaster("This inn is my new home");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"set_home_success", "This inn is my new home", {}));
return true;
}
botAI->TellError("Can't find any innkeeper around");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"set_home_no_innkeeper_error", "Can't find any innkeeper around", {}));
return false;
}

View File

@ -6,6 +6,7 @@
#include "ShareQuestAction.h"
#include "Event.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool ShareQuestAction::Execute(Event event)
@ -32,7 +33,8 @@ bool ShareQuestAction::Execute(Event event)
WorldPacket p;
p << entry;
bot->GetSession()->HandlePushQuestToParty(p);
botAI->TellMaster("Quest shared");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_shared", "Quest shared", {}));
return true;
}
}
@ -98,7 +100,8 @@ bool AutoShareQuestAction::Execute(Event /*event*/)
WorldPacket p;
p << logQuest;
bot->GetSession()->HandlePushQuestToParty(p);
botAI->TellMaster("Quest shared");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"quest_shared", "Quest shared", {}));
shared = true;
}

View File

@ -15,6 +15,7 @@
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotFactory.h"
#include "PlayerbotTextMgr.h"
#include "SpellMgr.h"
#include "WorldSession.h"
@ -123,7 +124,8 @@ bool TameAction::Execute(Event event)
}
catch (...)
{
botAI->TellError("Invalid tame id.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_invalid_id_error", "Invalid tame id.", {}));
}
}
else if (mode == "family" && !value.empty())
@ -137,8 +139,10 @@ bool TameAction::Execute(Event event)
else
{
// Unrecognized command or missing argument; show usage
botAI->TellError(
"Usage: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_usage_error",
"Usage: tame name <name> | tame id <id> | tame family <family> | tame rename <new name> | tame abandon",
{}));
return false;
}
@ -157,12 +161,15 @@ bool TameAction::Execute(Event event)
if (!lastPetName.empty() && lastPetId != 0)
{
std::ostringstream oss;
oss << "Pet changed to " << lastPetName << ", ID: " << lastPetId << ".";
botAI->TellMaster(oss.str());
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_changed",
"Pet changed to %name, ID: %id.",
{{"%name", lastPetName}, {"%id", std::to_string(lastPetId)}}));
}
else
{
botAI->TellMaster("Pet changed and initialized!");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_changed_initialized", "Pet changed and initialized!", {}));
}
}
@ -197,7 +204,10 @@ bool TameAction::SetPetByName(const std::string& name)
// If the creature is exotic and the bot doesn't have Beast Mastery, show error and fail
if (IsExoticPet(&creature) && !HasBeastMastery(bot))
{
botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_exotic_requires_beast_mastery",
"I cannot use exotic pets unless I have the Beast Mastery talent.",
{}));
return false;
}
@ -214,7 +224,8 @@ bool TameAction::SetPetByName(const std::string& name)
}
// If no suitable pet found, show an error and return failure
botAI->TellError("No tameable pet found with name: " + name);
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_pet_by_name", "No tameable pet found with name: %name", {{"%name", name}}));
return false;
}
@ -231,21 +242,26 @@ bool TameAction::SetPetById(uint32 id)
if (!creature->IsTameable(true))
{
// If not tameable at all, show an error and fail
botAI->TellError("No tameable pet found with id: " + std::to_string(id));
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_pet_by_id", "No tameable pet found with id: %id", {{"%id", std::to_string(id)}}));
return false;
}
// If it's an exotic pet, make sure the bot has the Beast Mastery talent
if (IsExoticPet(creature) && !HasBeastMastery(bot))
{
botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_exotic_requires_beast_mastery",
"I cannot use exotic pets unless I have the Beast Mastery talent.",
{}));
return false;
}
// Check if the bot is actually allowed to tame this pet (honoring exotic pet rules)
if (!creature->IsTameable(bot->CanTameExoticPets()))
{
botAI->TellError("No tameable pet found with id: " + std::to_string(id));
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_pet_by_id", "No tameable pet found with id: %id", {{"%id", std::to_string(id)}}));
return false;
}
@ -257,7 +273,8 @@ bool TameAction::SetPetById(uint32 id)
}
// If no valid creature was found by id, show an error
botAI->TellError("No tameable pet found with id: " + std::to_string(id));
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_pet_by_id", "No tameable pet found with id: %id", {{"%id", std::to_string(id)}}));
return false;
}
@ -315,9 +332,13 @@ bool TameAction::SetPetByFamily(const std::string& family)
if (candidates.empty())
{
if (foundExotic && !HasBeastMastery(bot))
botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_exotic_requires_beast_mastery",
"I cannot use exotic pets unless I have the Beast Mastery talent.",
{}));
else
botAI->TellError("No tameable pet found with family: " + family);
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_pet_by_family", "No tameable pet found with family: %family", {{"%family", family}}));
return false;
}
@ -342,14 +363,18 @@ bool TameAction::RenamePet(const std::string& newName)
// Check if the bot currently has a pet
if (!pet)
{
botAI->TellError("You have no pet to rename.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_pet_to_rename", "You have no pet to rename.", {}));
return false;
}
// Validate the new name: must not be empty and max 12 characters
if (newName.empty() || newName.length() > 12)
{
botAI->TellError("Pet name must be between 1 and 12 alphabetic characters.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_name_length_error",
"Pet name must be between 1 and 12 alphabetic characters.",
{}));
return false;
}
@ -358,7 +383,10 @@ bool TameAction::RenamePet(const std::string& newName)
{
if (!std::isalpha(static_cast<unsigned char>(c)))
{
botAI->TellError("Pet name must only contain alphabetic characters (A-Z, a-z).");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_name_alpha_error",
"Pet name must only contain alphabetic characters (A-Z, a-z).",
{}));
return false;
}
}
@ -372,7 +400,10 @@ bool TameAction::RenamePet(const std::string& newName)
// Check if the new name is reserved or forbidden
if (sObjectMgr->IsReservedName(normalized))
{
botAI->TellError("That pet name is forbidden. Please choose another name.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_name_forbidden_error",
"That pet name is forbidden. Please choose another name.",
{}));
return false;
}
@ -382,8 +413,12 @@ bool TameAction::RenamePet(const std::string& newName)
bot->GetSession()->SendPetNameQuery(pet->GetGUID(), pet->GetEntry());
// Notify the master about the rename and give a tip to update the client name display
botAI->TellMaster("Your pet has been renamed to " + normalized + "!");
botAI->TellMaster("If you do not see the new name, please dismiss and recall your pet.");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_renamed", "Your pet has been renamed to %name!", {{"%name", normalized}}));
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_rename_refresh_hint",
"If you do not see the new name, please dismiss and recall your pet.",
{}));
// Remove the current pet and (re-)cast Call Pet spell if the bot is a hunter
bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT, true);
@ -401,7 +436,8 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry)
// Ensure the player is a hunter and at least level 10 (required for pets)
if (bot->getClass() != CLASS_HUNTER || bot->GetLevel() < 10)
{
botAI->TellError("Only level 10+ hunters can have pets.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_only_hunters_level_10", "Only level 10+ hunters can have pets.", {}));
return false;
}
@ -409,7 +445,8 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry)
CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(creatureEntry);
if (!creature)
{
botAI->TellError("Creature template not found.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_creature_template_not_found", "Creature template not found.", {}));
return false;
}
@ -430,7 +467,8 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry)
Pet* pet = bot->CreateTamedPetFrom(creatureEntry, 0);
if (!pet)
{
botAI->TellError("Failed to create pet.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_create_pet_failed", "Failed to create pet.", {}));
return false;
}
@ -485,13 +523,15 @@ bool TameAction::AbandonPet()
// Remove the pet from the bot and mark it as deleted in the database
bot->RemovePet(pet, PET_SAVE_AS_DELETED);
// Inform the bot's master/player that the pet was abandoned
botAI->TellMaster("Your pet has been abandoned.");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_pet_abandoned", "Your pet has been abandoned.", {}));
return true;
}
else
{
// If there is no hunter pet, show an error message
botAI->TellError("You have no hunter pet to abandon.");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"tame_no_hunter_pet_to_abandon", "You have no hunter pet to abandon.", {}));
return false;
}
}

View File

@ -7,6 +7,7 @@
#include "Event.h"
#include "LastMovementValue.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "PlayerbotAIConfig.h"
#include "Config.h"
@ -24,7 +25,8 @@ bool TaxiAction::Execute(Event event)
{
movement.taxiNodes.clear();
movement.Set(nullptr);
botAI->TellMaster("I am ready for the next flight");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"taxi_ready_next_flight", "I am ready for the next flight", {}));
return true;
}
@ -120,13 +122,15 @@ bool TaxiAction::Execute(Event event)
{
movement.taxiNodes.clear();
movement.Set(nullptr);
botAI->TellError("I can't fly with you");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"taxi_cant_fly_with_you", "I can't fly with you", {}));
return false;
}
return true;
}
botAI->TellError("Cannot find any flightmaster to talk");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"taxi_no_flightmaster_nearby", "Cannot find any flightmaster to talk", {}));
return false;
}

View File

@ -12,6 +12,7 @@
#include "ItemVisitors.h"
#include "PlayerbotMgr.h"
#include "PlayerbotSecurity.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "RandomPlayerbotMgr.h"
#include "SetCraftAction.h"
@ -28,13 +29,17 @@ bool TradeStatusAction::Execute(Event event)
// Allow the master and group members to trade
if (trader != master && !traderBotAI && (!bot->GetGroup() || !bot->GetGroup()->IsMember(trader->GetGUID())))
{
bot->Whisper("I'm kind of busy now", LANG_UNIVERSAL, trader);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_busy_now", "I'm kind of busy now", {}),
LANG_UNIVERSAL, trader);
return false;
}
if (sPlayerbotAIConfig.enableRandomBotTrading == 0 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot)))
{
bot->Whisper("Trading is disabled", LANG_UNIVERSAL, trader);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_disabled", "Trading is disabled", {}),
LANG_UNIVERSAL, trader);
return false;
}
@ -180,9 +185,15 @@ bool TradeStatusAction::CheckTrade()
{
if (bot->GetGroup() && bot->GetGroup()->IsMember(bot->GetTrader()->GetGUID()) &&
botAI->HasRealPlayerMaster())
botAI->TellMasterNoFacing("Thank you " + chat->FormatWorldobject(bot->GetTrader()));
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_thank_you_player",
"Thank you %player",
{{"%player", chat->FormatWorldobject(bot->GetTrader())}}));
else
bot->Say("Thank you " + chat->FormatWorldobject(bot->GetTrader()),
bot->Say(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_thank_you_player",
"Thank you %player",
{{"%player", chat->FormatWorldobject(bot->GetTrader())}}),
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
}
return isGettingItem;
@ -210,12 +221,16 @@ bool TradeStatusAction::CheckTrade()
int32 playerMoney = trader->GetTradeData()->GetMoney() + playerItemsMoney;
if (botItemsMoney > 0 && sPlayerbotAIConfig.enableRandomBotTrading == 2 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot)))
{
bot->Whisper("Selling is disabled.", LANG_UNIVERSAL, trader);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_selling_disabled", "Selling is disabled.", {}),
LANG_UNIVERSAL, trader);
return false;
}
if (playerItemsMoney && sPlayerbotAIConfig.enableRandomBotTrading == 3 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot)))
{
bot->Whisper("Buying is disabled.", LANG_UNIVERSAL, trader);
bot->Whisper(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_buying_disabled", "Buying is disabled.", {}),
LANG_UNIVERSAL, trader);
return false;
}
for (uint32 slot = 0; slot < TRADE_SLOT_TRADED_COUNT; ++slot)
@ -224,8 +239,10 @@ bool TradeStatusAction::CheckTrade()
if (item && !item->GetTemplate()->SellPrice && !item->GetTemplate()->IsConjuredConsumable())
{
std::ostringstream out;
out << chat->FormatItem(item->GetTemplate()) << " - This is not for sale";
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_item_not_for_sale",
"%item - This is not for sale",
{{"%item", chat->FormatItem(item->GetTemplate())}}));
botAI->PlaySound(TEXT_EMOTE_NO);
return false;
}
@ -239,8 +256,10 @@ bool TradeStatusAction::CheckTrade()
if ((botMoney && !item->GetTemplate()->BuyPrice) || usage == ITEM_USAGE_NONE)
{
std::ostringstream out;
out << chat->FormatItem(item->GetTemplate()) << " - I don't need this";
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_item_not_needed",
"%item - I don't need this",
{{"%item", chat->FormatItem(item->GetTemplate())}}));
botAI->PlaySound(TEXT_EMOTE_NO);
return false;
}
@ -252,7 +271,8 @@ bool TradeStatusAction::CheckTrade()
if (!botItemsMoney && !playerItemsMoney)
{
botAI->TellError("There are no items to trade");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_no_items_error", "There are no items to trade", {}));
return false;
}
@ -266,7 +286,8 @@ bool TradeStatusAction::CheckTrade()
{
if (moneyDelta < 0)
{
botAI->TellError("You can use discount to buy items only");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_discount_buy_only", "You can use discount to buy items only", {}));
botAI->PlaySound(TEXT_EMOTE_NO);
return false;
}
@ -282,16 +303,20 @@ bool TradeStatusAction::CheckTrade()
switch (urand(0, 4))
{
case 0:
botAI->TellMaster("A pleasure doing business with you");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_success_pleasure", "A pleasure doing business with you", {}));
break;
case 1:
botAI->TellMaster("Fair trade");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_success_fair_trade", "Fair trade", {}));
break;
case 2:
botAI->TellMaster("Thanks");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_success_thanks", "Thanks", {}));
break;
case 3:
botAI->TellMaster("Off with you");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_success_off_with_you", "Off with you", {}));
break;
}
@ -300,8 +325,10 @@ bool TradeStatusAction::CheckTrade()
}
std::ostringstream out;
out << "I want " << chat->formatMoney(-(delta + discount)) << " for this";
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"trade_want_money_for_this",
"I want %money for this",
{{"%money", chat->formatMoney(-(delta + discount))}}));
botAI->PlaySound(TEXT_EMOTE_NO);
return false;
}

View File

@ -5,10 +5,14 @@
#include "TrainerAction.h"
#include "AiFactory.h"
#include "BisListMgr.h"
#include "BudgetValues.h"
#include "Event.h"
#include "PlayerbotFactory.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "ReputationMgr.h"
#include "Trainer.h"
bool TrainerAction::Execute(Event event)
@ -269,6 +273,282 @@ bool MaintenanceAction::Execute(Event /*event*/)
return true;
}
bool BisGearAction::RunAutogearFallback(uint16 effectiveIlvl)
{
if (!sPlayerbotAIConfig.autoGearCommand)
{
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_autogear_unavailable_error",
"autogear command is not allowed, please check the configuration.", {}));
return false;
}
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_no_rows_fallback",
"No BiS for your tier/spec/level, check cfg, running autogear instead", {}));
// Wipe all equipped slots so autogear gears from scratch at the requested ilvl
// (avoids old high-tier items surviving the incremental 1.2x threshold).
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot)
{
if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY)
continue;
if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true);
}
uint32 gs = effectiveIlvl == 0
? 0
: PlayerbotFactory::CalcMixedGearScore(effectiveIlvl, sPlayerbotAIConfig.autoGearQualityLimit);
PlayerbotFactory factory(bot, bot->GetLevel(), sPlayerbotAIConfig.autoGearQualityLimit, gs);
factory.InitEquipment(false, sPlayerbotAIConfig.twoRoundsGearInit);
factory.InitAmmo();
if (bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel)
factory.ApplyEnchantAndGemsNew();
bot->DurabilityRepairAll(false, 1.0f, false);
return true;
}
bool BisGearAction::Execute(Event event)
{
if (!sPlayerbotAIConfig.autoGearBisCommand)
{
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_command_unavailable_error",
"bis command is not allowed, please check the configuration.", {}));
return false;
}
if (!sPlayerbotAIConfig.autoGearCommandAltBots &&
!sPlayerbotAIConfig.IsInRandomAccountList(bot->GetSession()->GetAccountId()))
{
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_altbot_refused_error", "You cannot use bis on alt bots.", {}));
return false;
}
if (sPlayerbotAIConfig.autoGearQualityLimit < 4)
{
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_quality_floor_error", "AutoGearQualityLimit must be 4 for BiS.", {}));
return false;
}
if (sRandomPlayerbotMgr.IsSpecPvp(bot->GetGUID().GetCounter(), bot->getClass()))
{
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_pvp_refused_error", "bis is PvE only, bot is configured as PvP.", {}));
return false;
}
uint16 ilvl = static_cast<uint16>(sPlayerbotAIConfig.autoGearScoreLimit);
// Optional explicit ilvl override: `/p autogear bis 55`.
// Garbage or out-of-range args are hard-rejected: no autogear fallback, no gear change.
std::string const param = event.getParam();
if (!param.empty())
{
unsigned long parsed = 0;
size_t pos = 0;
bool valid = false;
try
{
parsed = std::stoul(param, &pos);
valid = (parsed > 0 && pos == param.size() && parsed <= 0xFFFFu);
}
catch (...)
{
valid = false;
}
if (!valid)
{
std::map<std::string, std::string> phs;
phs["%param"] = param;
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_invalid_arg_error",
"Invalid BiS ilvl argument '%param'. Use a positive integer.", phs));
return false;
}
if (parsed > static_cast<unsigned long>(sPlayerbotAIConfig.autoGearScoreLimit))
{
std::map<std::string, std::string> phs;
phs["%requested"] = std::to_string(parsed);
phs["%limit"] = std::to_string(sPlayerbotAIConfig.autoGearScoreLimit);
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_arg_above_limit_error",
"BiS ilvl %requested exceeds AutoGearScoreLimit %limit, refusing", phs));
return false;
}
ilvl = static_cast<uint16>(parsed);
}
uint8 cls = bot->getClass();
uint8 tab = AiFactory::GetPlayerSpecTab(bot);
uint8 faction = bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 2;
// Druid Bear (Feral Tank) shares tab 1 with Cat. Use sentinel tab 10 when tank strategy active.
constexpr uint8 BIS_TAB_DRUID_BEAR = 10;
constexpr uint16 BIS_ILVL_FALLBACK_WINDOW = 20;
uint16 resolvedIlvl = 0;
std::map<uint8, uint32> bisMap;
if (cls == CLASS_DRUID && tab == DRUID_TAB_FERAL && PlayerbotAI::IsTank(bot))
bisMap = sBisListMgr->GetBisForNearest(ilvl, BIS_ILVL_FALLBACK_WINDOW, cls, BIS_TAB_DRUID_BEAR, faction,
&resolvedIlvl);
if (bisMap.empty())
bisMap = sBisListMgr->GetBisForNearest(ilvl, BIS_ILVL_FALLBACK_WINDOW, cls, tab, faction, &resolvedIlvl);
// No rows within fallback window -> full autogear fallback at the effective ilvl.
if (bisMap.empty())
{
std::map<std::string, std::string> phs;
phs["%ilvl"] = std::to_string(ilvl);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_no_rows_autogear_msg",
"No BiS at ilvl %ilvl, using Autogear %ilvl instead", phs));
return RunAutogearFallback(ilvl);
}
if (resolvedIlvl != ilvl)
{
std::map<std::string, std::string> phs;
phs["%requested"] = std::to_string(ilvl);
phs["%resolved"] = std::to_string(resolvedIlvl);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_closest_match_msg",
"No BiS at ilvl %requested, using closest match at ilvl %resolved", phs));
}
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_applying_msg", "Applying BiS gear", {}));
// 1. Wipe everything currently equipped so autogear starts from a clean slate.
// Old items linger in inventory otherwise and autogear leaves slots empty on bag conflicts.
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot)
{
if (slot == EQUIPMENT_SLOT_TABARD || slot == EQUIPMENT_SLOT_BODY)
continue;
if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true);
}
// Wipe equippable items from bags too. Autogear can shove old equipped items into bags
// (HandleAutoStoreBagItemOpcode), and a unique-equipped duplicate stuck in a bag blocks
// CanEquipNewItem on subsequent BiS runs. Spare consumables/reagents.
auto destroyIfEquippable = [&](uint8 bag, uint8 slot)
{
Item* item = bot->GetItemByPos(bag, slot);
if (!item)
return;
ItemTemplate const* tmpl = item->GetTemplate();
if (!tmpl)
return;
if (tmpl->Class == ITEM_CLASS_WEAPON || tmpl->Class == ITEM_CLASS_ARMOR)
bot->DestroyItem(bag, slot, true);
};
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
destroyIfEquippable(INVENTORY_SLOT_BAG_0, slot);
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
if (Bag* container = bot->GetBagByPos(bag))
for (uint32 slot = 0; slot < container->GetBagSize(); ++slot)
destroyIfEquippable(bag, slot);
}
// 2. Run full autogear on the empty bot so every slot gets a best-available pick.
// Uncovered slots will keep the autogear pick; BiS overwrites the rest below.
if (sPlayerbotAIConfig.autoGearCommand)
{
uint32 fillGs = ilvl == 0
? 0
: PlayerbotFactory::CalcMixedGearScore(ilvl, sPlayerbotAIConfig.autoGearQualityLimit);
PlayerbotFactory fillFactory(bot, bot->GetLevel(), sPlayerbotAIConfig.autoGearQualityLimit, fillGs);
fillFactory.InitEquipment(false, sPlayerbotAIConfig.twoRoundsGearInit);
}
// 2b. Pre-destroy autogear picks that would conflict with any BiS item by entry.
// Autogear may have placed the exact item BiS wants into trinket2/finger2 (or vice versa);
// unique-equipped enforcement would then make BiS's equip silently drop one copy.
std::set<uint32> bisEntries;
for (auto const& kv : bisMap)
bisEntries.insert(kv.second);
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot)
{
if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
if (bisEntries.count(item->GetEntry()))
bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true);
}
// 3. Apply BiS: only touch slots where the bot can actually equip the BiS item.
// If item requires reputation, grant the required rank first. If CanUseItem still
// fails (class/race/skill/level), keep autogear's pick for that slot.
for (auto const& kv : bisMap)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(kv.second);
if (!proto)
continue;
// Grant required reputation rank if the item gates on it.
if (proto->RequiredReputationFaction && proto->RequiredReputationRank > 0)
{
if (FactionEntry const* fac = sFactionStore.LookupEntry(proto->RequiredReputationFaction))
{
ReputationRank requiredRank = static_cast<ReputationRank>(proto->RequiredReputationRank);
if (bot->GetReputationRank(proto->RequiredReputationFaction) < requiredRank)
{
int32 standing = ReputationMgr::ReputationRankToStanding(
static_cast<ReputationRank>(requiredRank - 1)) + 1;
bot->GetReputationMgr().SetReputation(fac, standing);
}
}
}
if (bot->CanUseItem(proto) != EQUIP_ERR_OK)
continue;
uint8 slot = kv.first;
if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
bot->DestroyItem(INVENTORY_SLOT_BAG_0, slot, true);
uint16 dest = 0;
InventoryResult eqResult = bot->CanEquipNewItem(slot, dest, kv.second, false);
// Paired slots (finger 10<->11, trinket 12<->13): destroy paired slot and retry once
// when unique-equipped or autogear residue blocks the first attempt.
if (eqResult != EQUIP_ERR_OK)
{
uint8 pairedSlot = 0xFF;
if (slot == EQUIPMENT_SLOT_FINGER1) pairedSlot = EQUIPMENT_SLOT_FINGER2;
else if (slot == EQUIPMENT_SLOT_FINGER2) pairedSlot = EQUIPMENT_SLOT_FINGER1;
else if (slot == EQUIPMENT_SLOT_TRINKET1) pairedSlot = EQUIPMENT_SLOT_TRINKET2;
else if (slot == EQUIPMENT_SLOT_TRINKET2) pairedSlot = EQUIPMENT_SLOT_TRINKET1;
if (pairedSlot != 0xFF)
{
if (bot->GetItemByPos(INVENTORY_SLOT_BAG_0, pairedSlot))
bot->DestroyItem(INVENTORY_SLOT_BAG_0, pairedSlot, true);
eqResult = bot->CanEquipNewItem(slot, dest, kv.second, false);
}
}
if (eqResult == EQUIP_ERR_OK)
{
bot->EquipNewItem(dest, kv.second, true);
bot->AutoUnequipOffhandIfNeed();
}
}
PlayerbotFactory factory(bot, bot->GetLevel(), ITEM_QUALITY_EPIC, 0);
factory.InitAmmo();
if (bot->GetLevel() >= sPlayerbotAIConfig.minEnchantingBotLevel)
factory.ApplyEnchantAndGemsNew();
bot->DurabilityRepairAll(false, 1.0f, false);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"bis_applied_msg", "BiS applied", {}));
return true;
}
bool RemoveGlyphAction::Execute(Event /*event*/)
{
for (uint32 slotIndex = 0; slotIndex < MAX_GLYPH_SLOT_INDEX; ++slotIndex)

View File

@ -53,4 +53,14 @@ public:
bool Execute(Event event) override;
};
class BisGearAction : public Action
{
public:
BisGearAction(PlayerbotAI* botAI) : Action(botAI, "autogear bis") {}
bool Execute(Event event) override;
private:
bool RunAutogearFallback(uint16 effectiveIlvl);
};
#endif

View File

@ -9,6 +9,7 @@
#include "Event.h"
#include "ItemPackets.h"
#include "ItemUsageValue.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
bool UseItemAction::Execute(Event event)
@ -35,7 +36,8 @@ bool UseItemAction::Execute(Event event)
return UseItemOnGameObject(*items.begin(), *gos.begin());
}
botAI->TellError("No items (or game objects) available");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"use_item_none_available", "No items (or game objects) available", {}));
return false;
}
@ -48,8 +50,10 @@ bool UseItemAction::UseGameObject(ObjectGuid guid)
go->Use(bot);
std::ostringstream out;
out << "Using " << chat->FormatGameobject(go);
botAI->TellMasterNoFacing(out.str());
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"use_gameobject",
"Using %gameobject",
{{"%gameobject", chat->FormatGameobject(go)}}));
return true;
}
@ -92,16 +96,16 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
bool targetSelected = false;
std::ostringstream out;
out << "Using " << chat->FormatItem(item->GetTemplate());
std::string itemText = chat->FormatItem(item->GetTemplate());
std::string targetText;
if (item->GetTemplate()->Stackable > 1)
{
uint32 count = item->GetCount();
if (count > 1)
out << " (" << count << " available) ";
itemText += " (" + std::to_string(count) + " available)";
else
out << " (the last one!)";
itemText += " (the last one!)";
}
if (goGuid)
@ -114,7 +118,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
packet << targetFlag;
packet << goGuid.WriteAsPacked();
out << " on " << chat->FormatGameobject(go);
targetText = chat->FormatGameobject(go);
targetSelected = true;
}
@ -124,7 +128,8 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
{
bool fit = SocketItem(itemTarget, item) || SocketItem(itemTarget, item, true);
if (!fit)
botAI->TellMaster("Socket does not fit");
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"socket_does_not_fit", "Socket does not fit", {}));
return fit;
}
@ -133,7 +138,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
targetFlag = TARGET_FLAG_ITEM;
packet << targetFlag;
packet << itemTarget->GetGUID().WriteAsPacked();
out << " on " << chat->FormatItem(itemTarget->GetTemplate());
targetText = chat->FormatItem(itemTarget->GetTemplate());
targetSelected = true;
}
}
@ -149,7 +154,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
{
targetFlag = TARGET_FLAG_UNIT;
packet << targetFlag << masterSelection.WriteAsPacked();
out << " on " << unit->GetName();
targetText = unit->GetName();
targetSelected = true;
}
}
@ -159,7 +164,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
{
targetFlag = TARGET_FLAG_UNIT;
packet << targetFlag << unitTarget->GetGUID().WriteAsPacked();
out << " on " << unitTarget->GetName();
targetText = unitTarget->GetName();
targetSelected = true;
}
@ -173,9 +178,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
packet << uint32(0);
bot->GetSession()->HandleQuestgiverAcceptQuestOpcode(packet);
std::ostringstream out;
out << "Got quest " << chat->FormatQuest(qInfo);
botAI->TellMasterNoFacing(out.str());
botAI->TellMasterNoFacing("Got quest " + chat->FormatQuest(qInfo));
return true;
}
}
@ -217,7 +220,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
targetFlag = TARGET_FLAG_TRADE_ITEM;
packet << targetFlag << (uint8)1 << ObjectGuid((uint64)TRADE_SLOT_NONTRADED).WriteAsPacked();
targetSelected = true;
out << " on traded item";
targetText = "traded item";
}
else
{
@ -225,7 +228,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
packet << targetFlag;
packet << itemForSpell->GetGUID().WriteAsPacked();
targetSelected = true;
out << " on " << chat->FormatItem(itemForSpell->GetTemplate());
targetText = chat->FormatItem(itemForSpell->GetTemplate());
}
uint32 castTime = spellInfo->CalcCastTime();
botAI->SetNextCheckDelay(castTime + sPlayerbotAIConfig.reactDelay);
@ -246,17 +249,17 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
targetSelected = true;
if (unitTarget == bot || !unitTarget->IsInWorld() || unitTarget->IsDuringRemoveFromWorld())
out << " on self";
targetText = "self";
else if (unitTarget->IsHostileTo(bot))
out << " on self";
targetText = "self";
else
out << " on " << unitTarget->GetName();
targetText = unitTarget->GetName();
}
else
{
packet << bot->GetPackGUID();
targetSelected = true;
out << " on self";
targetText = "self";
}
}
@ -307,7 +310,12 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni
return false;
// botAI->SetNextCheckDelay(sPlayerbotAIConfig.globalCoolDown);
botAI->TellMasterNoFacing(out.str());
std::string useText = targetSelected
? PlayerbotTextMgr::instance().GetBotTextOrDefault(
"use_item_on_target", "Using %item on %target", {{"%item", itemText}, {"%target", targetText}})
: PlayerbotTextMgr::instance().GetBotTextOrDefault(
"use_item", "Using %item", {{"%item", itemText}});
botAI->TellMasterNoFacing(useText);
bot->GetSession()->HandleUseItemOpcode(packet);
return true;
}
@ -372,10 +380,10 @@ bool UseItemAction::SocketItem(Item* item, Item* gem, bool replace)
if (fits)
{
std::ostringstream out;
out << "Socketing " << chat->FormatItem(item->GetTemplate());
out << " with " << chat->FormatItem(gem->GetTemplate());
botAI->TellMaster(out);
botAI->TellMaster(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"socketing_item_with_gem",
"Socketing %item with %gem",
{{"%item", chat->FormatItem(item->GetTemplate())}, {"%gem", chat->FormatItem(gem->GetTemplate())}}));
WorldPackets::Item::SocketGems nicePacket(std::move(packet));
nicePacket.Read();

View File

@ -11,6 +11,7 @@
#include "GridNotifiersImpl.h"
#include "NearestGameObjects.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "PositionValue.h"
@ -36,7 +37,8 @@ bool UseMeetingStoneAction::Execute(Event event)
if (bot->IsInCombat())
{
botAI->TellError("I am in combat");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_in_combat", "I am in combat", {}));
return false;
}
@ -73,13 +75,15 @@ bool SummonAction::Execute(Event /*event*/)
if (SummonUsingGos(master, bot, true) || SummonUsingNpcs(master, bot, true))
{
botAI->TellMasterNoFacing("Hello!");
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"hello", "Hello!", {}));
return true;
}
if (SummonUsingGos(bot, master, true) || SummonUsingNpcs(bot, master, true))
{
botAI->TellMasterNoFacing("Welcome!");
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_welcome", "Welcome!", {}));
return true;
}
@ -99,7 +103,10 @@ bool SummonAction::SummonUsingGos(Player* summoner, Player* player, bool preserv
return Teleport(summoner, player, preserveAuras);
}
botAI->TellError(summoner == bot ? "There is no meeting stone nearby" : "There is no meeting stone near you");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
summoner == bot ? "meeting_stone_none_nearby" : "meeting_stone_none_near_you",
summoner == bot ? "There is no meeting stone nearby" : "There is no meeting stone near you",
{}));
return false;
}
@ -119,13 +126,19 @@ bool SummonAction::SummonUsingNpcs(Player* summoner, Player* player, bool preser
{
if (!player->HasItemCount(6948, 1, false))
{
botAI->TellError(player == bot ? "I have no hearthstone" : "You have no hearthstone");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
player == bot ? "meeting_stone_no_hearthstone_self" : "meeting_stone_no_hearthstone_you",
player == bot ? "I have no hearthstone" : "You have no hearthstone",
{}));
return false;
}
if (player->HasSpellCooldown(8690))
{
botAI->TellError(player == bot ? "My hearthstone is not ready" : "Your hearthstone is not ready");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
player == bot ? "meeting_stone_hearthstone_not_ready_self" : "meeting_stone_hearthstone_not_ready_you",
player == bot ? "My hearthstone is not ready" : "Your hearthstone is not ready",
{}));
return false;
}
@ -141,7 +154,10 @@ bool SummonAction::SummonUsingNpcs(Player* summoner, Player* player, bool preser
}
}
botAI->TellError(summoner == bot ? "There are no innkeepers nearby" : "There are no innkeepers near you");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
summoner == bot ? "meeting_stone_no_innkeepers_nearby" : "meeting_stone_no_innkeepers_near_you",
summoner == bot ? "There are no innkeepers nearby" : "There are no innkeepers near you",
{}));
return false;
}
@ -153,7 +169,8 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras
if (player->GetVehicle())
{
botAI->TellError("You cannot summon me while I'm on a vehicle");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_cannot_summon_vehicle", "You cannot summon me while I'm on a vehicle", {}));
return false;
}
@ -174,20 +191,29 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras
if (summoner->IsInCombat() && !sPlayerbotAIConfig.allowSummonInCombat)
{
botAI->TellError("You cannot summon me while you're in combat");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_cannot_summon_master_in_combat",
"You cannot summon me while you're in combat",
{}));
return false;
}
if (!summoner->IsAlive() && !sPlayerbotAIConfig.allowSummonWhenMasterIsDead)
{
botAI->TellError("You cannot summon me while you're dead");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_cannot_summon_master_dead",
"You cannot summon me while you're dead",
{}));
return false;
}
if (bot->isDead() && !bot->HasPlayerFlag(PLAYER_FLAGS_GHOST) &&
!sPlayerbotAIConfig.allowSummonWhenBotIsDead)
{
botAI->TellError("You cannot summon me while I'm dead, you need to release my spirit first");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_cannot_summon_bot_dead",
"You cannot summon me while I'm dead, you need to release my spirit first",
{}));
return false;
}
@ -199,7 +225,8 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras
{
bot->ResurrectPlayer(1.0f, false);
bot->SpawnCorpseBones();
botAI->TellMasterNoFacing("I live, again!");
botAI->TellMasterNoFacing(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_revived", "I live, again!", {}));
botAI->GetAiObjectContext()->GetValue<GuidVector>("prioritized targets")->Reset();
}
@ -229,6 +256,7 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras
}
if (summoner != player)
botAI->TellError("Not enough place to summon");
botAI->TellError(PlayerbotTextMgr::instance().GetBotTextOrDefault(
"meeting_stone_not_enough_space", "Not enough place to summon", {}));
return false;
}

View File

@ -139,6 +139,7 @@ public:
creators["maintenance"] = &ChatActionContext::maintenance;
creators["remove glyph"] = &ChatActionContext::remove_glyph;
creators["autogear"] = &ChatActionContext::autogear;
creators["autogear bis"] = &ChatActionContext::autogear_bis;
creators["equip upgrade"] = &ChatActionContext::equip_upgrade;
creators["attack my target"] = &ChatActionContext::attack_my_target;
creators["pull my target"] = &ChatActionContext::pull_my_target;
@ -261,6 +262,7 @@ private:
static Action* maintenance(PlayerbotAI* botAI) { return new MaintenanceAction(botAI); }
static Action* remove_glyph(PlayerbotAI* botAI) { return new RemoveGlyphAction(botAI); }
static Action* autogear(PlayerbotAI* botAI) { return new AutoGearAction(botAI); }
static Action* autogear_bis(PlayerbotAI* botAI) { return new BisGearAction(botAI); }
static Action* equip_upgrade(PlayerbotAI* botAI) { return new EquipUpgradeAction(botAI); }
static Action* co(PlayerbotAI* botAI) { return new ChangeCombatStrategyAction(botAI); }
static Action* nc(PlayerbotAI* botAI) { return new ChangeNonCombatStrategyAction(botAI); }

View File

@ -65,6 +65,7 @@ public:
creators["maintenance"] = &ChatTriggerContext::maintenance;
creators["remove glyph"] = &ChatTriggerContext::remove_glyph;
creators["autogear"] = &ChatTriggerContext::autogear;
creators["autogear bis"] = &ChatTriggerContext::autogear_bis;
creators["equip upgrade"] = &ChatTriggerContext::equip_upgrade;
creators["attack"] = &ChatTriggerContext::attack;
creators["pull"] = &ChatTriggerContext::pull;
@ -220,6 +221,7 @@ private:
static Trigger* maintenance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "maintenance"); }
static Trigger* remove_glyph(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "remove glyph"); }
static Trigger* autogear(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "autogear"); }
static Trigger* autogear_bis(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "autogear bis"); }
static Trigger* equip_upgrade(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "equip upgrade"); }
static Trigger* co(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "co"); }
static Trigger* nc(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "nc"); }

View File

@ -20,20 +20,23 @@ private:
}
};
// Commands where trigger name =/= action name.
void ChatCommandHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
PassTroughStrategy::InitTriggers(triggers);
// Keep single action triggers on one line, and multi-action triggers on multiple lines.
triggers.push_back(new TriggerNode("rep", { NextAction("reputation", relevance) }));
triggers.push_back(new TriggerNode("pvp stats", { NextAction("tell pvp stats", relevance) }));
triggers.push_back(new TriggerNode("q", { NextAction("query quest", relevance),
NextAction("query item usage", relevance) }));
triggers.push_back(new TriggerNode("add all loot", { NextAction("add all loot", relevance),
NextAction("loot", relevance) }));
triggers.push_back(new TriggerNode("q",
{ NextAction("query quest", relevance),
NextAction("query item usage", relevance) }));
triggers.push_back(new TriggerNode("add all loot",
{ NextAction("add all loot", relevance),
NextAction("loot", relevance) }));
triggers.push_back(new TriggerNode("u", { NextAction("use", relevance) }));
triggers.push_back(new TriggerNode("c", { NextAction("item count", relevance) }));
triggers.push_back(
new TriggerNode("items", { NextAction("item count", relevance) }));
triggers.push_back(new TriggerNode("items", { NextAction("item count", relevance) }));
triggers.push_back(new TriggerNode("inv", { NextAction("item count", relevance) }));
triggers.push_back(new TriggerNode("e", { NextAction("equip", relevance) }));
triggers.push_back(new TriggerNode("ue", { NextAction("unequip", relevance) }));
@ -42,81 +45,40 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
triggers.push_back(new TriggerNode("s", { NextAction("sell", relevance) }));
triggers.push_back(new TriggerNode("b", { NextAction("buy", relevance) }));
triggers.push_back(new TriggerNode("r", { NextAction("reward", relevance) }));
triggers.push_back(
new TriggerNode("attack", { NextAction("attack my target", relevance) }));
triggers.push_back(
new TriggerNode("accept", { NextAction("accept quest", relevance) }));
triggers.push_back(
new TriggerNode("follow", { NextAction("follow chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("stay", { NextAction("stay chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("move from group", { NextAction("move from group chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("flee", { NextAction("flee chat shortcut", relevance) }));
triggers.push_back(new TriggerNode(
"tank attack", { NextAction("tank attack chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("grind", { NextAction("grind chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("talk", { NextAction("gossip hello", relevance),
NextAction("talk to quest giver", relevance) }));
triggers.push_back(
new TriggerNode("enter vehicle", { NextAction("enter vehicle", relevance) }));
triggers.push_back(
new TriggerNode("leave vehicle", { NextAction("leave vehicle", relevance) }));
triggers.push_back(
new TriggerNode("cast", { NextAction("cast custom spell", relevance) }));
triggers.push_back(
new TriggerNode("castnc", { NextAction("cast custom nc spell", relevance) }));
triggers.push_back(
new TriggerNode("revive", { NextAction("spirit healer", relevance) }));
triggers.push_back(
new TriggerNode("runaway", { NextAction("runaway chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("warning", { NextAction("runaway chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("max dps", { NextAction("max dps chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("attackers", { NextAction("tell attackers", relevance) }));
triggers.push_back(
new TriggerNode("target", { NextAction("tell target", relevance) }));
triggers.push_back(
new TriggerNode("pull", { NextAction("pull my target", relevance) }));
triggers.push_back(
new TriggerNode("pull back", { NextAction("pull my target", relevance) }));
triggers.push_back(
new TriggerNode("pull rti", { NextAction("pull rti target", relevance) }));
triggers.push_back(
new TriggerNode("ready", { NextAction("ready check", relevance) }));
triggers.push_back(
new TriggerNode("naxx", {NextAction("naxx chat shortcut", relevance)}));
triggers.push_back(
new TriggerNode("bwl", { NextAction("bwl chat shortcut", relevance) }));
triggers.push_back(
new TriggerNode("dps", { NextAction("tell estimated dps", relevance) }));
triggers.push_back(
new TriggerNode("disperse", { NextAction("disperse set", relevance) }));
triggers.push_back(
new TriggerNode("open items", { NextAction("open items", relevance) }));
triggers.push_back(
new TriggerNode("qi", { NextAction("query item usage", relevance) }));
triggers.push_back(
new TriggerNode("unlock items", { NextAction("unlock items", relevance) }));
triggers.push_back(
new TriggerNode("unlock traded item", { NextAction("unlock traded item", relevance) }));
triggers.push_back(
new TriggerNode("wipe", { NextAction("wipe", relevance) }));
triggers.push_back(new TriggerNode("tame", { NextAction("tame", relevance) }));
triggers.push_back(new TriggerNode("glyphs", { NextAction("glyphs", relevance) })); // Added for custom Glyphs
triggers.push_back(new TriggerNode("glyph equip", { NextAction("glyph equip", relevance) })); // Added for custom Glyphs
triggers.push_back(new TriggerNode("pet", { NextAction("pet", relevance) }));
triggers.push_back(new TriggerNode("pet attack", { NextAction("pet attack", relevance) }));
triggers.push_back(new TriggerNode("roll", { NextAction("roll", relevance) }));
triggers.push_back(new TriggerNode("attack", { NextAction("attack my target", relevance) }));
triggers.push_back(new TriggerNode("accept", { NextAction("accept quest", relevance) }));
triggers.push_back(new TriggerNode("follow", { NextAction("follow chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("stay", { NextAction("stay chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("move from group", { NextAction("move from group chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("flee", { NextAction("flee chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("tank attack", { NextAction("tank attack chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("grind", { NextAction("grind chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("talk",
{ NextAction("gossip hello", relevance),
NextAction("talk to quest giver", relevance) }));
triggers.push_back(new TriggerNode("enter vehicle", { NextAction("enter vehicle", relevance) }));
triggers.push_back(new TriggerNode("leave vehicle", { NextAction("leave vehicle", relevance) }));
triggers.push_back(new TriggerNode("cast", { NextAction("cast custom spell", relevance) }));
triggers.push_back(new TriggerNode("castnc", { NextAction("cast custom nc spell", relevance) }));
triggers.push_back(new TriggerNode("revive", { NextAction("spirit healer", relevance) }));
triggers.push_back(new TriggerNode("runaway", { NextAction("runaway chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("warning", { NextAction("runaway chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("max dps", { NextAction("max dps chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("attackers", { NextAction("tell attackers", relevance) }));
triggers.push_back(new TriggerNode("target", { NextAction("tell target", relevance) }));
triggers.push_back(new TriggerNode("pull", { NextAction("pull my target", relevance) }));
triggers.push_back(new TriggerNode("pull back", { NextAction("pull my target", relevance) }));
triggers.push_back(new TriggerNode("pull rti", { NextAction("pull rti target", relevance) }));
triggers.push_back(new TriggerNode("ready", { NextAction("ready check", relevance) }));
triggers.push_back(new TriggerNode("naxx", {NextAction("naxx chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("bwl", { NextAction("bwl chat shortcut", relevance) }));
triggers.push_back(new TriggerNode("dps", { NextAction("tell estimated dps", relevance) }));
triggers.push_back(new TriggerNode("disperse", { NextAction("disperse set", relevance) }));
triggers.push_back(new TriggerNode("qi", { NextAction("query item usage", relevance) }));
triggers.push_back(new TriggerNode("focus heal", { NextAction("focus heal targets", relevance) }));
triggers.push_back(new TriggerNode("emblems", { NextAction("emblems", relevance) }));
}
// Commands where trigger name == action name.
ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI)
{
actionNodeFactories.Add(new ChatCommandActionNodeFactoryInternal());
@ -149,6 +111,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("maintenance");
supported.push_back("remove glyph");
supported.push_back("autogear");
supported.push_back("autogear bis");
supported.push_back("equip upgrade");
supported.push_back("chat");
supported.push_back("home");
@ -199,15 +162,15 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("rtsc");
supported.push_back("drink");
supported.push_back("calc");
supported.push_back("roll");
supported.push_back("open items");
supported.push_back("qi");
supported.push_back("unlock items");
supported.push_back("unlock traded item");
supported.push_back("wipe");
supported.push_back("tame");
supported.push_back("glyphs");
supported.push_back("glyph equip");
supported.push_back("pet");
supported.push_back("pet attack");
supported.push_back("wait for attack time");
supported.push_back("focus heal");
}
}

View File

@ -16,7 +16,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
triggers.push_back(
new TriggerNode("uninvite guid", { NextAction("uninvite", relevance) }));
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(
"not enough money", { NextAction("tell not enough money", relevance) }));
triggers.push_back(

View File

@ -7,6 +7,7 @@
#include <string>
#include "GenericBuffUtils.h"
#include "CreatureAI.h"
#include "ItemVisitors.h"
#include "LastSpellCastValue.h"
@ -34,54 +35,57 @@ bool MediumManaTrigger::IsActive()
AI_VALUE2(uint8, "mana", "self target") < sPlayerbotAIConfig.mediumMana;
}
bool LowEnergyTrigger::IsActive()
{
return AI_VALUE2(uint8, "energy", "self target") < threshold;
}
bool NoPetTrigger::IsActive()
{
return (bot->GetMinionGUID().IsEmpty()) && (!AI_VALUE(Unit*, "pet target")) && (!bot->GetGuardianPet()) &&
(!bot->GetFirstControlled()) && (!AI_VALUE2(bool, "mounted", "self target"));
return bot->GetMinionGUID().IsEmpty() && !AI_VALUE(Unit*, "pet target") && !bot->GetGuardianPet() &&
!bot->GetFirstControlled() && !AI_VALUE2(bool, "mounted", "self target");
}
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()
{
Guardian* pet = bot->GetGuardianPet();
if (!pet)
{
return false;
}
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
{
return false;
}
if (pet->GetVictim() == target && pet->GetCharmInfo()->IsCommandAttack())
{
return false;
}
if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat())
{
return false;
}
return true;
}
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()
{
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()
{
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; }
@ -96,9 +100,8 @@ bool TargetWithComboPointsLowerHealTrigger::IsActive()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false;
}
return ComboPointsAvailableTrigger::IsActive() &&
(target->GetHealth() / AI_VALUE(float, "estimated group dps")) <= lifeTime;
}
@ -159,19 +162,27 @@ bool BuffTrigger::IsActive()
Unit* target = GetTarget();
if (!target)
return false;
if (!SpellTrigger::IsActive())
return false;
Aura* aura = botAI->GetAura(spell, target, checkIsOwner, checkDuration);
if (!aura)
return true;
if (beforeDuration && aura->GetDuration() < beforeDuration)
if (!aura || (beforeDuration && aura->GetDuration() < beforeDuration))
return true;
return false;
}
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"); }
@ -204,13 +215,14 @@ bool MediumThreatTrigger::IsActive()
{
if (!AI_VALUE(Unit*, "main tank"))
return false;
return MyAttackerCountTrigger::IsActive();
}
bool LowTankThreatTrigger::IsActive()
{
Unit* mt = AI_VALUE(Unit*, "main tank");
if (!mt)
Unit* mainTank = AI_VALUE(Unit*, "main tank");
if (!mainTank)
return false;
Unit* current_target = AI_VALUE(Unit*, "current target");
@ -219,7 +231,7 @@ bool LowTankThreatTrigger::IsActive()
ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mt);
float tankThreat = mgr.GetThreat(mainTank);
return tankThreat == 0.0f || threat > tankThreat * 0.5f;
}
@ -227,9 +239,8 @@ bool AoeTrigger::IsActive()
{
Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target)
{
return false;
}
GuidVector attackers = context->GetValue<GuidVector>("attackers")->Get();
int attackers_count = 0;
for (ObjectGuid const guid : attackers)
@ -237,10 +248,9 @@ bool AoeTrigger::IsActive()
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive())
continue;
if (unit->GetDistance(current_target->GetPosition()) <= range)
{
attackers_count++;
}
}
return attackers_count >= amount;
}
@ -269,20 +279,19 @@ bool DebuffTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
{
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()
{
if (!DebuffTrigger::IsActive())
{
return false;
}
Creature* c = GetTarget()->ToCreature();
return c && ((c->IsDungeonBoss()) || (c->isWorldBoss()));
Creature* creature = GetTarget()->ToCreature();
return creature && (creature->IsDungeonBoss() || creature->isWorldBoss());
}
bool SpellTrigger::IsActive() { return GetTarget(); }
@ -312,9 +321,7 @@ bool SpellCooldownTrigger::IsActive()
}
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()
{
@ -325,6 +332,7 @@ bool RandomTrigger::IsActive()
int32 k = (int32)(probability / sPlayerbotAIConfig.randomChangeMultiplier);
if (k < 1)
k = 1;
return (rand() % k) == 0;
}
@ -363,9 +371,11 @@ bool BoostTrigger::IsActive()
{
if (!BuffTrigger::IsActive())
return false;
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->ToPlayer())
return true;
return AI_VALUE(uint8, "balance") <= balance;
}
@ -374,20 +384,19 @@ bool GenericBoostTrigger::IsActive()
Unit* target = AI_VALUE(Unit*, "current target");
if (target && target->ToPlayer())
return true;
return AI_VALUE(uint8, "balance") <= balance;
}
bool HealerShouldAttackTrigger::IsActive()
{
// nobody can help me
if (botAI->GetNearGroupMemberCount(sPlayerbotAIConfig.sightDistance) <= 1)
return true;
if (AI_VALUE2(uint8, "health", "party member to heal") < sPlayerbotAIConfig.almostFullHealth)
return false;
// special check for resto druid (dont remove tree of life frequently)
if (bot->GetAura(33891))
if (bot->GetAura(33891)) // Tree of Life
{
LastSpellCast& lastSpell = botAI->GetAiObjectContext()->GetValue<LastSpellCast&>("last spell cast")->Get();
if (lastSpell.timer + 5 > time(nullptr))
@ -396,7 +405,6 @@ bool HealerShouldAttackTrigger::IsActive()
int manaThreshold;
int balance = AI_VALUE(uint8, "balance");
// higher threshold in higher pressure
if (balance <= 50)
manaThreshold = 85;
else if (balance <= 100)
@ -420,13 +428,7 @@ bool InterruptSpellTrigger::IsActive()
bool DeflectSpellTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target)
return false;
if (!target->IsNonMeleeSpellCast(true))
return false;
if (target->GetTarget() != bot->GetGUID())
if (!target || !target->IsNonMeleeSpellCast(true) || target->GetTarget() != bot->GetGUID())
return false;
uint32 spellid = context->GetValue<uint32>("spell id", spell)->Get();
@ -457,6 +459,7 @@ bool DeflectSpellTrigger::IsActive()
return true;
}
}
return false;
}
@ -490,17 +493,16 @@ bool FearSleepSapTrigger::IsActive()
bool HasAuraStackTrigger::IsActive()
{
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);
// sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "HasAuraStackTrigger::IsActive %s %d", getName(), aura ?
// aura->GetStackAmount() : -1);
return aura;
return botAI->GetAura(getName(), GetTarget(), false, true, stack);
}
bool TimerTrigger::IsActive()
{
if (time(nullptr) != lastCheck)
time_t now = time(nullptr);
if (now != lastCheck)
{
lastCheck = time(nullptr);
lastCheck = now;
return true;
}
@ -547,9 +549,8 @@ bool IsBehindTargetTrigger::IsActive()
bool IsNotBehindTargetTrigger::IsActive()
{
if (botAI->HasStrategy("stay", botAI->GetState()))
{
return false;
}
Unit* target = AI_VALUE(Unit*, "current target");
return target && !AI_VALUE2(bool, "behind", "current target");
}
@ -557,9 +558,8 @@ bool IsNotBehindTargetTrigger::IsActive()
bool IsNotFacingTargetTrigger::IsActive()
{
if (botAI->HasStrategy("stay", botAI->GetState()))
{
return false;
}
return !AI_VALUE2(bool, "facing", "current target");
}
@ -576,12 +576,14 @@ bool NoPossibleTargetsTrigger::IsActive()
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()
{
Unit* target = AI_VALUE(Unit*, "current target");
// do not switch if enemy target
if (target && target->IsAlive())
{
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
@ -599,7 +601,6 @@ bool NotDpsAoeTargetActiveTrigger::IsActive()
Unit* target = AI_VALUE(Unit*, "current target");
Unit* enemy = AI_VALUE(Unit*, "enemy player target");
// do not switch if enemy target
if (target && target == enemy && target->IsAlive())
return false;
@ -633,7 +634,10 @@ Value<Unit*>* InterruptEnemyHealerTrigger::GetTargetValue()
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()
{
@ -713,43 +717,24 @@ bool AmmoCountTrigger::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;
// If bot has a pet, get its GUID
if (pet)
{
if (Pet* pet = bot->GetPet())
currentPetGuid = pet->GetGUID();
}
else
{
// If no pet, try to get a guardian pet and its GUID
guardian = bot->GetGuardianPet();
if (guardian)
currentPetGuid = guardian->GetGUID();
}
else if (Guardian* guardian = bot->GetGuardianPet())
currentPetGuid = guardian->GetGUID();
// If the current pet or guardian GUID has changed (including becoming empty), reset the trigger state
if (currentPetGuid != lastPetGuid)
{
triggered = false;
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)
{
triggered = true;
return true;
}
// Otherwise, do not activate
return false;
}

View File

@ -20,9 +20,7 @@ class StatAvailable : public Trigger
{
public:
StatAvailable(PlayerbotAI* botAI, int32 amount, std::string const name = "stat available")
: Trigger(botAI, name), amount(amount)
{
}
: Trigger(botAI, name), amount(amount) {}
protected:
int32 amount;
@ -118,8 +116,8 @@ public:
class TargetWithComboPointsLowerHealTrigger : public ComboPointsAvailableTrigger
{
public:
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* ai, int32 combo_point = 5, float lifeTime = 8.0f)
: ComboPointsAvailableTrigger(ai, combo_point), lifeTime(lifeTime)
TargetWithComboPointsLowerHealTrigger(PlayerbotAI* botAI, int32 combo_point = 5, float lifeTime = 8.0f)
: ComboPointsAvailableTrigger(botAI, combo_point), lifeTime(lifeTime)
{
}
bool IsActive() override;
@ -196,7 +194,6 @@ public:
bool IsActive() override;
};
// TODO: check other targets
class InterruptSpellTrigger : public SpellTrigger
{
public:
@ -217,9 +214,7 @@ class AttackerCountTrigger : public Trigger
{
public:
AttackerCountTrigger(PlayerbotAI* botAI, int32 amount, float distance = sPlayerbotAIConfig.sightDistance)
: Trigger(botAI), amount(amount), distance(distance)
{
}
: Trigger(botAI), amount(amount), distance(distance) {}
bool IsActive() override;
std::string const getName() override { return "attacker count"; }
@ -269,9 +264,7 @@ class AoeTrigger : public AttackerCountTrigger
{
public:
AoeTrigger(PlayerbotAI* botAI, int32 amount = 3, float range = 15.0f)
: AttackerCountTrigger(botAI, amount), range(range)
{
}
: AttackerCountTrigger(botAI, amount), range(range) {}
bool IsActive() override;
std::string const getName() override { return "aoe"; }
@ -317,7 +310,8 @@ public:
class BuffTrigger : public SpellTrigger
{
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)
{
this->checkIsOwner = checkIsOwner;
@ -339,11 +333,10 @@ class BuffOnPartyTrigger : public BuffTrigger
{
public:
BuffOnPartyTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1)
: BuffTrigger(botAI, spell, checkInterval)
{
}
: BuffTrigger(botAI, spell, checkInterval) {}
Value<Unit*>* GetTargetValue() override;
bool IsActive() override;
std::string const getName() override { return spell + " on party"; }
};
@ -393,9 +386,7 @@ class DebuffTrigger : public BuffTrigger
public:
DebuffTrigger(PlayerbotAI* botAI, std::string const spell, int32 checkInterval = 1, bool checkIsOwner = false,
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"; }
bool IsActive() override;
@ -408,9 +399,7 @@ class DebuffOnBossTrigger : public DebuffTrigger
{
public:
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;
};
@ -419,9 +408,7 @@ class DebuffOnAttackerTrigger : public DebuffTrigger
public:
DebuffOnAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
float needLifeTime = 8.0f)
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime)
{
}
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; }
@ -432,9 +419,7 @@ class DebuffOnMeleeAttackerTrigger : public DebuffTrigger
public:
DebuffOnMeleeAttackerTrigger(PlayerbotAI* botAI, std::string const spell, bool checkIsOwner = true,
float needLifeTime = 8.0f)
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime)
{
}
: DebuffTrigger(botAI, spell, 1, checkIsOwner, needLifeTime) {}
Value<Unit*>* GetTargetValue() override;
std::string const getName() override { return spell + " on attacker"; }
@ -444,9 +429,7 @@ class BoostTrigger : public BuffTrigger
{
public:
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;
@ -458,9 +441,7 @@ class GenericBoostTrigger : public Trigger
{
public:
GenericBoostTrigger(PlayerbotAI* botAI, float balance = 50.f)
: Trigger(botAI, "generic boost", 1), balance(balance)
{
}
: Trigger(botAI, "generic boost", 1), balance(balance) {}
bool IsActive() override;
@ -472,9 +453,7 @@ class HealerShouldAttackTrigger : public Trigger
{
public:
HealerShouldAttackTrigger(PlayerbotAI* botAI)
: Trigger(botAI, "healer should attack", 1)
{
}
: Trigger(botAI, "healer should attack", 1) {}
bool IsActive() override;
};
@ -550,6 +529,17 @@ public:
bool IsActive() override;
};
class LowEnergyTrigger : public Trigger
{
public:
LowEnergyTrigger(PlayerbotAI* botAI, uint8 threshold = 30) : Trigger(botAI, "low energy"), threshold(threshold) {}
bool IsActive() override;
private:
uint8 threshold;
};
BEGIN_TRIGGER(PanicTrigger, Trigger) // cppcheck-suppress unknownMacro
std::string const getName() override { return "panic"; }
END_TRIGGER()
@ -569,7 +559,7 @@ public:
class HasPetTrigger : public Trigger
{
public:
HasPetTrigger(PlayerbotAI* ai) : Trigger(ai, "has pet", 5 * 1000) {}
HasPetTrigger(PlayerbotAI* botAI) : Trigger(botAI, "has pet", 5 * 1000) {}
virtual bool IsActive() override;
};
@ -577,7 +567,7 @@ public:
class PetAttackTrigger : public Trigger
{
public:
PetAttackTrigger(PlayerbotAI* ai) : Trigger(ai, "pet attack") {}
PetAttackTrigger(PlayerbotAI* botAI) : Trigger(botAI, "pet attack") {}
virtual bool IsActive() override;
};
@ -586,9 +576,7 @@ class ItemCountTrigger : public Trigger
{
public:
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;
std::string const getName() override { return "item count"; }
@ -602,9 +590,7 @@ class AmmoCountTrigger : public ItemCountTrigger
{
public:
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;
};
@ -612,9 +598,7 @@ class HasAuraTrigger : public Trigger
{
public:
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"; }
bool IsActive() override;
@ -623,10 +607,8 @@ public:
class HasAuraStackTrigger : public Trigger
{
public:
HasAuraStackTrigger(PlayerbotAI* ai, std::string spell, int stack, int checkInterval = 1)
: Trigger(ai, spell, checkInterval), stack(stack)
{
}
HasAuraStackTrigger(PlayerbotAI* botAI, std::string spell, int stack, int checkInterval = 1)
: Trigger(botAI, spell, checkInterval), stack(stack) {}
std::string const GetTargetName() override { return "self target"; }
bool IsActive() override;
@ -847,9 +829,7 @@ class StayTimeTrigger : public Trigger
{
public:
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;
@ -866,7 +846,7 @@ public:
class ReturnToStayPositionTrigger : public Trigger
{
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;
};
@ -881,9 +861,7 @@ class GiveItemTrigger : public Trigger
{
public:
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;
@ -951,9 +929,7 @@ class BuffOnMainTankTrigger : public BuffTrigger
{
public:
BuffOnMainTankTrigger(PlayerbotAI* botAI, std::string spell, bool checkIsOwner = false, int checkInterval = 1)
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner)
{
}
: BuffTrigger(botAI, spell, checkInterval, checkIsOwner) {}
public:
virtual Value<Unit*>* GetTargetValue();
@ -962,7 +938,7 @@ public:
class SelfResurrectTrigger : public Trigger
{
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); }
};
@ -970,7 +946,7 @@ public:
class NewPetTrigger : public Trigger
{
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;

View File

@ -22,6 +22,15 @@ bool DeadTrigger::IsActive() { return AI_VALUE2(bool, "dead", GetTargetName());
bool AoeHealTrigger::IsActive() { return AI_VALUE2(uint8, "aoe heal", type) >= count; }
bool HealerLowManaTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target)
return false;
return target->GetPowerPct(POWER_MANA) < sPlayerbotAIConfig.lowMana;
}
bool AoeInGroupTrigger::IsActive()
{
int32 member = botAI->GetNearGroupMemberCount();

View File

@ -143,6 +143,15 @@ public:
TargetCriticalHealthTrigger(PlayerbotAI* botAI) : TargetLowHealthTrigger(botAI, 20) {}
};
class HealerLowManaTrigger : public Trigger
{
public:
HealerLowManaTrigger(PlayerbotAI* botAI) : Trigger(botAI, "healer low mana") {}
std::string const GetTargetName() override { return "healer low mana"; }
bool IsActive() override;
};
class PartyMemberDeadTrigger : public Trigger
{
public:

View File

@ -18,3 +18,19 @@ bool NoRtiTrigger::IsActive()
Unit* target = AI_VALUE(Unit*, "rti target");
return target == nullptr;
}
// Fires when the RTI CC target should be crowd controlled by this spell.
// Standard path: the target is already in the attackers list and "cc target" matches the RTI
// mark — delegates to HasCcTargetTrigger to confirm no one else is already CCing it.
bool RtiCcTrigger::IsActive()
{
Unit* rtiCcTarget = AI_VALUE(Unit*, "rti cc target");
if (!rtiCcTarget)
return false;
Unit* ccTarget = AI_VALUE2(Unit*, "cc target", getName());
if (ccTarget && ccTarget == rtiCcTarget)
return HasCcTargetTrigger::IsActive();
return botAI->CanCastSpell(getName(), rtiCcTarget);
}

View File

@ -6,6 +6,7 @@
#ifndef _PLAYERBOT_RTITRIGGERS_H
#define _PLAYERBOT_RTITRIGGERS_H
#include "GenericTriggers.h"
#include "Trigger.h"
class PlayerbotAI;
@ -18,4 +19,12 @@ public:
bool IsActive() override;
};
class RtiCcTrigger : public HasCcTargetTrigger
{
public:
RtiCcTrigger(PlayerbotAI* botAI, std::string const name) : HasCcTargetTrigger(botAI, name) {}
bool IsActive() override;
};
#endif

View File

@ -51,6 +51,7 @@ public:
creators["low mana"] = &TriggerContext::LowMana;
creators["medium mana"] = &TriggerContext::MediumMana;
creators["low energy"] = &TriggerContext::LowEnergy;
creators["high mana"] = &TriggerContext::HighMana;
creators["almost full mana"] = &TriggerContext::AlmostFullMana;
creators["enough mana"] = &TriggerContext::EnoughMana;
@ -59,6 +60,7 @@ public:
creators["party member low health"] = &TriggerContext::PartyMemberLowHealth;
creators["party member medium health"] = &TriggerContext::PartyMemberMediumHealth;
creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth;
creators["healer low mana"] = &TriggerContext::HealerLowMana;
creators["generic boost"] = &TriggerContext::generic_boost;
creators["loss of control"] = &TriggerContext::loss_of_control;
@ -312,6 +314,7 @@ private:
static Trigger* TargetCriticalHealth(PlayerbotAI* botAI) { return new TargetCriticalHealthTrigger(botAI); }
static Trigger* LowMana(PlayerbotAI* botAI) { return new LowManaTrigger(botAI); }
static Trigger* MediumMana(PlayerbotAI* botAI) { return new MediumManaTrigger(botAI); }
static Trigger* LowEnergy(PlayerbotAI* botAI) { return new LowEnergyTrigger(botAI); }
static Trigger* HighMana(PlayerbotAI* botAI) { return new HighManaTrigger(botAI); }
static Trigger* AlmostFullMana(PlayerbotAI* botAI) { return new AlmostFullManaTrigger(botAI); }
static Trigger* EnoughMana(PlayerbotAI* botAI) { return new EnoughManaTrigger(botAI); }
@ -383,6 +386,7 @@ private:
{
return new PartyMemberCriticalHealthTrigger(botAI);
}
static Trigger* HealerLowMana(PlayerbotAI* botAI) { return new HealerLowManaTrigger(botAI); }
static Trigger* protect_party_member(PlayerbotAI* botAI) { return new ProtectPartyMemberTrigger(botAI); }
static Trigger* no_pet(PlayerbotAI* botAI) { return new NoPetTrigger(botAI); }
static Trigger* has_pet(PlayerbotAI* botAI) { return new HasPetTrigger(botAI); }

View File

@ -4,23 +4,89 @@
*/
#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 "GameTime.h"
#include "Group.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include "SpellMgr.h"
#include "Unit.h"
#include "Value.h"
#include "Config.h"
#include "PlayerbotTextMgr.h"
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)
{
// Paladin
@ -34,27 +100,89 @@ namespace ai::buff
if (name == "arcane intellect") return "arcane intellect,arcane brilliance";
// Priest
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;
}
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
if (name == "mark of the wild") return "gift of the wild";
// Mage
if (name == "arcane intellect") return "arcane brilliance";
// Priest
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();
}
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)
{
if (!spellId)
@ -72,75 +200,33 @@ namespace ai::buff
return false;
}
}
// No reagent required
return true;
}
return false;
}
std::string UpgradeToGroupIfAppropriate(
Player* bot,
PlayerbotAI* botAI,
std::string const& baseName,
bool announceOnMissing,
std::function<void(std::string const&)> announce)
Player* bot, PlayerbotAI* botAI, std::string const& baseName)
{
std::string castName = baseName;
Group* g = bot->GetGroup();
if (!g || g->GetMembersCount() < static_cast<uint32>(sPlayerbotAIConfig.minBotsForGreaterBuff))
return castName; // Group too small: stay in solo mode
if (!IsGroupVariantEnabled(bot, baseName))
return baseName;
if (std::string const groupName = GroupVariantFor(baseName); !groupName.empty())
{
uint32 const groupVariantSpellId = botAI->GetAiObjectContext()
std::string const groupName = GroupVariantFor(baseName);
if (groupName.empty())
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();
// We check usefulness on the **basic** buff (not the greater version),
// because "spell cast useful" may return false for the greater variant.
bool const usefulBase = botAI->GetAiObjectContext()
->GetValue<bool>("spell cast useful", baseName)->Get();
if (groupSpellId && HasRequiredReagents(bot, groupSpellId))
return groupName;
if (groupVariantSpellId && HasRequiredReagents(bot, groupVariantSpellId))
{
// Learned + reagents OK -> switch to greater
return groupName;
}
// Missing reagents -> announce if (a) greater is known, (b) base buff is useful,
// (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;
return baseName;
}
}

View File

@ -6,63 +6,40 @@
#pragma once
#include <string>
#include <functional>
#include "Common.h"
#include "Group.h"
#include "Chat.h"
#include "Language.h"
class Player;
class PlayerbotAI;
class Unit;
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);
// Returns the group spell name for a given single-target buff.
// If no group equivalent exists, returns "".
std::string GroupVariantFor(std::string const& name);
// Checks if the bot has the required reagents to cast a spell (by its spellId).
// Returns false if the spellId is invalid.
bool NeedsPostLoginBuffGrace(std::string const& name);
bool ShouldDeferPartyBuffEvaluationForRecentLogin(
Player* bot,
Unit* target,
std::string const& spell);
bool ShouldDeferGreaterBlessingAssignmentForRecentLogin(Player* bot);
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(
Player* bot,
PlayerbotAI* botAI,
std::string const& baseName,
bool announceOnMissing = false,
std::function<void(std::string const&)> announce = {}
);
std::string const& baseName);
}
namespace ai::spell
{
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

@ -29,7 +29,7 @@ public:
if (!botAI->GetBot()->IsWithinLOSInMap(unit))
return false;
return botAI->IsMovementImpaired(unit);
return botAI->IsMovementImpaired(unit) && !botAI->HasAnyAuraOf(unit, "stealth", "prowl", nullptr);
}
};

View File

@ -135,6 +135,32 @@ bool PartyMemberToHeal::Check(Unit* player)
bot->GetDistance2d(player) < sPlayerbotAIConfig.healDistance * 2 && bot->IsWithinLOSInMap(player);
}
Unit* HealerLowMana::Calculate()
{
Group* group = bot->GetGroup();
if (!group)
return nullptr;
MinValueCalculator calc(100);
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* player = gref->GetSource();
if (!player || player == bot)
continue;
if (player->IsGameMaster() || !player->IsAlive())
continue;
if (!botAI->IsHeal(player))
continue;
float mana = player->GetPowerPct(POWER_MANA);
if (mana < calc.minValue)
calc.probe(mana, player);
}
return (Unit*)calc.param;
}
Unit* PartyMemberToProtect::Calculate()
{
return nullptr;

View File

@ -37,4 +37,13 @@ protected:
Unit* Calculate() override;
};
class HealerLowMana : public PartyMemberValue
{
public:
HealerLowMana(PlayerbotAI* botAI) : PartyMemberValue(botAI, "healer low mana") {}
protected:
Unit* Calculate() override;
};
#endif

View File

@ -132,6 +132,7 @@ public:
creators["attacker without aura"] = &ValueContext::attacker_without_aura;
creators["melee attacker without aura"] = &ValueContext::melee_attacker_without_aura;
creators["party member to heal"] = &ValueContext::party_member_to_heal;
creators["healer low mana"] = &ValueContext::healer_low_mana;
creators["party member to resurrect"] = &ValueContext::party_member_to_resurrect;
creators["current target"] = &ValueContext::current_target;
creators["self target"] = &ValueContext::self_target;
@ -451,6 +452,7 @@ private:
return new MeleeAttackerWithoutAuraTargetValue(botAI);
}
static UntypedValue* party_member_to_heal(PlayerbotAI* botAI) { return new PartyMemberToHeal(botAI); }
static UntypedValue* healer_low_mana(PlayerbotAI* botAI) { return new HealerLowMana(botAI); }
static UntypedValue* party_member_to_resurrect(PlayerbotAI* botAI) { return new PartyMemberToResurrect(botAI); }
static UntypedValue* party_member_to_dispel(PlayerbotAI* botAI) { return new PartyMemberToDispel(botAI); }
static UntypedValue* party_member_to_protect(PlayerbotAI* botAI) { return new PartyMemberToProtect(botAI); }

View File

@ -41,6 +41,7 @@ void GenericDKNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
{
NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) }));
triggers.push_back(
new TriggerNode("horn of winter", { NextAction("horn of winter", 21.0f) }));
triggers.push_back(

View File

@ -11,6 +11,9 @@
#include "AoeValues.h"
#include "TargetValue.h"
constexpr uint32 SPELL_ECLIPSE_SOLAR = 48517;
constexpr uint32 SPELL_ECLIPSE_LUNAR = 48518;
namespace
{
bool PrepareThornsTarget(PlayerbotAI* botAI, Unit* target)
@ -64,16 +67,89 @@ bool CastThornsOnMainTankAction::Execute(Event event)
return PrepareThornsTarget(botAI, GetTarget()) && BuffOnMainTankAction::Execute(event);
}
Value<Unit*>* CastEntanglingRootsCcAction::GetTargetValue()
bool CastWrathAction::isUseful()
{
return context->GetValue<Unit*>("cc target", "entangling roots");
time_t now = time(nullptr);
time_t solarTime = context->GetValue<time_t>("eclipse solar proc time")->Get();
time_t lunarTime = context->GetValue<time_t>("eclipse lunar proc time")->Get();
// --- Update Solar Eclipse tracking ---
// Wrath is selected during Solar Eclipse (eclipse trigger at 20.0f), so we reliably see it here.
if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && !solarTime)
context->GetValue<time_t>("eclipse solar proc time")->Set(now);
// Lunar procced — Solar fishing window is over, new cycle begins
if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && solarTime)
context->GetValue<time_t>("eclipse solar proc time")->Set(0);
// 30 s cooldown window expired
if (solarTime && (now - solarTime) >= 30)
context->GetValue<time_t>("eclipse solar proc time")->Set(0);
// --- Update Lunar Eclipse tracking (belt-and-suspenders in case Starfire isn't evaluated) ---
if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && !lunarTime)
context->GetValue<time_t>("eclipse lunar proc time")->Set(now);
// Solar procced — Lunar fishing window is over
if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && lunarTime)
context->GetValue<time_t>("eclipse lunar proc time")->Set(0);
if (lunarTime && (now - lunarTime) >= 30)
context->GetValue<time_t>("eclipse lunar proc time")->Set(0);
// Block Wrath while in Lunar Eclipse / post-Lunar fishing window
if (context->GetValue<time_t>("eclipse lunar proc time")->Get())
return false;
return CastSpellAction::isUseful();
}
bool CastEntanglingRootsCcAction::Execute(Event /*event*/) { return botAI->CastSpell("entangling roots", GetTarget()); }
bool CastStarfireAction::isUseful()
{
time_t now = time(nullptr);
time_t solarTime = context->GetValue<time_t>("eclipse solar proc time")->Get();
time_t lunarTime = context->GetValue<time_t>("eclipse lunar proc time")->Get();
Value<Unit*>* CastHibernateCcAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", "hibernate"); }
// --- Update Lunar Eclipse tracking ---
// Starfire is selected during Lunar Eclipse (eclipse trigger at 20.0f), so we reliably see it here.
if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && !lunarTime)
context->GetValue<time_t>("eclipse lunar proc time")->Set(now);
// Solar procced — Lunar fishing window is over, new cycle begins
if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && lunarTime)
context->GetValue<time_t>("eclipse lunar proc time")->Set(0);
// 30 s cooldown window expired
if (lunarTime && (now - lunarTime) >= 30)
context->GetValue<time_t>("eclipse lunar proc time")->Set(0);
// --- Update Solar Eclipse tracking (belt-and-suspenders in case Wrath isn't evaluated) ---
if (bot->HasAura(SPELL_ECLIPSE_SOLAR) && !solarTime)
context->GetValue<time_t>("eclipse solar proc time")->Set(now);
// Lunar procced — Solar fishing window is over
if (bot->HasAura(SPELL_ECLIPSE_LUNAR) && solarTime)
context->GetValue<time_t>("eclipse solar proc time")->Set(0);
if (solarTime && (now - solarTime) >= 30)
context->GetValue<time_t>("eclipse solar proc time")->Set(0);
// Block Starfire while in Solar Eclipse / post-Solar fishing window
if (context->GetValue<time_t>("eclipse solar proc time")->Get())
return false;
return CastSpellAction::isUseful();
}
Value<Unit*>* CastEntanglingRootsCcAction::GetTargetValue()
{
return context->GetValue<Unit*>("rti cc target");
}
Value<Unit*>* CastHibernateCcAction::GetTargetValue() { return context->GetValue<Unit*>("rti cc target"); }
Value<Unit*>* CastCycloneCcAction::GetTargetValue() { return context->GetValue<Unit*>("rti cc target"); }
bool CastTyphoonAction::isUseful()
{
bool facingTarget = AI_VALUE2(bool, "facing", "current target");
bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan(
AI_VALUE2(float, "distance", GetTargetName()), 15.f);
return facingTarget && targetClose;
}
bool CastHibernateCcAction::Execute(Event /*event*/) { return botAI->CastSpell("hibernate", GetTarget()); }
bool CastStarfallAction::isUseful()
{
if (!CastSpellAction::isUseful())
@ -89,12 +165,26 @@ bool CastStarfallAction::isUseful()
return false;
}
// Avoid single-target usage on initial pull
uint8 aoeCount = *context->GetValue<uint8>("aoe count");
if (aoeCount < 2)
// Suppress if any unengaged hostile unit is within 40 yards — Starfall's 36-yard radius would pull them.
Unit* currentTarget = AI_VALUE(Unit*, "current target");
GuidVector const& nearbyNpcs = AI_VALUE(GuidVector, "possible targets");
for (ObjectGuid const& guid : nearbyNpcs)
{
Unit* target = context->GetValue<Unit*>("current target")->Get();
if (!target || (!botAI->HasAura("moonfire", target) && !botAI->HasAura("insect swarm", target)))
Unit* unit = botAI->GetUnit(guid);
// Standard null/world-state guard before touching the unit.
if (!unit || !unit->IsAlive() || !unit->IsInWorld())
continue;
// Already our target — its in-combat flag covers it.
if (unit == currentTarget)
continue;
// Safety net for any hostile-faction trigger creature that carries NON_ATTACKABLE flags.
if (!bot->IsValidAttackTarget(unit))
continue;
// Outside Starfall's actual radius; no pull risk.
if (ServerFacade::instance().GetDistance2d(bot, unit) > 40.0f)
continue;
// Unengaged mob within range — casting would pull it.
if (!unit->IsInCombat())
return false;
}
@ -119,6 +209,24 @@ bool CastRebirthAction::isUseful()
AI_VALUE2(float, "distance", GetTargetName()) <= sPlayerbotAIConfig.spellDistance;
}
bool CastInnervateOnHealerAction::isPossible()
{
Unit* target = GetTarget();
if (!target || !target->IsInWorld())
return false;
if (botAI->HasAura("innervate", target))
return false;
uint32 spellId = AI_VALUE2(uint32, "spell id", "innervate");
return spellId && !bot->HasSpellCooldown(spellId);
}
std::vector<NextAction> CastInnervateOnHealerAction::getPrerequisites()
{
return { NextAction("caster form") };
}
Unit* CastRejuvenationOnNotFullAction::GetTarget()
{
Group* group = bot->GetGroup();
@ -149,3 +257,63 @@ bool CastRejuvenationOnNotFullAction::isUseful()
{
return GetTarget();
}
// --- Blanket HoT actions ---
Unit* CastBlanketHotAction::GetBlanketTarget(std::string const& auraName)
{
Group* group = bot->GetGroup();
if (!group)
return nullptr;
auto eligible = [&](Player* member) -> bool
{
return member && member->IsAlive() &&
!member->IsGameMaster() &&
bot->GetDistance2d(member) <= sPlayerbotAIConfig.spellDistance &&
!botAI->HasAura(auraName, member, false, true);
};
Player* firstMelee = nullptr;
Player* firstRanged = nullptr;
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!eligible(member))
continue;
if (PlayerbotAI::IsTank(member))
return member;
else if (!firstMelee && PlayerbotAI::IsMelee(member) && !PlayerbotAI::IsTank(member))
firstMelee = member;
else if (!firstRanged && PlayerbotAI::IsRanged(member))
firstRanged = member;
if (firstMelee && firstRanged)
break;
}
if (firstMelee) return firstMelee;
return firstRanged;
}
Unit* CastRejuvenationBlanketAction::GetTarget()
{
return GetBlanketTarget("rejuvenation");
}
bool CastRejuvenationBlanketAction::isUseful()
{
return GetTarget() != nullptr;
}
Unit* CastWildGrowthBlanketAction::GetTarget()
{
return GetBlanketTarget("wild growth");
}
bool CastWildGrowthBlanketAction::isUseful()
{
return GetTarget() != nullptr;
}

View File

@ -8,6 +8,7 @@
#include "GenericSpellActions.h"
#include "SharedDefines.h"
#include "Value.h"
class PlayerbotAI;
class Unit;
@ -64,7 +65,7 @@ class CastHealingTouchOnPartyAction : public HealPartyMemberAction
{
public:
CastHealingTouchOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "healing touch", 50.0f, HealingManaEfficiency::LOW)
: HealPartyMemberAction(botAI, "healing touch", 50.0f, HealingManaEfficiency::MEDIUM)
{
}
};
@ -86,16 +87,16 @@ public:
bool isUseful() override;
};
class CastMarkOfTheWildAction : public CastBuffSpellAction
class CastMarkOfTheWildAction : public GroupBuffSpellAction
{
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:
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "mark of the wild") {}
CastMarkOfTheWildOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "mark of the wild") {}
};
class CastSurvivalInstinctsAction : public CastBuffSpellAction
@ -142,16 +143,11 @@ public:
bool isUseful() override;
};
class CastOmenOfClarityAction : public CastBuffSpellAction
{
public:
CastOmenOfClarityAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "omen of clarity") {}
};
class CastWrathAction : public CastSpellAction
{
public:
CastWrathAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "wrath") {}
bool isUseful() override;
};
class CastStarfallAction : public CastSpellAction
@ -169,6 +165,14 @@ public:
ActionThreatType getThreatType() override { return ActionThreatType::Aoe; }
};
class CastTyphoonAction : public CastSpellAction
{
public:
CastTyphoonAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "typhoon") {}
ActionThreatType getThreatType() override { return ActionThreatType::Aoe; }
bool isUseful() override;
};
class CastMoonfireAction : public CastDebuffSpellAction
{
public:
@ -185,6 +189,7 @@ class CastStarfireAction : public CastSpellAction
{
public:
CastStarfireAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "starfire") {}
bool isUseful() override;
};
class CastEntanglingRootsAction : public CastSpellAction
@ -193,12 +198,11 @@ public:
CastEntanglingRootsAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "entangling roots") {}
};
class CastEntanglingRootsCcAction : public CastSpellAction
class CastEntanglingRootsCcAction : public CastCrowdControlSpellAction
{
public:
CastEntanglingRootsCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "entangling roots on cc") {}
CastEntanglingRootsCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "entangling roots") {}
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};
class CastHibernateAction : public CastSpellAction
@ -207,12 +211,18 @@ public:
CastHibernateAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hibernate") {}
};
class CastHibernateCcAction : public CastSpellAction
class CastHibernateCcAction : public CastCrowdControlSpellAction
{
public:
CastHibernateCcAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "hibernate on cc") {}
CastHibernateCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "hibernate") {}
Value<Unit*>* GetTargetValue() override;
};
class CastCycloneCcAction : public CastCrowdControlSpellAction
{
public:
CastCycloneCcAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "cyclone") {}
Value<Unit*>* GetTargetValue() override;
bool Execute(Event event) override;
};
class CastNaturesGraspAction : public CastBuffSpellAction
@ -264,6 +274,16 @@ public:
std::string const GetTargetName() override { return "self target"; }
};
class CastInnervateOnHealerAction : public CastSpellAction
{
public:
CastInnervateOnHealerAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "innervate") {}
std::string const GetTargetName() override { return "healer low mana"; }
bool isPossible() override;
std::vector<NextAction> getPrerequisites() override;
};
class CastTranquilityAction : public CastAoeHealSpellAction
{
public:
@ -312,13 +332,15 @@ public:
class CastInsectSwarmOnAttackerAction : public CastDebuffSpellOnAttackerAction
{
public:
CastInsectSwarmOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "insect swarm") {}
CastInsectSwarmOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "insect swarm", true, 0.0f) {}
bool isUseful() override { return CastAuraSpellAction::isUseful(); }
};
class CastMoonfireOnAttackerAction : public CastDebuffSpellOnAttackerAction
{
public:
CastMoonfireOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "moonfire") {}
CastMoonfireOnAttackerAction(PlayerbotAI* ai) : CastDebuffSpellOnAttackerAction(ai, "moonfire", true, 0.0f) {}
bool isUseful() override { return CastAuraSpellAction::isUseful(); }
};
class CastEnrageAction : public CastBuffSpellAction
@ -344,4 +366,48 @@ public:
CastForceOfNatureAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "force of nature") {}
};
// Base for blanket HoT actions. Provides GetBlanketTarget() as a member so
// subclasses can use AI_VALUE and the standard context machinery.
class CastBlanketHotAction : public CastSpellAction
{
public:
CastBlanketHotAction(PlayerbotAI* ai, std::string const& spell) : CastSpellAction(ai, spell)
{
range = botAI->GetRange("heal");
}
protected:
Unit* GetBlanketTarget(std::string const& auraName);
};
class CastRejuvenationBlanketAction : public CastBlanketHotAction
{
public:
CastRejuvenationBlanketAction(PlayerbotAI* ai) : CastBlanketHotAction(ai, "rejuvenation") {}
bool isUseful() override;
Unit* GetTarget() override;
std::string const getName() override { return "rejuvenation blanket"; }
};
class CastWildGrowthBlanketAction : public CastBlanketHotAction
{
public:
CastWildGrowthBlanketAction(PlayerbotAI* ai) : CastBlanketHotAction(ai, "wild growth") {}
bool isUseful() override;
Unit* GetTarget() override;
std::string const getName() override { return "wild growth blanket"; }
};
class EclipseSolarProcTimeValue : public ManualSetValue<time_t>
{
public:
EclipseSolarProcTimeValue(PlayerbotAI* botAI) : ManualSetValue<time_t>(botAI, 0) {}
};
class EclipseLunarProcTimeValue : public ManualSetValue<time_t>
{
public:
EclipseLunarProcTimeValue(PlayerbotAI* botAI) : ManualSetValue<time_t>(botAI, 0) {}
};
#endif

View File

@ -9,12 +9,23 @@
#include "GenericSpellActions.h"
#include "ReachTargetActions.h"
constexpr uint32 SPELL_POUNCE_RANK_1 = 9005;
constexpr uint32 SPELL_RAVAGE_RANK_1 = 6785;
class PlayerbotAI;
class CastFeralChargeCatAction : public CastReachTargetSpellAction
{
public:
CastFeralChargeCatAction(PlayerbotAI* botAI) : CastReachTargetSpellAction(botAI, "feral charge - cat", 1.5f) {}
bool isUseful() override
{
if (botAI->HasAura("prowl", bot))
return false;
return CastReachTargetSpellAction::isUseful();
}
};
class CastCowerAction : public CastBuffSpellAction
@ -48,28 +59,47 @@ public:
CastRakeAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "rake", true, 6.0f) {}
};
class CastRakeOnMeleeAttackersAction : public CastDebuffSpellOnMeleeAttackerAction
{
public:
CastRakeOnMeleeAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnMeleeAttackerAction(botAI, "rake", true, 6.0f) {}
};
class CastClawAction : public CastMeleeSpellAction
{
public:
CastClawAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "claw") {}
bool isUseful() override
{
// Block Claw once Pounce is learned; Claw remains available as the stealth opener before then.
if (botAI->HasAura("prowl", bot) && bot->HasSpell(SPELL_POUNCE_RANK_1))
return false;
return CastMeleeSpellAction::isUseful();
}
};
class CastMangleCatAction : public CastMeleeSpellAction
{
public:
CastMangleCatAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "mangle (cat)") {}
bool isUseful() override
{
if (botAI->HasAura("prowl", bot))
return false;
return CastMeleeSpellAction::isUseful();
}
};
class CastSwipeCatAction : public CastMeleeSpellAction
{
public:
CastSwipeCatAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "swipe (cat)") {}
bool isUseful() override
{
if (botAI->HasAura("prowl", bot))
return false;
return CastMeleeSpellAction::isUseful();
}
};
class CastFerociousBiteAction : public CastMeleeSpellAction
@ -78,6 +108,21 @@ public:
CastFerociousBiteAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "ferocious bite") {}
};
class CastMaimAction : public CastMeleeSpellAction
{
public:
CastMaimAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "maim") {}
bool isUseful() override
{
Unit* target = GetTarget();
if (!target || !target->ToPlayer())
return false;
return CastMeleeSpellAction::isUseful();
}
};
class CastRipAction : public CastMeleeDebuffSpellAction
{
public:
@ -88,6 +133,14 @@ class CastShredAction : public CastMeleeSpellAction
{
public:
CastShredAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shred") {}
bool isUseful() override
{
if (botAI->HasAura("prowl", bot) && bot->HasSpell(SPELL_RAVAGE_RANK_1))
return false;
return CastMeleeSpellAction::isUseful();
}
};
class CastProwlAction : public CastBuffSpellAction
@ -106,12 +159,28 @@ class CastRavageAction : public CastMeleeSpellAction
{
public:
CastRavageAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "ravage") {}
bool isUseful() override
{
if (!botAI->HasAura("prowl", bot))
return false;
return CastMeleeSpellAction::isUseful();
}
};
class CastPounceAction : public CastMeleeSpellAction
{
public:
CastPounceAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "pounce") {}
bool isUseful() override
{
if (!botAI->HasAura("prowl", bot))
return false;
return CastMeleeSpellAction::isUseful();
}
};
#endif

View File

@ -5,9 +5,9 @@
#include "DruidAiObjectContext.h"
#include "BearTankDruidStrategy.h"
#include "CasterDruidStrategy.h"
#include "CatDpsDruidStrategy.h"
#include "BalanceDruidStrategy.h"
#include "BearDruidStrategy.h"
#include "CatDruidStrategy.h"
#include "DruidActions.h"
#include "DruidBearActions.h"
#include "DruidCatActions.h"
@ -15,9 +15,7 @@
#include "DruidTriggers.h"
#include "GenericDruidNonCombatStrategy.h"
#include "GenericDruidStrategy.h"
#include "HealDruidStrategy.h"
#include "MeleeDruidStrategy.h"
#include "OffhealDruidCatStrategy.h"
#include "RestoDruidStrategy.h"
#include "Playerbots.h"
#include "DruidPullStrategy.h"
@ -28,30 +26,31 @@ public:
{
creators["nc"] = &DruidStrategyFactoryInternal::nc;
creators["pull"] = &DruidStrategyFactoryInternal::pull;
creators["cat aoe"] = &DruidStrategyFactoryInternal::cat_aoe;
creators["caster aoe"] = &DruidStrategyFactoryInternal::caster_aoe;
creators["caster debuff"] = &DruidStrategyFactoryInternal::caster_debuff;
creators["dps debuff"] = &DruidStrategyFactoryInternal::caster_debuff;
creators["aoe"] = &DruidStrategyFactoryInternal::aoe;
creators["cure"] = &DruidStrategyFactoryInternal::cure;
creators["melee"] = &DruidStrategyFactoryInternal::melee;
creators["buff"] = &DruidStrategyFactoryInternal::buff;
creators["boost"] = &DruidStrategyFactoryInternal::boost;
creators["cc"] = &DruidStrategyFactoryInternal::cc;
creators["healer dps"] = &DruidStrategyFactoryInternal::healer_dps;
creators["offheal"] = &DruidStrategyFactoryInternal::offheal;
creators["blanketing"] = &DruidStrategyFactoryInternal::blanketing;
creators["tranquility"] = &DruidStrategyFactoryInternal::tranquility;
creators["feral charge"] = &DruidStrategyFactoryInternal::feral_charge;
}
private:
static Strategy* nc(PlayerbotAI* botAI) { return new GenericDruidNonCombatStrategy(botAI); }
static Strategy* pull(PlayerbotAI* botAI) { return new DruidPullStrategy(botAI); }
static Strategy* cat_aoe(PlayerbotAI* botAI) { return new CatAoeDruidStrategy(botAI); }
static Strategy* caster_aoe(PlayerbotAI* botAI) { return new CasterDruidAoeStrategy(botAI); }
static Strategy* caster_debuff(PlayerbotAI* botAI) { return new CasterDruidDebuffStrategy(botAI); }
static Strategy* aoe(PlayerbotAI* botAI) { return new DruidAoeStrategy(botAI); }
static Strategy* cure(PlayerbotAI* botAI) { return new DruidCureStrategy(botAI); }
static Strategy* melee(PlayerbotAI* botAI) { return new MeleeDruidStrategy(botAI); }
static Strategy* buff(PlayerbotAI* botAI) { return new GenericDruidBuffStrategy(botAI); }
static Strategy* boost(PlayerbotAI* botAI) { return new DruidBoostStrategy(botAI); }
static Strategy* cc(PlayerbotAI* botAI) { return new DruidCcStrategy(botAI); }
static Strategy* healer_dps(PlayerbotAI* botAI) { return new DruidHealerDpsStrategy(botAI); }
static Strategy* offheal(PlayerbotAI* botAI) { return new CatOffhealStrategy(botAI); }
static Strategy* blanketing(PlayerbotAI* botAI) { return new DruidBlanketStrategy(botAI); }
static Strategy* tranquility(PlayerbotAI* botAI) { return new DruidTranquilityStrategy(botAI); }
static Strategy* feral_charge(PlayerbotAI* botAI) { return new FeralChargeDruidStrategy(botAI); }
};
class DruidDruidStrategyFactoryInternal : public NamedObjectContext<Strategy>
@ -62,18 +61,16 @@ public:
creators["bear"] = &DruidDruidStrategyFactoryInternal::bear;
creators["tank"] = &DruidDruidStrategyFactoryInternal::bear;
creators["cat"] = &DruidDruidStrategyFactoryInternal::cat;
creators["caster"] = &DruidDruidStrategyFactoryInternal::caster;
creators["balance"] = &DruidDruidStrategyFactoryInternal::balance;
creators["dps"] = &DruidDruidStrategyFactoryInternal::cat;
creators["heal"] = &DruidDruidStrategyFactoryInternal::heal;
creators["offheal"] = &DruidDruidStrategyFactoryInternal::offheal;
creators["resto"] = &DruidDruidStrategyFactoryInternal::heal;
}
private:
static Strategy* bear(PlayerbotAI* botAI) { return new BearTankDruidStrategy(botAI); }
static Strategy* cat(PlayerbotAI* botAI) { return new CatDpsDruidStrategy(botAI); }
static Strategy* caster(PlayerbotAI* botAI) { return new CasterDruidStrategy(botAI); }
static Strategy* heal(PlayerbotAI* botAI) { return new HealDruidStrategy(botAI); }
static Strategy* offheal(PlayerbotAI* botAI) { return new OffhealDruidCatStrategy(botAI); }
static Strategy* bear(PlayerbotAI* botAI) { return new BearDruidStrategy(botAI); }
static Strategy* cat(PlayerbotAI* botAI) { return new CatDruidStrategy(botAI); }
static Strategy* balance(PlayerbotAI* botAI) { return new BalanceDruidStrategy(botAI); }
static Strategy* heal(PlayerbotAI* botAI) { return new RestoDruidStrategy(botAI); }
};
class DruidTriggerFactoryInternal : public NamedObjectContext<Trigger>
@ -81,7 +78,6 @@ class DruidTriggerFactoryInternal : public NamedObjectContext<Trigger>
public:
DruidTriggerFactoryInternal()
{
creators["omen of clarity"] = &DruidTriggerFactoryInternal::omen_of_clarity;
creators["clearcasting"] = &DruidTriggerFactoryInternal::clearcasting;
creators["thorns"] = &DruidTriggerFactoryInternal::thorns;
creators["thorns on party"] = &DruidTriggerFactoryInternal::thorns_on_party;
@ -90,10 +86,12 @@ public:
creators["faerie fire (feral)"] = &DruidTriggerFactoryInternal::faerie_fire_feral;
creators["faerie fire"] = &DruidTriggerFactoryInternal::faerie_fire;
creators["insect swarm"] = &DruidTriggerFactoryInternal::insect_swarm;
creators["insect swarm on attacker"] = &DruidTriggerFactoryInternal::insect_swarm_on_attacker;
creators["moonfire"] = &DruidTriggerFactoryInternal::moonfire;
creators["moonfire on attacker"] = &DruidTriggerFactoryInternal::moonfire_on_attacker;
creators["nature's grasp"] = &DruidTriggerFactoryInternal::natures_grasp;
creators["tiger's fury"] = &DruidTriggerFactoryInternal::tigers_fury;
creators["berserk"] = &DruidTriggerFactoryInternal::berserk;
creators["berserk active"] = &DruidTriggerFactoryInternal::berserk_active;
creators["savage roar"] = &DruidTriggerFactoryInternal::savage_roar;
creators["rake"] = &DruidTriggerFactoryInternal::rake;
creators["mark of the wild"] = &DruidTriggerFactoryInternal::mark_of_the_wild;
@ -110,17 +108,34 @@ public:
creators["eclipse (lunar)"] = &DruidTriggerFactoryInternal::eclipse_lunar;
creators["bash on enemy healer"] = &DruidTriggerFactoryInternal::bash_on_enemy_healer;
creators["nature's swiftness"] = &DruidTriggerFactoryInternal::natures_swiftness;
creators["nature's swiftness active"] = &DruidTriggerFactoryInternal::natures_swiftness_active;
creators["party member remove curse"] = &DruidTriggerFactoryInternal::party_member_remove_curse;
creators["eclipse (solar) cooldown"] = &DruidTriggerFactoryInternal::eclipse_solar_cooldown;
creators["eclipse (lunar) cooldown"] = &DruidTriggerFactoryInternal::eclipse_lunar_cooldown;
creators["mangle (bear)"] = &DruidTriggerFactoryInternal::mangle_bear_trigger;
creators["lacerate"] = &DruidTriggerFactoryInternal::lacerate_trigger;
creators["demoralizing roar"] = &DruidTriggerFactoryInternal::demoralize_roar;
creators["mangle (cat)"] = &DruidTriggerFactoryInternal::mangle_cat;
creators["ferocious bite time"] = &DruidTriggerFactoryInternal::ferocious_bite_time;
creators["ferocious bite execute"] = &DruidTriggerFactoryInternal::ferocious_bite_execute;
creators["hurricane channel check"] = &DruidTriggerFactoryInternal::hurricane_channel_check;
creators["no healer dps strategy"] = &DruidTriggerFactoryInternal::no_healer_dps_strategy;
creators["starfall"] = &DruidTriggerFactoryInternal::starfall;
creators["force of nature"] = &DruidTriggerFactoryInternal::force_of_nature;
creators["cyclone"] = &DruidTriggerFactoryInternal::cyclone;
creators["predator's swiftness"] = &DruidTriggerFactoryInternal::predators_swiftness;
creators["predator's swiftness and cyclone"] = &DruidTriggerFactoryInternal::predators_swiftness_and_cyclone;
creators["predator's swiftness and hibernate"] = &DruidTriggerFactoryInternal::predators_swiftness_and_hibernate;
creators["predator's swiftness and entangling roots"] = &DruidTriggerFactoryInternal::predators_swiftness_and_entangling_roots;
creators["predator's swiftness and combat party member dead"] = &DruidTriggerFactoryInternal::predators_swiftness_and_combat_party_member_dead;
creators["clearcasting and medium aoe"] = &DruidTriggerFactoryInternal::clearcasting_and_medium_aoe;
creators["prowl"] = &DruidTriggerFactoryInternal::prowl_trigger;
creators["rejuvenation blanket"] = &DruidTriggerFactoryInternal::rejuvenation_blanket;
creators["wild growth blanket"] = &DruidTriggerFactoryInternal::wild_growth_blanket;
creators["aquatic form"] = &DruidTriggerFactoryInternal::aquatic_form;
}
private:
static Trigger* natures_swiftness(PlayerbotAI* botAI) { return new NaturesSwiftnessTrigger(botAI); }
static Trigger* natures_swiftness_active(PlayerbotAI* botAI) { return new NaturesSwiftnessActiveTrigger(botAI); }
static Trigger* clearcasting(PlayerbotAI* botAI) { return new ClearcastingTrigger(botAI); }
static Trigger* eclipse_solar(PlayerbotAI* botAI) { return new EclipseSolarTrigger(botAI); }
static Trigger* eclipse_lunar(PlayerbotAI* botAI) { return new EclipseLunarTrigger(botAI); }
@ -130,11 +145,13 @@ private:
static Trigger* bash(PlayerbotAI* botAI) { return new BashInterruptSpellTrigger(botAI); }
static Trigger* faerie_fire_feral(PlayerbotAI* botAI) { return new FaerieFireFeralTrigger(botAI); }
static Trigger* insect_swarm(PlayerbotAI* botAI) { return new InsectSwarmTrigger(botAI); }
static Trigger* insect_swarm_on_attacker(PlayerbotAI* botAI) { return new InsectSwarmOnAttackerTrigger(botAI); }
static Trigger* moonfire(PlayerbotAI* botAI) { return new MoonfireTrigger(botAI); }
static Trigger* moonfire_on_attacker(PlayerbotAI* botAI) { return new MoonfireOnAttackerTrigger(botAI); }
static Trigger* faerie_fire(PlayerbotAI* botAI) { return new FaerieFireTrigger(botAI); }
static Trigger* natures_grasp(PlayerbotAI* botAI) { return new NaturesGraspTrigger(botAI); }
static Trigger* tigers_fury(PlayerbotAI* botAI) { return new TigersFuryTrigger(botAI); }
static Trigger* berserk(PlayerbotAI* botAI) { return new BerserkTrigger(botAI); }
static Trigger* berserk_active(PlayerbotAI* botAI) { return new BerserkActiveTrigger(botAI); }
static Trigger* savage_roar(PlayerbotAI* botAI) { return new SavageRoarTrigger(botAI); }
static Trigger* rake(PlayerbotAI* botAI) { return new RakeTrigger(botAI); }
static Trigger* mark_of_the_wild(PlayerbotAI* botAI) { return new MarkOfTheWildTrigger(botAI); }
@ -148,14 +165,28 @@ private:
static Trigger* cat_form(PlayerbotAI* botAI) { return new CatFormTrigger(botAI); }
static Trigger* tree_form(PlayerbotAI* botAI) { return new TreeFormTrigger(botAI); }
static Trigger* bash_on_enemy_healer(PlayerbotAI* botAI) { return new BashInterruptEnemyHealerSpellTrigger(botAI); }
static Trigger* omen_of_clarity(PlayerbotAI* botAI) { return new OmenOfClarityTrigger(botAI); }
static Trigger* party_member_remove_curse(PlayerbotAI* ai) { return new DruidPartyMemberRemoveCurseTrigger(ai); }
static Trigger* eclipse_solar_cooldown(PlayerbotAI* ai) { return new EclipseSolarCooldownTrigger(ai); }
static Trigger* eclipse_lunar_cooldown(PlayerbotAI* ai) { return new EclipseLunarCooldownTrigger(ai); }
static Trigger* mangle_bear_trigger(PlayerbotAI* botAI) { return new MangleBearTrigger(botAI); }
static Trigger* lacerate_trigger(PlayerbotAI* botAI) { return new LacerateTrigger(botAI); }
static Trigger* demoralize_roar(PlayerbotAI* botAI) { return new DemoralizeRoarTrigger(botAI); }
static Trigger* mangle_cat(PlayerbotAI* ai) { return new MangleCatTrigger(ai); }
static Trigger* ferocious_bite_time(PlayerbotAI* ai) { return new FerociousBiteTimeTrigger(ai); }
static Trigger* ferocious_bite_execute(PlayerbotAI* ai) { return new FerociousBiteExecuteTrigger(ai); }
static Trigger* hurricane_channel_check(PlayerbotAI* ai) { return new HurricaneChannelCheckTrigger(ai); }
static Trigger* no_healer_dps_strategy(PlayerbotAI* ai) { return new NoHealerDpsStrategyTrigger(ai); }
static Trigger* starfall(PlayerbotAI* ai) { return new StarfallTrigger(ai); }
static Trigger* force_of_nature(PlayerbotAI* ai) { return new ForceOfNatureTrigger(ai); }
static Trigger* cyclone(PlayerbotAI* ai) { return new CycloneTrigger(ai); }
static Trigger* predators_swiftness(PlayerbotAI* ai) { return new PredatorsSwiftnessTrigger(ai); }
static Trigger* predators_swiftness_and_cyclone(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "cyclone"); }
static Trigger* predators_swiftness_and_hibernate(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "hibernate"); }
static Trigger* predators_swiftness_and_entangling_roots(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "entangling roots"); }
static Trigger* predators_swiftness_and_combat_party_member_dead(PlayerbotAI* ai) { return new TwoTriggers(ai, "predator's swiftness", "combat party member dead"); }
static Trigger* clearcasting_and_medium_aoe(PlayerbotAI* ai) { return new TwoTriggers(ai, "clearcasting", "medium aoe"); }
static Trigger* prowl_trigger(PlayerbotAI* ai) { return new ProwlTrigger(ai); }
static Trigger* rejuvenation_blanket(PlayerbotAI* ai) { return new BuffOnPartyTrigger(ai, "rejuvenation"); }
static Trigger* wild_growth_blanket(PlayerbotAI* ai) { return new BuffOnPartyTrigger(ai, "wild growth"); }
static Trigger* aquatic_form(PlayerbotAI* ai) { return new AquaticFormTrigger(ai); }
};
class DruidAiObjectContextInternal : public NamedObjectContext<Action>
@ -193,8 +224,8 @@ public:
creators["hibernate"] = &DruidAiObjectContextInternal::hibernate;
creators["entangling roots"] = &DruidAiObjectContextInternal::entangling_roots;
creators["entangling roots on cc"] = &DruidAiObjectContextInternal::entangling_roots_on_cc;
creators["hibernate"] = &DruidAiObjectContextInternal::hibernate;
creators["hibernate on cc"] = &DruidAiObjectContextInternal::hibernate_on_cc;
creators["cyclone on cc"] = &DruidAiObjectContextInternal::cyclone_on_cc;
creators["wrath"] = &DruidAiObjectContextInternal::wrath;
creators["starfall"] = &DruidAiObjectContextInternal::starfall;
creators["insect swarm"] = &DruidAiObjectContextInternal::insect_swarm;
@ -205,9 +236,9 @@ public:
creators["mangle (cat)"] = &DruidAiObjectContextInternal::mangle_cat;
creators["swipe (cat)"] = &DruidAiObjectContextInternal::swipe_cat;
creators["rake"] = &DruidAiObjectContextInternal::rake;
creators["rake on attacker"] = &DruidAiObjectContextInternal::rake_on_attacker;
creators["ferocious bite"] = &DruidAiObjectContextInternal::ferocious_bite;
creators["rip"] = &DruidAiObjectContextInternal::rip;
creators["maim"] = &DruidAiObjectContextInternal::maim;
creators["cower"] = &DruidAiObjectContextInternal::cower;
creators["survival instincts"] = &DruidAiObjectContextInternal::survival_instincts;
creators["frenzied regeneration"] = &DruidAiObjectContextInternal::frenzied_regeneration;
@ -237,9 +268,9 @@ public:
creators["lacerate"] = &DruidAiObjectContextInternal::lacerate;
creators["hurricane"] = &DruidAiObjectContextInternal::hurricane;
creators["innervate"] = &DruidAiObjectContextInternal::innervate;
creators["innervate on healer"] = &DruidAiObjectContextInternal::innervate_on_healer;
creators["tranquility"] = &DruidAiObjectContextInternal::tranquility;
creators["bash on enemy healer"] = &DruidAiObjectContextInternal::bash_on_enemy_healer;
creators["omen of clarity"] = &DruidAiObjectContextInternal::omen_of_clarity;
creators["nature's swiftness"] = &DruidAiObjectContextInternal::natures_swiftness;
creators["prowl"] = &DruidAiObjectContextInternal::prowl;
creators["dash"] = &DruidAiObjectContextInternal::dash;
@ -254,11 +285,13 @@ public:
creators["moonfire on attacker"] = &DruidAiObjectContextInternal::moonfire_on_attacker;
creators["enrage"] = &DruidAiObjectContextInternal::enrage;
creators["force of nature"] = &DruidAiObjectContextInternal::force_of_nature;
creators["typhoon"] = &DruidAiObjectContextInternal::typhoon;
creators["rejuvenation blanket"] = &DruidAiObjectContextInternal::rejuvenation_blanket;
creators["wild growth blanket"] = &DruidAiObjectContextInternal::wild_growth_blanket;
}
private:
static Action* natures_swiftness(PlayerbotAI* botAI) { return new CastNaturesSwiftnessAction(botAI); }
static Action* omen_of_clarity(PlayerbotAI* botAI) { return new CastOmenOfClarityAction(botAI); }
static Action* tranquility(PlayerbotAI* botAI) { return new CastTranquilityAction(botAI); }
static Action* feral_charge_bear(PlayerbotAI* botAI) { return new CastFeralChargeBearAction(botAI); }
static Action* feral_charge_cat(PlayerbotAI* botAI) { return new CastFeralChargeCatAction(botAI); }
@ -291,6 +324,7 @@ private:
static Action* entangling_roots(PlayerbotAI* botAI) { return new CastEntanglingRootsAction(botAI); }
static Action* hibernate_on_cc(PlayerbotAI* botAI) { return new CastHibernateCcAction(botAI); }
static Action* entangling_roots_on_cc(PlayerbotAI* botAI) { return new CastEntanglingRootsCcAction(botAI); }
static Action* cyclone_on_cc(PlayerbotAI* botAI) { return new CastCycloneCcAction(botAI); }
static Action* wrath(PlayerbotAI* botAI) { return new CastWrathAction(botAI); }
static Action* starfall(PlayerbotAI* botAI) { return new CastStarfallAction(botAI); }
static Action* insect_swarm(PlayerbotAI* botAI) { return new CastInsectSwarmAction(botAI); }
@ -301,9 +335,9 @@ private:
static Action* mangle_cat(PlayerbotAI* botAI) { return new CastMangleCatAction(botAI); }
static Action* swipe_cat(PlayerbotAI* botAI) { return new CastSwipeCatAction(botAI); }
static Action* rake(PlayerbotAI* botAI) { return new CastRakeAction(botAI); }
static Action* rake_on_attacker(PlayerbotAI* botAI) { return new CastRakeOnMeleeAttackersAction(botAI); }
static Action* ferocious_bite(PlayerbotAI* botAI) { return new CastFerociousBiteAction(botAI); }
static Action* rip(PlayerbotAI* botAI) { return new CastRipAction(botAI); }
static Action* maim(PlayerbotAI* botAI) { return new CastMaimAction(botAI); }
static Action* cower(PlayerbotAI* botAI) { return new CastCowerAction(botAI); }
static Action* survival_instincts(PlayerbotAI* botAI) { return new CastSurvivalInstinctsAction(botAI); }
static Action* frenzied_regeneration(PlayerbotAI* botAI) { return new CastFrenziedRegenerationAction(botAI); }
@ -333,6 +367,7 @@ private:
static Action* lacerate(PlayerbotAI* botAI) { return new CastLacerateAction(botAI); }
static Action* hurricane(PlayerbotAI* botAI) { return new CastHurricaneAction(botAI); }
static Action* innervate(PlayerbotAI* botAI) { return new CastInnervateAction(botAI); }
static Action* innervate_on_healer(PlayerbotAI* botAI) { return new CastInnervateOnHealerAction(botAI); }
static Action* bash_on_enemy_healer(PlayerbotAI* botAI) { return new CastBashOnEnemyHealerAction(botAI); }
static Action* ravage(PlayerbotAI* botAI) { return new CastRavageAction(botAI); }
static Action* pounce(PlayerbotAI* botAI) { return new CastPounceAction(botAI); }
@ -347,6 +382,9 @@ private:
static Action* moonfire_on_attacker(PlayerbotAI* ai) { return new CastMoonfireOnAttackerAction(ai); }
static Action* enrage(PlayerbotAI* ai) { return new CastEnrageAction(ai); }
static Action* force_of_nature(PlayerbotAI* ai) { return new CastForceOfNatureAction(ai); }
static Action* typhoon(PlayerbotAI* ai) { return new CastTyphoonAction(ai); }
static Action* rejuvenation_blanket(PlayerbotAI* ai) { return new CastRejuvenationBlanketAction(ai); }
static Action* wild_growth_blanket(PlayerbotAI* ai) { return new CastWildGrowthBlanketAction(ai); }
};
SharedNamedObjectContextList<Strategy> DruidAiObjectContext::sharedStrategyContexts;
@ -386,7 +424,22 @@ void DruidAiObjectContext::BuildSharedTriggerContexts(SharedNamedObjectContextLi
triggerContexts.Add(new DruidTriggerFactoryInternal());
}
class DruidValueContextInternal : public NamedObjectContext<UntypedValue>
{
public:
DruidValueContextInternal()
{
creators["eclipse solar proc time"] = &DruidValueContextInternal::eclipse_solar_proc_time;
creators["eclipse lunar proc time"] = &DruidValueContextInternal::eclipse_lunar_proc_time;
}
private:
static UntypedValue* eclipse_solar_proc_time(PlayerbotAI* botAI) { return new EclipseSolarProcTimeValue(botAI); }
static UntypedValue* eclipse_lunar_proc_time(PlayerbotAI* botAI) { return new EclipseLunarProcTimeValue(botAI); }
};
void DruidAiObjectContext::BuildSharedValueContexts(SharedNamedObjectContextList<UntypedValue>& valueContexts)
{
AiObjectContext::BuildSharedValueContexts(valueContexts);
valueContexts.Add(new DruidValueContextInternal());
}

View File

@ -0,0 +1,118 @@
/*
* 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 "DruidTriggers.h"
#include "DynamicObject.h"
#include "Player.h"
#include "Playerbots.h"
#include "ServerFacade.h"
bool MarkOfTheWildTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
}
bool ThornsOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget());
}
bool EntanglingRootsKiteTrigger::IsActive()
{
return DebuffTrigger::IsActive() && AI_VALUE(uint8, "attacker count") < 3 && !GetTarget()->GetPower(POWER_MANA);
}
bool ThornsTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget()); }
bool BearFormTrigger::IsActive() { return !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr); }
bool TreeFormTrigger::IsActive() { return !botAI->HasAura(33891, bot); }
bool CatFormTrigger::IsActive() { return !botAI->HasAura("cat form", bot); }
bool AquaticFormTrigger::IsActive()
{
return !bot->IsInCombat() && !botAI->HasAura("aquatic form", bot) &&
bot->GetLiquidData().Status == LIQUID_MAP_UNDER_WATER;
}
bool ProwlTrigger::IsActive()
{
if (botAI->HasAura("prowl", bot) || bot->IsInCombat())
return false;
uint32 prowlId = botAI->GetAiObjectContext()->GetValue<uint32>("spell id", "prowl")->Get();
if (!prowlId || !bot->HasSpell(prowlId) || bot->HasSpellCooldown(prowlId))
return false;
float distance = 30.f;
Unit* target = AI_VALUE(Unit*, "enemy player target");
if (target && !target->IsInWorld())
return false;
if (!target)
target = AI_VALUE(Unit*, "grind target");
if (!target)
target = AI_VALUE(Unit*, "dps target");
if (!target)
return false;
if (target && target->GetVictim())
distance -= 10;
if (target->isMoving() && target->GetVictim())
distance -= 10;
if (bot->InBattleground())
distance += 15;
if (bot->InArena())
distance += 15;
return target && ServerFacade::instance().GetDistance2d(bot, target) < distance;
}
const std::set<uint32> HurricaneChannelCheckTrigger::HURRICANE_SPELL_IDS = {
16914, // Hurricane Rank 1
17401, // Hurricane Rank 2
17402, // Hurricane Rank 3
27012, // Hurricane Rank 4
48467 // Hurricane Rank 5
};
bool HurricaneChannelCheckTrigger::IsActive()
{
if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL))
{
if (!HURRICANE_SPELL_IDS.count(spell->m_spellInfo->Id))
return false;
// Find this bot's own Hurricane DynamicObject
DynamicObject* dynObj = nullptr;
for (uint32 spellId : HURRICANE_SPELL_IDS)
{
dynObj = bot->GetDynObject(spellId);
if (dynObj)
break;
}
if (!dynObj)
return false;
// Count attackers actually inside the Hurricane AoE
float radius = dynObj->GetRadius();
GuidVector attackers = AI_VALUE(GuidVector, "attackers");
uint32 count = 0;
for (ObjectGuid const& guid : attackers)
{
Unit* unit = botAI->GetUnit(guid);
if (!unit || !unit->IsAlive())
continue;
if (unit->GetDistance(dynObj->GetPosition()) <= radius)
count++;
}
return count < minEnemies;
}
return false;
}

View File

@ -8,6 +8,7 @@
#include "CureTriggers.h"
#include "GenericTriggers.h"
#include "RtiTriggers.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
@ -15,20 +16,20 @@
#include "Trigger.h"
#include <set>
constexpr uint32 AURA_OMEN_OF_CLARITY = 16864;
class PlayerbotAI;
class MarkOfTheWildOnPartyTrigger : public BuffOnPartyTrigger
{
public:
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 2 * 2000) {}
bool IsActive() override;
MarkOfTheWildOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "mark of the wild", 4 * 2000) {}
};
class MarkOfTheWildTrigger : public BuffTrigger
{
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;
};
@ -55,22 +56,30 @@ public:
bool IsActive() override;
};
class OmenOfClarityTrigger : public BuffTrigger
{
public:
OmenOfClarityTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "omen of clarity") {}
};
class ClearcastingTrigger : public HasAuraTrigger
{
public:
ClearcastingTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "clearcasting") {}
};
class PredatorsSwiftnessTrigger : public HasAuraTrigger
{
public:
PredatorsSwiftnessTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "predator's swiftness") {}
};
class NaturesSwiftnessActiveTrigger : public HasAuraTrigger
{
public:
NaturesSwiftnessActiveTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "nature's swiftness") {}
bool IsActive() override { return botAI->HasAura("nature's swiftness", bot); }
};
class RakeTrigger : public DebuffTrigger
{
public:
RakeTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "rake", 1, true) {}
bool IsActive() override { return !botAI->HasAura("prowl", bot) && DebuffTrigger::IsActive(); }
};
class InsectSwarmTrigger : public DebuffTrigger
@ -79,12 +88,26 @@ public:
InsectSwarmTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "insect swarm", 1, true) {}
};
class InsectSwarmOnAttackerTrigger : public DebuffOnAttackerTrigger
{
public:
InsectSwarmOnAttackerTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "insect swarm", true) {}
bool IsActive() override { return BuffTrigger::IsActive(); }
};
class MoonfireTrigger : public DebuffTrigger
{
public:
MoonfireTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "moonfire", 1, true) {}
};
class MoonfireOnAttackerTrigger : public DebuffOnAttackerTrigger
{
public:
MoonfireOnAttackerTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "moonfire", true) {}
bool IsActive() override { return BuffTrigger::IsActive(); }
};
class FaerieFireTrigger : public DebuffTrigger
{
public:
@ -95,6 +118,35 @@ class FaerieFireFeralTrigger : public DebuffTrigger
{
public:
FaerieFireFeralTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "faerie fire (feral)") {}
bool IsActive() override
{
if (!bot->IsInCombat())
return false;
// Bear: every cast generates immediate threat/damage for free — spam it
if (botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr))
{
Unit* target = GetTarget();
return target && target->IsAlive() && target->IsInWorld();
}
if (!botAI->HasAura("cat form", bot))
return false;
if (botAI->HasAura("prowl", bot))
return false;
// Cat with Omen of Clarity: spam to fish for Clearcasting procs
if (bot->HasAura(AURA_OMEN_OF_CLARITY))
{
Unit* target = GetTarget();
return target && target->IsAlive() && target->IsInWorld();
}
// Cat without Omen of Clarity: apply as a normal debuff, don't reapply
return DebuffTrigger::IsActive();
}
};
class BashInterruptSpellTrigger : public InterruptSpellTrigger
@ -103,18 +155,18 @@ public:
BashInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "bash") {}
};
class TigersFuryTrigger : public BuffTrigger
{
public:
TigersFuryTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "tiger's fury") {}
};
class BerserkTrigger : public BoostTrigger
{
public:
BerserkTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "berserk") {}
};
class BerserkActiveTrigger : public HasAuraTrigger
{
public:
BerserkActiveTrigger(PlayerbotAI* botAI) : HasAuraTrigger(botAI, "berserk") {}
};
class SavageRoarTrigger : public BuffTrigger
{
public:
@ -127,10 +179,10 @@ public:
NaturesGraspTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "nature's grasp") {}
};
class EntanglingRootsTrigger : public HasCcTargetTrigger
class EntanglingRootsTrigger : public RtiCcTrigger
{
public:
EntanglingRootsTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "entangling roots") {}
EntanglingRootsTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "entangling roots") {}
};
class EntanglingRootsKiteTrigger : public DebuffTrigger
@ -141,10 +193,16 @@ public:
bool IsActive() override;
};
class HibernateTrigger : public HasCcTargetTrigger
class HibernateTrigger : public RtiCcTrigger
{
public:
HibernateTrigger(PlayerbotAI* botAI) : HasCcTargetTrigger(botAI, "hibernate") {}
HibernateTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "hibernate") {}
};
class CycloneTrigger : public RtiCcTrigger
{
public:
CycloneTrigger(PlayerbotAI* botAI) : RtiCcTrigger(botAI, "cyclone") {}
};
class CurePoisonTrigger : public NeedCureTrigger
@ -185,6 +243,14 @@ public:
bool IsActive() override;
};
class AquaticFormTrigger : public Trigger
{
public:
AquaticFormTrigger(PlayerbotAI* botAI) : Trigger(botAI, "aquatic form") {}
bool IsActive() override;
};
class EclipseSolarTrigger : public HasAuraTrigger
{
public:
@ -218,18 +284,69 @@ public:
}
};
class EclipseSolarCooldownTrigger : public SpellCooldownTrigger
class StarfallTrigger : public SpellNoCooldownTrigger
{
public:
EclipseSolarCooldownTrigger(PlayerbotAI* ai) : SpellCooldownTrigger(ai, "eclipse (solar)") {}
bool IsActive() override { return bot->HasSpellCooldown(48517); }
StarfallTrigger(PlayerbotAI* botAI) : SpellNoCooldownTrigger(botAI, "starfall") {}
};
class EclipseLunarCooldownTrigger : public SpellCooldownTrigger
class ForceOfNatureTrigger : public BoostTrigger
{
public:
EclipseLunarCooldownTrigger(PlayerbotAI* ai) : SpellCooldownTrigger(ai, "eclipse (lunar)") {}
bool IsActive() override { return bot->HasSpellCooldown(48518); }
ForceOfNatureTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "force of nature") {}
};
class MangleBearTrigger : public DebuffTrigger
{
public:
MangleBearTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "mangle (bear)") {}
bool IsActive() override
{
if (!bot->IsInCombat() || !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr))
return false;
Unit* target = GetTarget();
return target && target->IsAlive() && target->IsInWorld();
}
};
class LacerateTrigger : public DebuffTrigger
{
public:
LacerateTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "lacerate") {}
bool IsActive() override
{
if (!bot->IsInCombat() || !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr))
return false;
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
return false;
Aura* lacerate = botAI->GetAura("lacerate", target, false, false);
if (!lacerate)
return true;
if (lacerate->GetStackAmount() < 5)
return true;
return lacerate->GetDuration() <= 6000;
}
};
class DemoralizeRoarTrigger : public DebuffTrigger
{
public:
DemoralizeRoarTrigger(PlayerbotAI* botAI) : DebuffTrigger(botAI, "demoralizing roar") {}
bool IsActive() override
{
return DebuffTrigger::IsActive()
&& !botAI->HasAura("curse of weakness", GetTarget(), false, false)
&& !botAI->HasAura("demoralizing shout", GetTarget(), false, false)
&& !botAI->HasAura("vindication", GetTarget(), false, false);
}
};
class MangleCatTrigger : public DebuffTrigger
@ -238,6 +355,8 @@ public:
MangleCatTrigger(PlayerbotAI* ai) : DebuffTrigger(ai, "mangle (cat)", 1, false, 0.0f) {}
bool IsActive() override
{
if (botAI->HasAura("prowl", bot))
return false;
return DebuffTrigger::IsActive() && !botAI->HasAura("mangle (bear)", GetTarget(), false, false, -1, true)
&& !botAI->HasAura("trauma", GetTarget(), false, false, -1, true);
}
@ -271,10 +390,36 @@ public:
}
};
class FerociousBiteExecuteTrigger : public Trigger
{
public:
FerociousBiteExecuteTrigger(PlayerbotAI* ai) : Trigger(ai, "ferocious bite execute") {}
bool IsActive() override
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target || !target->IsAlive())
return false;
if (!AI_VALUE2(uint32, "spell id", "ferocious bite"))
return false;
if (AI_VALUE2(uint8, "combo", "current target") < 1)
return false;
if (target->GetHealthPct() >= 25.0f)
return false;
if (target->GetHealth() >= 20000)
return false;
return true;
}
};
class HurricaneChannelCheckTrigger : public Trigger
{
public:
HurricaneChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 2)
HurricaneChannelCheckTrigger(PlayerbotAI* botAI, uint32 minEnemies = 3)
: Trigger(botAI, "hurricane channel check"), minEnemies(minEnemies)
{
}
@ -297,4 +442,12 @@ public:
}
};
class ProwlTrigger : public Trigger
{
public:
ProwlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "prowl") {}
bool IsActive() override;
};
#endif

View File

@ -3,15 +3,15 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "CasterDruidStrategy.h"
#include "BalanceDruidStrategy.h"
#include "AiObjectContext.h"
#include "FeralDruidStrategy.h"
class CasterDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
class BalanceDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
CasterDruidStrategyActionNodeFactory()
BalanceDruidStrategyActionNodeFactory()
{
creators["faerie fire"] = &faerie_fire;
creators["hibernate"] = &hibernate;
@ -23,6 +23,10 @@ public:
creators["moonfire"] = &moonfire;
creators["starfire"] = &starfire;
creators["moonkin form"] = &moonkin_form;
creators["typhoon"] = &typhoon;
creators["hurricane"] = &hurricane;
creators["force of nature"] = &force_of_nature;
creators["cyclone on cc"] = &cyclone_on_cc;
}
private:
@ -125,130 +129,76 @@ private:
/*C*/ {}
);
}
static ActionNode* typhoon([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"typhoon",
/*P*/ { NextAction("moonkin form") },
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* hurricane([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"hurricane",
/*P*/ { NextAction("moonkin form") },
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* force_of_nature([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"force of nature",
/*P*/ { NextAction("moonkin form") },
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* cyclone_on_cc([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"cyclone on cc",
/*P*/ { NextAction("moonkin form") },
/*A*/ {},
/*C*/ {}
);
}
};
CasterDruidStrategy::CasterDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI)
BalanceDruidStrategy::BalanceDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI)
{
actionNodeFactories.Add(new CasterDruidStrategyActionNodeFactory());
actionNodeFactories.Add(new BalanceDruidStrategyActionNodeFactory());
actionNodeFactories.Add(new ShapeshiftDruidStrategyActionNodeFactory());
}
std::vector<NextAction> CasterDruidStrategy::getDefaultActions()
std::vector<NextAction> BalanceDruidStrategy::getDefaultActions()
{
return {
NextAction("starfall", ACTION_HIGH + 1.0f),
NextAction("force of nature", ACTION_DEFAULT + 1.0f),
NextAction("wrath", ACTION_DEFAULT + 0.1f),
NextAction("starfire", 5.4f),
NextAction("wrath", 5.3f),
};
}
void CasterDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
void BalanceDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
GenericDruidStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode(
"eclipse (lunar) cooldown",
{
NextAction("starfire", ACTION_DEFAULT + 0.2f)
}
)
);
triggers.push_back(
new TriggerNode(
"eclipse (solar) cooldown",
{
NextAction("wrath", ACTION_DEFAULT + 0.2f)
}
)
);
triggers.push_back(
new TriggerNode(
"insect swarm",
{
NextAction("insect swarm", ACTION_NORMAL + 5)
}
)
);
triggers.push_back(
new TriggerNode(
"moonfire",
{
NextAction("moonfire", ACTION_NORMAL + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"eclipse (solar)",
{
NextAction("wrath", ACTION_NORMAL + 6)
}
)
);
triggers.push_back(
new TriggerNode(
"eclipse (lunar)",
{
NextAction("starfire", ACTION_NORMAL + 6)
}
)
);
triggers.push_back(
new TriggerNode(
"medium mana",
{
NextAction("innervate", ACTION_HIGH + 9)
}
)
);
triggers.push_back(
new TriggerNode(
"enemy too close for spell",
{
NextAction("flee", ACTION_MOVE + 9)
}
)
);
}
// Debuffs and DoTs
triggers.push_back(new TriggerNode("faerie fire", { NextAction("faerie fire", 29.5f) }));
triggers.push_back(new TriggerNode("insect swarm", { NextAction("insect swarm", 18.0f) }));
triggers.push_back(new TriggerNode("moonfire", { NextAction("moonfire", 17.5f) }));
void CasterDruidAoeStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode(
"hurricane channel check",
{
NextAction("cancel channel", ACTION_HIGH + 2)
}
)
);
triggers.push_back(
new TriggerNode(
"medium aoe",
{
NextAction("hurricane", ACTION_HIGH + 1)
}
)
);
triggers.push_back(
new TriggerNode(
"light aoe",
{
NextAction("insect swarm on attacker", ACTION_NORMAL + 3),
NextAction("moonfire on attacker", ACTION_NORMAL + 3)
}
)
);
}
// Eclipse procs
triggers.push_back(new TriggerNode("eclipse (solar)", { NextAction("wrath", 20.0f) }));
triggers.push_back(new TriggerNode("eclipse (lunar)", { NextAction("starfire", 20.0f) }));
void CasterDruidDebuffStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode(
"faerie fire",
{
NextAction("faerie fire", ACTION_HIGH)
}
)
);
// Utility/Defensive
triggers.push_back(new TriggerNode("medium mana", { NextAction("innervate", 29.0f) }));
triggers.push_back(new TriggerNode("enemy too close for spell", { NextAction("flee", 39.0f) }));
}

View File

@ -0,0 +1,25 @@
/*
* 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_BALANCEDRUIDSTRATEGY_H
#define _PLAYERBOT_BALANCEDRUIDSTRATEGY_H
#include "GenericDruidStrategy.h"
class PlayerbotAI;
class BalanceDruidStrategy : public GenericDruidStrategy
{
public:
BalanceDruidStrategy(PlayerbotAI* botAI);
public:
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "balance"; }
std::vector<NextAction> getDefaultActions() override;
uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_RANGED; }
};
#endif

View File

@ -3,19 +3,17 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "BearTankDruidStrategy.h"
#include "BearDruidStrategy.h"
#include "Playerbots.h"
class BearTankDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
class BearDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
BearTankDruidStrategyActionNodeFactory()
BearDruidStrategyActionNodeFactory()
{
creators["melee"] = &melee;
creators["feral charge - bear"] = &feral_charge_bear;
creators["swipe (bear)"] = &swipe_bear;
creators["faerie fire (feral)"] = &faerie_fire_feral;
creators["bear form"] = &bear_form;
creators["dire bear form"] = &dire_bear_form;
creators["mangle (bear)"] = &mangle_bear;
@ -28,16 +26,6 @@ public:
}
private:
static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"melee",
/*P*/ { NextAction("feral charge - bear") },
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* feral_charge_bear([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
@ -58,16 +46,6 @@ private:
);
}
static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"faerie fire (feral)",
/*P*/ { NextAction("feral charge - bear") },
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* bear_form([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
@ -159,99 +137,81 @@ private:
}
};
BearTankDruidStrategy::BearTankDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI)
BearDruidStrategy::BearDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI)
{
actionNodeFactories.Add(new BearTankDruidStrategyActionNodeFactory());
actionNodeFactories.Add(new BearDruidStrategyActionNodeFactory());
}
std::vector<NextAction> BearTankDruidStrategy::getDefaultActions()
std::vector<NextAction> BearDruidStrategy::getDefaultActions()
{
return {
NextAction("mangle (bear)", ACTION_DEFAULT + 0.5f),
NextAction("faerie fire (feral)", ACTION_DEFAULT + 0.4f),
NextAction("lacerate", ACTION_DEFAULT + 0.3f),
NextAction("maul", ACTION_DEFAULT + 0.2f),
NextAction("enrage", ACTION_DEFAULT + 0.1f),
NextAction("melee", ACTION_DEFAULT)
NextAction("maul", 5.2f),
NextAction("enrage", 5.1f),
NextAction("melee", 5.0f)
};
}
void BearTankDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
void BearDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
FeralDruidStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode(
"enemy out of melee",
{
NextAction("feral charge - bear", ACTION_NORMAL + 8)
}
)
);
triggers.push_back(
new TriggerNode(
"bear form",
{
NextAction("dire bear form", ACTION_HIGH + 8)
}
{ NextAction("dire bear form", 28.0f) }
)
);
triggers.push_back(
new TriggerNode(
"low health",
{
NextAction("frenzied regeneration", ACTION_HIGH + 7)
}
)
);
triggers.push_back(
new TriggerNode(
"faerie fire (feral)",
{
NextAction("faerie fire (feral)", ACTION_HIGH + 7)
}
)
);
triggers.push_back(new TriggerNode("high aoe", {NextAction("challenging roar", ACTION_HIGH + 8)}));
triggers.push_back(
new TriggerNode(
"lose aggro",
{
NextAction("growl", ACTION_HIGH + 8)
}
"medium health",
{ NextAction("frenzied regeneration", 27.0f) }
)
);
triggers.push_back(new TriggerNode(
"mangle (bear)", { NextAction("mangle (bear)", 17.5f) }
));
triggers.push_back(new TriggerNode(
"faerie fire (feral)", { NextAction("faerie fire (feral)", 17.0f) }
));
triggers.push_back(new TriggerNode(
"lacerate", { NextAction("lacerate", 16.0f) }
));
triggers.push_back(new TriggerNode(
"demoralizing roar", { NextAction("demoralizing roar", 15.5f) }
));
triggers.push_back(new TriggerNode("high aoe", { NextAction("challenging roar", 26.5f) }));
triggers.push_back(new TriggerNode("lose aggro",
{
NextAction("growl", 26.0f),
NextAction("faerie fire (feral)", 25.5f)
}
));
triggers.push_back(new TriggerNode("berserk active", { NextAction("mangle (bear)", 25.0f) }));
triggers.push_back(
new TriggerNode(
"medium aoe",
{
NextAction("demoralizing roar", ACTION_HIGH + 6),
NextAction("swipe (bear)", ACTION_HIGH + 6)
NextAction("demoralizing roar", 24.5f),
NextAction("swipe (bear)", 24.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"light aoe",
{
NextAction("swipe (bear)", ACTION_HIGH + 5)
}
{ NextAction("swipe (bear)", 24.0f) }
)
);
triggers.push_back(
new TriggerNode(
"bash",
{
NextAction("bash", ACTION_INTERRUPT + 2)
}
{ NextAction("bash", 42.0f) }
)
);
triggers.push_back(
new TriggerNode(
"bash on enemy healer",
{
NextAction("bash on enemy healer", ACTION_INTERRUPT + 1)
}
{ NextAction("bash on enemy healer", 41.0f) }
)
);
}

View File

@ -3,17 +3,17 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_BEARTANKDRUIDSTRATEGY_H
#define _PLAYERBOT_BEARTANKDRUIDSTRATEGY_H
#ifndef _PLAYERBOT_BEARDRUIDSTRATEGY_H
#define _PLAYERBOT_BEARDRUIDSTRATEGY_H
#include "FeralDruidStrategy.h"
class PlayerbotAI;
class BearTankDruidStrategy : public FeralDruidStrategy
class BearDruidStrategy : public FeralDruidStrategy
{
public:
BearTankDruidStrategy(PlayerbotAI* botAI);
BearDruidStrategy(PlayerbotAI* botAI);
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "bear"; }

View File

@ -1,45 +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.
*/
#ifndef _PLAYERBOT_CASTERDRUIDSTRATEGY_H
#define _PLAYERBOT_CASTERDRUIDSTRATEGY_H
#include "GenericDruidStrategy.h"
class PlayerbotAI;
class CasterDruidStrategy : public GenericDruidStrategy
{
public:
CasterDruidStrategy(PlayerbotAI* botAI);
public:
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "caster"; }
std::vector<NextAction> getDefaultActions() override;
uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_RANGED; }
};
class CasterDruidAoeStrategy : public CombatStrategy
{
public:
CasterDruidAoeStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {}
public:
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "caster aoe"; }
};
class CasterDruidDebuffStrategy : public CombatStrategy
{
public:
CasterDruidDebuffStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {}
public:
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "caster debuff"; }
};
#endif

View File

@ -1,314 +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 "CatDpsDruidStrategy.h"
#include "AiObjectContext.h"
class CatDpsDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
CatDpsDruidStrategyActionNodeFactory()
{
creators["faerie fire (feral)"] = &faerie_fire_feral;
creators["melee"] = &melee;
creators["feral charge - cat"] = &feral_charge_cat;
creators["cat form"] = &cat_form;
creators["claw"] = &claw;
creators["mangle (cat)"] = &mangle_cat;
creators["rake"] = &rake;
creators["ferocious bite"] = &ferocious_bite;
creators["rip"] = &rip;
creators["pounce"] = &pounce;
creators["ravage"] = &ravage;
}
private:
static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"faerie fire (feral)",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"melee",
/*P*/ { NextAction("feral charge - cat") },
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* feral_charge_cat([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"feral charge - cat",
/*P*/ {},
/*A*/ { NextAction("reach melee") },
/*C*/ {}
);
}
static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"cat form",
/*P*/ { NextAction("caster form") },
/*A*/ { NextAction("bear form") },
/*C*/ {}
);
}
static ActionNode* claw([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"claw",
/*P*/ {},
/*A*/ { NextAction("melee") },
/*C*/ {}
);
}
static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"mangle (cat)",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rake",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"ferocious bite",
/*P*/ {},
/*A*/ { NextAction("rip") },
/*C*/ {}
);
}
static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rip",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* pounce([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"pounce",
/*P*/ {},
/*A*/ { NextAction("ravage") },
/*C*/ {}
);
}
static ActionNode* ravage([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"ravage",
/*P*/ {},
/*A*/ { NextAction("shred") },
/*C*/ {}
);
}
};
CatDpsDruidStrategy::CatDpsDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI)
{
actionNodeFactories.Add(new CatDpsDruidStrategyActionNodeFactory());
}
std::vector<NextAction> CatDpsDruidStrategy::getDefaultActions()
{
return {
NextAction("tiger's fury", ACTION_DEFAULT + 0.1f)
};
}
void CatDpsDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
FeralDruidStrategy::InitTriggers(triggers);
// Default priority
triggers.push_back(
new TriggerNode(
"almost full energy available",
{
NextAction("shred", ACTION_DEFAULT + 0.4f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full",
{
NextAction("shred", ACTION_DEFAULT + 0.4f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available",
{
NextAction("mangle (cat)", ACTION_DEFAULT + 0.3f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full and high energy",
{
NextAction("mangle (cat)", ACTION_DEFAULT + 0.3f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available",
{
NextAction("claw", ACTION_DEFAULT + 0.2f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full and high energy",
{
NextAction("claw", ACTION_DEFAULT + 0.2f)
}
)
);
triggers.push_back(
new TriggerNode(
"faerie fire (feral)",
{
NextAction("faerie fire (feral)", ACTION_DEFAULT + 0.0f)
}
)
);
// Main spell
triggers.push_back(
new TriggerNode(
"cat form", {
NextAction("cat form", ACTION_HIGH + 8)
}
)
);
triggers.push_back(
new TriggerNode(
"savage roar", {
NextAction("savage roar", ACTION_HIGH + 7)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points 5 available",
{
NextAction("rip", ACTION_HIGH + 6)
}
)
);
triggers.push_back(
new TriggerNode(
"ferocious bite time",
{
NextAction("ferocious bite", ACTION_HIGH + 5)
}
)
);
triggers.push_back(
new TriggerNode(
"target with combo points almost dead",
{
NextAction("ferocious bite", ACTION_HIGH + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"mangle (cat)",
{
NextAction("mangle (cat)", ACTION_HIGH + 3)
}
)
);
triggers.push_back(
new TriggerNode(
"rake",
{
NextAction("rake", ACTION_HIGH + 2)
}
)
);
triggers.push_back(
new TriggerNode(
"medium threat",
{
NextAction("cower", ACTION_HIGH + 1)
}
)
);
// AOE
triggers.push_back(
new TriggerNode(
"medium aoe",
{
NextAction("swipe (cat)", ACTION_HIGH + 3)
}
)
);
triggers.push_back(
new TriggerNode(
"light aoe",
{
NextAction("rake on attacker", ACTION_HIGH + 2)
}
)
);
// Reach target
triggers.push_back(
new TriggerNode(
"enemy out of melee",
{
NextAction("feral charge - cat", ACTION_HIGH + 9)
}
)
);
triggers.push_back(
new TriggerNode(
"enemy out of melee",
{
NextAction("dash", ACTION_HIGH + 8)
}
)
);
}
void CatAoeDruidStrategy::InitTriggers(std::vector<TriggerNode*>& /*triggers*/) {}

View File

@ -0,0 +1,446 @@
/*
* 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 "CatDruidStrategy.h"
#include "AiObjectContext.h"
class CatDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
CatDruidStrategyActionNodeFactory()
{
creators["faerie fire (feral)"] = &faerie_fire_feral;
creators["melee"] = &melee;
creators["feral charge - cat"] = &feral_charge_cat;
creators["cat form"] = &cat_form;
creators["claw"] = &claw;
creators["mangle (cat)"] = &mangle_cat;
creators["rake"] = &rake;
creators["ferocious bite"] = &ferocious_bite;
creators["rip"] = &rip;
creators["pounce"] = &pounce;
creators["ravage"] = &ravage;
creators["prowl"] = &prowl;
}
private:
static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"faerie fire (feral)",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"melee",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* feral_charge_cat([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"feral charge - cat",
/*P*/ {},
/*A*/ { NextAction("reach melee") },
/*C*/ {}
);
}
static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"cat form",
/*P*/ { NextAction("caster form") },
/*A*/ { NextAction("bear form") },
/*C*/ {}
);
}
static ActionNode* claw([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"claw",
/*P*/ {},
/*A*/ { NextAction("melee") },
/*C*/ {}
);
}
static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"mangle (cat)",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rake",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"ferocious bite",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rip",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* ravage([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"ravage",
/*P*/ {},
/*A*/ { NextAction("pounce") },
/*C*/ {}
);
}
static ActionNode* pounce([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"pounce",
/*P*/ {},
/*A*/ { NextAction("shred") },
/*C*/ {}
);
}
static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"prowl",
/*P*/ { NextAction("cat form") },
/*A*/ {},
/*C*/ {}
);
}
};
CatDruidStrategy::CatDruidStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI)
{
actionNodeFactories.Add(new CatDruidStrategyActionNodeFactory());
}
std::vector<NextAction> CatDruidStrategy::getDefaultActions()
{
return {
NextAction("melee", ACTION_DEFAULT)
};
}
void CatDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
FeralDruidStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode(
"healer low mana", {
NextAction("innervate on healer", 35.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"prowl", {
NextAction("prowl", 29.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"enemy out of melee", {
NextAction("dash", 28.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"cat form", {
NextAction("cat form", 28.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"low energy", {
NextAction("tiger's fury", 27.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"savage roar", {
NextAction("savage roar", 26.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points 5 available", {
NextAction("rip", 23.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points 5 available", {
NextAction("maim", 23.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"ferocious bite execute", {
NextAction("ferocious bite", 24.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"clearcasting", {
NextAction("shred", 24.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"ferocious bite time", {
NextAction("ferocious bite", 22.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"mangle (cat)", {
NextAction("mangle (cat)", 22.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"rake", {
NextAction("rake", 21.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"medium threat", {
NextAction("cower", 21.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available", {
NextAction("ravage", 5.6f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full", {
NextAction("ravage", 5.6f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available", {
NextAction("pounce", 5.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full", {
NextAction("pounce", 5.5f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available", {
NextAction("shred", 5.4f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full", {
NextAction("shred", 5.4f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available", {
NextAction("mangle (cat)", 5.3f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full and high energy", {
NextAction("mangle (cat)", 5.3f)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available", {
NextAction("claw", 5.2f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full and high energy", {
NextAction("claw", 5.2f)
}
)
);
triggers.push_back(
new TriggerNode(
"faerie fire (feral)", {
NextAction("faerie fire (feral)", 5.0f)
}
)
);
}
// ============================================================
// CatOffhealStrategy
// Additive overlay — only the healing triggers. Designed to be
// stacked on top of "cat" so the bot stays in cat form for DPS
// but shifts out to heal when the party needs it.
// ============================================================
class CatOffhealStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
CatOffhealStrategyActionNodeFactory()
{
creators["healing touch on party"] = &healing_touch_on_party;
creators["regrowth on party"] = &regrowth_on_party;
creators["rejuvenation on party"] = &rejuvenation_on_party;
}
private:
// P: shift to caster form before casting C: shift back to cat form afterwards
static ActionNode* healing_touch_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"healing touch on party",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ { NextAction("cat form") }
);
}
static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"regrowth on party",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ { NextAction("cat form") }
);
}
static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rejuvenation on party",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ { NextAction("cat form") }
);
}
};
CatOffhealStrategy::CatOffhealStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI)
{
actionNodeFactories.Add(new CatOffhealStrategyActionNodeFactory());
}
void CatOffhealStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode(
"party member critical health",
{
NextAction("regrowth on party", 36.0f),
NextAction("healing touch on party", 35.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"party member low health",
{
NextAction("healing touch on party", 25.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"party member medium health",
{
NextAction("rejuvenation on party", 18.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"party member to heal out of spell range",
{
NextAction("reach party member to heal", 93.0f)
}
)
);
triggers.push_back(
new TriggerNode(
"low mana",
{
NextAction("innervate", 24.0f)
}
)
);
}

View File

@ -3,17 +3,17 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_CATDPSDRUIDSTRATEGY_H
#define _PLAYERBOT_CATDPSDRUIDSTRATEGY_H
#ifndef _PLAYERBOT_CATDRUIDSTRATEGY_H
#define _PLAYERBOT_CATDRUIDSTRATEGY_H
#include "FeralDruidStrategy.h"
class PlayerbotAI;
class CatDpsDruidStrategy : public FeralDruidStrategy
class CatDruidStrategy : public FeralDruidStrategy
{
public:
CatDpsDruidStrategy(PlayerbotAI* botAI);
CatDruidStrategy(PlayerbotAI* botAI);
public:
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
@ -22,14 +22,16 @@ public:
uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; }
};
class CatAoeDruidStrategy : public CombatStrategy
// Optional additive strategy. Layers emergency heals on top of the "cat" strategy.
// Enable : co +offheal
// Disable: co -offheal
class CatOffhealStrategy : public CombatStrategy
{
public:
CatAoeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {}
CatOffhealStrategy(PlayerbotAI* botAI);
public:
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "cat aoe"; }
std::string const getName() override { return "offheal"; }
};
#endif

View File

@ -14,12 +14,10 @@ public:
{
creators["survival instincts"] = &survival_instincts;
creators["thorns"] = &thorns;
creators["omen of clarity"] = &omen_of_clarity;
creators["cure poison"] = &cure_poison;
creators["cure poison on party"] = &cure_poison_on_party;
creators["abolish poison"] = &abolish_poison;
creators["abolish poison on party"] = &abolish_poison_on_party;
creators["prowl"] = &prowl;
}
private:
@ -39,14 +37,6 @@ private:
/*C*/ {});
}
static ActionNode* omen_of_clarity([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("omen of clarity",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ {});
}
static ActionNode* cure_poison([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("cure poison",
@ -79,13 +69,6 @@ private:
/*C*/ {});
}
static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("prowl",
/*P*/ { NextAction("cat form") },
/*A*/ {},
/*C*/ {});
}
};
FeralDruidStrategy::FeralDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI)
@ -99,15 +82,23 @@ void FeralDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
GenericDruidStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode(
"enemy out of melee", { NextAction("reach melee", ACTION_HIGH + 1) }));
"enemy out of melee", { NextAction("reach melee", 21.0f) }));
triggers.push_back(new TriggerNode(
"critical health", { NextAction("survival instincts", ACTION_EMERGENCY + 1) }));
triggers.push_back(new TriggerNode(
"omen of clarity", { NextAction("omen of clarity", ACTION_HIGH + 9) }));
"low health", { NextAction("survival instincts", 91.0f) }));
triggers.push_back(new TriggerNode("player has flag",
{ NextAction("dash", ACTION_EMERGENCY + 2) }));
{ NextAction("dash", 92.0f) }));
triggers.push_back(new TriggerNode("enemy flagcarrier near",
{ NextAction("dash", ACTION_EMERGENCY + 2) }));
triggers.push_back(
new TriggerNode("berserk", { NextAction("berserk", ACTION_HIGH + 6) }));
{ NextAction("dash", 92.0f) }));
}
void FeralChargeDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
Player* bot = botAI->GetBot();
if (bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE))
triggers.push_back(new TriggerNode(
"enemy out of melee", { NextAction("feral charge - cat", 29.0f) }));
else
triggers.push_back(new TriggerNode(
"enemy out of melee", { NextAction("feral charge - bear", 18.0f) }));
}

View File

@ -3,11 +3,14 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_FERALRUIDSTRATEGY_H
#define _PLAYERBOT_FERALRUIDSTRATEGY_H
#ifndef _PLAYERBOT_FERALDRUIDSTRATEGY_H
#define _PLAYERBOT_FERALDRUIDSTRATEGY_H
#include "GenericDruidStrategy.h"
constexpr uint32 SPELL_CAT_FORM = 768;
constexpr uint32 AURA_THICK_HIDE = 16931;
class PlayerbotAI;
class ShapeshiftDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
@ -83,4 +86,18 @@ public:
uint32 GetType() const override { return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_MELEE; }
};
// Optional strategy — enabled by default for cat and bear.
// Registers the "enemy out of melee" → Feral Charge trigger, spec-gated at
// init time so cats get Feral Charge (Cat) and bears get Feral Charge (Bear).
// Disable with: co -feral charge
// Re-enable with: co +feral charge
class FeralChargeDruidStrategy : public CombatStrategy
{
public:
FeralChargeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "feral charge"; }
};
#endif

View File

@ -17,12 +17,13 @@ public:
creators["thorns on party"] = &thorns_on_party;
creators["mark of the wild"] = &mark_of_the_wild;
creators["mark of the wild on party"] = &mark_of_the_wild_on_party;
// creators["innervate"] = &innervate;
creators["regrowth_on_party"] = &regrowth_on_party;
creators["rejuvenation on party"] = &rejuvenation_on_party;
creators["remove curse on party"] = &remove_curse_on_party;
creators["abolish poison on party"] = &abolish_poison_on_party;
creators["revive"] = &revive;
creators["prowl"] = &prowl;
creators["aquatic form"] = &aquatic_form;
}
private:
@ -92,6 +93,23 @@ private:
/*A*/ {},
/*C*/ {});
}
static ActionNode* prowl([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("prowl",
/*P*/ { NextAction("cat form") },
/*A*/ {},
/*C*/ {});
}
static ActionNode* aquatic_form([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("aquatic form",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ {});
}
};
GenericDruidNonCombatStrategy::GenericDruidNonCombatStrategy(PlayerbotAI* botAI) : NonCombatStrategy(botAI)
@ -165,11 +183,16 @@ void GenericDruidNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& trig
NextAction("remove curse on party", ACTION_DISPEL + 7),
}));
triggers.push_back(new TriggerNode("aquatic form", { NextAction("aquatic form", 10.0f) }));
int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot());
if (specTab == 0 || specTab == 2) // Balance or Restoration
if (specTab == DRUID_TAB_BALANCE || specTab == DRUID_TAB_RESTORATION)
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) }));
if (specTab == 1) // Feral
if (specTab == DRUID_TAB_FERAL)
{
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) }));
triggers.push_back(new TriggerNode("prowl", { NextAction("prowl", ACTION_INTERRUPT) }));
}
}

View File

@ -5,6 +5,8 @@
#include "GenericDruidStrategy.h"
#include "AiFactory.h"
#include "FeralDruidStrategy.h"
#include "Playerbots.h"
class GenericDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
@ -20,6 +22,8 @@ public:
creators["abolish poison on party"] = &abolish_poison_on_party;
creators["rebirth"] = &rebirth;
creators["entangling roots on cc"] = &entangling_roots_on_cc;
creators["cyclone on cc"] = &cyclone_on_cc;
creators["hibernate on cc"] = &hibernate_on_cc;
creators["innervate"] = &innervate;
}
@ -88,6 +92,22 @@ private:
/*C*/ {});
}
static ActionNode* cyclone_on_cc([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("cyclone on cc",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ {});
}
static ActionNode* hibernate_on_cc([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("hibernate on cc",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ {});
}
static ActionNode* innervate([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("innervate",
@ -107,41 +127,95 @@ void GenericDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
CombatStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode("low health", { NextAction("barkskin", ACTION_HIGH + 7) }));
new TriggerNode("almost full health", { NextAction("barkskin", 40.0f) }));
Player* bot = botAI->GetBot();
int tab = AiFactory::GetPlayerSpecTab(bot);
if (tab == DRUID_TAB_FERAL)
{
if (!bot->HasAura(16931) /*thick hide — bear spec*/)
{
triggers.push_back(new TriggerNode("predator's swiftness and combat party member dead",
{ NextAction("rebirth", 29.0f) }));
triggers.push_back(new TriggerNode("combat party member dead",
{ NextAction("rebirth", 28.5f) }));
}
}
else
{
triggers.push_back(new TriggerNode("combat party member dead",
{ NextAction("rebirth", 29.0f) }));
}
triggers.push_back(new TriggerNode("combat party member dead",
{ NextAction("rebirth", ACTION_HIGH + 9) }));
triggers.push_back(new TriggerNode("being attacked",
{ NextAction("nature's grasp", ACTION_HIGH + 1) }));
triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) }));
{ NextAction("nature's grasp", 39.0f) }));
}
void DruidCureStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode("party member cure poison",
{ NextAction("abolish poison on party", ACTION_DISPEL + 1) }));
{ NextAction("abolish poison on party", 51.0f) }));
triggers.push_back(
new TriggerNode("party member remove curse",
{ NextAction("remove curse on party", ACTION_DISPEL + 7) }));
{ NextAction("remove curse on party", 57.0f) }));
}
void DruidBoostStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode(
"nature's swiftness", { NextAction("nature's swiftness", ACTION_HIGH + 9) }));
Player* bot = botAI->GetBot();
int tab = AiFactory::GetPlayerSpecTab(bot);
if (tab == DRUID_TAB_BALANCE)
{
triggers.push_back(new TriggerNode("force of nature", { NextAction("force of nature", 29.0f) }));
triggers.push_back(new TriggerNode("new pet", { NextAction("set pet stance", 60.0f) }));
}
if (tab == DRUID_TAB_FERAL)
{
triggers.push_back(new TriggerNode("berserk", { NextAction("berserk", 27.5f) }));
}
}
void DruidCcStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode(
"entangling roots", { NextAction("entangling roots on cc", ACTION_HIGH + 2) }));
triggers.push_back(new TriggerNode(
"entangling roots kite", { NextAction("entangling roots", ACTION_HIGH + 2) }));
triggers.push_back(new TriggerNode(
"hibernate", { NextAction("hibernate on cc", ACTION_HIGH + 3) }));
Player* bot = botAI->GetBot();
int tab = AiFactory::GetPlayerSpecTab(bot);
if (tab == DRUID_TAB_BALANCE || tab == DRUID_TAB_RESTORATION)
{
triggers.push_back(new TriggerNode(
"cyclone", { NextAction("cyclone on cc", 42.0f) }));
triggers.push_back(new TriggerNode(
"hibernate", { NextAction("hibernate on cc", 41.0f) }));
triggers.push_back(new TriggerNode(
"entangling roots", { NextAction("entangling roots on cc", 40.0f) }));
}
if (tab == DRUID_TAB_FERAL)
{
if (bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE))
{
triggers.push_back(new TriggerNode(
"predator's swiftness and cyclone", { NextAction("cyclone on cc", 42.0f) }));
triggers.push_back(new TriggerNode(
"predator's swiftness and hibernate", { NextAction("hibernate on cc", 41.0f) }));
triggers.push_back(new TriggerNode(
"predator's swiftness and entangling roots", { NextAction("entangling roots on cc", 40.0f) }));
}
else
{
triggers.push_back(new TriggerNode(
"cyclone", { NextAction("cyclone on cc", 42.0f) }));
triggers.push_back(new TriggerNode(
"hibernate", { NextAction("hibernate on cc", 41.0f) }));
triggers.push_back(new TriggerNode(
"entangling roots", { NextAction("entangling roots on cc", 40.0f) }));
}
}
}
void DruidHealerDpsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
@ -149,10 +223,39 @@ void DruidHealerDpsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back(
new TriggerNode("healer should attack",
{
NextAction("cancel tree form", ACTION_DEFAULT + 0.4f),
NextAction("moonfire", ACTION_DEFAULT + 0.3f),
NextAction("wrath", ACTION_DEFAULT + 0.2f),
NextAction("starfire", ACTION_DEFAULT + 0.1f),
}));
NextAction("cancel tree form", 5.4f),
NextAction("moonfire", 5.3f),
NextAction("wrath", 5.2f),
NextAction("starfire", 5.1f),
}));
}
void DruidAoeStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
Player* bot = botAI->GetBot();
int tab = AiFactory::GetPlayerSpecTab(bot);
if (tab == DRUID_TAB_BALANCE)
{
triggers.push_back(new TriggerNode("hurricane channel check", { NextAction("cancel channel", 22.0f) }));
triggers.push_back(new TriggerNode("starfall", { NextAction("starfall", 28.5f) }));
triggers.push_back(new TriggerNode("medium aoe", { NextAction("hurricane", 23.0f) }));
triggers.push_back(new TriggerNode("enemy within melee", { NextAction("typhoon", 40.0f) }));
triggers.push_back(new TriggerNode("insect swarm on attacker", { NextAction("insect swarm on attacker", 5.2f) }));
triggers.push_back(new TriggerNode("moonfire on attacker", { NextAction("moonfire on attacker", 5.1f) }));
}
if (tab == DRUID_TAB_RESTORATION)
{
triggers.push_back(new TriggerNode("hurricane channel check", { NextAction("cancel channel", 22.0f) }));
triggers.push_back(new TriggerNode("medium aoe", { NextAction("hurricane", 23.0f) }));
triggers.push_back(new TriggerNode("insect swarm on attacker", { NextAction("insect swarm on attacker", 5.2f) }));
triggers.push_back(new TriggerNode("moonfire on attacker", { NextAction("moonfire on attacker", 5.1f) }));
}
if (tab == DRUID_TAB_FERAL && bot->HasSpell(SPELL_CAT_FORM) && !bot->HasAura(AURA_THICK_HIDE))
{
triggers.push_back(new TriggerNode("clearcasting and medium aoe", { NextAction("swipe (cat)", 25.5f) }));
triggers.push_back(new TriggerNode("medium aoe", { NextAction("swipe (cat)", 25.0f) }));
}
}

View File

@ -55,4 +55,13 @@ public:
std::string const getName() override { return "healer dps"; }
};
class DruidAoeStrategy : public Strategy
{
public:
DruidAoeStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "aoe"; }
};
#endif

View File

@ -1,108 +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 "HealDruidStrategy.h"
#include "Playerbots.h"
class HealDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
HealDruidStrategyActionNodeFactory() {
creators["nourish on party"] = &nourtish_on_party;
}
private:
static ActionNode* nourtish_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("nourish on party",
/*P*/ {},
/*A*/ { NextAction("healing touch on party") },
/*C*/ {});
}
};
HealDruidStrategy::HealDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI)
{
actionNodeFactories.Add(new HealDruidStrategyActionNodeFactory());
}
void HealDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
GenericDruidStrategy::InitTriggers(triggers);
// no healer dps strategy
triggers.push_back(new TriggerNode("no healer dps strategy",
{ NextAction("tree form", ACTION_DEFAULT) }));
triggers.push_back(new TriggerNode(
"party member to heal out of spell range",
{ NextAction("reach party member to heal", ACTION_CRITICAL_HEAL + 9) }));
// CRITICAL
triggers.push_back(
new TriggerNode("party member critical health",
{
NextAction("tree form", ACTION_CRITICAL_HEAL + 4.1f),
NextAction("swiftmend on party", ACTION_CRITICAL_HEAL + 4),
NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 3),
NextAction("wild growth on party", ACTION_CRITICAL_HEAL + 2),
NextAction("nourish on party", ACTION_CRITICAL_HEAL + 1),
}));
triggers.push_back(
new TriggerNode("party member critical health",
{ NextAction("nature's swiftness", ACTION_CRITICAL_HEAL + 4) }));
triggers.push_back(new TriggerNode("clearcasting",
{ NextAction("lifebloom on main tank", ACTION_CRITICAL_HEAL - 1) }));
triggers.push_back(new TriggerNode(
"group heal setting",
{
NextAction("tree form", ACTION_MEDIUM_HEAL + 2.3f),
NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 2.2f),
NextAction("rejuvenation on not full", ACTION_MEDIUM_HEAL + 2.1f),
}));
triggers.push_back(
new TriggerNode("medium group heal setting",
{
NextAction("tree form", ACTION_CRITICAL_HEAL + 0.6f),
NextAction("tranquility", ACTION_CRITICAL_HEAL + 0.5f) }));
// LOW
triggers.push_back(
new TriggerNode("party member low health",
{ NextAction("tree form", ACTION_MEDIUM_HEAL + 1.5f),
NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 1.4f),
NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 1.3f),
NextAction("swiftmend on party", ACTION_MEDIUM_HEAL + 1.2),
NextAction("nourish on party", ACTION_MEDIUM_HEAL + 1.1f),
}));
// MEDIUM
triggers.push_back(
new TriggerNode("party member medium health",
{
NextAction("tree form", ACTION_MEDIUM_HEAL + 0.5f),
NextAction("wild growth on party", ACTION_MEDIUM_HEAL + 0.4f),
NextAction("rejuvenation on party", ACTION_MEDIUM_HEAL + 0.3f),
NextAction("regrowth on party", ACTION_MEDIUM_HEAL + 0.2f),
NextAction("nourish on party", ACTION_MEDIUM_HEAL + 0.1f) }));
// almost full
triggers.push_back(
new TriggerNode("party member almost full health",
{ NextAction("wild growth on party", ACTION_LIGHT_HEAL + 0.3f),
NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 0.2f),
NextAction("regrowth on party", ACTION_LIGHT_HEAL + 0.1f) }));
triggers.push_back(
new TriggerNode("medium mana", { NextAction("innervate", ACTION_HIGH + 5) }));
triggers.push_back(new TriggerNode("enemy too close for spell",
{ NextAction("flee", ACTION_MOVE + 9) }));
}

View File

@ -1,24 +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.
*/
#ifndef _PLAYERBOT_HEALDRUIDSTRATEGY_H
#define _PLAYERBOT_HEALDRUIDSTRATEGY_H
#include "GenericDruidStrategy.h"
#include "Strategy.h"
class PlayerbotAI;
class HealDruidStrategy : public GenericDruidStrategy
{
public:
HealDruidStrategy(PlayerbotAI* botAI);
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "heal"; }
uint32 GetType() const override { return STRATEGY_TYPE_RANGED | STRATEGY_TYPE_HEAL; }
};
#endif

View File

@ -1,32 +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 "MeleeDruidStrategy.h"
#include "Playerbots.h"
MeleeDruidStrategy::MeleeDruidStrategy(PlayerbotAI* botAI) : CombatStrategy(botAI) {}
std::vector<NextAction> MeleeDruidStrategy::getDefaultActions()
{
return {
NextAction("faerie fire", ACTION_DEFAULT + 0.1f),
NextAction("melee", ACTION_DEFAULT)
};
}
void MeleeDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode(
"omen of clarity",
{
NextAction("omen of clarity", ACTION_HIGH + 9)
}
)
);
CombatStrategy::InitTriggers(triggers);
}

View File

@ -1,21 +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.
*/
#ifndef _PLAYERBOT_MELEEDRUIDSTRATEGY_H
#define _PLAYERBOT_MELEEDRUIDSTRATEGY_H
#include "CombatStrategy.h"
class MeleeDruidStrategy : public CombatStrategy
{
public:
MeleeDruidStrategy(PlayerbotAI* botAI);
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "melee"; }
std::vector<NextAction> getDefaultActions() override;
};
#endif

View File

@ -1,307 +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 "OffhealDruidCatStrategy.h"
#include "Playerbots.h"
#include "Strategy.h"
class OffhealDruidCatStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
OffhealDruidCatStrategyActionNodeFactory()
{
creators["cat form"] = &cat_form;
creators["mangle (cat)"] = &mangle_cat;
creators["shred"] = &shred;
creators["rake"] = &rake;
creators["rip"] = &rip;
creators["ferocious bite"] = &ferocious_bite;
creators["savage roar"] = &savage_roar;
creators["faerie fire (feral)"] = &faerie_fire_feral;
creators["healing touch on party"] = &healing_touch_on_party;
creators["regrowth on party"] = &regrowth_on_party;
creators["rejuvenation on party"] = &rejuvenation_on_party;
}
private:
static ActionNode* cat_form([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"cat form",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* mangle_cat([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"mangle (cat)",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* shred([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"shred",
/*P*/ {},
/*A*/ { NextAction("claw") },
/*C*/ {}
);
}
static ActionNode* rake([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rake",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* rip([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rip",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* ferocious_bite([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"ferocious bite",
/*P*/ {},
/*A*/ { NextAction("rip") },
/*C*/ {}
);
}
static ActionNode* savage_roar([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"savage roar",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* faerie_fire_feral([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"faerie fire (feral)",
/*P*/ {},
/*A*/ {},
/*C*/ {}
);
}
static ActionNode* healing_touch_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"healing touch on party",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ { NextAction("cat form") }
);
}
static ActionNode* regrowth_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"regrowth on party",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ { NextAction("cat form") }
);
}
static ActionNode* rejuvenation_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"rejuvenation on party",
/*P*/ { NextAction("caster form") },
/*A*/ {},
/*C*/ { NextAction("cat form") }
);
}
};
OffhealDruidCatStrategy::OffhealDruidCatStrategy(PlayerbotAI* botAI) : FeralDruidStrategy(botAI)
{
actionNodeFactories.Add(new OffhealDruidCatStrategyActionNodeFactory());
}
std::vector<NextAction> OffhealDruidCatStrategy::getDefaultActions()
{
return {
NextAction("mangle (cat)", ACTION_DEFAULT + 0.5f),
NextAction("shred", ACTION_DEFAULT + 0.4f),
NextAction("rake", ACTION_DEFAULT + 0.3f),
NextAction("melee", ACTION_DEFAULT),
NextAction("cat form", ACTION_DEFAULT - 0.1f)
};
}
void OffhealDruidCatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
FeralDruidStrategy::InitTriggers(triggers);
triggers.push_back(
new TriggerNode(
"cat form",
{
NextAction("cat form", ACTION_HIGH + 8)
}
)
);
triggers.push_back(
new TriggerNode(
"savage roar",
{
NextAction("savage roar", ACTION_HIGH + 7)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points 5 available",
{
NextAction("rip", ACTION_HIGH + 6)
}
)
);
triggers.push_back(
new TriggerNode(
"ferocious bite time",
{
NextAction("ferocious bite", ACTION_HIGH + 5)
}
)
);
triggers.push_back(
new TriggerNode(
"target with combo points almost dead",
{
NextAction("ferocious bite", ACTION_HIGH + 4)
}
)
);
triggers.push_back(
new TriggerNode(
"mangle (cat)",
{
NextAction("mangle (cat)", ACTION_HIGH + 3)
}
)
);
triggers.push_back(
new TriggerNode(
"rake",
{
NextAction("rake", ACTION_HIGH + 2)
}
)
);
triggers.push_back(
new TriggerNode(
"almost full energy available",
{
NextAction("shred", ACTION_DEFAULT + 0.4f)
}
)
);
triggers.push_back(
new TriggerNode(
"combo points not full",
{
NextAction("shred", ACTION_DEFAULT + 0.4f)
}
)
);
triggers.push_back(
new TriggerNode(
"faerie fire (feral)",
{
NextAction("faerie fire (feral)", ACTION_NORMAL)
}
)
);
triggers.push_back(
new TriggerNode(
"enemy out of melee",
{
NextAction("feral charge - cat", ACTION_HIGH + 9),
NextAction("dash", ACTION_HIGH + 8)
}
)
);
triggers.push_back(
new TriggerNode(
"medium aoe",
{
NextAction("swipe (cat)", ACTION_HIGH + 3)
}
)
);
triggers.push_back(
new TriggerNode(
"tiger's fury",
{
NextAction("tiger's fury", ACTION_NORMAL + 1)
}
)
);
triggers.push_back(
new TriggerNode(
"party member critical health",
{
NextAction("regrowth on party", ACTION_CRITICAL_HEAL + 6),
NextAction("healing touch on party", ACTION_CRITICAL_HEAL + 5)
}
)
);
triggers.push_back(
new TriggerNode(
"party member low health",
{
NextAction("healing touch on party", ACTION_MEDIUM_HEAL + 5)
}
)
);
triggers.push_back(
new TriggerNode(
"party member medium health",
{
NextAction("rejuvenation on party", ACTION_LIGHT_HEAL + 8)
}
)
);
triggers.push_back(
new TriggerNode(
"party member to heal out of spell range",
{
NextAction("reach party member to heal", ACTION_EMERGENCY + 3)
}
)
);
triggers.push_back(
new TriggerNode(
"low mana",
{
NextAction("innervate", ACTION_HIGH + 4)
}
)
);
}

View File

@ -1,27 +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.
*/
#ifndef _PLAYERBOT_OFFHEALDRUIDCATSTRATEGY_H
#define _PLAYERBOT_OFFHEALDRUIDCATSTRATEGY_H
#include "FeralDruidStrategy.h"
class PlayerbotAI;
class OffhealDruidCatStrategy : public FeralDruidStrategy
{
public:
OffhealDruidCatStrategy(PlayerbotAI* botAI);
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "offheal"; }
std::vector<NextAction> getDefaultActions() override;
uint32 GetType() const override
{
return STRATEGY_TYPE_COMBAT | STRATEGY_TYPE_DPS | STRATEGY_TYPE_HEAL | STRATEGY_TYPE_MELEE;
}
};
#endif

View File

@ -0,0 +1,120 @@
/*
* 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 "RestoDruidStrategy.h"
#include "Playerbots.h"
class RestoDruidStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
{
public:
RestoDruidStrategyActionNodeFactory() {
creators["nourish on party"] = &nourish_on_party;
}
private:
static ActionNode* nourish_on_party([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("nourish on party",
/*P*/ {},
/*A*/ {},
/*C*/ {});
}
};
RestoDruidStrategy::RestoDruidStrategy(PlayerbotAI* botAI) : GenericDruidStrategy(botAI)
{
actionNodeFactories.Add(new RestoDruidStrategyActionNodeFactory());
}
void RestoDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
GenericDruidStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode("no healer dps strategy",
{ NextAction("tree form", 5.0f) }));
triggers.push_back(new TriggerNode(
"party member to heal out of spell range",
{ NextAction("reach party member to heal", 39.0f) }));
triggers.push_back(
new TriggerNode("party member critical health",
{
NextAction("tree form", 34.1f),
NextAction("swiftmend on party", 34.0f),
NextAction("wild growth on party", 33.0f),
NextAction("nourish on party", 32.0f),
NextAction("regrowth on party", 31.0f),
NextAction("healing touch on party", 30.0f),
}));
triggers.push_back(
new TriggerNode("party member critical health",
{ NextAction("nature's swiftness", 58.0f) }));
triggers.push_back(new TriggerNode(
"nature's swiftness active",
{ NextAction("healing touch on party", 55.0f) }));
triggers.push_back(new TriggerNode("clearcasting",
{ NextAction("lifebloom on main tank", 13.0f) }));
// LOW
triggers.push_back(
new TriggerNode("party member low health",
{
NextAction("tree form", 21.5f),
NextAction("swiftmend on party", 21.4f),
NextAction("wild growth on party", 21.3f),
NextAction("nourish on party", 21.2f),
NextAction("regrowth on party", 21.1f),
NextAction("healing touch on party", 21.0f),
}));
// MEDIUM
triggers.push_back(
new TriggerNode("party member medium health",
{
NextAction("tree form", 20.5f),
NextAction("swiftmend on party", 20.4f),
NextAction("wild growth on party", 20.3f),
NextAction("nourish on party", 20.2f),
NextAction("regrowth on party", 20.1f),
NextAction("healing touch on party", 20.0f),
}));
// ALMOST FULL
triggers.push_back(
new TriggerNode("party member almost full health",
{
NextAction("wild growth on party", 10.3f),
NextAction("rejuvenation on party", 10.2f),
NextAction("regrowth on party", 10.1f),
}));
triggers.push_back(
new TriggerNode("medium mana", { NextAction("innervate", 25.0f) }));
triggers.push_back(new TriggerNode("enemy too close for spell",
{ NextAction("flee", 39.0f) }));
}
void DruidTranquilityStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode("medium group heal setting",
{ NextAction("tree form", 30.6f), NextAction("tranquility", 30.5f) }));
}
void DruidBlanketStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(new TriggerNode(
"wild growth blanket",
{ NextAction("tree form", 8.1f), NextAction("wild growth blanket", 8.0f) }));
triggers.push_back(new TriggerNode(
"rejuvenation blanket",
{ NextAction("tree form", 6.1f), NextAction("rejuvenation blanket", 6.0f) }));
}

View File

@ -0,0 +1,42 @@
/*
* 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_RESTODRUIDSTRATEGY_H
#define _PLAYERBOT_RESTODRUIDSTRATEGY_H
#include "GenericDruidStrategy.h"
#include "Strategy.h"
class PlayerbotAI;
class RestoDruidStrategy : public GenericDruidStrategy
{
public:
RestoDruidStrategy(PlayerbotAI* botAI);
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "resto"; }
uint32 GetType() const override { return STRATEGY_TYPE_RANGED | STRATEGY_TYPE_HEAL; }
};
class DruidBlanketStrategy : public Strategy
{
public:
DruidBlanketStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "blanketing"; }
};
class DruidTranquilityStrategy : public Strategy
{
public:
DruidTranquilityStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
void InitTriggers(std::vector<TriggerNode*>& triggers) override;
std::string const getName() override { return "tranquility"; }
};
#endif

View File

@ -1,63 +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 "DruidTriggers.h"
#include "Player.h"
#include "Playerbots.h"
bool MarkOfTheWildOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
}
bool MarkOfTheWildTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("gift of the wild", GetTarget());
}
bool ThornsOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget());
}
bool EntanglingRootsKiteTrigger::IsActive()
{
return DebuffTrigger::IsActive() && AI_VALUE(uint8, "attacker count") < 3 && !GetTarget()->GetPower(POWER_MANA);
}
bool ThornsTrigger::IsActive() { return BuffTrigger::IsActive() && !botAI->HasAura("thorns", GetTarget()); }
bool BearFormTrigger::IsActive() { return !botAI->HasAnyAuraOf(bot, "bear form", "dire bear form", nullptr); }
bool TreeFormTrigger::IsActive() { return !botAI->HasAura(33891, bot); }
bool CatFormTrigger::IsActive() { return !botAI->HasAura("cat form", bot); }
const std::set<uint32> HurricaneChannelCheckTrigger::HURRICANE_SPELL_IDS = {
16914, // Hurricane Rank 1
17401, // Hurricane Rank 2
17402, // Hurricane Rank 3
27012, // Hurricane Rank 4
48467 // Hurricane Rank 5
};
bool HurricaneChannelCheckTrigger::IsActive()
{
Player* bot = botAI->GetBot();
// Check if the bot is channeling a spell
if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL))
{
// Only trigger if the spell being channeled is Hurricane
if (HURRICANE_SPELL_IDS.count(spell->m_spellInfo->Id))
{
uint8 attackerCount = AI_VALUE(uint8, "attacker count");
return attackerCount < minEnemies;
}
}
// Not channeling Hurricane
return false;
}

View File

@ -11,7 +11,21 @@
#include "ServerFacade.h"
#include "SharedDefines.h"
Value<Unit*>* CastPolymorphAction::GetTargetValue() { return context->GetValue<Unit*>("cc target", getName()); }
std::vector<NextAction> CastMoltenArmorAction::getAlternatives()
{
if (!AI_VALUE2(uint32, "spell id", "molten armor"))
return NextAction::merge({ NextAction("mage armor") }, CastBuffSpellAction::getAlternatives());
return CastBuffSpellAction::getAlternatives();
}
std::vector<NextAction> CastMageArmorAction::getAlternatives()
{
if (!AI_VALUE2(uint32, "spell id", "mage armor"))
return NextAction::merge({ NextAction("ice armor") }, CastBuffSpellAction::getAlternatives());
return CastBuffSpellAction::getAlternatives();
}
bool UseManaSapphireAction::isUseful()
{
@ -52,22 +66,23 @@ bool UseManaAgateAction::isUseful()
bool CastFrostNovaAction::isUseful()
{
Unit* target = AI_VALUE(Unit*, "current target");
if (!target || !target->IsInWorld())
if (!target || !target->IsInWorld() || target->isFrozen() ||
(target->ToCreature() &&
target->ToCreature()->HasMechanicTemplateImmunity(1 << (MECHANIC_FREEZE - 1))))
{
return false;
}
if (target->ToCreature() && target->ToCreature()->HasMechanicTemplateImmunity(1 << (MECHANIC_FREEZE - 1)))
return false;
if (target->isFrozen())
return false;
return ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", GetTargetName()), 10.f);
return ServerFacade::instance().IsDistanceLessOrEqualThan(
AI_VALUE2(float, "distance", GetTargetName()), 10.f);
}
bool CastConeOfColdAction::isUseful()
{
bool facingTarget = AI_VALUE2(bool, "facing", "current target");
bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan(AI_VALUE2(float, "distance", GetTargetName()), 10.f);
bool targetClose = ServerFacade::instance().IsDistanceLessOrEqualThan(
AI_VALUE2(float, "distance", GetTargetName()), 10.f);
return facingTarget && targetClose;
}
@ -76,6 +91,7 @@ bool CastDragonsBreathAction::isUseful()
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
bool facingTarget = AI_VALUE2(bool, "facing", "current target");
bool targetClose = bot->IsWithinCombatRange(target, 10.0f);
return facingTarget && targetClose;
@ -86,6 +102,7 @@ bool CastBlastWaveAction::isUseful()
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
bool targetClose = bot->IsWithinCombatRange(target, 10.0f);
return targetClose;
}
@ -102,14 +119,11 @@ Unit* CastFocusMagicOnPartyAction::GetTarget()
for (GroupReference* ref = group->GetFirstMember(); ref; ref = ref->next())
{
Player* member = ref->GetSource();
if (!member || member == bot || !member->IsAlive())
continue;
if (member->GetMap() != bot->GetMap() || bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance)
continue;
if (member->HasAura(54646))
if (!member || member == bot || !member->IsAlive() || member->GetMap() != bot->GetMap() ||
bot->GetDistance(member) > sPlayerbotAIConfig.spellDistance || member->HasAura(54646)) // Focus Magic
{
continue;
}
if (member->getClass() == CLASS_MAGE)
return member;
@ -138,7 +152,7 @@ bool CastBlinkBackAction::Execute(Event event)
Unit* target = AI_VALUE(Unit*, "current target");
if (!target)
return false;
// can cast spell check passed in isUseful()
bot->SetOrientation(bot->GetAngle(target) + M_PI);
return CastSpellAction::Execute(event);
}

View File

@ -18,12 +18,14 @@ class CastMoltenArmorAction : public CastBuffSpellAction
{
public:
CastMoltenArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "molten armor") {}
std::vector<NextAction> getAlternatives() override;
};
class CastMageArmorAction : public CastBuffSpellAction
{
public:
CastMageArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "mage armor") {}
std::vector<NextAction> getAlternatives() override;
};
class CastIceArmorAction : public CastBuffSpellAction
@ -38,16 +40,16 @@ public:
CastFrostArmorAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "frost armor") {}
};
class CastArcaneIntellectAction : public CastBuffSpellAction
class CastArcaneIntellectAction : public GroupBuffSpellAction
{
public:
CastArcaneIntellectAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "arcane intellect") {}
CastArcaneIntellectAction(PlayerbotAI* botAI) : GroupBuffSpellAction(botAI, "arcane intellect") {}
};
class CastArcaneIntellectOnPartyAction : public BuffOnPartyAction
class CastArcaneIntellectOnPartyAction : public GroupBuffOnPartyAction
{
public:
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "arcane intellect") {}
CastArcaneIntellectOnPartyAction(PlayerbotAI* botAI) : GroupBuffOnPartyAction(botAI, "arcane intellect") {}
};
class CastFocusMagicOnPartyAction : public CastSpellAction
@ -60,7 +62,8 @@ public:
class CastSummonWaterElementalAction : public CastBuffSpellAction
{
public:
CastSummonWaterElementalAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "summon water elemental") {}
CastSummonWaterElementalAction(PlayerbotAI* botAI)
: CastBuffSpellAction(botAI, "summon water elemental") {}
};
// Boost Actions
@ -215,11 +218,10 @@ public:
// CC, Interrupt, and Dispel Actions
class CastPolymorphAction : public CastBuffSpellAction
class CastPolymorphAction : public CastCrowdControlSpellAction
{
public:
CastPolymorphAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "polymorph") {}
Value<Unit*>* GetTargetValue() override;
CastPolymorphAction(PlayerbotAI* botAI) : CastCrowdControlSpellAction(botAI, "polymorph") {}
};
class CastSpellstealAction : public CastSpellAction
@ -237,7 +239,8 @@ public:
class CastCounterspellOnEnemyHealerAction : public CastSpellOnEnemyHealerAction
{
public:
CastCounterspellOnEnemyHealerAction(PlayerbotAI* botAI) : CastSpellOnEnemyHealerAction(botAI, "counterspell") {}
CastCounterspellOnEnemyHealerAction(PlayerbotAI* botAI)
: CastSpellOnEnemyHealerAction(botAI, "counterspell") {}
};
class CastFrostNovaAction : public CastSpellAction
@ -276,9 +279,7 @@ class CastRemoveLesserCurseOnPartyAction : public CurePartyMemberAction
{
public:
CastRemoveLesserCurseOnPartyAction(PlayerbotAI* botAI)
: CurePartyMemberAction(botAI, "remove lesser curse", DISPEL_CURSE)
{
}
: CurePartyMemberAction(botAI, "remove lesser curse", DISPEL_CURSE) {}
};
// Damage and Debuff Actions
@ -332,7 +333,6 @@ public:
CastLivingBombAction(PlayerbotAI* botAI) : CastDebuffSpellAction(botAI, "living bomb", true) {}
bool isUseful() override
{
// Bypass TTL check
return CastAuraSpellAction::isUseful();
}
};
@ -343,7 +343,6 @@ public:
CastLivingBombOnAttackersAction(PlayerbotAI* botAI) : CastDebuffSpellOnAttackerAction(botAI, "living bomb", true) {}
bool isUseful() override
{
// Bypass TTL check
return CastAuraSpellAction::isUseful();
}
};

View File

@ -89,6 +89,7 @@ public:
creators["arcane intellect"] = &MageTriggerFactoryInternal::arcane_intellect;
creators["arcane intellect on party"] = &MageTriggerFactoryInternal::arcane_intellect_on_party;
creators["mage armor"] = &MageTriggerFactoryInternal::mage_armor;
creators["molten armor"] = &MageTriggerFactoryInternal::molten_armor;
creators["remove curse"] = &MageTriggerFactoryInternal::remove_curse;
creators["remove curse on party"] = &MageTriggerFactoryInternal::remove_curse_on_party;
creators["counterspell"] = &MageTriggerFactoryInternal::counterspell;
@ -143,6 +144,7 @@ private:
static Trigger* arcane_intellect(PlayerbotAI* botAI) { return new ArcaneIntellectTrigger(botAI); }
static Trigger* arcane_intellect_on_party(PlayerbotAI* botAI) { return new ArcaneIntellectOnPartyTrigger(botAI); }
static Trigger* mage_armor(PlayerbotAI* botAI) { return new MageArmorTrigger(botAI); }
static Trigger* molten_armor(PlayerbotAI* botAI) { return new MoltenArmorTrigger(botAI); }
static Trigger* remove_curse(PlayerbotAI* botAI) { return new RemoveCurseTrigger(botAI); }
static Trigger* remove_curse_on_party(PlayerbotAI* botAI) { return new PartyMemberRemoveCurseTrigger(botAI); }
static Trigger* counterspell(PlayerbotAI* botAI) { return new CounterspellInterruptSpellTrigger(botAI); }

View File

@ -4,7 +4,6 @@
*/
#include "MageTriggers.h"
#include "MageActions.h"
#include "Playerbots.h"
#include "Player.h"
#include "Spell.h"
@ -23,7 +22,7 @@ bool NoManaGemTrigger::IsActive()
5513, // Mana Jade
5514 // Mana Agate
};
Player* bot = botAI->GetBot();
for (uint32 gemId : gemIds)
{
if (bot->GetItemCount(gemId, false) > 0) // false = only in bags
@ -32,11 +31,6 @@ bool NoManaGemTrigger::IsActive()
return true;
}
bool ArcaneIntellectOnPartyTrigger::IsActive()
{
return BuffOnPartyTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
}
bool ArcaneIntellectTrigger::IsActive()
{
return BuffTrigger::IsActive() && !botAI->HasAura("arcane brilliance", GetTarget());
@ -45,17 +39,35 @@ bool ArcaneIntellectTrigger::IsActive()
bool MageArmorTrigger::IsActive()
{
Unit* target = GetTarget();
if (botAI->HasAura("mage armor", target))
return false;
if (AI_VALUE2(uint32, "spell id", "mage armor"))
return true;
return !botAI->HasAura("ice armor", target) && !botAI->HasAura("frost armor", target) &&
!botAI->HasAura("molten armor", target) && !botAI->HasAura("mage armor", target);
!botAI->HasAura("molten armor", target);
}
bool MoltenArmorTrigger::IsActive()
{
Unit* target = GetTarget();
if (botAI->HasAura("molten armor", target))
return false;
if (AI_VALUE2(uint32, "spell id", "molten armor"))
return true;
return !botAI->HasAura("ice armor", target) && !botAI->HasAura("frost armor", target) &&
!botAI->HasAura("mage armor", target);
}
bool FrostNovaOnTargetTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false;
}
return botAI->HasAura(spell, target);
}
@ -63,15 +75,14 @@ bool FrostbiteOnTargetTrigger::IsActive()
{
Unit* target = GetTarget();
if (!target || !target->IsAlive() || !target->IsInWorld())
{
return false;
}
return botAI->HasAura(spell, target);
}
bool NoFocusMagicTrigger::IsActive()
{
if (!bot->HasSpell(54646))
if (!bot->HasSpell(54646)) // Focus Magic
return false;
Group* group = bot->GetGroup();
@ -92,24 +103,19 @@ bool NoFocusMagicTrigger::IsActive()
bool DeepFreezeCooldownTrigger::IsActive()
{
Player* bot = botAI->GetBot();
static const uint32 DEEP_FREEZE_SPELL_ID = 44572;
// If the bot does NOT have Deep Freeze, treat as "on cooldown"
if (!bot->HasSpell(DEEP_FREEZE_SPELL_ID))
if (!bot->HasSpell(44572)) // Deep Freeze
return true;
// Otherwise, use the default cooldown logic
return SpellCooldownTrigger::IsActive();
}
const std::set<uint32> FlamestrikeNearbyTrigger::FLAMESTRIKE_SPELL_IDS = {2120, 2121, 8422, 8423, 10215,
10216, 27086, 42925, 42926};
const std::unordered_set<uint32> FlamestrikeNearbyTrigger::FLAMESTRIKE_SPELL_IDS = {
2120, 2121, 8422, 8423, 10215, 10216, 27086, 42925, 42926
};
bool FlamestrikeNearbyTrigger::IsActive()
{
Player* bot = botAI->GetBot();
for (uint32 spellId : FLAMESTRIKE_SPELL_IDS)
{
Aura* aura = bot->GetAura(spellId, bot->GetGUID());
@ -133,7 +139,6 @@ bool ImprovedScorchTrigger::IsActive()
if (!target || !target->IsAlive() || !target->IsInWorld())
return false;
// List of all spell IDs for Improved Scorch, Winter's Chill, and Shadow Mastery
static const uint32 ImprovedScorchExclusiveDebuffs[] = {// Shadow Mastery
17794, 17797, 17798, 17799, 17800,
// Winter's Chill
@ -147,11 +152,10 @@ bool ImprovedScorchTrigger::IsActive()
return false;
}
// Use default DebuffTrigger logic for the rest (only trigger if debuff is missing or expiring)
return DebuffTrigger::IsActive();
}
const std::set<uint32> BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = {
const std::unordered_set<uint32> BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = {
10, // Blizzard Rank 1
6141, // Blizzard Rank 2
8427, // Blizzard Rank 3
@ -165,19 +169,12 @@ const std::set<uint32> BlizzardChannelCheckTrigger::BLIZZARD_SPELL_IDS = {
bool BlizzardChannelCheckTrigger::IsActive()
{
Player* bot = botAI->GetBot();
// Check if the bot is channeling a spell
if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL))
if (Spell* spell = bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL);
spell && BLIZZARD_SPELL_IDS.count(spell->m_spellInfo->Id))
{
// Only trigger if the spell being channeled is Blizzard
if (BLIZZARD_SPELL_IDS.count(spell->m_spellInfo->Id))
{
uint8 attackerCount = AI_VALUE(uint8, "attacker count");
return attackerCount < minEnemies;
}
uint8 attackerCount = AI_VALUE(uint8, "attacker count");
return attackerCount < minEnemies;
}
// Not channeling Blizzard
return false;
}

View File

@ -11,26 +11,21 @@
#include "SharedDefines.h"
#include "Trigger.h"
#include "Playerbots.h"
#include "PlayerbotAI.h"
#include <set>
#include <unordered_set>
class PlayerbotAI;
// Buff and Out of Combat Triggers
class ArcaneIntellectOnPartyTrigger : public BuffOnPartyTrigger
{
public:
ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "arcane intellect", 2 * 2000) {}
bool IsActive() override;
ArcaneIntellectOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "arcane intellect", 4 * 2000) {}
};
class ArcaneIntellectTrigger : public BuffTrigger
{
public:
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 2 * 2000) {}
ArcaneIntellectTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "arcane intellect", 4 * 2000) {}
bool IsActive() override;
};
@ -41,6 +36,13 @@ public:
bool IsActive() override;
};
class MoltenArmorTrigger : public BuffTrigger
{
public:
MoltenArmorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "molten armor", 5 * 2000) {}
bool IsActive() override;
};
class NoFocusMagicTrigger : public Trigger
{
public:
@ -58,7 +60,6 @@ class NoManaGemTrigger : public Trigger
{
public:
NoManaGemTrigger(PlayerbotAI* botAI) : Trigger(botAI, "no mana gem") {}
bool IsActive() override;
};
@ -109,10 +110,8 @@ public:
class ArcaneBlast4StacksAndMissileBarrageTrigger : public TwoTriggers
{
public:
ArcaneBlast4StacksAndMissileBarrageTrigger(PlayerbotAI* ai)
: TwoTriggers(ai, "arcane blast stack", "missile barrage")
{
}
ArcaneBlast4StacksAndMissileBarrageTrigger(PlayerbotAI* botAI)
: TwoTriggers(botAI, "arcane blast stack", "missile barrage") {}
};
class CombustionTrigger : public BoostTrigger
@ -138,7 +137,7 @@ public:
class ColdSnapTrigger : public TwoTriggers
{
public:
ColdSnapTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "icy veins on cd", "deep freeze on cd") {}
ColdSnapTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "icy veins on cd", "deep freeze on cd") {}
};
class MirrorImageTrigger : public BoostTrigger
@ -181,9 +180,8 @@ public:
class PartyMemberRemoveCurseTrigger : public PartyMemberNeedCureTrigger
{
public:
PartyMemberRemoveCurseTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "remove curse", DISPEL_CURSE)
{
}
PartyMemberRemoveCurseTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "remove curse", DISPEL_CURSE) {}
};
class SpellstealTrigger : public TargetAuraDispelTrigger
@ -216,7 +214,7 @@ public:
class LivingBombOnAttackersTrigger : public DebuffOnAttackerTrigger
{
public:
LivingBombOnAttackersTrigger(PlayerbotAI* ai) : DebuffOnAttackerTrigger(ai, "living bomb", true) {}
LivingBombOnAttackersTrigger(PlayerbotAI* botAI) : DebuffOnAttackerTrigger(botAI, "living bomb", true) {}
bool IsActive() override { return BuffTrigger::IsActive(); }
};
@ -282,13 +280,13 @@ public:
protected:
float radius;
static const std::set<uint32> FLAMESTRIKE_SPELL_IDS;
static const std::unordered_set<uint32> FLAMESTRIKE_SPELL_IDS;
};
class FlamestrikeBlizzardTrigger : public TwoTriggers
{
public:
FlamestrikeBlizzardTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "flamestrike nearby", "medium aoe") {}
FlamestrikeBlizzardTrigger(PlayerbotAI* botAI) : TwoTriggers(botAI, "flamestrike nearby", "medium aoe") {}
};
class BlizzardChannelCheckTrigger : public Trigger
@ -301,7 +299,7 @@ public:
protected:
uint32 minEnemies;
static const std::set<uint32> BLIZZARD_SPELL_IDS;
static const std::unordered_set<uint32> BLIZZARD_SPELL_IDS;
};
class BlastWaveOffCdTrigger : public SpellNoCooldownTrigger
@ -313,7 +311,8 @@ public:
class BlastWaveOffCdTriggerAndMediumAoeTrigger : public TwoTriggers
{
public:
BlastWaveOffCdTriggerAndMediumAoeTrigger(PlayerbotAI* ai) : TwoTriggers(ai, "blast wave off cd", "medium aoe") {}
BlastWaveOffCdTriggerAndMediumAoeTrigger(PlayerbotAI* botAI)
: TwoTriggers(botAI, "blast wave off cd", "medium aoe") {}
};
class NoFirestarterStrategyTrigger : public Trigger

View File

@ -13,22 +13,18 @@ public:
ArcaneMageStrategyActionNodeFactory()
{
creators["arcane blast"] = &arcane_blast;
creators["arcane barrage"] = &arcane_barrage;
creators["arcane missiles"] = &arcane_missiles;
creators["fire blast"] = &fire_blast;
creators["frostbolt"] = &frostbolt;
creators["arcane power"] = &arcane_power;
creators["icy veins"] = &icy_veins;
}
private:
static ActionNode* arcane_blast(PlayerbotAI*) { return new ActionNode("arcane blast", {}, {}, {}); }
static ActionNode* arcane_barrage(PlayerbotAI*) { return new ActionNode("arcane barrage", {}, {}, {}); }
static ActionNode* arcane_missiles(PlayerbotAI*) { return new ActionNode("arcane missiles", {}, {}, {}); }
static ActionNode* fire_blast(PlayerbotAI*) { return new ActionNode("fire blast", {}, {}, {}); }
static ActionNode* frostbolt(PlayerbotAI*) { return new ActionNode("frostbolt", {}, {}, {}); }
static ActionNode* arcane_power(PlayerbotAI*) { return new ActionNode("arcane power", {}, {}, {}); }
static ActionNode* icy_veins(PlayerbotAI*) { return new ActionNode("icy veins", {}, {}, {}); }
// Arcane Barrage is the alternate for Arcane Blast (cast while moving, or
// when Arcane Blast is unavailable - e.g. not yet learned at low levels).
static ActionNode* arcane_blast([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode("arcane blast",
/*P*/ {},
/*A*/ { NextAction("arcane barrage") },
/*C*/ {});
}
};
// ===== Single Target Strategy =====

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