Compare commits

..

10 Commits

Author SHA1 Message Date
Keleborn
531282e4be
Merge pull request #2359 from mod-playerbots/test-staging
Test staging
2026-05-08 10:36:36 -07:00
Keleborn
38caa1daa7
Randombots respect realm PVP setting (#2342)
<!--
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 an issue where bots would eventually have pvp set by reset. THis
ensures bot pvp states are consistent with realm type.


## 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?
    - - [ ] No
    - - [x] Yes (**explain why**)
Corrects behavior to match server intent. 


- 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.
-->
searching code, writing 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-03 21:41:45 +02:00
Keleborn
ccce14238e
Core Update, change to DeserterCheck and signature (#2354)
<!--
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 -->
Required change for
https://github.com/azerothcore/azerothcore-wotlk/pull/24641


## 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?
    - - [ ] No, not at all
    - - [ ] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)



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



- Does this change add new decision branches or increase maintenance
complexity?
    - - [ ] 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
- - [ ] 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

- - [ ] 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-03 07:16:34 -07:00
Crow
104a1b9ee1
Clean up unnecessary includes in raid strategy and trigger-context headers (#2347)
<!--
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 trims redundant includes from raid Strategy.h and
TriggerContext.h headers. I noticed a consistent pattern of including
Multiplier.h when it was not needed in Strategy.h and including
AiObjectContext.h in TriggerContext.h when only the narrower
NamedObjectContext.h is needed (both of which I was guilty of also).
Since we make new raid strategies based on existing raid strategies, I
figure let's go for the low-hanging fruit and just fix this so we stop
doing it wrong going forward.

While I was at it, I removed other unnecessary includes but in those two
files only (across dungeon and raid strategies).

Edit: Made a couple of other minor code cleanups I'd been intending to
do. Notably, we shouldn't be including a .cpp in PlayerbotAI.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.



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

I had GPT-5.4 do the actual work because doing it myself file-by-file
would've been such a snoozefest.

<!--
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-02 12:20:18 -07:00
Crow
94195c3b9b
Bots Don't Autoequip Tools & Other Misc Weapons (#2346)
<!--
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 -->
Solve the rest of #2344

Now, bots won't autoequip any weapon from ITEM_SUBCLASS_WEAPON_MISC,
which includes all of the basic tools and some other crap that they have
no need to autoequip, either. Bots are still eligible to equip those
weapons (such as through the "e" command).

Note that MISC includes the Argent Tournament lances. I've not played
WotLK, but I assume those might be relevant for a strategy. It shouldn't
be a problem though because I've intentionally not made bots ineligible
for MISC weapons; they just won't consider them upgrades on their own.

I also cleaned up ItemUsageValue::QueryItemUsageForEquip to consolidate
checks and so on. None of that should be functional, or I screwed up.
The check for MISC is on lines 219 through 221.

## 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.
-->
Activate selfbot. Unequip all weapons and have nothing in the inventory
except for a MISC weapon such as a skinning knife. Whisper self "equip
upgrade"--nothing should happen. Whisper self "e [LINK TO WEAPON]"--the
bot should equip the 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?
    - - [ ] No, not at all
    - - [x] Minimal impact (**explain below**)
    - - [ ] Moderate impact (**explain below**)

There's an extra check but totally meaningless with respect to
performance.

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

They won't auto-equip crap that will prevent them from using abilities.

- 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 evaluate different spots where I thought an exclusion
could be added before settling on this one.

<!--
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-02 12:20:03 -07:00
kadeshar
063eabc16e
Spam guild fix (#2341)
<!--
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 messages in failed attempts of buying tabard.
Related with: #1885 

## 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 bot with guild strategy. Spam should not appear.

## 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-02 12:19:51 -07:00
kadeshar
cc6f6c2c3a
Thorns reapply fix (#2338)
<!--
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 -->

Allowed druid cast Thorns on target which already have Thorns on him to
extend duration.
Related with: #2290 

## 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 2 bot (one of them must be druid which can cast thorns)
2. Select second bot and use commad `cast thorns`
3. Wait until buff timer decrease
4. Use again same command
5. Druid should cast spell

## 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 understand 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-02 12:19:37 -07:00
Keleborn
c819516325
Fix rpg travel flying (#2324)
<!--
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 -->
Clean up values that were incorrectly translated from the sql search
into the dbc search.
Refactors structure for cities in TravelMgr to try to resolve some
duplication issues.
Change to position based search, so that bots dont get stuck if they
fail to resolve the flightmaster game object when it hasnt spawned.
TravelFlight state now stores flight master entry + world position
instead of ObjectGuid, so the bot can move back into range and
re-resolve the NPC locally via FindNearestCreature
Bundles reliability cleanup in NewRpgTravelFlightAction: uses
info.ChangeToIdle() consistently and adds the missing return true after
a failed taxi path

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

No expected shanges. 

## 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 server and check if zones are getting populated well. 

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

Run with 4k bots, no issues. 

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

It should correctly send bots to the areas appropriate for their level
in an equally weighted manner.


- 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.
-->
Refactoring the data structure based on my instruction. 
All parts 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-02 12:19:23 -07:00
HennyWilly
410ce134fe
Fix Deep Breath issues during Onyxia encounter (#2318)
<!--
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 current strategy for Onyxia causes bots to get hit by her breath
attack relatively consistently during phase 2.
The problem was that the safe zone coordinates always use the bot's
z-coordinate. If the bots are standing at the lower altitude part of the
arena, `SearchForBestPath` inside `MoveTo` causes `INVALID_HEIGHT`,
resulting in the bot not moving at all and getting hit by the breath
attack.

This PR fixes this behavior by using the actual terrain z-coordinates
for the predefined safe zones instead of always using the bot's
z-coordinate. These values are chosen to ensure valid pathfinding
regardless of the bot's current elevation.
Additionally, bots now interrupt their spells if they are not inside a
safe zone during the breath. This causes the bots to immediately start
running instead of finishing their casts first.

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

Replaced the use of `bot->GetPositionZ()` in `GetSafeZonesForBreath`
with predefined safe zone z-coordinates to ensure valid pathfinding.
Added `AttackStop` and `InterruptNonMeleeSpells` to guarantee immediate
movement when outside safe zones.
No additional condition checks or branching logic were introduced.

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

Minimal. The logic only runs within the Onyxia encounter script and
calling `AttackStop` and `InterruptNonMeleeSpells` should be negligible.


## 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.
-->
Enter Onyxia's Lair (10, 25 or 40 (mod-individual-progression)) and
engage Onyxia with the appropriate number of bots.
During phase 2 (Onyxia takes off), check if the bots move to the safe
zones during the breath attack.
Tip: Mark Onyxia as moon (RTI), so that phase 2 doesn't end too quickly.

## 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 calls of `AttackStop` and `InterruptNonMeleeSpells` cause minimal
overhead compared to the original strategy. This should be negligible.

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

Yes (encounter-specific). Bots will now interrupt casts earlier during
Onyxia phase 2 to prioritize movement to safe zones.

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

The strategy for Onyxia might need additional work:
For example, the Onyxian Lair Guards are completely ignored while whelps
are alive and their Blast Nova doesn't get handled at all.
This PR focuses on fixing the Deep Breath behavior. Handling of Onyxian
Lair Guards is not included and should be implemented in a separate PR.
2026-05-02 12:19:11 -07:00
Alex Dcnh
4a79a46da5
Add argument "all" to "rep" command and new "emblems" command (#2035)
## Summary
- restrict `reputation all` to a curated list of WotLK/BC/Classic
faction IDs (filtered by team)
- reuse a shared formatter for reputation lines
- add an `emblems` chat command to report emblem counts

### Multibot will need a update

<img width="420" height="131" alt="image"
src="https://github.com/user-attachments/assets/bedf9dd8-e8de-465f-96d0-f9c2f1dacfc1"
/>

<img width="601" height="623" alt="image"
src="https://github.com/user-attachments/assets/1edde264-baed-4cfb-a401-208bea189139"
/>

<img width="670" height="661" alt="image"
src="https://github.com/user-attachments/assets/a70e2174-dd1d-4e14-b6e4-2938c26ccb29"
/>

<img width="650" height="48" alt="image"
src="https://github.com/user-attachments/assets/241e332a-23ce-4d81-be53-4d83e10d246a"
/>

---------

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-02 12:18:54 -07:00
59 changed files with 522 additions and 343 deletions

View File

@ -32,7 +32,7 @@
# LEVELS
# GEAR
# QUESTS
# ACTIVITIES
# ACTIVITY
# SPELLS
# STRATEGIES
# RPG STRATEGY

View File

@ -343,7 +343,7 @@ bool BGJoinAction::isUseful()
return false;
// check Deserter debuff
if (!bot->CanJoinToBattleground())
if (bot->IsDeserter())
return false;
// check if has free queue slots (pointless as already making sure not in queue)

View File

@ -213,13 +213,7 @@ bool BuyAction::Execute(Event event)
}
}
if (!vendored)
{
botAI->TellError("There are no vendors nearby");
return false;
}
return true;
return vendored;
}
bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto)

View File

@ -296,7 +296,7 @@ bool PetitionTurnInAction::isUseful()
bool BuyTabardAction::Execute(Event /*event*/)
{
bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"));
bool canBuy = botAI->DoSpecificAction("buy", Event("buy tabard", "Hitem:5976:"), true);
if (canBuy && AI_VALUE2(uint32, "item count", chat->FormatQItem(5976)))
return true;

View File

@ -0,0 +1,39 @@
/*
* 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 "TellEmblemsAction.h"
#include <array>
#include "Event.h"
#include "Playerbots.h"
bool TellEmblemsAction::Execute(Event /*event*/)
{
static std::array<uint32, 6> const emblemIds = {
29434, // Badge of Justice
40752, // Emblem of Heroism
40753, // Emblem of Valor
45624, // Emblem of Conquest
47241, // Emblem of Triumph
49426 // Emblem of Frost
};
botAI->TellMaster("=== Emblems ===");
for (uint32 itemId : emblemIds)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
if (!proto)
continue;
uint32 count = bot->GetItemCount(itemId, false);
std::ostringstream out;
out << chat->FormatItem(proto, count);
botAI->TellMaster(out);
}
return true;
}

View File

@ -0,0 +1,21 @@
/*
* 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_TELLEMBLEMSACTION_H
#define _PLAYERBOT_TELLEMBLEMSACTION_H
#include "InventoryAction.h"
class PlayerbotAI;
class TellEmblemsAction : public InventoryAction
{
public:
TellEmblemsAction(PlayerbotAI* botAI) : InventoryAction(botAI, "emblems") {}
bool Execute(Event event) override;
};
#endif

View File

@ -5,34 +5,23 @@
#include "TellReputationAction.h"
#include <algorithm>
#include "Event.h"
#include "PlayerbotAI.h"
#include "ReputationMgr.h"
bool TellReputationAction::Execute(Event /*event*/)
#include "SharedDefines.h"
std::string TellReputationAction::BuildReputationLine(FactionEntry const* entry)
{
Player* master = GetMaster();
if (!master)
return false;
ObjectGuid selection = master->GetTarget();
if (selection.IsEmpty())
return false;
Unit* unit = ObjectAccessor::GetUnit(*master, selection);
if (!unit)
return false;
FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry();
uint32 faction = factionTemplate->faction;
FactionEntry const* entry = sFactionStore.LookupEntry(faction);
int32 reputation = bot->GetReputationMgr().GetReputation(faction);
ReputationMgr& repMgr = bot->GetReputationMgr();
ReputationRank rank = repMgr.GetRank(entry);
int32 reputation = repMgr.GetReputation(entry->ID);
std::ostringstream out;
out << entry->name[0] << ": ";
out << "|cff";
out << entry->name[0] << ": |cff";
ReputationRank rank = bot->GetReputationMgr().GetRank(entry);
switch (rank)
{
case REP_HATED:
@ -71,7 +60,65 @@ bool TellReputationAction::Execute(Event /*event*/)
base -= ReputationMgr::PointsInRank[i];
out << " (" << (reputation - base) << "/" << ReputationMgr::PointsInRank[rank] << ")";
botAI->TellMaster(out);
return out.str();
}
bool TellReputationAction::Execute(Event event)
{
std::string const param = event.getParam();
if (param == "all")
{
ReputationMgr& repMgr = bot->GetReputationMgr();
std::vector<std::string> lines;
FactionStateList const& stateList = repMgr.GetStateList();
lines.reserve(stateList.size());
for (auto const& itr : stateList)
{
FactionState const& faction = itr.second;
if (!(faction.Flags & FACTION_FLAG_VISIBLE))
continue;
if (faction.Flags & (FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED) &&
!(faction.Flags & FACTION_FLAG_SPECIAL))
continue;
FactionEntry const* entry = sFactionStore.LookupEntry(faction.ID);
if (!entry)
continue;
lines.push_back(BuildReputationLine(entry));
}
std::sort(lines.begin(), lines.end());
botAI->TellMaster("=== Reputations ===");
for (auto const& line : lines)
botAI->TellMaster(line);
return true;
}
Player* master = GetMaster();
if (!master)
return false;
ObjectGuid selection = master->GetTarget();
if (selection.IsEmpty())
return false;
Unit* unit = ObjectAccessor::GetUnit(*master, selection);
if (!unit)
return false;
FactionTemplateEntry const* factionTemplate = unit->GetFactionTemplateEntry();
FactionEntry const* entry = sFactionStore.LookupEntry(factionTemplate->faction);
if (!entry)
return false;
botAI->TellMaster(BuildReputationLine(entry));
return true;
}

View File

@ -6,8 +6,11 @@
#ifndef _PLAYERBOT_TELLREPUTATIONACTION_H
#define _PLAYERBOT_TELLREPUTATIONACTION_H
#include <string>
#include "Action.h"
struct FactionEntry;
class PlayerbotAI;
class TellReputationAction : public Action
@ -16,6 +19,9 @@ public:
TellReputationAction(PlayerbotAI* botAI) : Action(botAI, "reputation") {}
bool Execute(Event event) override;
private:
std::string BuildReputationLine(FactionEntry const* entry);
};
#endif

View File

@ -66,6 +66,7 @@
#include "TaxiAction.h"
#include "TeleportAction.h"
#include "TellCastFailedAction.h"
#include "TellEmblemsAction.h"
#include "TellItemCountAction.h"
#include "TellLosAction.h"
#include "TellReputationAction.h"
@ -120,6 +121,7 @@ public:
creators["teleport"] = &ChatActionContext::teleport;
creators["taxi"] = &ChatActionContext::taxi;
creators["repair"] = &ChatActionContext::repair;
creators["emblems"] = &ChatActionContext::emblems;
creators["use"] = &ChatActionContext::use;
creators["item count"] = &ChatActionContext::item_count;
creators["equip"] = &ChatActionContext::equip;
@ -276,6 +278,7 @@ private:
static Action* item_count(PlayerbotAI* botAI) { return new TellItemCountAction(botAI); }
static Action* use(PlayerbotAI* botAI) { return new UseItemAction(botAI); }
static Action* repair(PlayerbotAI* botAI) { return new RepairAllAction(botAI); }
static Action* emblems(PlayerbotAI* botAI) { return new TellEmblemsAction(botAI); }
static Action* taxi(PlayerbotAI* botAI) { return new TaxiAction(botAI); }
static Action* teleport(PlayerbotAI* botAI) { return new TeleportAction(botAI); }
static Action* release(PlayerbotAI* botAI) { return new ReleaseSpiritAction(botAI); }

View File

@ -41,6 +41,7 @@ public:
creators["teleport"] = &ChatTriggerContext::teleport;
creators["taxi"] = &ChatTriggerContext::taxi;
creators["repair"] = &ChatTriggerContext::repair;
creators["emblems"] = &ChatTriggerContext::emblems;
creators["u"] = &ChatTriggerContext::use;
creators["use"] = &ChatTriggerContext::use;
creators["c"] = &ChatTriggerContext::item_count;
@ -235,6 +236,7 @@ private:
static Trigger* item_count(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "c"); }
static Trigger* use(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "use"); }
static Trigger* repair(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "repair"); }
static Trigger* emblems(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "emblems"); }
static Trigger* taxi(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "taxi"); }
static Trigger* teleport(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "teleport"); }
static Trigger* q(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "q"); }

View File

@ -114,6 +114,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
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("focus heal", { NextAction("focus heal targets", relevance) }));
triggers.push_back(new TriggerNode("emblems", { NextAction("emblems", relevance) }));
}
ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : PassTroughStrategy(botAI)
@ -138,6 +139,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("teleport");
supported.push_back("taxi");
supported.push_back("repair");
supported.push_back("emblems");
supported.push_back("talents");
supported.push_back("spells");
supported.push_back("co");
@ -202,8 +204,8 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("unlock items");
supported.push_back("unlock traded item");
supported.push_back("tame");
supported.push_back("glyphs"); // Added for custom Glyphs
supported.push_back("glyph equip"); // Added for custom Glyphs
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");

View File

@ -297,7 +297,7 @@ bool PlayerWantsInBattlegroundTrigger::IsActive()
if (bot->GetBattleground() && bot->GetBattleground()->GetStatus() == STATUS_IN_PROGRESS)
return false;
if (!bot->CanJoinToBattleground())
if (bot->IsDeserter())
return false;
return true;

View File

@ -180,19 +180,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
delete pItem;
if (result != EQUIP_ERR_OK && result != EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
{
return ITEM_USAGE_NONE;
}
// Check is unique items are equipped or not
bool needToCheckUnique = false;
if (result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS)
{
needToCheckUnique = true;
}
else if (itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
needToCheckUnique = true;
}
// Check if unique items are equipped or not
bool needToCheckUnique = result == EQUIP_ERR_CANT_CARRY_MORE_OF_THIS ||
itemProto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE);
if (needToCheckUnique)
{
@ -206,14 +198,11 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped)
{
return ITEM_USAGE_NONE; // Item is already equipped
}
// If not equipped, continue processing
}
if (itemProto->Class == ITEM_CLASS_QUIVER)
if (bot->getClass() != CLASS_HUNTER)
if (itemProto->Class == ITEM_CLASS_QUIVER && bot->getClass() != CLASS_HUNTER)
return ITEM_USAGE_NONE;
if (itemProto->Class == ITEM_CLASS_CONTAINER)
@ -221,13 +210,15 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
if (itemProto->SubClass != ITEM_SUBCLASS_CONTAINER)
return ITEM_USAGE_NONE; // Todo add logic for non-bag containers. We want to look at professions/class and
// only replace if non-bag is larger than bag.
if (GetSmallestBagSize() >= itemProto->ContainerSlots)
return ITEM_USAGE_NONE;
return ITEM_USAGE_EQUIP;
}
if (itemProto->Class == ITEM_CLASS_WEAPON && itemProto->SubClass == ITEM_SUBCLASS_WEAPON_MISC)
return ITEM_USAGE_NONE;
bool shouldEquip = false;
// uint32 statWeight = sRandomItemMgr.GetLiveStatWeight(bot, itemProto->ItemId);
StatsWeightCalculator calculator(bot);
@ -254,19 +245,14 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
uint8 dstSlot = botAI->FindEquipSlot(itemProto, NULL_SLOT, true);
// Check if dest wasn't set correctly by CanEquipItem and use FindEquipSlot instead
// This occurs with unique items that are already in the bots bags when CanEquipItem is called
if (dest == 0)
{
if (dstSlot != NULL_SLOT)
if (dest == 0 && dstSlot != NULL_SLOT)
{
// Construct dest from dstSlot
dest = (INVENTORY_SLOT_BAG_0 << 8) | dstSlot;
}
}
if (dstSlot == EQUIPMENT_SLOT_FINGER1 || dstSlot == EQUIPMENT_SLOT_TRINKET1)
{
possibleSlots = 2;
}
// Check weapon case separately to keep things a bit cleaner
bool have2HWeapon = false;
@ -283,14 +269,9 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
itemProto->SubClass == ITEM_SUBCLASS_WEAPON_SWORD2);
// If the bot can Titan Grip, ignore any 2H weapon that isn't a 2H sword, mace, or axe.
if (bot->CanTitanGrip())
{
// If this weapon is 2H but not one of the valid TG weapon types, do not equip it at all.
if (itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
{
if (bot->CanTitanGrip() && itemProto->InventoryType == INVTYPE_2HWEAPON && !isValidTGWeapon)
return ITEM_USAGE_NONE;
}
}
// Now handle the logic for equipping and possible offhand slots
// If the bot can Dual Wield and:
@ -317,10 +298,8 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
if (shouldEquipInSlot)
return ITEM_USAGE_EQUIP;
else
{
return ITEM_USAGE_BAD_EQUIP;
}
}
ItemTemplate const* oldItemProto = oldItem->GetTemplate();
float oldScore = calculator.CalculateItem(oldItemProto->ItemId, oldItem->GetInt32Value(ITEM_FIELD_RANDOM_PROPERTIES_ID));
@ -328,23 +307,17 @@ ItemUsage ItemUsageValue::QueryItemUsageForEquip(ItemTemplate const* itemProto,
{
// uint32 oldStatWeight = sRandomItemMgr.GetLiveStatWeight(bot, oldItemProto->ItemId);
if (itemScore || oldScore)
{
shouldEquipInSlot = itemScore > oldScore * sPlayerbotAIConfig.equipUpgradeThreshold;
}
}
// Bigger quiver
if (itemProto->Class == ITEM_CLASS_QUIVER)
{
if (!oldItem || oldItemProto->ContainerSlots < itemProto->ContainerSlots)
{
return ITEM_USAGE_EQUIP;
}
else
{
return ITEM_USAGE_NONE;
}
}
bool existingShouldEquip = true;
if (oldItemProto->Class == ITEM_CLASS_WEAPON && !sRandomItemMgr.CanEquipWeapon(bot->getClass(), oldItemProto))

View File

@ -11,6 +11,22 @@
#include "AoeValues.h"
#include "TargetValue.h"
namespace
{
bool PrepareThornsTarget(PlayerbotAI* botAI, Unit* target)
{
if (!target)
return false;
Aura* existingThorns = botAI->GetAura("thorns", target, true);
if (!existingThorns)
return true;
target->RemoveOwnedAura(existingThorns, AURA_REMOVE_BY_CANCEL);
return true;
}
}
std::vector<NextAction> CastAbolishPoisonAction::getAlternatives()
{
return NextAction::merge({ NextAction("cure poison") },
@ -33,6 +49,21 @@ bool CastLifebloomOnMainTankAction::isUseful()
return !lifebloom || lifebloom->GetStackAmount() < 3 || lifebloom->GetDuration() < 2000;
}
bool CastThornsAction::Execute(Event event)
{
return PrepareThornsTarget(botAI, GetTarget()) && CastBuffSpellAction::Execute(event);
}
bool CastThornsOnPartyAction::Execute(Event event)
{
return PrepareThornsTarget(botAI, GetTarget()) && BuffOnPartyAction::Execute(event);
}
bool CastThornsOnMainTankAction::Execute(Event event)
{
return PrepareThornsTarget(botAI, GetTarget()) && BuffOnMainTankAction::Execute(event);
}
Value<Unit*>* CastEntanglingRootsCcAction::GetTargetValue()
{
return context->GetValue<Unit*>("cc target", "entangling roots");

View File

@ -114,18 +114,24 @@ class CastThornsAction : public CastBuffSpellAction
{
public:
CastThornsAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "thorns") {}
bool Execute(Event event) override;
};
class CastThornsOnPartyAction : public BuffOnPartyAction
{
public:
CastThornsOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "thorns") {}
bool Execute(Event event) override;
};
class CastThornsOnMainTankAction : public BuffOnMainTankAction
{
public:
CastThornsOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "thorns", false) {}
bool Execute(Event event) override;
};
class CastLifebloomOnMainTankAction : public BuffOnMainTankAction

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDAQ20TRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidAq20Triggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDAQ20STRATEGY_H
#define _PLAYERBOT_RAIDAQ20STRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidAq20Strategy : public Strategy

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDBWLTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidBwlTriggers.h"

View File

@ -2,8 +2,6 @@
#ifndef _PLAYERBOT_RAIDBWLSTRATEGY_H
#define _PLAYERBOT_RAIDBWLSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidBwlStrategy : public Strategy

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDEOETRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidEoETriggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDEOESTRATEGY_H
#define _PLAYERBOT_RAIDEOESTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidEoEStrategy : public Strategy

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDGRUULSLAIRTRIGGERCONTEXT_H
#include "RaidGruulsLairTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidGruulsLairTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDGRUULSLAIRSTRATEGY_H
#include "Strategy.h"
#include "Multiplier.h"
class RaidGruulsLairStrategy : public Strategy
{

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDICCTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidIccTriggers.h"

View File

@ -1,10 +1,7 @@
#ifndef _PLAYERBOT_RAIDICCSTRATEGY_H
#define _PLAYERBOT_RAIDICCSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
#include "RaidIccMultipliers.h"
class RaidIccStrategy : public Strategy
{

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDKARAZHANTRIGGERCONTEXT_H
#include "RaidKarazhanTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidKarazhanTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDKARAZHANSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidKarazhanStrategy : public Strategy
{

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDMAGTHERIDONTRIGGERCONTEXT_H
#include "RaidMagtheridonTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidMagtheridonTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDMAGTHERIDONSTRATEGY_H
#include "Strategy.h"
#include "Multiplier.h"
class RaidMagtheridonStrategy : public Strategy
{

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDMCTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "BossAuraTriggers.h"
#include "NamedObjectContext.h"
#include "RaidMcTriggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDMCSTRATEGY_H
#define _PLAYERBOT_RAIDMCSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidMcStrategy : public Strategy

View File

@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDNAXXTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidNaxxTriggers.h"

View File

@ -2,8 +2,6 @@
#ifndef _PLAYERBOT_RAIDNAXXSTRATEGY_H
#define _PLAYERBOT_RAIDNAXXSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidNaxxStrategy : public Strategy

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDOSTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidOsTriggers.h"

View File

@ -1,8 +1,6 @@
#ifndef _PLAYERBOT_RAIDOSSTRATEGY_H
#define _PLAYERBOT_RAIDOSSTRATEGY_H
#include "AiObjectContext.h"
#include "Multiplier.h"
#include "Strategy.h"
class RaidOsStrategy : public Strategy

View File

@ -99,8 +99,12 @@ bool RaidOnyxiaMoveToSafeZoneAction::Execute(Event /*event*/)
if (bot->IsWithinDist2d(bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->radius))
return false; // Already safe
// Stop current spell first
bot->AttackStop();
bot->InterruptNonMeleeSpells(false);
// bot->Yell("Moving to Safe Zone!", LANG_UNIVERSAL);
return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bot->GetPositionZ(),
return MoveTo(bot->GetMapId(), bestZone->pos.GetPositionX(), bestZone->pos.GetPositionY(), bestZone->pos.GetPositionZ(),
false, false, false, false, MovementPriority::MOVEMENT_COMBAT);
}

View File

@ -2,7 +2,6 @@
#ifndef _PLAYERBOT_RAIDONYXIAACTIONS_H_
#define _PLAYERBOT_RAIDONYXIAACTIONS_H_
#include "Action.h"
#include "AttackAction.h"
#include "GenericSpellActions.h"
#include "MovementActions.h"
@ -45,42 +44,45 @@ public:
bool Execute(Event event) override;
private:
std::vector<SafeZone> GetSafeZonesForBreath(uint32 spellId)
static std::vector<SafeZone> GetSafeZonesForBreath(uint32 spellId)
{
// Define your safe zone coordinates based on the map
// Example assumes Onyxia's lair map coordinates
float z = bot->GetPositionZ(); // Stay at current height
// Safe zone coordinates based on the map
// Assumes Onyxia's lair map coordinates
switch (spellId)
{
case 17086: // N to S
case 18351: // S to N
return {SafeZone{Position(-10.0f, -180.0f, z), 5.0f},
SafeZone{Position(-20.0f, -250.0f, z), 5.0f}}; // Bottom Safe Zone
return {
SafeZone{Position(-10.0f, -180.0f, -87.0f), 5.0f},
SafeZone{Position(-20.0f, -250.0f, -88.0f), 5.0f}
}; // Bottom Safe Zone
case 18576: // E to W
case 18609: // W to E
return {
SafeZone{Position(20.0f, -210.0f, z), 5.0f},
SafeZone{Position(-75.0f, -210.0f, z), 5.0f},
SafeZone{Position(20.0f, -210.0f, -85.5f), 5.0f},
SafeZone{Position(-75.0f, -210.0f, -83.4f), 5.0f},
}; // Left Safe Zone
case 18564: // SE to NW
case 18584: // NW to SE
return {
SafeZone{Position(-60.0f, -195.0f, z), 5.0f},
SafeZone{Position(10.0f, -240.0f, z), 5.0f},
SafeZone{Position(-60.0f, -195.0f, -85.0f), 5.0f},
SafeZone{Position(10.0f, -240.0f, -85.9f), 5.0f},
}; // NW Safe Zone
case 18596: // SW to NE
case 18617: // NE to SW
return {
SafeZone{Position(7.0f, -185.0f, z), 5.0f},
SafeZone{Position(-60.0f, -240.0f, z), 5.0f},
SafeZone{Position(7.0f, -185.0f, -86.2f), 5.0f},
SafeZone{Position(-60.0f, -240.0f, -85.2f), 5.0f},
}; // NE Safe Zone
default:
return {SafeZone{Position(0.0f, 0.0f, z), 5.0f}}; // Fallback center - shouldn't ever happen
return {
SafeZone{Position(-40.0f, -214.0f, -86.6f), 5.0f}
}; // Fallback center - shouldn't ever happen
}
}
};

View File

@ -1,7 +1,6 @@
#ifndef _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDONYXIATRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidOnyxiaTriggers.h"

View File

@ -7,7 +7,7 @@
#define _PLAYERBOT_RAIDSSCTRIGGERCONTEXT_H
#include "RaidSSCTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidSSCTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -7,7 +7,6 @@
#define _PLAYERBOT_RAIDSSCSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidSSCStrategy : public Strategy
{

View File

@ -2,7 +2,7 @@
#define _PLAYERBOT_RAIDTEMPESTKEEPTRIGGERCONTEXT_H
#include "RaidTempestKeepTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidTempestKeepTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -2,7 +2,6 @@
#define _PLAYERBOT_RAIDTEMPESTKEEPSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidTempestKeepStrategy : public Strategy
{

View File

@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDULDUARTRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
#include "RaidUlduarTriggers.h"
#include "BossAuraTriggers.h"

View File

@ -2,7 +2,6 @@
#ifndef _PLAYERBOT_RAIDULDUARSTRATEGY_H
#define _PLAYERBOT_RAIDULDUARSTRATEGY_H
#include "AiObjectContext.h"
#include "Strategy.h"
class RaidUlduarStrategy : public Strategy

View File

@ -6,7 +6,6 @@
#ifndef _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H
#define _PLAYERBOT_RAIDVOATRIGGERCONTEXT_H
#include "AiObjectContext.h"
#include "BossAuraTriggers.h"
#include "NamedObjectContext.h"
#include "RaidVoATriggers.h"

View File

@ -3,10 +3,6 @@
#define _PLAYERBOT_RAIDVOASTRATEGY_H
#include "Strategy.h"
#include "PlayerbotAI.h"
#include "string"
#include "Trigger.h"
#include "vector"
class RaidVoAStrategy : public Strategy
{

View File

@ -7,7 +7,7 @@
#define _PLAYERBOT_RAIDZULAMANTRIGGERCONTEXT_H
#include "RaidZulAmanTriggers.h"
#include "AiObjectContext.h"
#include "NamedObjectContext.h"
class RaidZulAmanTriggerContext : public NamedObjectContext<Trigger>
{

View File

@ -7,7 +7,6 @@
#define _PLAYERBOT_RAIDZULAMANSTRATEGY_H_
#include "Strategy.h"
#include "Multiplier.h"
class RaidZulAmanStrategy : public Strategy
{

View File

@ -3,6 +3,7 @@
#include <cmath>
#include <cstdlib>
#include "AreaDefines.h"
#include "BroadcastHelper.h"
#include "ChatHelper.h"
#include "G3D/Vector2.h"
@ -468,10 +469,14 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
data.inFlight = true;
return false;
}
Creature* flightMaster = ObjectAccessor::GetCreature(*bot, data.fromFlightMaster);
if (bot->GetDistance(data.flightMasterPos) > INTERACTION_DISTANCE)
return MoveFarTo(data.flightMasterPos);
Creature* flightMaster = bot->FindNearestCreature(data.flightMasterEntry, INTERACTION_DISTANCE * 3);
if (!flightMaster || !flightMaster->IsAlive())
{
botAI->rpgInfo.ChangeToIdle();
info.ChangeToIdle();
return true;
}
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
@ -487,7 +492,8 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
{
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
botAI->rpgInfo.ChangeToIdle();
info.ChangeToIdle();
return true;
}
return true;
}

View File

@ -1027,19 +1027,21 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
return dest;
}
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path)
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path)
{
flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
if (!flightMaster)
TravelMgr::FlightMasterInfo const* info = sTravelMgr.GetNearestFlightMasterInfo(bot);
if (!info)
return false;
std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
if (availablePaths.empty())
return false;
flightMasterEntry = info->templateEntry;
flightMasterPos = info->pos;
path = availablePaths[urand(0, availablePaths.size() - 1)];
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)",
bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size());
bot->GetName(), flightMasterEntry, path[0], path[path.size() - 1], availablePaths.size());
return true;
}
@ -1139,11 +1141,12 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
}
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 flightMasterEntry = 0;
WorldPosition flightMasterPos;
std::vector<uint32> path;
if (SelectRandomFlightTaxiNode(flightMaster, path))
if (SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path))
{
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
botAI->rpgInfo.ChangeToTravelFlight(flightMasterEntry, flightMasterPos, path);
return true;
}
return false;
@ -1220,9 +1223,10 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
}
case RPG_TRAVEL_FLIGHT:
{
ObjectGuid flightMaster;
uint32 flightMasterEntry = 0;
WorldPosition flightMasterPos;
std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, path);
return SelectRandomFlightTaxiNode(flightMasterEntry, flightMasterPos, path);
}
case RPG_OUTDOOR_PVP:
{

View File

@ -54,7 +54,7 @@ protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo>& poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);
static WorldPosition SelectRandomCampPos(Player* bot);
bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path);
bool SelectRandomFlightTaxiNode(uint32& flightMasterEntry, WorldPosition& flightMasterPos, std::vector<uint32>& path);
bool RandomChangeStatus(std::vector<NewRpgStatus> candidateStatus);
bool CheckRpgStatusAvailable(NewRpgStatus status);

View File

@ -37,11 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
data = do_quest;
}
void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path)
void NewRpgInfo::ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path)
{
startT = getMSTime();
TravelFlight flight;
flight.fromFlightMaster = fromFlightMaster;
flight.flightMasterEntry = flightMasterEntry;
flight.flightMasterPos = flightMasterPos;
flight.path = std::move(path);
flight.inFlight = false;
data = flight;
@ -157,7 +158,7 @@ std::string NewRpgInfo::ToString()
else if constexpr (std::is_same_v<T, TravelFlight>)
{
out << "TRAVEL_FLIGHT";
out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry();
out << "\nflightMasterEntry: " << arg.flightMasterEntry;
out << "\nfromNode: " << arg.path[0];
out << "\ntoNode: " << arg.path[arg.path.size() - 1];
out << "\ninFlight: " << arg.inFlight;

View File

@ -49,7 +49,8 @@ struct NewRpgInfo
// RPG_TRAVEL_FLIGHT
struct TravelFlight
{
ObjectGuid fromFlightMaster{};
uint32 flightMasterEntry{0};
WorldPosition flightMasterPos{};
std::vector<uint32> path;
bool inFlight{false};
};
@ -96,7 +97,7 @@ struct NewRpgInfo
void ChangeToWanderNpc();
void ChangeToWanderRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest);
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path);
void ChangeToTravelFlight(uint32 flightMasterEntry, WorldPosition flightMasterPos, std::vector<uint32> path);
void ChangeToOutdoorPvp(ObjectGuid::LowType capturePointSpawnId = 0);
void ChangeToRest();
void ChangeToIdle();

View File

@ -619,7 +619,7 @@ void RandomPlayerbotFactory::CreateRandomBots()
else
password = accountName;
AccountMgr::CreateAccount(accountName, password);
sAccountMgr->CreateAccount(accountName, password);
LOG_DEBUG("playerbots", "Account {} created for random bots", accountName.c_str());
}

View File

@ -54,9 +54,9 @@
#include "Unit.h"
#include "UpdateTime.h"
#include "Vehicle.h"
#include "../../../../src/server/scripts/Spells/spell_dk.cpp"
const int SPELL_TITAN_GRIP = 49152;
constexpr uint32 SPELL_TITAN_GRIP = 49152;
constexpr uint32 SPELL_DK_FROST_PRESENCE = 48263;
std::vector<std::string> PlayerbotAI::dispel_whitelist = {
"mutating injection",

View File

@ -2077,7 +2077,7 @@ void RandomPlayerbotMgr::Refresh(Player* bot)
bot->DurabilityRepairAll(false, 1.0f, false);
bot->SetFullHealth();
bot->SetPvP(true);
bot->SetPvP(sWorld->IsPvPRealm());
PlayerbotFactory factory(bot, bot->GetLevel());
factory.Refresh();
@ -2642,6 +2642,7 @@ void RandomPlayerbotMgr::OnPlayerLogin(Player* player)
{
// ObjectGuid::LowType guid = player->GetGUID().GetCounter(); //not used, conditional could be rewritten for
// simplicity. line marked for removal.
player->SetPvP(sWorld->IsPvPRealm());
}
else
{

View File

@ -8,6 +8,7 @@
#include <iomanip>
#include <numeric>
#include "AreaDefines.h"
#include "Creature.h"
#include "Log.h"
#include "ObjectAccessor.h"
@ -28,67 +29,60 @@
// Navigation data
enum class CityId : uint8
struct Capital
{
STORMWIND,
IRONFORGE,
DARNASSUS,
EXODAR,
ORGRIMMAR,
UNDERCITY,
THUNDER_BLUFF,
SILVERMOON_CITY,
SHATTRATH_CITY,
DALARAN
uint32 zoneId;
TeamId team;
char const* name;
std::vector<uint16> bankers;
};
static const std::unordered_map<uint16, std::pair<CityId, TeamId>> bankerToCity = {
{2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}},
{2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}},
{4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}},
{17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}},
{3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}},
{4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}},
{2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}},
{17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}},
{16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}},
{19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}},
{19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}},
{30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}},
{28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}}
static const std::vector<Capital> capitals = {
{ AREA_STORMWIND_CITY, TEAM_ALLIANCE, "Stormwind", {2455, 2456, 2457} },
{ AREA_IRONFORGE, TEAM_ALLIANCE, "Ironforge", {2460, 2461, 5099} },
{ AREA_DARNASSUS, TEAM_ALLIANCE, "Darnassus", {4155, 4208, 4209} },
{ AREA_THE_EXODAR, TEAM_ALLIANCE, "Exodar", {17773, 18350, 16710} },
{ AREA_ORGRIMMAR, TEAM_HORDE, "Orgrimmar", {3320, 3309, 3318} },
{ AREA_UNDERCITY, TEAM_HORDE, "Undercity", {4549, 2459, 2458, 4550} },
{ AREA_THUNDER_BLUFF, TEAM_HORDE, "Thunder Bluff", {2996, 8356, 8357} },
{ AREA_SILVERMOON_CITY, TEAM_HORDE, "Silvermoon", {17631, 17632, 17633, 16615, 16616, 16617} },
{ AREA_SHATTRATH_CITY, TEAM_NEUTRAL, "Shattrath", {19246, 19338, 19034, 19318} },
{ AREA_DALARAN, TEAM_NEUTRAL, "Dalaran", {30604, 30605, 30607, 28675, 28676, 28677, 29530} }
};
static const std::unordered_map<CityId, std::vector<uint16>> cityToBankers = {
{CityId::STORMWIND, {2455, 2456, 2457}},
{CityId::IRONFORGE, {2460, 2461, 5099}},
{CityId::DARNASSUS, {4155, 4208, 4209}},
{CityId::EXODAR, {17773, 18350, 16710}},
{CityId::ORGRIMMAR, {3320, 3309, 3318}},
{CityId::UNDERCITY, {4549, 2459, 2458, 4550}},
{CityId::THUNDER_BLUFF, {2996, 8356, 8357}},
{CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}},
{CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}},
{CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}}
};
static int GetCityWeight(CityId city)
static Capital const* FindCapitalByZone(uint32 zoneId)
{
int weight = 0;
switch (city)
for (Capital const& capital : capitals)
if (capital.zoneId == zoneId)
return &capital;
return nullptr;
}
static Capital const* FindCapitalByBanker(uint16 bankerEntry)
{
for (Capital const& capital : capitals)
for (uint16 bankerId : capital.bankers)
if (bankerId == bankerEntry)
return &capital;
return nullptr;
}
static int GetCityWeight(uint32 zoneId)
{
switch (zoneId)
{
case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break;
case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break;
case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break;
case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break;
case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break;
case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break;
case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break;
case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break;
case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break;
case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break;
default: weight = 0; break;
case AREA_STORMWIND_CITY: return sPlayerbotAIConfig.weightTeleToStormwind;
case AREA_IRONFORGE: return sPlayerbotAIConfig.weightTeleToIronforge;
case AREA_DARNASSUS: return sPlayerbotAIConfig.weightTeleToDarnassus;
case AREA_THE_EXODAR: return sPlayerbotAIConfig.weightTeleToExodar;
case AREA_ORGRIMMAR: return sPlayerbotAIConfig.weightTeleToOrgrimmar;
case AREA_UNDERCITY: return sPlayerbotAIConfig.weightTeleToUndercity;
case AREA_THUNDER_BLUFF: return sPlayerbotAIConfig.weightTeleToThunderBluff;
case AREA_SILVERMOON_CITY: return sPlayerbotAIConfig.weightTeleToSilvermoonCity;
case AREA_SHATTRATH_CITY: return sPlayerbotAIConfig.weightTeleToShattrathCity;
case AREA_DALARAN: return sPlayerbotAIConfig.weightTeleToDalaran;
}
return weight;
return 0;
}
WorldPosition::WorldPosition(std::string const str)
@ -4369,76 +4363,117 @@ void TravelMgr::Init()
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
}
Creature* TravelMgr::GetNearestFlightMaster(Player* bot)
TravelMgr::FlightMasterInfo const* TravelMgr::GetNearestFlightMasterInfo(Player* bot) const
{
std::map<uint32, WorldPosition>& flightMasterCache =
auto const& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
FlightMasterInfo const* nearest = nullptr;
float nearestDistance = std::numeric_limits<float>::max();
for (auto const& [entry, pos] : flightMasterCache)
for (auto const& [dbGuid, info] : flightMasterCache)
{
if (pos.GetMapId() != bot->GetMapId())
if (info.pos.GetMapId() != bot->GetMapId())
continue;
float distance = bot->GetExactDist2dSq(pos);
if (distance > nearestDistance)
continue;
Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
if (flightMaster)
float distance = bot->GetExactDist2dSq(info.pos);
if (distance < nearestDistance)
{
nearestDistance = distance;
nearestFlightMaster = flightMaster;
nearest = &info;
}
}
return nearestFlightMaster;
return nearest;
}
ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot)
std::vector<uint32> TravelMgr::GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode) const
{
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
if (!nearestFlightMaster)
return ObjectGuid::Empty;
return nearestFlightMaster->GetGUID();
auto const& cache = (team == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
std::unordered_set<uint32> seen;
std::vector<uint32> result;
for (auto const& [entry, info] : cache)
{
if (info.zoneId != zoneId || info.taxiNodeId == 0 || info.taxiNodeId == excludeNode)
continue;
if (seen.insert(info.taxiNodeId).second)
result.push_back(info.taxiNodeId);
}
return result;
}
std::vector<std::vector<uint32>> TravelMgr::GetOptimalFlightDestinations(Player* bot)
{
std::vector<std::vector<uint32>> validDestinations;
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f)
FlightMasterInfo const* nearestFlightMaster = GetNearestFlightMasterInfo(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster->pos) > 500.0f)
return validDestinations;
uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(),
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(),
bot->GetTeamId());
uint32 fromNode = nearestFlightMaster->taxiNodeId;
if (!fromNode)
return validDestinations;
std::vector<WorldLocation> candidateLocations;
if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
candidateLocations = GetCityLocations(bot);
TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
if (!startNode)
return validDestinations;
std::vector<WorldLocation> hubLocations = GetTravelHubs(bot);
candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end());
uint32 botLevel = bot->GetLevel();
for (auto const& loc : candidateLocations)
// Bots already in a capital shouldn't have another capital picked as a
// flight destination — that just shuffles them between cities.
bool botInCapital = false;
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(bot->GetZoneId()))
botInCapital = (area->flags & AREA_FLAG_CAPITAL) != 0;
//Simplify destination delection. Its either target cities (Based on config value) or target world.
std::vector<uint32> candidateZones;
if (botLevel >= 10 && !botInCapital && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
{
uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(),
loc.GetPositionZ(), loc.GetMapId(),
bot->GetTeamId());
if (!candidateNode)
TeamId botTeam = bot->GetTeamId();
for (Capital const& capital : capitals)
{
if (capital.team != TEAM_NEUTRAL && capital.team != botTeam)
continue;
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
if (!path.empty())
validDestinations.push_back(path);
candidateZones.push_back(capital.zoneId);
}
}
if (candidateZones.empty())
{
for (auto const& [zoneId, bracket] : zone2LevelBracket)
{
if (botLevel < bracket.low || botLevel > bracket.high)
continue;
if (GetFlightNodesInZone(zoneId, bot->GetTeamId(), fromNode).empty())
continue;
candidateZones.push_back(zoneId);
}
}
if (candidateZones.empty())
return validDestinations;
while (!candidateZones.empty())
{
uint32 zoneIndex = urand(0, candidateZones.size() - 1);
uint32 pickedZone = candidateZones[zoneIndex];
std::vector<uint32> usableNodes = GetFlightNodesInZone(pickedZone, bot->GetTeamId(), fromNode);
if (!usableNodes.empty())
{
uint32 pickedNode = usableNodes[urand(0, usableNodes.size() - 1)];
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, pickedNode);
if (!path.empty())
{
validDestinations.push_back(std::move(path));
return validDestinations;
}
}
candidateZones.erase(candidateZones.begin() + zoneIndex);
}
return validDestinations;
}
@ -4472,34 +4507,34 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations;
TeamId botTeamId = bot->GetTeamId();
std::unordered_set<CityId> validBankerCities;
std::unordered_set<uint32> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
{
auto cityIt = bankerToCity.find(loc.entry);
if (cityIt == bankerToCity.end())
Capital const* capital = FindCapitalByBanker(loc.entry);
if (!capital)
continue;
TeamId cityTeamId = cityIt->second.second;
TeamId cityTeamId = capital->team;
if (cityTeamId == botTeamId ||
(cityTeamId == TEAM_NEUTRAL)
)
validBankerCities.insert(cityIt->second.first);
validBankerCities.insert(capital->zoneId);
}
// Fallback if no valid cities
if (validBankerCities.empty())
return fallbackLocations;
// Apply weights to valid cities
std::vector<CityId> weightedCities;
for (CityId city : validBankerCities)
std::vector<uint32> weightedCities;
for (uint32 zoneId : validBankerCities)
{
int weight = GetCityWeight(city);
int weight = GetCityWeight(zoneId);
if (weight <= 0)
continue;
for (int i = 0; i < weight; ++i)
weightedCities.push_back(city);
weightedCities.push_back(zoneId);
}
// Fallback if no valid cities
@ -4507,9 +4542,11 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
return fallbackLocations;
// Pick a weighted city randomly, then a random banker in that city
CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
auto const& bankers = cityToBankers.at(selectedCity);
uint32 selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
Capital const* selectedCapital = FindCapitalByZone(selectedCity);
if (!selectedCapital)
return fallbackLocations;
auto const& bankers = selectedCapital->bankers;
uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
if (locIt != bankerEntryToLocation.end())
@ -4520,78 +4557,78 @@ std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
void TravelMgr::PrepareZone2LevelBracket()
{
// Classic WoW - Low - level zones
zone2LevelBracket[1] = {5, 12}; // Dun Morogh
zone2LevelBracket[12] = {5, 12}; // Elwynn Forest
zone2LevelBracket[14] = {5, 12}; // Durotar
zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades
zone2LevelBracket[141] = {5, 12}; // Teldrassil
zone2LevelBracket[215] = {5, 12}; // Mulgore
zone2LevelBracket[3430] = {5, 12}; // Eversong Woods
zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle
// Classic WoW - starter zones
zone2LevelBracket[AREA_DUN_MOROGH] = {5, 12};
zone2LevelBracket[AREA_ELWYNN_FOREST] = {5, 12};
zone2LevelBracket[AREA_DUROTAR] = {5, 12};
zone2LevelBracket[AREA_TIRISFAL_GLADES] = {5, 12};
zone2LevelBracket[AREA_TELDRASSIL] = {5, 12};
zone2LevelBracket[AREA_MULGORE] = {5, 12};
zone2LevelBracket[AREA_EVERSONG_WOODS] = {5, 12};
zone2LevelBracket[AREA_AZUREMYST_ISLE] = {5, 12};
// Classic WoW - Mid - level zones
zone2LevelBracket[17] = {10, 25}; // Barrens
zone2LevelBracket[38] = {10, 20}; // Loch Modan
zone2LevelBracket[40] = {10, 21}; // Westfall
zone2LevelBracket[130] = {10, 23}; // Silverpine Forest
zone2LevelBracket[148] = {10, 21}; // Darkshore
zone2LevelBracket[3433] = {10, 22}; // Ghostlands
zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle
// Classic WoW - low level zones
zone2LevelBracket[AREA_THE_BARRENS] = {10, 25};
zone2LevelBracket[AREA_LOCH_MODAN] = {10, 20};
zone2LevelBracket[AREA_WESTFALL] = {10, 21};
zone2LevelBracket[AREA_SILVERPINE_FOREST] = {10, 23};
zone2LevelBracket[AREA_DARKSHORE] = {10, 21};
zone2LevelBracket[AREA_GHOSTLANDS] = {10, 22};
zone2LevelBracket[AREA_BLOODMYST_ISLE] = {10, 21};
// Classic WoW - High - level zones
zone2LevelBracket[10] = {19, 33}; // Deadwind Pass
zone2LevelBracket[11] = {21, 30}; // Wetlands
zone2LevelBracket[44] = {16, 28}; // Redridge Mountains
zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills
zone2LevelBracket[331] = {18, 33}; // Ashenvale
zone2LevelBracket[400] = {24, 36}; // Thousand Needles
zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains
// Classic WoW - mid-level zones
zone2LevelBracket[AREA_DUSKWOOD] = {19, 33};
zone2LevelBracket[AREA_WETLANDS] = {21, 30};
zone2LevelBracket[AREA_REDRIDGE_MOUNTAINS] = {16, 28};
zone2LevelBracket[AREA_HILLSBRAD_FOOTHILLS] = {20, 34};
zone2LevelBracket[AREA_ASHENVALE] = {18, 33};
zone2LevelBracket[AREA_THOUSAND_NEEDLES] = {24, 36};
zone2LevelBracket[AREA_STONETALON_MOUNTAINS] = {16, 29};
// Classic WoW - Higher - level zones
zone2LevelBracket[3] = {36, 46}; // Badlands
zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows
zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh
zone2LevelBracket[16] = {45, 52}; // Azshara
zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale
zone2LevelBracket[45] = {30, 42}; // Arathi Highlands
zone2LevelBracket[47] = {42, 51}; // Hinterlands
zone2LevelBracket[51] = {45, 51}; // Searing Gorge
zone2LevelBracket[357] = {40, 52}; // Feralas
zone2LevelBracket[405] = {30, 41}; // Desolace
zone2LevelBracket[440] = {41, 52}; // Tanaris
// Classic WoW - 30-52 zones
zone2LevelBracket[AREA_BADLANDS] = {36, 46};
zone2LevelBracket[AREA_SWAMP_OF_SORROWS] = {36, 46};
zone2LevelBracket[AREA_DUSTWALLOW_MARSH] = {35, 46};
zone2LevelBracket[AREA_AZSHARA] = {45, 52};
zone2LevelBracket[AREA_STRANGLETHORN_VALE] = {32, 47};
zone2LevelBracket[AREA_ARATHI_HIGHLANDS] = {30, 42};
zone2LevelBracket[AREA_THE_HINTERLANDS] = {42, 51};
zone2LevelBracket[AREA_SEARING_GORGE] = {45, 51};
zone2LevelBracket[AREA_FERALAS] = {40, 52};
zone2LevelBracket[AREA_DESOLACE] = {30, 41};
zone2LevelBracket[AREA_TANARIS] = {41, 52};
// Classic WoW - Top - level zones
zone2LevelBracket[4] = {52, 57}; // Blasted Lands
zone2LevelBracket[28] = {50, 60}; // Western Plaguelands
zone2LevelBracket[46] = {51, 60}; // Burning Steppes
zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands
zone2LevelBracket[361] = {47, 57}; // Felwood
zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater
zone2LevelBracket[618] = {54, 61}; // Winterspring
zone2LevelBracket[1377] = {54, 63}; // Silithus
// Classic WoW - top level zones
zone2LevelBracket[AREA_BLASTED_LANDS] = {52, 57};
zone2LevelBracket[AREA_WESTERN_PLAGUELANDS] = {50, 60};
zone2LevelBracket[AREA_BURNING_STEPPES] = {51, 60};
zone2LevelBracket[AREA_EASTERN_PLAGUELANDS] = {54, 62};
zone2LevelBracket[361] = {47, 57}; // Felwood (no AREA_ define)
zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater (no AREA_ define)
zone2LevelBracket[AREA_WINTERSPRING] = {54, 61};
zone2LevelBracket[AREA_SILITHUS] = {54, 63};
// The Burning Crusade - Zones
zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula
zone2LevelBracket[3518] = {64, 70}; // Nagrand
zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest
zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley
zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh
zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains
zone2LevelBracket[3523] = {67, 73}; // Netherstorm
zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas
// The Burning Crusade zones
zone2LevelBracket[AREA_HELLFIRE_PENINSULA] = {58, 66};
zone2LevelBracket[AREA_NAGRAND] = {64, 70};
zone2LevelBracket[AREA_TEROKKAR_FOREST] = {62, 73};
zone2LevelBracket[AREA_SHADOWMOON_VALLEY] = {66, 73};
zone2LevelBracket[AREA_ZANGARMARSH] = {60, 67};
zone2LevelBracket[AREA_BLADES_EDGE_MOUNTAINS] = {64, 73};
zone2LevelBracket[AREA_NETHERSTORM] = {67, 73};
zone2LevelBracket[AREA_ISLE_OF_QUEL_DANAS] = {68, 73};
// Wrath of the Lich King - Zones
zone2LevelBracket[65] = {71, 77}; // Dragonblight
zone2LevelBracket[66] = {74, 80}; // Zul'Drak
zone2LevelBracket[67] = {77, 80}; // Storm Peaks
zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier
zone2LevelBracket[394] = {72, 78}; // Grizzly Hills
zone2LevelBracket[495] = {68, 74}; // Howling Fjord
zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest
zone2LevelBracket[3537] = {68, 75}; // Borean Tundra
zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin
zone2LevelBracket[4197] = {79, 80}; // Wintergrasp
// Wrath of the Lich King zones
zone2LevelBracket[AREA_DRAGONBLIGHT] = {71, 77};
zone2LevelBracket[AREA_ZUL_DRAK] = {74, 80};
zone2LevelBracket[AREA_THE_STORM_PEAKS] = {77, 80};
zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier (no AREA_ define)
zone2LevelBracket[AREA_GRIZZLY_HILLS] = {72, 78};
zone2LevelBracket[AREA_HOWLING_FJORD] = {68, 74};
zone2LevelBracket[AREA_CRYSTALSONG_FOREST] = {77, 80};
zone2LevelBracket[AREA_BOREAN_TUNDRA] = {68, 75};
zone2LevelBracket[AREA_SHOLAZAR_BASIN] = {75, 80};
zone2LevelBracket[AREA_WINTERGRASP] = {79, 80};
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
@ -4650,13 +4687,15 @@ void TravelMgr::PrepareDestinationCache()
(creatureTemplate->unit_flags & 4096) == 0 &&
creatureTemplate->rank == 0)
{
uint32 roundX = (x / 50.0f) * 10.0f;
uint32 roundY = (y / 50.0f) * 10.0f;
uint32 roundZ = (z / 50.0f) * 10.0f;
uint32 roundX = static_cast<uint32>(std::round(x / 50.0f));
uint32 roundY = static_cast<uint32>(std::round(y / 50.0f));
uint32 roundZ = static_cast<uint32>(std::round(z / 50.0f));
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
}
// FLIGHT MASTERS
// Entry 29480 is Grimwing (Storm Peaks)
// Entry 3838 is Vesprystus in Rut'Theran. Need Travel Node system to resolve this one.
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
@ -4669,23 +4708,39 @@ void TravelMgr::PrepareDestinationCache()
{
WorldPosition pos(mapId, x, y, z, orient);
if (forHorde)
hordeFlightMasterCache[guid] = pos;
{
FlightMasterInfo info;
info.pos = pos;
info.zoneId = areaId;
info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_HORDE);
info.templateEntry = templateEntry;
info.dbGuid = guid;
hordeFlightMasterCache[guid] = info;
}
if (forAlliance)
allianceFlightMasterCache[guid] = pos;
{
FlightMasterInfo info;
info.pos = pos;
info.zoneId = areaId;
info.taxiNodeId = sObjectMgr->GetNearestTaxiNode(x, y, z, mapId, TEAM_ALLIANCE);
info.templateEntry = templateEntry;
info.dbGuid = guid;
allianceFlightMasterCache[guid] = info;
}
flightMastersCount++;
// Zones that have flight masters but no innkeepers — use flight master as hub
static const std::set<uint32> zonesWithoutInnkeeper = {
4, // Blasted Lands (52-57)
16, // Azshara (45-52)
28, // Western Plaguelands (50-60)
46, // Burning Steppes (51-60)
51, // Searing Gorge (45-51)
AREA_BLASTED_LANDS,
AREA_AZSHARA,
AREA_WESTERN_PLAGUELANDS,
AREA_BURNING_STEPPES,
AREA_SEARING_GORGE,
361, // Felwood (47-57)
490, // Un'Goro Crater (49-56)
2817, // Crystalsong Forest (77-80)
4197 // Wintergrasp (79-80)
AREA_CRYSTALSONG_FOREST,
AREA_WINTERGRASP
};
if (zonesWithoutInnkeeper.count(areaId))
{
@ -4756,7 +4811,7 @@ void TravelMgr::PrepareDestinationCache()
// Process temporary caches
for (auto const& [gridTuple, creatureDataList] : tempLocsCache)
{
if (creatureDataList.size() > 2)
if (creatureDataList.size() >= 2)
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;

View File

@ -846,6 +846,21 @@ protected:
class TravelMgr
{
public:
struct NpcLocation
{
WorldLocation loc;
uint32 entry;
};
struct FlightMasterInfo
{
WorldPosition pos;
uint32 zoneId; // resolved once at cache load
uint32 taxiNodeId; // DBC taxi node nearest to this flight master
uint32 templateEntry; // creature template ID (for ObjectGuid construction)
uint32 dbGuid; // DB spawn GUID (for ObjectGuid construction)
};
static TravelMgr& instance()
{
static TravelMgr instance;
@ -858,12 +873,14 @@ public:
// Navigation
void Init();
Creature* GetNearestFlightMaster(Player* bot);
ObjectGuid GetNearestFlightMasterGuid(Player* bot);
FlightMasterInfo const* GetNearestFlightMasterInfo(Player* bot) const;
std::vector<std::vector<uint32>> GetOptimalFlightDestinations(Player* bot);
const std::vector<WorldLocation> GetTeleportLocations(Player* bot);
const std::vector<WorldLocation> GetTravelHubs(Player* bot);
std::vector<WorldLocation> GetCityLocations(Player* bot);
std::vector<uint32> GetFlightNodesInZone(uint32 zoneId, TeamId team, uint32 excludeNode = 0) const;
bool SelectAuctioneerByMap(Player* bot, NpcLocation& outAuctioneer);
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
template <class D, class W, class URBG>
@ -975,8 +992,8 @@ private:
};
// Navigation caches
std::map<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
std::map<uint32, FlightMasterInfo> allianceFlightMasterCache;
std::map<uint32, FlightMasterInfo> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;

View File

@ -2467,7 +2467,7 @@ std::vector<uint32> TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode)
TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode);
if (!startNode || !endNode || startNode->map_id != endNode->map_id)
if (!startNode || !endNode)
return {};
auto cacheItr = taxiPathCache.find(fromNode);