Compare commits

...

18 Commits

Author SHA1 Message Date
Keleborn
c8dce882d6
Merge pull request #2225 from mod-playerbots/test-staging
Test staging
2026-03-27 08:39:03 -07:00
kadeshar
f00fe15ff1
PR template checkboxes displaying fix (#2232)
Maintenance PR
2026-03-22 22:31:24 -07:00
kadeshar
d0d1171e06
Fixed typo (#2230)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

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

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

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

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

## 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.
-->
- compile test-staging with 20260320-ac-merge


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



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



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



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

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

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



## Final Checklist

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

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-03-22 15:22:03 +01:00
Keleborn
9f875a7c81
CoreUpdate - ThreatMgr (#2228)
<!--
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 -->
Modification to threat system required for current core update PR. 



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

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



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



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



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



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



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

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

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


## Final Checklist

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

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-03-21 23:41:07 +01:00
dillyns
32af1b95de
Paladin Seal of wisdom fallback fix for Ret/Prot Paladins (#2147)
# Pull Request

This PR removes the fallback for Seal of Wisdom action from generic
Paladin strategy and moves it to Holy Paladin only. This is necessary
because a paladin who does not have Seal of Wisdom yet who goes low mana
and triggers the seal of wisdom action will end up falling back to Seal
of Righteousness, even though a better seal may be available, such as
Seal of Command.
The fallback is added to Holy Paladin so that low level holy paladins
still use Righteousness until they get Wisdom

---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended
behavior?
Ret paladin without Seal of Wisdom shouldn't change seals on low mana.
- Describe the **cheapest implementation** that produces an acceptable
result?
Cheapest implementation is to remove seal of wisdom fallback for
non-holy paladins.
- Describe the **runtime cost** when this logic executes across many
bots?
No difference in cost compared to existing logic.
---

## How to Test the Changes

Use a ret paladin bot who has Seal of Command but who does not have Seal
of Wisdom. A paladin under level 38 will do.
Order them to attack something, like a test dummy, until they eventually
run low on mana.

Before this change: The paladin will switch to Seal of Righteousness
when they get low mana.
After this change: The paladin leaves Seal of Command on when they get
low mana.

## Complexity & Impact

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

Does this change increase per-bot or per-tick processing?
- - [x] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [x] No
- - [ ] Yes (**explain why**)
---

## Defaults & Configuration

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

If this introduces more advanced or AI-heavy logic:
- - [x] Lightweight mode remains the default
- - [ ] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [x] No
- - [ ] Yes (**explain below**)

If yes, please specify:

- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.)
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation)
- Which parts of the change were influenced or generated
- Whether the result was manually reviewed and adapted

AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
Any AI-influenced changes must be verified against existing CORE and PB
logic. We expect contributors to be honest
about what they do and do not understand.

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.
2026-03-21 15:19:22 -07:00
Keleborn
2b273f6a2c
Fix merge error in test staging (#2226)
<!--
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.
-->

Fix merge error we missed due to core sync issues. 

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



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



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

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

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



## Final Checklist

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

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-03-21 08:23:07 +01:00
kadeshar
98395a1090
Added cancellation druid form actions (#2194)
# Pull Request

Added new (for now manual) actions to cancel druid forms. 
Resolve: https://github.com/mod-playerbots/mod-playerbots/issues/1788

---

## Feature Evaluation

Please answer the following:

- order bot enter some form like `do travel form`
- order bot cancel form like `do cancel travel form`

---

## How to Test the Changes

- order bot enter some form like `do travel form`
- order bot cancel form like `do cancel travel form`

## Complexity & Impact

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

Does this change increase per-bot or per-tick processing?
- - [x] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [x] No
- - [ ] Yes (**explain why**)
---

## Defaults & Configuration

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

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [x] No
- - [ ] Yes (**explain below**)

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---
2026-03-20 20:42:22 +01:00
oskov
f160420d70
Fix/talent tree ordered map (#2222)
Fixes #2050

InitTalents builds a map of talentRow → [TalentEntry*] and iterates it
to teach talents row by row. WoW's talent system requires each row to be
filled before unlocking the next, so iteration must happen in ascending
row order. Commit
b474dc4 ("Performance optim") changed the container from std::map to
std::unordered_map, which has no guaranteed key ordering. As a result,
bots would frequently attempt to learn talents in a row whose
prerequisites hadn't been met
  yet, silently skipping them. I belive it's the reason of #2050 issue.

The fix is a one-character type change: restoring std::map<uint32, ...>,
which guarantees ascending key (row) order.

  How to Test the Changes

   1. Make fresh installation
   2. Create new character
   3. Observe talents tree of fresh rnd bots
  

  Was AI assistance used while working on this change?

- [X] Yes — GitHub Copilot CLI was used to identify the root cause
(unordered_map introduced in b474dc4 breaking talent row ordering),
stage the one-line fix, and draft this PR description. The code change
was reviewed and fully
  understood before submission.

Root cause commit: b474dc44bb6323430a84fc17c1ec046f9919a101
("Performance optim") — changed std::map to std::unordered_map in
InitTalents, breaking the row-ordering guarantee that WoW's talent
prerequisite system depends on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 20:42:07 +01:00
Crow
d5762b7e0f
Remove Vertical Speed Limit from Knockback Packet (#2223)
<!--
Thank you for contributing to mod-playerbots, please make sure that
you...
1. Submit your PR to the test-staging branch, not master.
2. Read the guidelines below before submitting.
3. Don't delete parts of this template.

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

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

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

## Pull Request Description
This PR removes the break from SMSG_MOVE_KNOCK_BACK for knockbacks with
vertical speed of >35.0f. This break is the reason for many vertical
knockbacks having no effect on bots, including Shade of Aran's Flame
Wreath, High Astromancer Solarian's Wrath of the Astromancer, and
Archimonde's Air Burst. There is a comment that indicates that the limit
was originally added due to bots getting stuck from high-speed vertical
knockbacks. I have not observed this at all and have been playing with
this break removed for several months.

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

I honestly cannot say if there is impact on processing cost because I
have no understanding of packets. I would be surprised if there are any
performance issues since knockback packets are ordinarily getting sent
all the time, it's just a small number of moves that get skipped due to
this break.

## 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. .go creature name High Astromancer Solarian
2. Start combat and wait until a bot gets hit with Wrath of the
Astromancer
3. Wait for the aura to expire and watch the bot fly to Mars and fall
back down

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

I do not know for sure, but as noted above, I would be surprised if
there was any notable performance impact.

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



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



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

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

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



## Final Checklist

- [x] Stability is not compromised.
- [ ] Performance impact is understood, tested, and acceptable. <- I
can't say for sure, but I've not had any issues. I would appreciate
getting thoughts from somebody knowledgeable about packet use, however.
- [x] Added logic complexity is justified and explained.
- [x] Documentation updated if needed (Conf comments, WiKi commands).

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

---------

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-03-20 20:41:47 +01:00
kadeshar
c6a07ad012
Every Man for Himself racial support (#2198)
# Pull Request

Added Every Man for Himself racial support
Partially resolves:
https://github.com/mod-playerbots/mod-playerbots/issues/2002

---

## How to Test the Changes

- when human bot is in combat apply aura via command `.aura 20066`
- bot should use "Every Man for Himself" to remove aura

## Complexity & Impact

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

Does this change increase per-bot or per-tick processing?
- - [x] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [x] No
- - [ ] Yes (**explain why**)
---

## Defaults & Configuration

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

Human bots now using "Every Man for Himself" by default where in combat

If this introduces more advanced or AI-heavy logic:
- - [x] Lightweight mode remains the default
- - [x] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [ ] No
- - [x] Yes (**explain below**)

Copilot CLI to review changes

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Test result:
<img width="358" height="97" alt="obraz"
src="https://github.com/user-attachments/assets/66044a93-d73b-4706-ae2f-ea8ae6e25438"
/>
2026-03-20 20:41:22 +01:00
Crow
cba6af27ad
Fix Assassination Rogue Finishers and add Cold Blood (#2215)
<!--
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 -->

**Note for reviewers**: The Rogue files are very confusing, so for
background, there is DpsRogueStrategy, which is for all Rogues and
represented by the “dps” strategy in game, and there is also
AssassinationRogueStrategy, which is for Assassination and Subtlety
specs and represented by the “melee” strategy in game. So Combat has
only the dps strategy, while Assassination and Subtlety have the dps and
melee strategies.
- The main focus of this PR is to fix an issue with Assassination Rogues
that caused them to use Eviscerate instead of Envenom about 1/3 of the
time they should have been using Envenom, which was significantly
reducing their DPS. See the bottom of this post for an explanation for
why this was happening and why the fix works. Well, LMK if you think
it's wrong, but this is how I am understanding things, and my
back-of-the-envelope math (also below) supports it.

- After this PR, Assassination Rogues will use Eviscerate only if they
are unable to use Envenom (don't have the ability learned or no Deadly
Poison on the target) or if they don’t have Rank 3 in Master Poisoner.

- Additionally, Assassination Rogues previously would use
Envenom/Eviscerate at 3 or more combo points. This is suboptimal so I
created a new “combo points 4 available” trigger that will fire at 4 or
5 combo points only. They will still use the finisher at 3 combo points
if the mob is almost dead (via the existing “target with combo points
almost dead” trigger).

- I then added Cold Blood, which Rogues previously would not use at all.
Now there is a ColdBloodAction(), and Cold Blood is used when a Rogue
has at least 4 combo points, right before using Envenom (or Eviscerate).
I implemented it as a standard BuffTrigger so they’ll just use the
ability off cooldown.

- While looking at the combo point triggers, I thought it was confusing
that the “combo points available” trigger actually meant 5 combo points
(presumably because the default parameter for combo points in
ComboPointsAvailableTrigger() is 5). I changed the string to “combo
points 5 available” so it’s less confusing going forward. This
necessitated some changes in the Druid files too.

- Next, I cleaned up DpsRogueStrategy a bit. Not a lot to say, just some
duplicative or useless logic was removed. There shouldn’t be any impact
on gameplay from the changes.

- In the process of making the edits in the Druid files, I noticed that
the trigger for Tiger’s Fury in OffhealDruidCatStrategy was “low
energy,” which does not exist (there is a “light energy available,” but
the EnergyAvailable triggers are for when energy is AT LEAST the
designated level, not AT MOST the designated level). So I replaced the
trigger with the already-existing “tiger’s fury” trigger, which I think
is just a generic BuffTrigger so I don’t actually know why it exists
(i.e., Druid will use the spell off cooldown). But this particular
change is just a quick fix and not intended to be thoughtful (that would
be outside the scope of this PR).


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

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

There should be no relevant impact on performance. This PR adds one new
action triggered by the standard BuffTrigger. Otherwise, these are just
fixes to existing logic.

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

The easiest way to test is to fight a boss that doesn't tend to result
in downtime (since downtime can lead to the loss of deadly poison
stacks, in which case Eviscerate will (and should be) used by
Assassination Rogues). You can use a damage meter such as Skada to track
ability use. You should see:
- Assassination Rogues don't use Eviscerate at all, or very few times.
- Assassination Rogues use Cold Blood.
- Offheal Cat Druids use Tiger's Fury.
- Otherwise, Rogue and Cat Druid behavior should remain the same.


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


Default behavior for Assassination Rogues was broken, as explained
above.


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



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

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

## AI Assistance
<!--
AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
We expect contributors to be honest about what they do and do not
understand.
-->
Was AI assistance used while working on this change?
- [ ] 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 Claude help me diagnose the initial issue and help me understand
the queue system. And I had it implement the changes that were just
busywork (like combo point triggers).


## Final Checklist

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

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

The reason for why Assassination Rogues were using Eviscerate so
frequently is due to the fact that Envenom and Eviscerate were part of
the same TriggerNode. When actions are part of the same TriggerNode,
they're processed together, and the actions are queued by priority. When
the higher-priority action is executed, the lower-priority action is not
cleared--it remains in the queue for expireActionTime from the config,
which is 5 seconds by default. Then, as soon as the lower-priority
action can be executed (without regard for triggers because it is
already triggered, just sitting in the queue), it will execute.

This pattern of code works fine ingame if (1) you are actually trying to
queue actions, like what I did with Cold Blood -> Envenom, (2) there are
other guards like IsUseful() and IsPossible() that keep unwanted actions
from executing, or (3) the trigger is just constantly firing so the
higher-priority action is always evaluated. But TriggerNode isn't really
the right way to implement action priority--that's through ActionNode.
AssassinationRogueStrategy had Envenom and Eviscerate in the same
TriggerNode, and then the corresponding ActionNode had Rupture as a
fallback. Now, I changed it so Eviscerate is instead a fallback in the
Envenom ActionNode (and Rupture is removed entirely because
Assassination Rogues just shouldn't be using it, except maybe on very
high-armor targets that are immune to poison, but that is very niche).

~

I did some back-of-the-envelope math to check this pattern. Say we're in
a situation where Deadly Poison is up so ideally the Rogue should use
Envenom 100% of the time. Through the old system, what would happen when
the trigger fired?
- Rogue uses Envenom since it's the higher-priority action.
- Due to the Ruthlessness talent, Rogue has a 60% chance of having 1
combo point after the finisher, 40% chance of 0 combo points. If it has
1 combo point, it uses Eviscerate immediately.
- If it has 0 combo points, it uses Mutilate. Mutilate grants 2 combo
points, unless it crits, in which case it grants 3 due to Seal Fate. If
Mutilate doesn't crit, the Rogue has 2 combo points, and it uses
Eviscerate. If Mutilate does crit, the Rogue has 3 combo points, and it
uses Envenom.
- So let's assume Mutilate has a 55% crit chance (very reasonable for a
Rogue in entry-level raid gear with raid buffs due to Opportunity giving
+20% crit chance to Mutilate). Mutilate hits twice, and if either hit
crits, Seal Fate Procs. The chance of at least one crit with two hits at
a 55% crit chance is ~80%. That means if Ruthlessness doesn't give a
combo point, there is an 80% chance that Envenom will be used and a 20%
chance that Eviscerate will be used.
- Combine the above, and the result of one trigger firing is you get 1
guaranteed Envenom + 0.6 Eviscerates (Ruthlessness proc path) + 0.32
Envenoms (No Ruthlessness proc but Seal Fate proc path) + 0.08
Eviscerates (No Ruthlessness proc and no Seal Fate proc path) = 1.32
Envenoms to each 0.68 Eviscerates, or a 1.94:1 ratio of Envenoms to
Eviscerates. That is basically identical to what I saw in practice of
roughly a 2:1 ratio of Envenoms to Eviscerates.
- I understand the above is simplistic and it assumes that the Rogue
gets a combo point within 5 seconds following using Envenom (very
likely) and that there are not two opportunities to use Envenom or
Eviscerate in the 5-second queue period after using Envenom (it can
happen but is uncommon). That's all at the margins and isn't going to
impact the math very much.

---------

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-03-20 20:40:59 +01:00
Keleborn
957eca0263
Feat. Enable multi node flying, and refactor into travel manager (#2156)
# Pull Request

Feature - Enable multi node flying for bots
- Bots currently only do node to node flying. This PR makes it so they
can connect multiple noted.
-- This is enabled by sending a vector containing the node sequence
instead of a single destination node
-- To minimize the run-time cost of searching for available nodes and
connection, a cache of all possible connections is prepared at start up
using a BFS search algorithm.

Refactor 
- Move all world destination logic (cities, banks, inns) to existing
Travel manager
- Eliminate flightmastercache and integrate to new manager
- replace SQLs calls with in-memory data search by core
- Add in new map that stores creature areas by template. 

Clean up
- Move other rpg files to related folder. 
(Next steps) The selection for where bots fly to should be smarter than
it is. Instead of trying to determine where a bot can go, it should
first decide where it should go, and then identify the correct way to
get there.
---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended
behavior?
- Describe the **cheapest implementation** that produces an acceptable
result?
- Describe the **runtime 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, bots, specific
configuration)
- Expected behavior and how to verify it

## Complexity & Impact

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

Does this change increase per-bot or per-tick processing?
- - [x] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [x] No
- - [ ] Yes (**explain why**)
The call itself is fairly infrequent, and although now there are a
greater number of paths available for the bots, I dont think it would be
significant.

## Defaults & Configuration

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

If this introduces more advanced or AI-heavy logic:
- - [x] Lightweight mode remains the default
- - [ ] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [ ] No
- - [x] Yes (**explain below**)
Gemini first suggested the use of a BFS algorithm. This was rewritten by
me to actually work as intended. Verification by additional logging not
present in final code.

Claude code converted the SQL filtering to the atrocious if statements
found in PrepareDestinationCache, but after verifying them it works. If
there are better ways to do this Im open to it.

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.
2026-03-20 20:39:53 +01:00
NoxMax
5c63aacd60
Drop server initialization time message. Show number of bots set to login. (#2209)
<!--
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 server initialization time on login is neither relevant to the
typical user, nor is it accurate. It simply takes `maxRandomBots`, does
some arbitrary multiplications and divisions on it, and declare that as
the time it takes the server to load. It does not take into account any
other of your server configurations nor your server capabilities.
Here we exchange that message with one more relevant to the user,
telling them the number of logged in bot (or set to be logged in with
DisabledWithoutRealPlayer enabled). But honestly, even removing that
whole snippet is a better idea than keeping the misleading message.

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

Alternatively the whole snippet can be removed.

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

Login and see the welcome messages.

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



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



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



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

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

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



## Final Checklist

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

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

---------

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-03-20 20:39:24 +01:00
Aldori
4877dcc573
fix: ByteBufferException error (opcode: 149) (#2206)
Fixes #2204 

## Pull Request Description
Fixes an opcode 149 ByteBufferException when Questie-335 (or other
addons that send addon messages) is used in a party with Playerbots.

The issue was caused by addon-language packets reaching parsing logic
they should not have reached. This change adjusts the early return for
`LANG_ADDON` packets before further handling.

## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Moved the early return for `LANG_ADDON` packets in the outgoing packet
handler.

- Describe the **processing cost** when this logic executes across many
bots.
  - Negligible. It's a simple conditional check with an early return.

## How to Test the Changes
1. Install and enable Questie-335.
2. Invite at least 1 Playerbot to a party.
3. Accept a quest, abandon a quest, or progress a quest objective such
as kill credit or looting a quest item.
4. Verify the worldserver no longer logs opcode 149 ByteBufferException
errors.

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

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

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

## Messages to Translate
Does this change add bot messages to translate?
- [x] No
- [ ] Yes (**list messages in the table**)

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

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

## Final Checklist

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

## Notes for Reviewers
This is a small fix intended only to prevent addon language packets from
reaching incompatible packet parsing logic.

---------

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-03-20 20:38:57 +01:00
Keleborn
4c0cb30f0b
New readme (#2202)
# Pull Request

Add references to wiki with me detailed installation information, and
troubleshooting page based on frequently observed issues in support.

Also included are

https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide
https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting

---------

Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
2026-03-20 20:38:27 +01:00
Crow
35a0282ca6
Add Sense Undead for Paladins (#2200)
# Pull Request

This PR adds the sense undead ability for Paladins, which they will keep
active at all times. This is mildly useful because the associated minor
glyph provides a 1% damage increase against undead while the ability is
active.

Sense undead is also added to InitClassSpells(). I understand that it is
a trainer spell so would normally be covered by InitAvailableSpells(),
but those playing with mod-individual-progression will not receive the
spell through InitAvailableSpells() because it is removed from trainers
by the mod (in TBC, a quest was required to obtain the spell).

Finally, the minor glyph of sense undead is now added to the config as a
default glyph for all PvE specs. It is not added for PvP specs because
Forsaken do not count as undead so the glyph is useless in PvP. I also
made some other tweaks to Paladin default minor glyphs that are not
worth spending any time talking about.

Edit: I also did some minor reformatting of code and replaced some
numbers with existing constants.

---

## Design Philosophy

We prioritize **stability, performance, and predictability** over
behavioral realism.
Complex player-mimicking logic is intentionally limited due to its
negative impact on scalability, maintainability, and
long-term robustness.

Excessive processing overhead can lead to server hiccups, increased CPU
usage, and degraded performance for all
participants. Because every action and
decision tree is executed **per bot and per trigger**, even small
increases in logic complexity can scale poorly and
negatively affect both players and
world (random) bots. Bots are not expected to behave perfectly, and
perfect simulation of human decision-making is not a
project goal. Increased behavioral
realism often introduces disproportionate cost, reduced predictability,
and significantly higher maintenance overhead.

Every additional branch of logic increases long-term responsibility. All
decision paths must be tested, validated, and
maintained continuously as the system evolves.
If advanced or AI-intensive behavior is introduced, the **default
configuration must remain the lightweight decision
model**. More complex behavior should only be
available as an **explicit opt-in option**, clearly documented as having
a measurable performance cost.

Principles:

- **Stability before intelligence**  
  A stable system is always preferred over a smarter one.

- **Performance is a shared resource**  
  Any increase in bot cost affects all players and all bots.

- **Simple logic scales better than smart logic**  
Predictable behavior under load is more valuable than perfect decisions.

- **Complexity must justify itself**  
  If a feature cannot clearly explain its cost, it should not exist.

- **Defaults must be cheap**  
  Expensive behavior must always be optional and clearly communicated.

- **Bots should look reasonable, not perfect**  
  The goal is believable behavior, not human simulation.

Before submitting, confirm that this change aligns with those
principles.

---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended
behavior?
- Describe the **cheapest implementation** that produces an acceptable
result?
- Describe the **runtime cost** when this logic executes across many
bots?

The implementation just checks if a Paladin has the sense undead aura,
and if not, the Paladin will activate sense undead. It is simple and
cheap.

---

## How to Test the Changes

- Step-by-step instructions to test the change
- Any required setup (e.g. multiple players, bots, specific
configuration)
- Expected behavior and how to verify it

## Complexity & Impact

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

Does this change increase per-bot or per-tick processing?
- - [ ] No
- - [x] Yes (**describe and justify impact**)

Infinitesimally 

Could this logic scale poorly under load?
- - [x] No
- - [ ] Yes (**explain why**)

## Defaults & Configuration

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

Paladin bots will by default have sense undead enabled. There is no
disadvantage to this.

If this introduces more advanced or AI-heavy logic:
- - [x] Lightweight mode remains the default
- - [ ] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [x] No
- - [ ] Yes (**explain below**)

If yes, please specify:

- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.)
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation)
- Which parts of the change were influenced or generated
- Whether the result was manually reviewed and adapted

AI assistance is allowed, but all submitted code must be fully
understood, reviewed, and owned by the contributor.
Any AI-influenced changes must be verified against existing CORE and PB
logic. We expect contributors to be honest
about what they do and do not understand.

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.

---------

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-03-20 20:38:06 +01:00
XYUU
473b2ab5c6
Fix: WLK shaman totem quest vs relic totems: avoid keeping 4 totem items when relic exists #2119 (#2197)
## Summary
* Detects shaman relics (relic type, totem subclass) in bags/equipment.
* Skips adding the four classic totem items (5175–5178) when a relic
exists.
* Cleans up any existing totem items from bags/equipment/bank when a
relic exists, while keeping Ankh handling intact.
## Test plan
Verified manually (local environment).

---------

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>
Co-authored-by: github-actions <github-actions@users.noreply.github.com>
2026-03-20 20:37:44 +01:00
Keleborn
2ce8993986
Correct Loot rolling behavior (#2190)
# Pull Request

This fixes the loot rolling behavior issue created by #2068 . 
Introduce the ability for enchanter bots to disenchant items they dont
need, and roll need on recipes they also need.
Make it so ITEM_USAGE_AH ensures the item is not BOP.
Try to reduce the call for item_usage in CalculateRollVote by passing
usage if available.

---

## Design Philosophy

We prioritize **stability, performance, and predictability** over
behavioral realism.
Complex player-mimicking logic is intentionally limited due to its
negative impact on scalability, maintainability, and
long-term robustness.

Excessive processing overhead can lead to server hiccups, increased CPU
usage, and degraded performance for all
participants. Because every action and
decision tree is executed **per bot and per trigger**, even small
increases in logic complexity can scale poorly and
negatively affect both players and
world (random) bots. Bots are not expected to behave perfectly, and
perfect simulation of human decision-making is not a
project goal. Increased behavioral
realism often introduces disproportionate cost, reduced predictability,
and significantly higher maintenance overhead.

Every additional branch of logic increases long-term responsibility. All
decision paths must be tested, validated, and
maintained continuously as the system evolves.
If advanced or AI-intensive behavior is introduced, the **default
configuration must remain the lightweight decision
model**. More complex behavior should only be
available as an **explicit opt-in option**, clearly documented as having
a measurable performance cost.

Principles:

- **Stability before intelligence**  
  A stable system is always preferred over a smarter one.

- **Performance is a shared resource**  
  Any increase in bot cost affects all players and all bots.

- **Simple logic scales better than smart logic**  
Predictable behavior under load is more valuable than perfect decisions.

- **Complexity must justify itself**  
  If a feature cannot clearly explain its cost, it should not exist.

- **Defaults must be cheap**  
  Expensive behavior must always be optional and clearly communicated.

- **Bots should look reasonable, not perfect**  
  The goal is believable behavior, not human simulation.

Before submitting, confirm that this change aligns with those
principles.

---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended
behavior?
-- Add a new check that downgrades greed rolls to desired levels, or
bools for the other two options.
- Describe the **cheapest implementation** that produces an acceptable
result?
-- As implemented.
- Describe the **runtime cost** when this logic executes across many
bots?
-- Same as before. Item usage is the heaviest part, and that hasnt
changed to accommodate this.

---

## How to Test the Changes

- multiple bots in a group with group loot on, do a dungeon or
something. One bot should be an enchanter.

## Complexity & Impact

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

Does this change increase per-bot or per-tick processing?
- - [X] No
- - [ ] Yes (**describe and justify impact**)

Could this logic scale poorly under load?
- - [X] No
- - [ ] Yes (**explain why**)
---

## Defaults & Configuration

Does this change modify default bot behavior?
- - [ ] No
- - [X] Yes (**explain why**)
- - - Corrects the looting behavior to original design. 

If this introduces more advanced or AI-heavy logic:
- - [ ] Lightweight mode remains the default
- - [X] More complex behavior is optional and thereby configurable
---

## AI Assistance

Was AI assistance (e.g. ChatGPT or similar tools) used while working on
this change?
- - [x] No
- - [ ] Yes (**explain below**)

---

## Final Checklist

- - [x] Stability is not compromised
- - [x] Performance impact is understood, tested, and acceptable
- - [x] Added logic complexity is justified and explained
- - [x] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.
2026-03-20 20:37:02 +01:00
73 changed files with 1224 additions and 1079 deletions

View File

@ -67,7 +67,7 @@ Bot messages have to be translatable, but you don't need to do the translations
the message is in a translatable format, and list in the table the message_key and the default English message. the message is in a translatable format, and list in the table the message_key and the default English message.
Search for GetBotTextOrDefault in the codebase for examples. Search for GetBotTextOrDefault in the codebase for examples.
--> -->
Does this change add bot messages to translate? - Does this change add bot messages to translate?
- - [ ] No - - [ ] No
- - [ ] Yes (**list messages in the table**) - - [ ] Yes (**list messages in the table**)
@ -81,7 +81,7 @@ Does this change add bot messages to translate?
AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. 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. We expect contributors to be honest about what they do and do not understand.
--> -->
Was AI assistance used while working on this change? - Was AI assistance used while working on this change?
- - [ ] No - - [ ] No
- - [ ] Yes (**explain below**) - - [ ] Yes (**explain below**)
<!-- <!--

View File

@ -34,11 +34,9 @@ We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can
Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support.
**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below. > **Important:** All `mod-playerbots` installations require a custom fork of AzerothCore: [mod-playerbots/azerothcore-wotlk (Playerbot branch)](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). The standard AzerothCore repository will **not** work.
### Cloning the Repositories ### Quick Start
To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following:
```bash ```bash
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot
@ -46,44 +44,18 @@ cd azerothcore-wotlk/modules
git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master
``` ```
For more information, refer to the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. Then build the server following the platform-specific instructions in our **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)**.
### Docker Installation > **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues.
Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module: ### Detailed Guides
```bash | Guide | Description |
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot |---|---|
cd azerothcore-wotlk/modules | **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)** | Full step-by-step instructions for clean installs, migrating from existing AzerothCore, Docker setup, adding modules, and updating |
git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master | **[Troubleshooting](https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting)** | Solutions to the most common build errors, database issues, configuration mistakes, crashes, and platform-specific problems |
```
Afterwards, create a `docker-compose.override.yml` file in the `azerothcore-wotlk` directory. This override file allows for mounting the modules directory to the `ac-worldserver` service which is required for it to run. Put the following inside and save: For additional references, see the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages.
```yml
services:
ac-worldserver:
volumes:
- ./modules:/azerothcore/modules:ro
```
Additionally, this override file can be used to set custom configuration settings for `ac-worldserver` and any modules you install as environment variables:
```yml
services:
ac-worldserver:
environment:
AC_RATE_XP_KILL: "1"
AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "1"
volumes:
- ./modules:/azerothcore/modules:ro
```
For example, to double the experience gain rate per kill, take the setting `Rate.XP.Kill = 1` from [woldserver.conf](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/src/server/apps/worldserver/worldserver.conf.dist), convert it to an environment variable, and change it to the desired setting in the override file to get `AC_RATE_XP_KILL: "2"`. If you wanted to disable random bots from logging in automatically, take the `AiPlayerbot.RandomBotAutologin = 1` setting from [playerbots.conf](https://github.com/mod-playerbots/mod-playerbots/blob/master/conf/playerbots.conf.dist) and do the same to get `AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "0"`. For more information on how to configure Azerothcore, Playerbots, and other module settings as environment variables in Docker Compose, see the "Configuring AzerothCore in Containers" section in the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) guide.
Before building, consider setting the database password. One way to do this is to create a `.env` file in the root `azerothcore-wotlk` directory using the [template](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/conf/dist/env.docker). This file also allows you to set the user and group Docker uses for the services in case you run into any permissions issues, which are the most common cause for Docker installation problems.
Use `docker compose up -d --build` to build and run the server. For more information, including how to create an account and taking backups, refer to the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) page.
## Documentation ## Documentation

View File

@ -298,9 +298,24 @@ AiPlayerbot.TwoRoundsGearInit = 0
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.FreeMethodLoot = 0 AiPlayerbot.FreeMethodLoot = 0
# Bots' loot roll level (0 = pass, 1 = greed, 2 = need) # Bots' Roll level bots will use for items they Need (0 = pass, 1 = greed, 2 = need)
# Default: 1 (greed) # Default: 1 (greed)
AiPlayerbot.LootRollLevel = 1 AiPlayerbot.LootNeedRollLevel = 1
# Enable bots to roll GREED on items (global toggle)
# If disabled, bots will PASS instead of GREED on all items
# Default: 0 (disabled - bots only NEED or PASS)
AiPlayerbot.LootGreedRollLevel = 0
# Enable bots to roll on recipes. Will NEED on learnable profession recipes they don't already know
# Bots will roll GREED on BoE recipes they can't learn if LootRollGreed is enabled.
# Default: 0 (disabled)
AiPlayerbot.LootRollRecipe = 0
# Bots with enchanting will roll DISENCHANT instead of GREED on disenchantable items
# If disabled, bots will GREED on disenchantable items instead
# Default: 0 (disabled)
AiPlayerbot.LootRollDisenchant = 0
# #
# #
@ -1391,28 +1406,28 @@ AiPlayerbot.PremadeSpecLink.1.5.80 = 0502300123-3-250031220223012521332113321
# #
AiPlayerbot.PremadeSpecName.2.0 = holy pve AiPlayerbot.PremadeSpecName.2.0 = holy pve
AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43369,43365,41109 AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43368,43365,41109
AiPlayerbot.PremadeSpecLink.2.0.60 = 50350151020013053100515221 AiPlayerbot.PremadeSpecLink.2.0.60 = 50350151020013053100515221
AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312 AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312
AiPlayerbot.PremadeSpecName.2.1 = prot pve AiPlayerbot.PremadeSpecName.2.1 = prot pve
AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43369,43365,45745 AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43368,43369,45745
AiPlayerbot.PremadeSpecLink.2.1.60 = -05005135203102311333112321 AiPlayerbot.PremadeSpecLink.2.1.60 = -05005135203102311333112321
AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003 AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003
AiPlayerbot.PremadeSpecName.2.2 = ret pve AiPlayerbot.PremadeSpecName.2.2 = ret pve
AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43369,43365,43869 AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43368,43369,43869
AiPlayerbot.PremadeSpecLink.2.2.60 = --05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.60 = --05230051203331302133231131
AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131
AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331 AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331
AiPlayerbot.PremadeSpecName.2.3 = holy pvp AiPlayerbot.PremadeSpecName.2.3 = holy pvp
AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43366,43365,45747 AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43369,43365,45747
AiPlayerbot.PremadeSpecLink.2.3.60 = 50332150300013050133215221 AiPlayerbot.PremadeSpecLink.2.3.60 = 50332150300013050133215221
AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122 AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122
AiPlayerbot.PremadeSpecName.2.4 = prot pvp AiPlayerbot.PremadeSpecName.2.4 = prot pvp
AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43369,41101,43368,43365,45745 AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43367,41101,43369,43365,45745
AiPlayerbot.PremadeSpecLink.2.4.60 = -15320130223122311323311321 AiPlayerbot.PremadeSpecLink.2.4.60 = -15320130223122311323311321
AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502 AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502
AiPlayerbot.PremadeSpecName.2.5 = ret pvp AiPlayerbot.PremadeSpecName.2.5 = ret pvp
AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43369,41102,43368,43365,45747 AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43367,41102,43369,43365,45747
AiPlayerbot.PremadeSpecLink.2.5.60 = --05230250203331222133201321 AiPlayerbot.PremadeSpecLink.2.5.60 = --05230250203331222133201321
AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321 AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321

View File

@ -163,6 +163,7 @@ public:
creators["war stomp"] = &ActionContext::war_stomp; creators["war stomp"] = &ActionContext::war_stomp;
creators["blood fury"] = &ActionContext::blood_fury; creators["blood fury"] = &ActionContext::blood_fury;
creators["berserking"] = &ActionContext::berserking; creators["berserking"] = &ActionContext::berserking;
creators["every man for himself"] = &ActionContext::every_man_for_himself;
creators["use trinket"] = &ActionContext::use_trinket; creators["use trinket"] = &ActionContext::use_trinket;
creators["auto talents"] = &ActionContext::auto_talents; creators["auto talents"] = &ActionContext::auto_talents;
creators["auto share quest"] = &ActionContext::auto_share_quest; creators["auto share quest"] = &ActionContext::auto_share_quest;
@ -357,6 +358,7 @@ private:
static Action* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); } static Action* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); }
static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); } static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); }
static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); } static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); }
static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); }
static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); } static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); }
static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); }
static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); }

View File

@ -311,6 +311,30 @@ bool CastVehicleSpellAction::Execute(Event /*event*/)
return botAI->CastVehicleSpell(spellId, GetTarget()); return botAI->CastVehicleSpell(spellId, GetTarget());
} }
bool CastEveryManForHimselfAction::isPossible()
{
uint32 spellId = AI_VALUE2(uint32, "spell id", spell);
if (!spellId)
return false;
if (!bot->HasSpell(spellId))
return false;
if (bot->HasSpellCooldown(spellId))
return false;
return true;
}
bool CastEveryManForHimselfAction::isUseful()
{
return bot->HasAuraType(SPELL_AURA_MOD_STUN) ||
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM);
}
bool UseTrinketAction::Execute(Event /*event*/) bool UseTrinketAction::Execute(Event /*event*/)
{ {
Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1); Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1);

View File

@ -284,6 +284,16 @@ public:
CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {} CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {}
}; };
class CastEveryManForHimselfAction : public CastSpellAction
{
public:
CastEveryManForHimselfAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "every man for himself") {}
std::string const GetTargetName() override { return "self target"; }
bool isPossible() override;
bool isUseful() override;
};
class UseTrinketAction : public Action class UseTrinketAction : public Action
{ {
public: public:

View File

@ -22,10 +22,10 @@ bool LootRollAction::Execute(Event /*event*/)
std::vector<Roll*> rolls = group->GetRolls(); std::vector<Roll*> rolls = group->GetRolls();
for (Roll*& roll : rolls) for (Roll*& roll : rolls)
{ {
if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET) auto voteItr = roll->playerVote.find(bot->GetGUID());
{ if (voteItr == roll->playerVote.end() || voteItr->second != NOT_EMITED_YET)
continue; continue;
}
ObjectGuid guid = roll->itemGUID; ObjectGuid guid = roll->itemGUID;
uint32 itemId = roll->itemid; uint32 itemId = roll->itemid;
int32 randomProperty = 0; int32 randomProperty = 0;
@ -41,27 +41,22 @@ bool LootRollAction::Execute(Event /*event*/)
std::string itemUsageParam; std::string itemUsageParam;
if (randomProperty != 0) if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
}
else else
{
itemUsageParam = std::to_string(itemId); itemUsageParam = std::to_string(itemId);
}
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);
// Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality. // Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality.
if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC) if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC)
{ {
if (CanBotUseToken(proto, bot)) if (CanBotUseToken(proto, bot))
{
vote = NEED; // Eligible for "Need" vote = NEED; // Eligible for "Need"
}
else else
{
vote = GREED; // Not eligible, so "Greed" vote = GREED; // Not eligible, so "Greed"
} }
} else if (usage == ITEM_USAGE_DISENCHANT)
vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
else else
{ {
switch (proto->Class) switch (proto->Class)
@ -69,40 +64,34 @@ bool LootRollAction::Execute(Event /*event*/)
case ITEM_CLASS_WEAPON: case ITEM_CLASS_WEAPON:
case ITEM_CLASS_ARMOR: case ITEM_CLASS_ARMOR:
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP)
{
vote = NEED; vote = NEED;
}
else if (usage != ITEM_USAGE_NONE) else if (usage != ITEM_USAGE_NONE)
{
vote = GREED; vote = GREED;
} break;
case ITEM_CLASS_RECIPE:
if (!sPlayerbotAIConfig.lootRollRecipe)
vote = PASS;
else if (usage == ITEM_USAGE_SKILL)
vote = NEED; // Bot can learn this recipe
else if (proto->Bonding != BIND_WHEN_PICKED_UP)
vote = GREED; // BoE recipe bot can't learn - GREED for AH/trade
break; break;
default: default:
if (StoreLootAction::IsLootAllowed(itemId, botAI)) if (StoreLootAction::IsLootAllowed(itemId, botAI))
vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior vote = CalculateRollVote(proto, usage);
break; break;
} }
} }
if (sPlayerbotAIConfig.lootRollLevel == 0)
{
vote = PASS;
}
else if (sPlayerbotAIConfig.lootRollLevel == 1)
{
// Level 1 = "greed" mode: bots greed on useful items but never need
// Only downgrade NEED to GREED, preserve GREED votes as-is
if (vote == NEED) if (vote == NEED)
{ {
if (RollUniqueCheck(proto, bot)) if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot))
{
vote = PASS; vote = PASS;
} else if (sPlayerbotAIConfig.lootNeedRollLevel == 1)
else
{
vote = GREED; vote = GREED;
} }
} else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel)
} vote = PASS;
switch (group->GetLootMethod()) switch (group->GetLootMethod())
{ {
case MASTER_LOOT: case MASTER_LOOT:
@ -120,11 +109,14 @@ bool LootRollAction::Execute(Event /*event*/)
return false; return false;
} }
RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage)
{ {
if (usage == ITEM_USAGE_NONE)
{
std::ostringstream out; std::ostringstream out;
out << proto->ItemId; out << proto->ItemId;
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str()); usage = AI_VALUE2(ItemUsage, "item usage", out.str());
}
RollVote needVote = PASS; RollVote needVote = PASS;
switch (usage) switch (usage)
@ -137,11 +129,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto)
break; break;
case ITEM_USAGE_SKILL: case ITEM_USAGE_SKILL:
case ITEM_USAGE_USE: case ITEM_USAGE_USE:
case ITEM_USAGE_DISENCHANT:
case ITEM_USAGE_AH: case ITEM_USAGE_AH:
case ITEM_USAGE_VENDOR: case ITEM_USAGE_VENDOR:
needVote = GREED; needVote = GREED;
break; break;
case ITEM_USAGE_DISENCHANT:
needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED;
break;
default: default:
break; break;
} }
@ -195,9 +189,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot)
// Check if the bot's class is allowed to use the token // Check if the bot's class is allowed to use the token
if (proto->AllowableClass & botClassMask) if (proto->AllowableClass & botClassMask)
{
return true; // Bot's class is eligible to use this token return true; // Bot's class is eligible to use this token
}
return false; // Bot's class cannot use this token return false; // Bot's class cannot use this token
} }
@ -213,13 +205,9 @@ bool RollUniqueCheck(ItemTemplate const* proto, Player* bot)
// Determine if the unique item is already equipped // Determine if the unique item is already equipped
bool isEquipped = (totalItemCount > bagItemCount); bool isEquipped = (totalItemCount > bagItemCount);
if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE)) if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE))
{
return true; // Unique Item is already equipped return true; // Unique Item is already equipped
}
else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1)) else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1))
{
return true; // Unique item already in bag, don't roll for it return true; // Unique item already in bag, don't roll for it
}
return false; // Item is not equipped or in bags, roll for it return false; // Item is not equipped or in bags, roll for it
} }

View File

@ -22,7 +22,7 @@ public:
bool Execute(Event event) override; bool Execute(Event event) override;
protected: protected:
RollVote CalculateRollVote(ItemTemplate const* proto); RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE);
}; };
bool CanBotUseToken(ItemTemplate const* proto, Player* bot); bool CanBotUseToken(ItemTemplate const* proto, Player* bot);

View File

@ -1387,8 +1387,8 @@ bool MovementAction::Flee(Unit* target)
} }
} }
HostileReference* ref = target->GetThreatMgr().getCurrentVictim(); Unit* currentVictim = target->GetThreatMgr().GetCurrentVictim();
if (ref && ref->getTarget() == bot) // bot is target - try to flee to tank or master if (currentVictim && currentVictim == bot) // bot is target - try to flee to tank or master
{ {
if (Group* group = bot->GetGroup()) if (Group* group = bot->GetGroup())
{ {

View File

@ -6,7 +6,8 @@
#include "TellTargetAction.h" #include "TellTargetAction.h"
#include "Event.h" #include "Event.h"
#include "ThreatMgr.h" #include "CombatManager.h"
#include "ThreatManager.h"
#include "AiObjectContext.h" #include "AiObjectContext.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
@ -42,21 +43,21 @@ bool TellAttackersAction::Execute(Event /*event*/)
botAI->TellMaster("--- Threat ---"); botAI->TellMaster("--- Threat ---");
HostileReference* ref = bot->getHostileRefMgr().getFirst(); auto const& threatenedByMe = bot->GetThreatMgr().GetThreatenedByMeList();
if (!ref) if (threatenedByMe.empty())
return true; return true;
while (ref) for (auto const& [guid, ref] : threatenedByMe)
{ {
ThreatMgr* threatMgr = ref->GetSource(); Unit* unit = ref->GetOwner();
Unit* unit = threatMgr->GetOwner(); if (!unit)
continue;
float threat = ref->GetThreat(); float threat = ref->GetThreat();
std::ostringstream out; std::ostringstream out;
out << unit->GetName() << " (" << threat << ")"; out << unit->GetName() << " (" << threat << ")";
botAI->TellMaster(out); botAI->TellMaster(out);
ref = ref->next();
} }
return true; return true;

View File

@ -104,6 +104,13 @@ public:
creators["target"] = &ChatTriggerContext::target; creators["target"] = &ChatTriggerContext::target;
creators["formation"] = &ChatTriggerContext::formation; creators["formation"] = &ChatTriggerContext::formation;
creators["stance"] = &ChatTriggerContext::stance; creators["stance"] = &ChatTriggerContext::stance;
creators["cancel tree form"] = &ChatTriggerContext::cancel_tree_form;
creators["cancel travel form"] = &ChatTriggerContext::cancel_travel_form;
creators["cancel bear form"] = &ChatTriggerContext::cancel_bear_form;
creators["cancel dire bear form"] = &ChatTriggerContext::cancel_dire_bear_form;
creators["cancel cat form"] = &ChatTriggerContext::cancel_cat_form;
creators["cancel moonkin form"] = &ChatTriggerContext::cancel_moonkin_form;
creators["cancel aquatic form"] = &ChatTriggerContext::cancel_aquatic_form;
creators["sendmail"] = &ChatTriggerContext::sendmail; creators["sendmail"] = &ChatTriggerContext::sendmail;
creators["mail"] = &ChatTriggerContext::mail; creators["mail"] = &ChatTriggerContext::mail;
creators["outfit"] = &ChatTriggerContext::outfit; creators["outfit"] = &ChatTriggerContext::outfit;
@ -159,6 +166,13 @@ private:
static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); } static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); }
static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); } static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); }
static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); } static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); }
static Trigger* cancel_tree_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel tree form"); }
static Trigger* cancel_travel_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel travel form"); }
static Trigger* cancel_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel bear form"); }
static Trigger* cancel_dire_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel dire bear form"); }
static Trigger* cancel_cat_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel cat form"); }
static Trigger* cancel_moonkin_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel moonkin form"); }
static Trigger* cancel_aquatic_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel aquatic form"); }
static Trigger* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); } static Trigger* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); }
static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); } static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); }
static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); } static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); }

View File

@ -160,6 +160,13 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas
supported.push_back("save mana"); supported.push_back("save mana");
supported.push_back("formation"); supported.push_back("formation");
supported.push_back("stance"); supported.push_back("stance");
supported.push_back("cancel tree form");
supported.push_back("cancel travel form");
supported.push_back("cancel bear form");
supported.push_back("cancel dire bear form");
supported.push_back("cancel cat form");
supported.push_back("cancel moonkin form");
supported.push_back("cancel aquatic form");
supported.push_back("sendmail"); supported.push_back("sendmail");
supported.push_back("mail"); supported.push_back("mail");
supported.push_back("outfit"); supported.push_back("outfit");

View File

@ -34,6 +34,9 @@ void RacialsStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
NextAction("berserking", ACTION_NORMAL + 5), NextAction("berserking", ACTION_NORMAL + 5),
NextAction("use trinket", ACTION_NORMAL + 4) })); NextAction("use trinket", ACTION_NORMAL + 4) }));
triggers.push_back(new TriggerNode(
"loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) }));
} }
RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI) RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI)

View File

@ -16,7 +16,7 @@
#include "PositionValue.h" #include "PositionValue.h"
#include "SharedDefines.h" #include "SharedDefines.h"
#include "TemporarySummon.h" #include "TemporarySummon.h"
#include "ThreatMgr.h" #include "ThreatManager.h"
#include "Timer.h" #include "Timer.h"
#include "PlayerbotAI.h" #include "PlayerbotAI.h"
#include "Player.h" #include "Player.h"
@ -217,7 +217,7 @@ bool LowTankThreatTrigger::IsActive()
if (!current_target) if (!current_target)
return false; return false;
ThreatMgr& mgr = current_target->GetThreatMgr(); ThreatManager& mgr = current_target->GetThreatMgr();
float threat = mgr.GetThreat(bot); float threat = mgr.GetThreat(bot);
float tankThreat = mgr.GetThreat(mt); float tankThreat = mgr.GetThreat(mt);
return tankThreat == 0.0f || threat > tankThreat * 0.5f; return tankThreat == 0.0f || threat > tankThreat * 0.5f;
@ -464,6 +464,15 @@ bool AttackerCountTrigger::IsActive() { return AI_VALUE(uint8, "attacker count")
bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); } bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); }
bool LossOfControlTrigger::IsActive()
{
return bot->HasAuraType(SPELL_AURA_MOD_STUN) ||
bot->HasAuraType(SPELL_AURA_MOD_FEAR) ||
bot->HasAuraType(SPELL_AURA_MOD_ROOT) ||
bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) ||
bot->HasAuraType(SPELL_AURA_MOD_CHARM);
}
bool HasAuraStackTrigger::IsActive() bool HasAuraStackTrigger::IsActive()
{ {
Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack);

View File

@ -746,6 +746,14 @@ public:
bool IsActive() override; bool IsActive() override;
}; };
class LossOfControlTrigger : public Trigger
{
public:
LossOfControlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "loss of control", 1) {}
bool IsActive() override;
};
class IsSwimmingTrigger : public Trigger class IsSwimmingTrigger : public Trigger
{ {
public: public:

View File

@ -59,6 +59,7 @@ public:
creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth; creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth;
creators["generic boost"] = &TriggerContext::generic_boost; creators["generic boost"] = &TriggerContext::generic_boost;
creators["loss of control"] = &TriggerContext::loss_of_control;
creators["protect party member"] = &TriggerContext::protect_party_member; creators["protect party member"] = &TriggerContext::protect_party_member;
@ -103,7 +104,8 @@ public:
creators["enemy within melee"] = &TriggerContext::enemy_within_melee; creators["enemy within melee"] = &TriggerContext::enemy_within_melee;
creators["party member to heal out of spell range"] = &TriggerContext::party_member_to_heal_out_of_spell_range; creators["party member to heal out of spell range"] = &TriggerContext::party_member_to_heal_out_of_spell_range;
creators["combo points available"] = &TriggerContext::ComboPointsAvailable; creators["combo points 5 available"] = &TriggerContext::ComboPoints5Available;
creators["combo points 4 available"] = &TriggerContext::ComboPoints4Available;
creators["combo points 3 available"] = &TriggerContext::ComboPoints3Available; creators["combo points 3 available"] = &TriggerContext::ComboPoints3Available;
creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead; creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead;
creators["combo points not full"] = &TriggerContext::ComboPointsNotFull; creators["combo points not full"] = &TriggerContext::ComboPointsNotFull;
@ -338,7 +340,8 @@ private:
{ {
return new PartyMemberToHealOutOfSpellRangeTrigger(botAI); return new PartyMemberToHealOutOfSpellRangeTrigger(botAI);
} }
static Trigger* ComboPointsAvailable(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI); } static Trigger* ComboPoints5Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 5); }
static Trigger* ComboPoints4Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 4); }
static Trigger* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); } static Trigger* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); }
static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai) static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai)
{ {
@ -361,6 +364,7 @@ private:
return new PartyMemberAlmostFullHealthTrigger(botAI); return new PartyMemberAlmostFullHealthTrigger(botAI);
} }
static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); }
static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); }
static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI)
{ {
return new PartyMemberCriticalHealthTrigger(botAI); return new PartyMemberCriticalHealthTrigger(botAI);

View File

@ -92,21 +92,15 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set<Unit*>& t
if (!player || !player->IsInWorld() || player->IsBeingTeleported()) if (!player || !player->IsInWorld() || player->IsBeingTeleported())
return; return;
HostileRefMgr& refManager = player->getHostileRefMgr(); for (auto const& [guid, ref] : player->GetThreatMgr().GetThreatenedByMeList())
HostileReference* ref = refManager.getFirst();
if (!ref)
return;
while (ref)
{ {
ThreatMgr* threatMgr = ref->GetSource(); Unit* attacker = ref->GetOwner();
Unit* attacker = threatMgr->GetOwner(); if (!attacker)
continue;
if (player->IsValidAttackTarget(attacker) && if (player->IsValidAttackTarget(attacker) &&
player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance) player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance)
targets.insert(attacker); targets.insert(attacker);
ref = ref->next();
} }
} }
@ -131,7 +125,6 @@ bool AttackersValue::hasRealThreat(Unit* attacker)
return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() && return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() &&
// !attacker->isInRoots() && // !attacker->isInRoots() &&
!attacker->IsFriendlyTo(bot); !attacker->IsFriendlyTo(bot);
(attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast<Player*>(attacker));
} }
bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/)
@ -241,9 +234,6 @@ bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range
bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot)
{ {
return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker); return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker);
// (attacker->GetThreatMgr().getCurrentVictim() || attacker->GetGuidValue(UNIT_FIELD_TARGET) ||
// attacker->GetGUID().IsPlayer() || attacker->GetGUID() ==
// GET_PLAYERBOT_AI(bot)->GetAiObjectContext()->GetValue<ObjectGuid>("pull target")->Get());
} }
bool PossibleAddsValue::Calculate() bool PossibleAddsValue::Calculate()
@ -255,13 +245,11 @@ bool PossibleAddsValue::Calculate()
{ {
if (find(attackers.begin(), attackers.end(), guid) != attackers.end()) if (find(attackers.begin(), attackers.end(), guid) != attackers.end())
continue; continue;
Unit* add = botAI->GetUnit(guid);
if (Unit* add = botAI->GetUnit(guid)) if (!add || !add->IsInWorld() || add->IsDuringRemoveFromWorld())
{
if (!add->IsInWorld() || add->IsDuringRemoveFromWorld())
continue; continue;
if (!add->GetTarget() && !add->GetThreatMgr().getCurrentVictim() && add->IsHostileTo(bot)) if (!add->GetTarget() && !add->GetThreatMgr().GetLastVictim() && add->IsHostileTo(bot))
{ {
for (ObjectGuid const attackerGUID : attackers) for (ObjectGuid const attackerGUID : attackers)
{ {
@ -278,7 +266,6 @@ bool PossibleAddsValue::Calculate()
} }
} }
} }
}
return false; return false;
} }

View File

@ -20,7 +20,7 @@ public:
} }
public: public:
void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override
{ {
Player* bot = botAI->GetBot(); Player* bot = botAI->GetBot();
if (!botAI->CanCastSpell(spell, creature)) if (!botAI->CanCastSpell(spell, creature))

View File

@ -13,7 +13,7 @@ public:
{ {
} }
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{ {
if (botAI->HasAura(spell, attacker)) if (botAI->HasAura(spell, attacker))
result = attacker; result = attacker;

View File

@ -13,16 +13,14 @@ class FindMaxThreatGapTargetStrategy : public FindTargetStrategy
public: public:
FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), minThreat(0) {} FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), minThreat(0) {}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{ {
if (!attacker->IsAlive()) if (!attacker->IsAlive())
{
return; return;
}
if (foundHighPriority) if (foundHighPriority)
{
return; return;
}
if (IsHighPriority(attacker)) if (IsHighPriority(attacker))
{ {
result = attacker; result = attacker;
@ -32,7 +30,7 @@ public:
if (!result || CalcThreatGap(attacker, threatMgr) > CalcThreatGap(result, &result->GetThreatMgr())) if (!result || CalcThreatGap(attacker, threatMgr) > CalcThreatGap(result, &result->GetThreatMgr()))
result = attacker; result = attacker;
} }
float CalcThreatGap(Unit* attacker, ThreatMgr* threatMgr) float CalcThreatGap(Unit* attacker, ThreatManager* threatMgr)
{ {
Unit* victim = attacker->GetVictim(); Unit* victim = attacker->GetVictim();
return threatMgr->GetThreat(victim) - threatMgr->GetThreat(attacker); return threatMgr->GetThreat(victim) - threatMgr->GetThreat(attacker);
@ -52,7 +50,7 @@ public:
result = nullptr; result = nullptr;
} }
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{ {
if (Group* group = botAI->GetBot()->GetGroup()) if (Group* group = botAI->GetBot()->GetGroup())
{ {
@ -61,13 +59,11 @@ public:
return; return;
} }
if (!attacker->IsAlive()) if (!attacker->IsAlive())
{
return; return;
}
if (foundHighPriority) if (foundHighPriority)
{
return; return;
}
if (IsHighPriority(attacker)) if (IsHighPriority(attacker))
{ {
result = attacker; result = attacker;
@ -90,24 +86,19 @@ public:
int new_level = GetIntervalLevel(new_unit); int new_level = GetIntervalLevel(new_unit);
int old_level = GetIntervalLevel(old_unit); int old_level = GetIntervalLevel(old_unit);
if (new_level != old_level) if (new_level != old_level)
{
return new_level > old_level; return new_level > old_level;
}
int32_t level = new_level; int32_t level = new_level;
if (level % 10 == 2 || level % 10 == 0) if (level % 10 == 2 || level % 10 == 0)
{
return new_time < old_time; return new_time < old_time;
}
// dont switch targets when all of them with low health // dont switch targets when all of them with low health
Unit* currentTarget = botAI->GetAiObjectContext()->GetValue<Unit*>("current target")->Get(); Unit* currentTarget = botAI->GetAiObjectContext()->GetValue<Unit*>("current target")->Get();
if (currentTarget == new_unit) if (currentTarget == new_unit)
{
return true; return true;
}
if (currentTarget == old_unit) if (currentTarget == old_unit)
{
return false; return false;
}
return new_time > old_time; return new_time > old_time;
} }
int32_t GetIntervalLevel(Unit* unit) int32_t GetIntervalLevel(Unit* unit)
@ -119,13 +110,11 @@ public:
attackRange += 5.0f; attackRange += 5.0f;
int level = dis < attackRange ? 10 : 0; int level = dis < attackRange ? 10 : 0;
if (time >= 5 && time <= 30) if (time >= 5 && time <= 30)
{
return level + 2; return level + 2;
}
if (time > 30) if (time > 30)
{
return level; return level;
}
return level + 1; return level + 1;
} }
@ -143,7 +132,7 @@ public:
{ {
} }
void CheckAttacker(Unit* attacker, ThreatMgr*) override void CheckAttacker(Unit* attacker, ThreatManager*) override
{ {
if (Group* group = botAI->GetBot()->GetGroup()) if (Group* group = botAI->GetBot()->GetGroup())
{ {
@ -152,13 +141,11 @@ public:
return; return;
} }
if (!attacker->IsAlive()) if (!attacker->IsAlive())
{
return; return;
}
if (foundHighPriority) if (foundHighPriority)
{
return; return;
}
if (IsHighPriority(attacker)) if (IsHighPriority(attacker))
{ {
result = attacker; result = attacker;
@ -186,9 +173,8 @@ public:
// attack enemy in range and with lowest health // attack enemy in range and with lowest health
int level = new_level; int level = new_level;
if (level == 10) if (level == 10)
{
return new_time < old_time; return new_time < old_time;
}
// all targets are far away, choose the closest one // all targets are far away, choose the closest one
return botAI->GetBot()->GetDistance(new_unit) < botAI->GetBot()->GetDistance(old_unit); return botAI->GetBot()->GetDistance(new_unit) < botAI->GetBot()->GetDistance(old_unit);
} }
@ -216,7 +202,7 @@ public:
{ {
} }
void CheckAttacker(Unit* attacker, ThreatMgr*) override void CheckAttacker(Unit* attacker, ThreatManager*) override
{ {
if (Group* group = botAI->GetBot()->GetGroup()) if (Group* group = botAI->GetBot()->GetGroup())
{ {
@ -225,13 +211,11 @@ public:
return; return;
} }
if (!attacker->IsAlive()) if (!attacker->IsAlive())
{
return; return;
}
if (foundHighPriority) if (foundHighPriority)
{
return; return;
}
if (IsHighPriority(attacker)) if (IsHighPriority(attacker))
{ {
result = attacker; result = attacker;
@ -254,9 +238,8 @@ public:
int new_level = GetIntervalLevel(new_unit); int new_level = GetIntervalLevel(new_unit);
int old_level = GetIntervalLevel(old_unit); int old_level = GetIntervalLevel(old_unit);
if (new_level != old_level) if (new_level != old_level)
{
return new_level > old_level; return new_level > old_level;
}
// attack enemy in range and with lowest health // attack enemy in range and with lowest health
int level = new_level; int level = new_level;
Player* bot = botAI->GetBot(); Player* bot = botAI->GetBot();
@ -264,9 +247,8 @@ public:
{ {
Unit* combo_unit = bot->GetComboTarget(); Unit* combo_unit = bot->GetComboTarget();
if (new_unit == combo_unit) if (new_unit == combo_unit)
{
return true; return true;
}
return new_time < old_time; return new_time < old_time;
} }
// all targets are far away, choose the closest one // all targets are far away, choose the closest one
@ -319,7 +301,7 @@ class FindMaxHpTargetStrategy : public FindTargetStrategy
public: public:
FindMaxHpTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), maxHealth(0) {} FindMaxHpTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), maxHealth(0) {}
void CheckAttacker(Unit* attacker, ThreatMgr*) override void CheckAttacker(Unit* attacker, ThreatManager*) override
{ {
if (Group* group = botAI->GetBot()->GetGroup()) if (Group* group = botAI->GetBot()->GetGroup())
{ {

View File

@ -5,6 +5,7 @@
#include "EnemyPlayerValue.h" #include "EnemyPlayerValue.h"
#include "CombatManager.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "ServerFacade.h" #include "ServerFacade.h"
#include "Vehicle.h" #include "Vehicle.h"
@ -51,35 +52,22 @@ Unit* EnemyPlayerValue::Calculate()
controllingVehicle = true; controllingVehicle = true;
} }
// 1. Check units we are currently in combat with. // 1. Check units we are currently in PvP combat with.
std::vector<Unit*> targets; std::vector<Unit*> targets;
Unit* pVictim = bot->GetVictim(); Unit* pVictim = bot->GetVictim();
HostileReference* pReference = bot->getHostileRefMgr().getFirst(); for (auto const& [guid, combatRef] : bot->GetCombatManager().GetPvPCombatRefs())
while (pReference)
{ {
ThreatMgr* threatMgr = pReference->GetSource(); Unit* pTarget = combatRef->GetOther(bot);
if (Unit* pTarget = threatMgr->GetOwner()) if (!pTarget || pTarget == pVictim || !pTarget->IsPlayer() || !pTarget->CanSeeOrDetect(bot) ||
{ !bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL))
if (pTarget != pVictim && pTarget->IsPlayer() && pTarget->CanSeeOrDetect(bot) && continue;
bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL))
{ if ((bot->GetTeamId() == TEAM_HORDE && pTarget->HasAura(23333)) ||
if (bot->GetTeamId() == TEAM_HORDE) (bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335)))
{
if (pTarget->HasAura(23333))
return pTarget; return pTarget;
}
else
{
if (pTarget->HasAura(23335))
return pTarget;
}
targets.push_back(pTarget); targets.push_back(pTarget);
} }
}
pReference = pReference->next();
}
if (!targets.empty()) if (!targets.empty())
{ {

View File

@ -153,9 +153,8 @@ ItemUsage ItemUsageValue::Calculate()
// Need to add something like free bagspace or item value. // Need to add something like free bagspace or item value.
if (proto->SellPrice > 0) if (proto->SellPrice > 0)
{ {
if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP)
return ITEM_USAGE_AH; return ITEM_USAGE_AH;
else else
return ITEM_USAGE_VENDOR; return ITEM_USAGE_VENDOR;
} }

View File

@ -13,7 +13,7 @@ class FindLeastHpTargetStrategy : public FindNonCcTargetStrategy
public: public:
FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {} FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{ {
if (IsCcTarget(attacker)) if (IsCcTarget(attacker))
return; return;

View File

@ -15,12 +15,11 @@ class FindTargetForTankStrategy : public FindNonCcTargetStrategy
public: public:
FindTargetForTankStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minThreat(0) {} FindTargetForTankStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minThreat(0) {}
void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override
{ {
if (!creature || !creature->IsAlive()) if (!creature || !creature->IsAlive())
{
return; return;
}
Player* bot = botAI->GetBot(); Player* bot = botAI->GetBot();
float threat = threatMgr->GetThreat(bot); float threat = threatMgr->GetThreat(bot);
if (!result) if (!result)
@ -29,15 +28,11 @@ public:
result = creature; result = creature;
} }
// neglect if victim is main tank, or no victim (for untauntable target) // neglect if victim is main tank, or no victim (for untauntable target)
if (threatMgr->getCurrentVictim()) if (Unit* victim = threatMgr->GetCurrentVictim())
{
// float max_threat = threatMgr->GetThreat(threatMgr->getCurrentVictim()->getTarget());
Unit* victim = threatMgr->getCurrentVictim()->getTarget();
if (victim && victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer()))
{ {
if (victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer()))
return; return;
} }
}
if (minThreat >= threat) if (minThreat >= threat)
{ {
minThreat = threat; minThreat = threat;
@ -54,7 +49,7 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy
public: public:
FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {}
void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override
{ {
if (Group* group = botAI->GetBot()->GetGroup()) if (Group* group = botAI->GetBot()->GetGroup())
{ {
@ -63,14 +58,11 @@ public:
return; return;
} }
if (!attacker->IsAlive()) if (!attacker->IsAlive())
{
return; return;
}
if (!result || IsBetter(attacker, result)) if (!result || IsBetter(attacker, result))
{
result = attacker; result = attacker;
} }
}
bool IsBetter(Unit* new_unit, Unit* old_unit) bool IsBetter(Unit* new_unit, Unit* old_unit)
{ {
Player* bot = botAI->GetBot(); Player* bot = botAI->GetBot();
@ -80,6 +72,7 @@ public:
{ {
if (old_unit == currentTarget) if (old_unit == currentTarget)
return false; return false;
if (new_unit == currentTarget) if (new_unit == currentTarget)
return true; return true;
} }
@ -89,26 +82,22 @@ public:
float old_dis = bot->GetDistance(old_unit); float old_dis = bot->GetDistance(old_unit);
// hasAggro? -> withinMelee? -> threat // hasAggro? -> withinMelee? -> threat
if (GetIntervalLevel(new_unit) != GetIntervalLevel(old_unit)) if (GetIntervalLevel(new_unit) != GetIntervalLevel(old_unit))
{
return GetIntervalLevel(new_unit) > GetIntervalLevel(old_unit); return GetIntervalLevel(new_unit) > GetIntervalLevel(old_unit);
}
int32_t interval = GetIntervalLevel(new_unit); int32_t interval = GetIntervalLevel(new_unit);
if (interval == 2) if (interval == 2)
{
return new_dis < old_dis; return new_dis < old_dis;
}
return new_threat < old_threat; return new_threat < old_threat;
} }
int32_t GetIntervalLevel(Unit* unit) int32_t GetIntervalLevel(Unit* unit)
{ {
if (!botAI->HasAggro(unit)) if (!botAI->HasAggro(unit))
{
return 2; return 2;
}
if (botAI->GetBot()->IsWithinMeleeRange(unit)) if (botAI->GetBot()->IsWithinMeleeRange(unit))
{
return 1; return 1;
}
return 0; return 0;
} }
}; };

View File

@ -5,12 +5,13 @@
#include "TargetValue.h" #include "TargetValue.h"
#include "CombatManager.h"
#include "LastMovementValue.h" #include "LastMovementValue.h"
#include "ObjectGuid.h" #include "ObjectGuid.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "RtiTargetValue.h" #include "RtiTargetValue.h"
#include "ScriptedCreature.h" #include "ScriptedCreature.h"
#include "ThreatMgr.h" #include "ThreatManager.h"
Unit* FindTargetStrategy::GetResult() { return result; } Unit* FindTargetStrategy::GetResult() { return result; }
@ -23,8 +24,8 @@ Unit* TargetValue::FindTarget(FindTargetStrategy* strategy)
if (!unit) if (!unit)
continue; continue;
ThreatMgr& ThreatMgr = unit->GetThreatMgr(); ThreatManager& threatMgr = unit->GetThreatMgr();
strategy->CheckAttacker(unit, &ThreatMgr); strategy->CheckAttacker(unit, &threatMgr);
} }
return strategy->GetResult(); return strategy->GetResult();
@ -144,24 +145,23 @@ Unit* FindTargetValue::Calculate()
{ {
return nullptr; return nullptr;
} }
HostileReference* ref = bot->getHostileRefMgr().getFirst(); for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList())
while (ref)
{ {
ThreatMgr* threatManager = ref->GetSource(); Unit* unit = ref->GetOwner();
Unit* unit = threatManager->GetOwner(); if (!unit)
continue;
std::wstring wnamepart; std::wstring wnamepart;
Utf8toWStr(unit->GetName(), wnamepart); Utf8toWStr(unit->GetName(), wnamepart);
wstrToLower(wnamepart); wstrToLower(wnamepart);
if (!qualifier.empty() && qualifier.length() == wnamepart.length() && Utf8FitTo(qualifier, wnamepart)) if (!qualifier.empty() && qualifier.length() == wnamepart.length() && Utf8FitTo(qualifier, wnamepart))
{
return unit; return unit;
} }
ref = ref->next();
}
return nullptr; return nullptr;
} }
void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatMgr* threatManager) void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* threatManager)
{ {
UnitAI* unitAI = attacker->GetAI(); UnitAI* unitAI = attacker->GetAI();
BossAI* bossAI = dynamic_cast<BossAI*>(unitAI); BossAI* bossAI = dynamic_cast<BossAI*>(unitAI);

View File

@ -11,7 +11,7 @@
#include "Value.h" #include "Value.h"
class PlayerbotAI; class PlayerbotAI;
class ThreatMgr; class ThreatManager;
class Unit; class Unit;
class FindTargetStrategy class FindTargetStrategy
@ -20,7 +20,7 @@ public:
FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {} FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {}
Unit* GetResult(); Unit* GetResult();
virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) = 0; virtual void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) = 0;
void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount); void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount);
bool IsHighPriority(Unit* attacker); bool IsHighPriority(Unit* attacker);
@ -129,7 +129,7 @@ class FindBossTargetStrategy : public FindTargetStrategy
{ {
public: public:
FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {} FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {}
virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatManager); virtual void CheckAttacker(Unit* attacker, ThreatManager* threatManager);
}; };
class BossTargetValue : public TargetValue, public Qualified class BossTargetValue : public TargetValue, public Qualified

View File

@ -6,7 +6,7 @@
#include "ThreatValues.h" #include "ThreatValues.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "ThreatMgr.h" #include "ThreatManager.h"
uint8 ThreatValue::Calculate() uint8 ThreatValue::Calculate()
{ {

View File

@ -44,13 +44,13 @@ bool CastCasterFormAction::isUseful()
AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth;
} }
bool CastCancelTreeFormAction::Execute(Event /*event*/) bool CastCancelDruidAction::Execute(Event /*event*/)
{ {
botAI->RemoveAura("tree of life"); botAI->RemoveAura(auraName);
return true; return true;
} }
bool CastCancelTreeFormAction::isUseful() { return botAI->HasAura(33891, bot); } bool CastCancelDruidAction::isUseful() { return botAI->HasAura(auraId, bot); }
bool CastTreeFormAction::isUseful() bool CastTreeFormAction::isUseful()
{ {

View File

@ -71,14 +71,78 @@ public:
bool isPossible() override { return true; } bool isPossible() override { return true; }
}; };
class CastCancelTreeFormAction : public CastBuffSpellAction class CastCancelDruidAction : public CastBuffSpellAction
{ {
public: public:
CastCancelTreeFormAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cancel tree form") {} CastCancelDruidAction(PlayerbotAI* botAI, std::string const& actionName, std::string const& auraName, uint32 auraId)
: CastBuffSpellAction(botAI, actionName), auraName(auraName), auraId(auraId)
{
}
bool Execute(Event event) override; bool Execute(Event event) override;
bool isUseful() override; bool isUseful() override;
bool isPossible() override { return true; } bool isPossible() override { return true; }
private:
std::string auraName;
uint32 auraId;
};
class CastCancelTreeFormAction : public CastCancelDruidAction
{
public:
CastCancelTreeFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel tree form", "tree of life", 33891)
{
}
};
class CastCancelTravelFormAction : public CastCancelDruidAction
{
public:
CastCancelTravelFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel travel form", "travel form", 783)
{
}
};
class CastCancelBearFormAction : public CastCancelDruidAction
{
public:
CastCancelBearFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel bear form", "bear form", 5487) {}
};
class CastCancelDireBearFormAction : public CastCancelDruidAction
{
public:
CastCancelDireBearFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel dire bear form", "dire bear form", 9634)
{
}
};
class CastCancelCatFormAction : public CastCancelDruidAction
{
public:
CastCancelCatFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel cat form", "cat form", 768) {}
};
class CastCancelMoonkinFormAction : public CastCancelDruidAction
{
public:
CastCancelMoonkinFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel moonkin form", "moonkin form", 24858)
{
}
};
class CastCancelAquaticFormAction : public CastCancelDruidAction
{
public:
CastCancelAquaticFormAction(PlayerbotAI* botAI)
: CastCancelDruidAction(botAI, "cancel aquatic form", "aquatic form", 1066)
{
}
}; };
#endif #endif

View File

@ -170,6 +170,12 @@ public:
creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form; creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form;
creators["caster form"] = &DruidAiObjectContextInternal::caster_form; creators["caster form"] = &DruidAiObjectContextInternal::caster_form;
creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form; creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form;
creators["cancel travel form"] = &DruidAiObjectContextInternal::cancel_travel_form;
creators["cancel bear form"] = &DruidAiObjectContextInternal::cancel_bear_form;
creators["cancel dire bear form"] = &DruidAiObjectContextInternal::cancel_dire_bear_form;
creators["cancel cat form"] = &DruidAiObjectContextInternal::cancel_cat_form;
creators["cancel moonkin form"] = &DruidAiObjectContextInternal::cancel_moonkin_form;
creators["cancel aquatic form"] = &DruidAiObjectContextInternal::cancel_aquatic_form;
creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear; creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear;
creators["maul"] = &DruidAiObjectContextInternal::maul; creators["maul"] = &DruidAiObjectContextInternal::maul;
creators["bash"] = &DruidAiObjectContextInternal::bash; creators["bash"] = &DruidAiObjectContextInternal::bash;
@ -258,6 +264,12 @@ private:
static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); } static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); }
static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); } static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); }
static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); } static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); }
static Action* cancel_travel_form(PlayerbotAI* botAI) { return new CastCancelTravelFormAction(botAI); }
static Action* cancel_bear_form(PlayerbotAI* botAI) { return new CastCancelBearFormAction(botAI); }
static Action* cancel_dire_bear_form(PlayerbotAI* botAI) { return new CastCancelDireBearFormAction(botAI); }
static Action* cancel_cat_form(PlayerbotAI* botAI) { return new CastCancelCatFormAction(botAI); }
static Action* cancel_moonkin_form(PlayerbotAI* botAI) { return new CastCancelMoonkinFormAction(botAI); }
static Action* cancel_aquatic_form(PlayerbotAI* botAI) { return new CastCancelAquaticFormAction(botAI); }
static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); } static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); }
static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); } static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); }
static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); }

View File

@ -228,7 +228,7 @@ void CatDpsDruidStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
); );
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"combo points available", "combo points 5 available",
{ {
NextAction("rip", ACTION_HIGH + 6) NextAction("rip", ACTION_HIGH + 6)
} }

View File

@ -176,7 +176,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
); );
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"combo points available", "combo points 5 available",
{ {
NextAction("rip", ACTION_HIGH + 6) NextAction("rip", ACTION_HIGH + 6)
} }
@ -257,7 +257,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
); );
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"low energy", "tiger's fury",
{ {
NextAction("tiger's fury", ACTION_NORMAL + 1) NextAction("tiger's fury", ACTION_NORMAL + 1)
} }

View File

@ -472,9 +472,8 @@ Unit* CastRighteousDefenseAction::GetTarget()
{ {
Unit* current_target = AI_VALUE(Unit*, "current target"); Unit* current_target = AI_VALUE(Unit*, "current target");
if (!current_target) if (!current_target)
{ return nullptr;
return NULL;
}
return current_target->GetVictim(); return current_target->GetVictim();
} }

View File

@ -91,9 +91,8 @@ public:
class CastBlessingOnPartyAction : public BuffOnPartyAction class CastBlessingOnPartyAction : public BuffOnPartyAction
{ {
public: public:
CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) : BuffOnPartyAction(botAI, name), name(name) CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name)
{ : BuffOnPartyAction(botAI, name), name(name) {}
}
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
@ -154,9 +153,7 @@ public:
class CastBlessingOfSanctuaryOnPartyAction : public BuffOnPartyAction class CastBlessingOfSanctuaryOnPartyAction : public BuffOnPartyAction
{ {
public: public:
CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {}
{
}
std::string const getName() override { return "blessing of sanctuary on party"; } std::string const getName() override { return "blessing of sanctuary on party"; }
Value<Unit*>* GetTargetValue() override; Value<Unit*>* GetTargetValue() override;
@ -173,18 +170,14 @@ class CastHolyShockOnPartyAction : public HealPartyMemberAction
{ {
public: public:
CastHolyShockOnPartyAction(PlayerbotAI* botAI) CastHolyShockOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) {}
{
}
}; };
class CastHolyLightOnPartyAction : public HealPartyMemberAction class CastHolyLightOnPartyAction : public HealPartyMemberAction
{ {
public: public:
CastHolyLightOnPartyAction(PlayerbotAI* botAI) CastHolyLightOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) {}
{
}
}; };
class CastFlashOfLightAction : public CastHealingSpellAction class CastFlashOfLightAction : public CastHealingSpellAction
@ -197,9 +190,7 @@ class CastFlashOfLightOnPartyAction : public HealPartyMemberAction
{ {
public: public:
CastFlashOfLightOnPartyAction(PlayerbotAI* botAI) CastFlashOfLightOnPartyAction(PlayerbotAI* botAI)
: HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) {}
{
}
}; };
class CastLayOnHandsAction : public CastHealingSpellAction class CastLayOnHandsAction : public CastHealingSpellAction
@ -357,9 +348,7 @@ class CastHammerOfJusticeOnEnemyHealerAction : public CastSpellOnEnemyHealerActi
{ {
public: public:
CastHammerOfJusticeOnEnemyHealerAction(PlayerbotAI* botAI) CastHammerOfJusticeOnEnemyHealerAction(PlayerbotAI* botAI)
: CastSpellOnEnemyHealerAction(botAI, "hammer of justice") : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") {}
{
}
}; };
class CastHammerOfJusticeSnareAction : public CastSnareSpellAction class CastHammerOfJusticeSnareAction : public CastSnareSpellAction
@ -368,6 +357,12 @@ public:
CastHammerOfJusticeSnareAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "hammer of justice") {} CastHammerOfJusticeSnareAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "hammer of justice") {}
}; };
class CastSenseUndeadAction : public CastBuffSpellAction
{
public:
CastSenseUndeadAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "sense undead") {}
};
class CastTurnUndeadAction : public CastBuffSpellAction class CastTurnUndeadAction : public CastBuffSpellAction
{ {
public: public:
@ -381,25 +376,25 @@ PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection");
class CastDivinePleaAction : public CastBuffSpellAction class CastDivinePleaAction : public CastBuffSpellAction
{ {
public: public:
CastDivinePleaAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "divine plea") {} CastDivinePleaAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "divine plea") {}
}; };
class ShieldOfRighteousnessAction : public CastMeleeSpellAction class ShieldOfRighteousnessAction : public CastMeleeSpellAction
{ {
public: public:
ShieldOfRighteousnessAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "shield of righteousness") {} ShieldOfRighteousnessAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shield of righteousness") {}
}; };
class CastBeaconOfLightOnMainTankAction : public BuffOnMainTankAction class CastBeaconOfLightOnMainTankAction : public BuffOnMainTankAction
{ {
public: public:
CastBeaconOfLightOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "beacon of light", true) {} CastBeaconOfLightOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "beacon of light", true) {}
}; };
class CastSacredShieldOnMainTankAction : public BuffOnMainTankAction class CastSacredShieldOnMainTankAction : public BuffOnMainTankAction
{ {
public: public:
CastSacredShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "sacred shield", false) {} CastSacredShieldOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "sacred shield", false) {}
}; };
class CastAvengingWrathAction : public CastBuffSpellAction class CastAvengingWrathAction : public CastBuffSpellAction
@ -428,4 +423,5 @@ public:
bool Execute(Event event) override; bool Execute(Event event) override;
bool isUseful() override; bool isUseful() override;
}; };
#endif #endif

View File

@ -132,6 +132,7 @@ public:
&PaladinTriggerFactoryInternal::hammer_of_justice_on_enemy_target; &PaladinTriggerFactoryInternal::hammer_of_justice_on_enemy_target;
creators["hammer of justice on snare target"] = creators["hammer of justice on snare target"] =
&PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target; &PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target;
creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead;
creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor; creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor;
creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead; creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead;
creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield; creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield;
@ -151,6 +152,7 @@ public:
} }
private: private:
static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); }
static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); } static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); }
static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); } static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); }
static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); } static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); }
@ -288,6 +290,7 @@ public:
creators["hammer of justice on snare target"] = creators["hammer of justice on snare target"] =
&PaladinAiObjectContextInternal::hammer_of_justice_on_snare_target; &PaladinAiObjectContextInternal::hammer_of_justice_on_snare_target;
creators["divine favor"] = &PaladinAiObjectContextInternal::divine_favor; creators["divine favor"] = &PaladinAiObjectContextInternal::divine_favor;
creators["sense undead"] = &PaladinAiObjectContextInternal::sense_undead;
creators["turn undead"] = &PaladinAiObjectContextInternal::turn_undead; creators["turn undead"] = &PaladinAiObjectContextInternal::turn_undead;
creators["blessing of protection on party"] = &PaladinAiObjectContextInternal::blessing_of_protection_on_party; creators["blessing of protection on party"] = &PaladinAiObjectContextInternal::blessing_of_protection_on_party;
creators["righteous defense"] = &PaladinAiObjectContextInternal::righteous_defense; creators["righteous defense"] = &PaladinAiObjectContextInternal::righteous_defense;
@ -312,6 +315,7 @@ private:
{ {
return new CastBlessingOfProtectionProtectAction(botAI); return new CastBlessingOfProtectionProtectAction(botAI);
} }
static Action* sense_undead(PlayerbotAI* botAI) { return new CastSenseUndeadAction(botAI); }
static Action* turn_undead(PlayerbotAI* botAI) { return new CastTurnUndeadAction(botAI); } static Action* turn_undead(PlayerbotAI* botAI) { return new CastTurnUndeadAction(botAI); }
static Action* divine_favor(PlayerbotAI* botAI) { return new CastDivineFavorAction(botAI); } static Action* divine_favor(PlayerbotAI* botAI) { return new CastDivineFavorAction(botAI); }
static Action* righteous_fury(PlayerbotAI* botAI) { return new CastRighteousFuryAction(botAI); } static Action* righteous_fury(PlayerbotAI* botAI) { return new CastRighteousFuryAction(botAI); }

View File

@ -15,9 +15,6 @@ public:
{ {
creators["sanctity aura"] = &sanctity_aura; creators["sanctity aura"] = &sanctity_aura;
creators["retribution aura"] = &retribution_aura; creators["retribution aura"] = &retribution_aura;
creators["seal of corruption"] = &seal_of_corruption;
creators["seal of vengeance"] = &seal_of_vengeance;
creators["seal of command"] = &seal_of_command;
creators["blessing of might"] = &blessing_of_might; creators["blessing of might"] = &blessing_of_might;
creators["crusader strike"] = &crusader_strike; creators["crusader strike"] = &crusader_strike;
creators["repentance"] = &repentance; creators["repentance"] = &repentance;
@ -27,36 +24,6 @@ public:
} }
private: private:
static ActionNode* seal_of_corruption([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"seal of corruption",
/*P*/ {},
/*A*/ { NextAction("seal of vengeance") },
/*C*/ {}
);
}
static ActionNode* seal_of_vengeance([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"seal of vengeance",
/*P*/ {},
/*A*/ { NextAction("seal of command") },
/*C*/ {}
);
}
static ActionNode* seal_of_command([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {}
);
}
static ActionNode* blessing_of_might([[maybe_unused]] PlayerbotAI* botAI) static ActionNode* blessing_of_might([[maybe_unused]] PlayerbotAI* botAI)
{ {
return new ActionNode( return new ActionNode(

View File

@ -19,14 +19,15 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector<TriggerNode*>& tr
NonCombatStrategy::InitTriggers(triggers); NonCombatStrategy::InitTriggers(triggers);
triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) })); triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) }));
triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", 25.0f) })); triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 5.0f) }));
triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", 26.0f) })); triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 6.0f) }));
triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", 27.0f) })); triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 7.0f) }));
triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", 28.0f) })); triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 8.0f) }));
triggers.push_back(new TriggerNode("not sensing undead", { NextAction("sense undead", ACTION_IDLE + 1.0f) }));
int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot()); int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot());
if (specTab == 0 || specTab == 1) // Holy or Protection if (specTab == PALADIN_TAB_HOLY || specTab == PALADIN_TAB_PROTECTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) }));
if (specTab == 2) // Retribution if (specTab == PALADIN_TAB_RETRIBUTION)
triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) }));
} }

View File

@ -22,6 +22,9 @@ public:
creators["cleanse magic"] = &cleanse_magic; creators["cleanse magic"] = &cleanse_magic;
creators["cleanse poison on party"] = &cleanse_poison_on_party; creators["cleanse poison on party"] = &cleanse_poison_on_party;
creators["cleanse disease on party"] = &cleanse_disease_on_party; creators["cleanse disease on party"] = &cleanse_disease_on_party;
creators["seal of corruption"] = &seal_of_corruption;
creators["seal of vengeance"] = &seal_of_vengeance;
creators["seal of command"] = &seal_of_command;
creators["seal of wisdom"] = &seal_of_wisdom; creators["seal of wisdom"] = &seal_of_wisdom;
creators["seal of justice"] = &seal_of_justice; creators["seal of justice"] = &seal_of_justice;
creators["hand of reckoning"] = &hand_of_reckoning; creators["hand of reckoning"] = &hand_of_reckoning;
@ -41,7 +44,6 @@ public:
creators["blessing of wisdom on party"] = &blessing_of_wisdom_on_party; creators["blessing of wisdom on party"] = &blessing_of_wisdom_on_party;
creators["blessing of sanctuary on party"] = &blessing_of_sanctuary_on_party; creators["blessing of sanctuary on party"] = &blessing_of_sanctuary_on_party;
creators["blessing of sanctuary"] = &blessing_of_sanctuary; creators["blessing of sanctuary"] = &blessing_of_sanctuary;
creators["seal of command"] = &seal_of_command;
creators["taunt spell"] = &hand_of_reckoning; creators["taunt spell"] = &hand_of_reckoning;
creators["righteous defense"] = &righteous_defense; creators["righteous defense"] = &righteous_defense;
creators["avenger's shield"] = &avengers_shield; creators["avenger's shield"] = &avengers_shield;
@ -155,18 +157,39 @@ private:
/*A*/ { NextAction("purify disease on party") }, /*A*/ { NextAction("purify disease on party") },
/*C*/ {}); /*C*/ {});
} }
static ActionNode* seal_of_corruption(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of corruption",
/*P*/ {},
/*A*/ { NextAction("seal of vengeance") },
/*C*/ {});
}
static ActionNode* seal_of_vengeance(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of vengeance",
/*P*/ {},
/*A*/ { NextAction("seal of command") },
/*C*/ {});
}
static ActionNode* seal_of_command(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {});
}
static ActionNode* seal_of_wisdom(PlayerbotAI* /* ai */) static ActionNode* seal_of_wisdom(PlayerbotAI* /* ai */)
{ {
return new ActionNode ("seal of wisdom", return new ActionNode ("seal of wisdom",
/*P*/ {}, /*P*/ {},
/*A*/ { NextAction("seal of righteousness") }, /*A*/ { NextAction("seal of corruption") },
/*C*/ {}); /*C*/ {});
} }
static ActionNode* seal_of_justice(PlayerbotAI* /* ai */) static ActionNode* seal_of_justice(PlayerbotAI* /* ai */)
{ {
return new ActionNode("seal of justice", return new ActionNode("seal of justice",
/*P*/ {}, /*P*/ {},
/*A*/ { NextAction("seal of righteousness") }, /*A*/ { NextAction("seal of corruption") },
/*C*/ {}); /*C*/ {});
} }
static ActionNode* hand_of_reckoning(PlayerbotAI* /* ai */) static ActionNode* hand_of_reckoning(PlayerbotAI* /* ai */)
@ -246,13 +269,6 @@ private:
/*A*/ {}, /*A*/ {},
/*C*/ {}); /*C*/ {});
} }
static ActionNode* seal_of_command(PlayerbotAI* /* ai */)
{
return new ActionNode("seal of command",
/*P*/ {},
/*A*/ { NextAction("seal of righteousness") },
/*C*/ {});
}
}; };
#endif #endif

View File

@ -30,7 +30,7 @@ void HealPaladinStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
new TriggerNode( new TriggerNode(
"seal", "seal",
{ {
NextAction("seal of wisdom", ACTION_HIGH) NextAction("seal of wisdom", ACTION_HIGH),
} }
) )
); );

View File

@ -30,3 +30,8 @@ bool BlessingTrigger::IsActive()
return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom",
"blessing of kings", "blessing of sanctuary", nullptr); "blessing of kings", "blessing of sanctuary", nullptr);
} }
bool NotSensingUndeadTrigger::IsActive()
{
return !botAI->HasAura("sense undead", bot);
}

View File

@ -77,9 +77,7 @@ class BlessingOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOnPartyTrigger(PlayerbotAI* botAI) BlessingOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) {}
{
}
}; };
class BlessingTrigger : public BuffTrigger class BlessingTrigger : public BuffTrigger
@ -93,7 +91,8 @@ public:
class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger
{ {
public: public:
HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "hammer of justice") {} HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI)
: InterruptSpellTrigger(botAI, "hammer of justice") {}
}; };
class HammerOfJusticeSnareTrigger : public SnareTargetTrigger class HammerOfJusticeSnareTrigger : public SnareTargetTrigger
@ -144,9 +143,7 @@ class CleanseCurePartyMemberDiseaseTrigger : public PartyMemberNeedCureTrigger
{ {
public: public:
CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI) CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) {}
{
}
}; };
class CleanseCurePoisonTrigger : public NeedCureTrigger class CleanseCurePoisonTrigger : public NeedCureTrigger
@ -159,9 +156,7 @@ class CleanseCurePartyMemberPoisonTrigger : public PartyMemberNeedCureTrigger
{ {
public: public:
CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI) CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI)
: PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) {}
{
}
}; };
class CleanseCureMagicTrigger : public NeedCureTrigger class CleanseCureMagicTrigger : public NeedCureTrigger
@ -173,15 +168,15 @@ public:
class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger
{ {
public: public:
CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI)
{ : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) {}
}
}; };
class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger
{ {
public: public:
HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI)
: InterruptEnemyHealerTrigger(botAI, "hammer of justice") {}
}; };
class DivineFavorTrigger : public BuffTrigger class DivineFavorTrigger : public BuffTrigger
@ -190,6 +185,14 @@ public:
DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {}
}; };
class NotSensingUndeadTrigger : public BuffTrigger
{
public:
NotSensingUndeadTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "not sensing undead") {}
bool IsActive() override;
};
class TurnUndeadTrigger : public HasCcTargetTrigger class TurnUndeadTrigger : public HasCcTargetTrigger
{ {
public: public:
@ -201,7 +204,8 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield");
class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger
{ {
public: public:
BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "beacon of light", true) {} BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai)
: BuffOnMainTankTrigger(ai, "beacon of light", true) {}
}; };
class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger
@ -213,34 +217,29 @@ public:
class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {}
}; };
class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
{
}
}; };
class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {}
{
}
}; };
class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger
{ {
public: public:
BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI)
: BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {}
{
}
}; };
class AvengingWrathTrigger : public BoostTrigger class AvengingWrathTrigger : public BoostTrigger
@ -248,4 +247,5 @@ class AvengingWrathTrigger : public BoostTrigger
public: public:
AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {}
}; };
#endif #endif

View File

@ -61,7 +61,7 @@ bool CastEnvenomAction::isUseful()
bool CastEnvenomAction::isPossible() bool CastEnvenomAction::isPossible()
{ {
// alternate to eviscerate if talents unlearned // alternate to eviscerate if talents unlearned
return botAI->HasAura(58410, bot) /* Master Poisoner */; return botAI->HasAura(58410, bot) /* Master Poisoner Rank 3 */;
} }
bool CastTricksOfTheTradeOnMainTankAction::isUseful() bool CastTricksOfTheTradeOnMainTankAction::isUseful()

View File

@ -78,6 +78,12 @@ public:
CastFeintAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feint") {} CastFeintAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feint") {}
}; };
class CastColdBloodAction : public CastBuffSpellAction
{
public:
CastColdBloodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold blood") {}
};
class CastDismantleAction : public CastSpellAction class CastDismantleAction : public CastSpellAction
{ {
public: public:

View File

@ -143,6 +143,7 @@ public:
creators["use instant poison on off hand"] = &RogueAiObjectContextInternal::use_instant_poison_off_hand; creators["use instant poison on off hand"] = &RogueAiObjectContextInternal::use_instant_poison_off_hand;
creators["fan of knives"] = &RogueAiObjectContextInternal::fan_of_knives; creators["fan of knives"] = &RogueAiObjectContextInternal::fan_of_knives;
creators["killing spree"] = &RogueAiObjectContextInternal::killing_spree; creators["killing spree"] = &RogueAiObjectContextInternal::killing_spree;
creators["cold blood"] = &RogueAiObjectContextInternal::cold_blood;
} }
private: private:
@ -184,6 +185,7 @@ private:
static Action* use_instant_poison_off_hand(PlayerbotAI* ai) { return new UseInstantPoisonOffHandAction(ai); } static Action* use_instant_poison_off_hand(PlayerbotAI* ai) { return new UseInstantPoisonOffHandAction(ai); }
static Action* fan_of_knives(PlayerbotAI* ai) { return new FanOfKnivesAction(ai); } static Action* fan_of_knives(PlayerbotAI* ai) { return new FanOfKnivesAction(ai); }
static Action* killing_spree(PlayerbotAI* ai) { return new CastKillingSpreeAction(ai); } static Action* killing_spree(PlayerbotAI* ai) { return new CastKillingSpreeAction(ai); }
static Action* cold_blood(PlayerbotAI* ai) { return new CastColdBloodAction(ai); }
}; };
SharedNamedObjectContextList<Strategy> RogueAiObjectContext::sharedStrategyContexts; SharedNamedObjectContextList<Strategy> RogueAiObjectContext::sharedStrategyContexts;

View File

@ -29,7 +29,7 @@ private:
return new ActionNode( return new ActionNode(
"envenom", "envenom",
/*P*/ {}, /*P*/ {},
/*A*/ { NextAction("rupture") }, /*A*/ { NextAction("eviscerate") },
/*C*/ {} /*C*/ {}
); );
} }
@ -108,10 +108,10 @@ void AssassinationRogueStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"combo points 3 available", "combo points 4 available",
{ {
NextAction("envenom", ACTION_HIGH + 5), NextAction("cold blood", ACTION_HIGH + 6),
NextAction("eviscerate", ACTION_HIGH + 3) NextAction("envenom", ACTION_HIGH + 5)
} }
) )
); );
@ -120,8 +120,7 @@ void AssassinationRogueStrategy::InitTriggers(std::vector<TriggerNode*>& trigger
new TriggerNode( new TriggerNode(
"target with combo points almost dead", "target with combo points almost dead",
{ {
NextAction("envenom", ACTION_HIGH + 4), NextAction("envenom", ACTION_HIGH + 4)
NextAction("eviscerate", ACTION_HIGH + 2)
} }
) )
); );

View File

@ -12,36 +12,14 @@ class DpsRogueStrategyActionNodeFactory : public NamedObjectFactory<ActionNode>
public: public:
DpsRogueStrategyActionNodeFactory() DpsRogueStrategyActionNodeFactory()
{ {
creators["mutilate"] = &mutilate;
creators["sinister strike"] = &sinister_strike; creators["sinister strike"] = &sinister_strike;
creators["kick"] = &kick; creators["kick"] = &kick;
creators["kidney shot"] = &kidney_shot; creators["kidney shot"] = &kidney_shot;
creators["backstab"] = &backstab; creators["backstab"] = &backstab;
creators["melee"] = &melee;
creators["rupture"] = &rupture; creators["rupture"] = &rupture;
} }
private: private:
static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"melee",
/*P*/ {},
/*A*/ {
NextAction("mutilate") },
/*C*/ {}
);
}
static ActionNode* mutilate([[maybe_unused]] PlayerbotAI* botAI)
{
return new ActionNode(
"mutilate",
/*P*/ {},
/*A*/ {
NextAction("sinister strike") },
/*C*/ {}
);
}
static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI) static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI)
{ {
return new ActionNode( return new ActionNode(
@ -77,7 +55,7 @@ private:
"backstab", "backstab",
/*P*/ {}, /*P*/ {},
/*A*/ { /*A*/ {
NextAction("mutilate") }, NextAction("sinister strike") },
/*C*/ {} /*C*/ {}
); );
} }
@ -140,7 +118,7 @@ void DpsRogueStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"combo points available", "combo points 5 available",
{ {
NextAction("rupture", ACTION_HIGH + 1), NextAction("rupture", ACTION_HIGH + 1),
NextAction("eviscerate", ACTION_HIGH) NextAction("eviscerate", ACTION_HIGH)
@ -335,7 +313,7 @@ void StealthedRogueStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{ {
triggers.push_back( triggers.push_back(
new TriggerNode( new TriggerNode(
"combo points available", "combo points 5 available",
{ {
NextAction("eviscerate", ACTION_HIGH) NextAction("eviscerate", ACTION_HIGH)
} }

View File

@ -231,7 +231,6 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/)
return false; return false;
auto& data = *dataPtr; auto& data = *dataPtr;
uint32 questId = data.questId; uint32 questId = data.questId;
const Quest* quest = data.quest;
uint8 questStatus = bot->GetQuestStatus(questId); uint8 questStatus = bot->GetQuestStatus(questId);
switch (questStatus) switch (questStatus)
{ {
@ -438,7 +437,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE)
return MoveFarTo(flightMaster); return MoveFarTo(flightMaster);
std::vector<uint32> nodes = {data.fromNode, data.toNode}; std::vector<uint32> nodes = data.path;
botAI->RemoveShapeshift(); botAI->RemoveShapeshift();
if (bot->IsMounted()) if (bot->IsMounted())
@ -447,7 +446,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/)
if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0)) if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0))
{ {
LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(),
flightMaster->GetEntry(), nodes[0], nodes[1]); flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]);
botAI->rpgInfo.ChangeToIdle(); botAI->rpgInfo.ChangeToIdle();
} }
return true; return true;

View File

@ -3,7 +3,6 @@
#include "BroadcastHelper.h" #include "BroadcastHelper.h"
#include "ChatHelper.h" #include "ChatHelper.h"
#include "Creature.h" #include "Creature.h"
#include "FlightMasterCache.h"
#include "G3D/Vector2.h" #include "G3D/Vector2.h"
#include "GameObject.h" #include "GameObject.h"
#include "GossipDef.h" #include "GossipDef.h"
@ -856,7 +855,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
{ {
const std::vector<WorldLocation>& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()]; const std::vector<WorldLocation>& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel());
float hiRange = 500.0f; float hiRange = 500.0f;
float loRange = 2500.0f; float loRange = 2500.0f;
if (bot->GetLevel() < 5) if (bot->GetLevel() < 5)
@ -914,9 +913,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
{ {
const std::vector<WorldLocation>& locs = IsAlliance(bot->getRace()) const std::vector<WorldLocation> locs = sTravelMgr.GetTravelHubs(bot);
? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()];
bool inCity = false; bool inCity = false;
@ -957,70 +954,19 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot)
return dest; return dest;
} }
bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode) bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector<uint32>& path)
{ {
Creature* nearestFlightMaster = FlightMasterCache::Instance().GetNearestFlightMaster(bot); flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot);
if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) if (!flightMaster)
return false; return false;
fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), std::vector<std::vector<uint32>> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot);
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), if (availablePaths.empty())
bot->GetTeamId());
if (!fromNode)
return false; return false;
std::vector<uint32> availableToNodes; path = availablePaths[urand(0, availablePaths.size() - 1)];
for (uint32 i = 1; i < sTaxiNodesStore.GetNumRows(); ++i)
{
if (fromNode == i)
continue;
TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(i);
// check map
if (!node || node->map_id != bot->GetMapId() ||
(!node->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0])) // dk flight
continue;
// check taxi node known
if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(i))
continue;
// check distance by level
if (!botAI->CheckLocationDistanceByLevel(bot, WorldLocation(node->map_id, node->x, node->y, node->z), false))
continue;
// check path
uint32 path, cost;
sObjectMgr->GetTaxiPath(fromNode, i, path, cost);
if (!path)
continue;
// check area level
uint32 nodeZoneId = bot->GetMap()->GetZoneId(bot->GetPhaseMask(), node->x, node->y, node->z);
bool capital = false;
if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(nodeZoneId))
{
capital = zone->flags & AREA_FLAG_CAPITAL;
}
auto itr = sRandomPlayerbotMgr.zone2LevelBracket.find(nodeZoneId);
if (!capital && itr == sRandomPlayerbotMgr.zone2LevelBracket.end())
continue;
if (!capital && (bot->GetLevel() < itr->second.low || bot->GetLevel() > itr->second.high))
continue;
availableToNodes.push_back(i);
}
if (availableToNodes.empty())
return false;
flightMaster = nearestFlightMaster->GetGUID();
toNode = availableToNodes[urand(0, availableToNodes.size() - 1)];
LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)",
bot->GetName(), flightMaster.GetEntry(), fromNode, toNode, availableToNodes.size()); bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size());
return true; return true;
} }
@ -1121,10 +1067,10 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector<NewRpgStatus> candidateSta
case RPG_TRAVEL_FLIGHT: case RPG_TRAVEL_FLIGHT:
{ {
ObjectGuid flightMaster; ObjectGuid flightMaster;
uint32 fromNode, toNode; std::vector<uint32> path;
if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode)) if (SelectRandomFlightTaxiNode(flightMaster, path))
{ {
botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode); botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path);
return true; return true;
} }
return false; return false;
@ -1197,8 +1143,8 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status)
case RPG_TRAVEL_FLIGHT: case RPG_TRAVEL_FLIGHT:
{ {
ObjectGuid flightMaster; ObjectGuid flightMaster;
uint32 fromNode, toNode; std::vector<uint32> path;
return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode); return SelectRandomFlightTaxiNode(flightMaster, path);
} }
default: default:
return false; return false;

View File

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

View File

@ -37,13 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
data = do_quest; data = do_quest;
} }
void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode) void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path)
{ {
startT = getMSTime(); startT = getMSTime();
TravelFlight flight; TravelFlight flight;
flight.fromFlightMaster = fromFlightMaster; flight.fromFlightMaster = fromFlightMaster;
flight.fromNode = fromNode; flight.path = std::move(path);
flight.toNode = toNode;
flight.inFlight = false; flight.inFlight = false;
data = flight; data = flight;
} }
@ -150,8 +149,8 @@ std::string NewRpgInfo::ToString()
{ {
out << "TRAVEL_FLIGHT"; out << "TRAVEL_FLIGHT";
out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry();
out << "\nfromNode: " << arg.fromNode; out << "\nfromNode: " << arg.path[0];
out << "\ntoNode: " << arg.toNode; out << "\ntoNode: " << arg.path[arg.path.size() - 1];
out << "\ninFlight: " << arg.inFlight; out << "\ninFlight: " << arg.inFlight;
} }
else else

View File

@ -50,8 +50,7 @@ struct NewRpgInfo
struct TravelFlight struct TravelFlight
{ {
ObjectGuid fromFlightMaster{}; ObjectGuid fromFlightMaster{};
uint32 fromNode{0}; std::vector<uint32> path;
uint32 toNode{0};
bool inFlight{false}; bool inFlight{false};
}; };
// RPG_REST // RPG_REST
@ -91,7 +90,7 @@ struct NewRpgInfo
void ChangeToWanderNpc(); void ChangeToWanderNpc();
void ChangeToWanderRandom(); void ChangeToWanderRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest); void ChangeToDoQuest(uint32 questId, const Quest* quest);
void ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode); void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector<uint32> path);
void ChangeToRest(); void ChangeToRest();
void ChangeToIdle(); void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status); bool CanChangeTo(NewRpgStatus status);

View File

@ -762,7 +762,7 @@ void PlayerbotFactory::InitPetTalents()
// pet_family->petTalentType); // pet_family->petTalentType);
return; return;
} }
std::unordered_map<uint32, std::vector<TalentEntry const*>> spells; std::map<uint32, std::vector<TalentEntry const*>> spells;
bool diveTypePet = (1LL << ci->family) & diveMask; bool diveTypePet = (1LL << ci->family) & diveMask;
for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i)
@ -2553,17 +2553,15 @@ void PlayerbotFactory::InitClassSpells()
bot->learnSpell(7386, false); // Sunder Armor bot->learnSpell(7386, false); // Sunder Armor
} }
if (level >= 30) if (level >= 30)
{
bot->learnSpell(2458, false); // Berserker Stance bot->learnSpell(2458, false); // Berserker Stance
}
break; break;
case CLASS_PALADIN: case CLASS_PALADIN:
bot->learnSpell(21084, true); bot->learnSpell(21084, true);
bot->learnSpell(635, true); bot->learnSpell(635, true);
if (level >= 12) if (level >= 12)
{
bot->learnSpell(7328, false); // Redemption bot->learnSpell(7328, false); // Redemption
} if (level >= 20)
bot->learnSpell(5502, false); // Sense Undead
break; break;
case CLASS_ROGUE: case CLASS_ROGUE:
bot->learnSpell(1752, true); bot->learnSpell(1752, true);
@ -2605,17 +2603,11 @@ void PlayerbotFactory::InitClassSpells()
bot->learnSpell(686, true); bot->learnSpell(686, true);
bot->learnSpell(688, false); // summon imp bot->learnSpell(688, false); // summon imp
if (level >= 10) if (level >= 10)
{
bot->learnSpell(697, false); // summon voidwalker bot->learnSpell(697, false); // summon voidwalker
}
if (level >= 20) if (level >= 20)
{
bot->learnSpell(712, false); // summon succubus bot->learnSpell(712, false); // summon succubus
}
if (level >= 30) if (level >= 30)
{
bot->learnSpell(691, false); // summon felhunter bot->learnSpell(691, false); // summon felhunter
}
break; break;
case CLASS_DRUID: case CLASS_DRUID:
bot->learnSpell(5176, true); bot->learnSpell(5176, true);
@ -2632,17 +2624,11 @@ void PlayerbotFactory::InitClassSpells()
bot->learnSpell(331, true); bot->learnSpell(331, true);
// bot->learnSpell(66747, true); // Totem of the Earthen Ring // bot->learnSpell(66747, true); // Totem of the Earthen Ring
if (level >= 4) if (level >= 4)
{
bot->learnSpell(8071, false); // stoneskin totem bot->learnSpell(8071, false); // stoneskin totem
}
if (level >= 10) if (level >= 10)
{
bot->learnSpell(3599, false); // searing totem bot->learnSpell(3599, false); // searing totem
}
if (level >= 20) if (level >= 20)
{
bot->learnSpell(5394, false); // healing stream totem bot->learnSpell(5394, false); // healing stream totem
}
break; break;
default: default:
break; break;
@ -2667,7 +2653,7 @@ void PlayerbotFactory::InitSpecialSpells()
void PlayerbotFactory::InitTalents(uint32 specNo) void PlayerbotFactory::InitTalents(uint32 specNo)
{ {
uint32 classMask = bot->getClassMask(); uint32 classMask = bot->getClassMask();
std::unordered_map<uint32, std::vector<TalentEntry const*>> spells; std::map<uint32, std::vector<TalentEntry const*>> spells;
for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i)
{ {
TalentEntry const* talentInfo = sTalentStore.LookupEntry(i); TalentEntry const* talentInfo = sTalentStore.LookupEntry(i);
@ -3342,18 +3328,36 @@ void PlayerbotFactory::InitReagents()
items.push_back({44615, 40}); // Devout Candle items.push_back({44615, 40}); // Devout Candle
break; break;
case CLASS_SHAMAN: case CLASS_SHAMAN:
{
HasRelicBySubclassVisitor relicVisitor(ITEM_SUBCLASS_ARMOR_TOTEM);
IterateItems(&relicVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP));
bool hasRelic = relicVisitor.found;
if (!hasRelic)
{
if (level >= 4) if (level >= 4)
items.push_back({5175, 1}); // Earth Totem items.push_back({5175, 1}); // Earth Totem
if (level >= 10) if (level >= 10)
items.push_back({5176, 1}); // Flame Totem items.push_back({5176, 1}); // Flame Totem
if (level >= 20) if (level >= 20)
items.push_back({5177, 1}); // Water Totem items.push_back({5177, 1}); // Water Totem
}
else
{
ItemIds totemIds = {5175, 5176, 5177, 5178};
FindItemByIdsVisitor totemVisitor(totemIds);
IterateItems(&totemVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP | ITERATE_ITEMS_IN_BANK));
for (Item* item : totemVisitor.GetResult())
bot->DestroyItem(item->GetBagSlot(), item->GetSlot(), true);
}
if (level >= 30) if (level >= 30)
{ {
if (!hasRelic)
items.push_back({5178, 1}); // Air Totem items.push_back({5178, 1}); // Air Totem
items.push_back({17030, 20}); // Ankh items.push_back({17030, 20}); // Ankh
} }
break; break;
}
case CLASS_WARLOCK: case CLASS_WARLOCK:
items.push_back({6265, 5}); // Soul Shard items.push_back({6265, 5}); // Soul Shard
break; break;

View File

@ -1119,6 +1119,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE) if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE)
return; return;
if (lang == LANG_ADDON)
return;
if (p.GetOpcode() == SMSG_GM_MESSAGECHAT) if (p.GetOpcode() == SMSG_GM_MESSAGECHAT)
{ {
p >> textLen; p >> textLen;
@ -1168,8 +1171,6 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID())
return; return;
if (lang == LANG_ADDON)
return;
if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) &&
(GetChatHelper()->ExtractAllItemIds(message).size() > 0 || (GetChatHelper()->ExtractAllItemIds(message).size() > 0 ||
@ -1249,17 +1250,10 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet)
p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed; p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed;
if (horizontalSpeed <= 0.1f) if (horizontalSpeed <= 0.1f)
{
horizontalSpeed = 0.11f; horizontalSpeed = 0.11f;
}
verticalSpeed = -verticalSpeed; verticalSpeed = -verticalSpeed;
// high vertical may result in stuck as bot can not handle gravity
if (verticalSpeed > 35.0f)
break;
// stop casting
InterruptSpell();
// stop movement InterruptSpell();
bot->StopMoving(); bot->StopMoving();
bot->GetMotionMaster()->Clear(); bot->GetMotionMaster()->Clear();
@ -6486,7 +6480,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st
return ChatChannelSource::SRC_UNDEFINED; return ChatChannelSource::SRC_UNDEFINED;
} }
bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp) bool PlayerbotAI::StarterLevelDistanceCheck(Player* player, const WorldLocation& loc, bool fromStartUp)
{ {
if (player->GetLevel() > 16) if (player->GetLevel() > 16)
return true; return true;

View File

@ -556,7 +556,7 @@ public:
bool IsSafe(WorldObject* obj); bool IsSafe(WorldObject* obj);
ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName); ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName);
bool CheckLocationDistanceByLevel(Player* player, const WorldLocation &loc, bool fromStartUp = false); bool StarterLevelDistanceCheck(Player* player, const WorldLocation &loc, bool fromStartUp = false);
bool HasCheat(BotCheatMask mask) bool HasCheat(BotCheatMask mask)
{ {

View File

@ -23,7 +23,6 @@
#include "DatabaseEnv.h" #include "DatabaseEnv.h"
#include "Define.h" #include "Define.h"
#include "FleeManager.h" #include "FleeManager.h"
#include "FlightMasterCache.h"
#include "GridNotifiers.h" #include "GridNotifiers.h"
#include "LFGMgr.h" #include "LFGMgr.h"
#include "MapMgr.h" #include "MapMgr.h"
@ -47,9 +46,7 @@
#include "World.h" #include "World.h"
#include "Cell.h" #include "Cell.h"
#include "GridNotifiers.h" #include "GridNotifiers.h"
// Required for Cell because of poor AC implementation
#include "CellImpl.h" #include "CellImpl.h"
// Required for GridNotifiers because of poor AC implementation
#include "GridNotifiersImpl.h" #include "GridNotifiersImpl.h"
struct GuidClassRaceInfo struct GuidClassRaceInfo
@ -59,48 +56,6 @@ struct GuidClassRaceInfo
uint32 rRace; uint32 rRace;
}; };
enum class CityId : uint8 {
STORMWIND, IRONFORGE, DARNASSUS, EXODAR,
ORGRIMMAR, UNDERCITY, THUNDER_BLUFF, SILVERMOON_CITY,
SHATTRATH_CITY, DALARAN
};
enum class FactionId : uint8 { ALLIANCE, HORDE, NEUTRAL };
// Map of banker entry → city + faction
static const std::unordered_map<uint16, std::pair<CityId, FactionId>> bankerToCity = {
{2455, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2456, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2457, {CityId::STORMWIND, FactionId::ALLIANCE}},
{2460, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {2461, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {5099, {CityId::IRONFORGE, FactionId::ALLIANCE}},
{4155, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4208, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4209, {CityId::DARNASSUS, FactionId::ALLIANCE}},
{17773, {CityId::EXODAR, FactionId::ALLIANCE}}, {18350, {CityId::EXODAR, FactionId::ALLIANCE}}, {16710, {CityId::EXODAR, FactionId::ALLIANCE}},
{3320, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3309, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3318, {CityId::ORGRIMMAR, FactionId::HORDE}},
{4549, {CityId::UNDERCITY, FactionId::HORDE}}, {2459, {CityId::UNDERCITY, FactionId::HORDE}}, {2458, {CityId::UNDERCITY, FactionId::HORDE}}, {4550, {CityId::UNDERCITY, FactionId::HORDE}},
{2996, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8356, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8357, {CityId::THUNDER_BLUFF, FactionId::HORDE}},
{17631, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17632, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17633, {CityId::SILVERMOON_CITY, FactionId::HORDE}},
{16615, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16616, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16617, {CityId::SILVERMOON_CITY, FactionId::HORDE}},
{19246, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}},
{19034, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}},
{30604, {CityId::DALARAN, FactionId::NEUTRAL}}, {30605, {CityId::DALARAN, FactionId::NEUTRAL}}, {30607, {CityId::DALARAN, FactionId::NEUTRAL}},
{28675, {CityId::DALARAN, FactionId::NEUTRAL}}, {28676, {CityId::DALARAN, FactionId::NEUTRAL}}, {28677, {CityId::DALARAN, FactionId::NEUTRAL}}
};
// Map of city → available banker entries
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}}
};
// Quick lookup map: banker entry → location
static std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); } void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); }
void activatePrintStatsThread() void activatePrintStatsThread()
@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
z = 0.05f + ground; z = 0.05f + ground;
if (!botAI->CheckLocationDistanceByLevel(bot, loc, true)) if (!botAI->StarterLevelDistanceCheck(bot, loc, true))
continue; continue;
const LocaleConstant& locale = sWorld->GetDefaultDbcLocale(); const LocaleConstant& locale = sWorld->GetDefaultDbcLocale();
@ -1762,329 +1717,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>&
// tlocs.size()); // tlocs.size());
} }
void RandomPlayerbotMgr::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 - 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 - High - level zones
zone2LevelBracket[10] = {19, 33}; // Duskwood
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 - 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 - 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
// 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
// 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
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
{
zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second};
}
}
void RandomPlayerbotMgr::PrepareTeleportCache()
{
uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
LOG_INFO("playerbots", "Preparing random teleport caches for {} levels...", maxLevel);
QueryResult results = WorldDatabase.Query(
"SELECT "
"g.map, "
"position_x, "
"position_y, "
"position_z, "
"t.minlevel, "
"t.maxlevel "
"FROM "
"(SELECT "
"map, "
"MIN( c.guid ) guid "
"FROM "
"creature c "
"INNER JOIN creature_template t ON c.id1 = t.entry "
"WHERE "
"t.npcflag = 0 "
"AND t.lootid != 0 "
"AND t.maxlevel - t.minlevel < 3 "
"AND map IN ({}) "
"AND t.entry not in (32820, 24196, 30627, 30617) "
"AND c.spawntimesecs < 1000 "
"AND t.faction not in (11, 71, 79, 85, 188, 1575) "
"AND (t.unit_flags & 256) = 0 "
"AND (t.unit_flags & 4096) = 0 "
"AND t.rank = 0 "
// "AND (t.flags_extra & 32768) = 0 "
"GROUP BY "
"map, "
"ROUND(position_x / 50), "
"ROUND(position_y / 50), "
"ROUND(position_z / 50) "
"HAVING "
"count(*) >= 2) "
"AS g "
"INNER JOIN creature c ON g.guid = c.guid "
"INNER JOIN creature_template t on c.id1 = t.entry "
"ORDER BY "
"t.minlevel;",
sPlayerbotAIConfig.randomBotMapsAsString.c_str());
uint32 collected_locs = 0;
if (results)
{
do
{
Field* fields = results->Fetch();
uint16 mapId = fields[0].Get<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
uint32 min_level = fields[4].Get<uint32>();
uint32 max_level = fields[5].Get<uint32>();
uint32 level = (min_level + max_level + 1) / 2;
WorldLocation loc(mapId, x, y, z, 0);
collected_locs++;
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{
if (l < 1 || l > maxLevel)
{
continue;
}
locsPerLevelCache[(uint8)l].push_back(loc);
}
} while (results->NextRow());
}
LOG_INFO("playerbots", ">> {} locations for level collected.", collected_locs);
if (sPlayerbotAIConfig.enableNewRpgStrategy)
{
PrepareZone2LevelBracket();
LOG_INFO("playerbots", "Preparing innkeepers / flightmasters locations for level...");
results = WorldDatabase.Query(
"SELECT "
"map, "
"position_x, "
"position_y, "
"position_z, "
"orientation, "
"t.faction, "
"t.entry, "
"t.npcflag, "
"c.guid "
"FROM "
"creature c "
"INNER JOIN creature_template t on c.id1 = t.entry "
"WHERE "
"t.npcflag & 73728 "
"AND map IN ({}) "
"ORDER BY "
"t.minlevel;",
sPlayerbotAIConfig.randomBotMapsAsString.c_str());
collected_locs = 0;
if (results)
{
do
{
Field* fields = results->Fetch();
uint16 mapId = fields[0].Get<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 faction = fields[5].Get<uint32>();
uint32 tEntry = fields[6].Get<uint32>();
uint32 tNpcflag = fields[7].Get<uint32>();
uint32 guid = fields[8].Get<uint32>();
if (tEntry == 3838 || tEntry == 29480)
continue;
const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction);
WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI);
collected_locs++;
Map* map = sMapMgr->FindMap(loc.GetMapId(), 0);
if (!map)
continue;
bool forHorde = !(entry->hostileMask & 4);
bool forAlliance = !(entry->hostileMask & 2);
if (tNpcflag & UNIT_NPC_FLAG_FLIGHTMASTER)
{
WorldPosition pos(mapId, x, y, z, orient);
if (forHorde)
FlightMasterCache::Instance().AddHordeFlightMaster(guid, pos);
if (forAlliance)
FlightMasterCache::Instance().AddAllianceFlightMaster(guid, pos);
}
const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z));
uint32 zoneId = area->zone ? area->zone : area->ID;
if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end())
continue;
LevelBracket bracket = zone2LevelBracket[zoneId];
for (int i = bracket.low; i <= bracket.high; i++)
{
if (forHorde)
hordeStarterPerLevelCache[i].push_back(loc);
if (forAlliance)
allianceStarterPerLevelCache[i].push_back(loc);
}
} while (results->NextRow());
}
// add all initial position
for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++)
{
for (uint32 j = 1; j < MAX_CLASSES; j++)
{
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j);
if (!info)
continue;
WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
for (int32 l = 1; l <= 5; l++)
{
if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask())
allianceStarterPerLevelCache[(uint8)l].push_back(pos);
else
hordeStarterPerLevelCache[(uint8)l].push_back(pos);
}
break;
}
}
LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs);
}
results = WorldDatabase.Query(
"SELECT "
"map, "
"position_x, "
"position_y, "
"position_z, "
"orientation, "
"t.minlevel, "
"t.entry "
"FROM "
"creature c "
"INNER JOIN creature_template t on c.id1 = t.entry "
"WHERE "
"t.npcflag & 131072 "
"AND t.npcflag != 135298 "
"AND t.minlevel != 55 "
"AND t.minlevel != 65 "
"AND t.faction not in (35, 474, 69, 57) "
"AND t.entry not in (30606, 30608, 29282) "
"AND map IN ({}) "
"ORDER BY "
"t.minlevel;",
sPlayerbotAIConfig.randomBotMapsAsString.c_str());
collected_locs = 0;
if (results)
{
do
{
Field* fields = results->Fetch();
uint16 mapId = fields[0].Get<uint16>();
float x = fields[1].Get<float>();
float y = fields[2].Get<float>();
float z = fields[3].Get<float>();
float orient = fields[4].Get<float>();
uint32 level = fields[5].Get<uint32>();
uint32 entry = fields[6].Get<uint32>();
BankerLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = entry;
collected_locs++;
for (int32 l = 1; l <= maxLevel; l++)
{
// Bots 1-60 go to base game bankers (all have minlevel 30 or 45)
if (l <=60 && level > 45)
{
continue;
}
// Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70)
if ((l >=61 && l <=70) && (level < 60 || level > 70))
{
continue;
}
// Bots 71+ go to Dalaran bankers (all have minlevel 75)
if ((l >=71) && level != 75)
{
continue;
}
bankerLocsPerLevelCache[(uint8)l].push_back(bLoc);
bankerEntryToLocation[bLoc.entry] = bLoc.loc;
}
} while (results->NextRow());
}
LOG_INFO("playerbots", ">> {} banker locations for level collected.", collected_locs);
}
void RandomPlayerbotMgr::PrepareAddclassCache() void RandomPlayerbotMgr::PrepareAddclassCache()
{ {
// Using accounts marked as type 2 (AddClass) // Using accounts marked as type 2 (AddClass)
@ -2125,11 +1757,6 @@ void RandomPlayerbotMgr::Init()
if (sPlayerbotAIConfig.addClassCommand) if (sPlayerbotAIConfig.addClassCommand)
sRandomPlayerbotMgr.PrepareAddclassCache(); sRandomPlayerbotMgr.PrepareAddclassCache();
if (sPlayerbotAIConfig.enabled)
{
sRandomPlayerbotMgr.PrepareTeleportCache();
}
if (sPlayerbotAIConfig.randomBotJoinBG) if (sPlayerbotAIConfig.randomBotJoinBG)
sRandomPlayerbotMgr.LoadBattleMastersCache(); sRandomPlayerbotMgr.LoadBattleMastersCache();
@ -2141,104 +1768,18 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot)
if (bot->InBattleground()) if (bot->InBattleground())
return; return;
uint32 level = bot->GetLevel(); std::vector<WorldLocation> locs = sTravelMgr.GetCityLocations(bot);
uint8 race = bot->getRace(); if (!locs.empty())
std::vector<WorldLocation>* locs = nullptr;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level];
else
locs = &locsPerLevelCache[level];
if (level >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
{ {
std::vector<WorldLocation> fallbackLocs; RandomTeleport(bot, locs, true);
for (auto& bLoc : bankerLocsPerLevelCache[level])
fallbackLocs.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
{
RandomTeleport(bot, fallbackLocs, true);
return; return;
} }
locs = sTravelMgr.GetTeleportLocations(bot);
// Collect valid cities based on bot faction. if (!locs.empty())
std::unordered_set<CityId> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
{ {
auto cityIt = bankerToCity.find(loc.entry); RandomTeleport(bot, locs, false);
if (cityIt == bankerToCity.end()) continue;
CityId cityId = cityIt->second.first;
FactionId cityFactionId = cityIt->second.second;
if ((IsAlliance(bot->getRace()) && cityFactionId == FactionId::ALLIANCE) ||
(!IsAlliance(bot->getRace()) && cityFactionId == FactionId::HORDE) ||
(cityFactionId == FactionId::NEUTRAL))
{
validBankerCities.insert(cityId);
}
}
// Fallback if no valid cities
if (validBankerCities.empty())
{
RandomTeleport(bot, fallbackLocs, true);
return; return;
} }
// Apply weights to valid cities
std::vector<CityId> weightedCities;
for (CityId city : validBankerCities)
{
int weight = 0;
switch (city)
{
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;
}
if (weight <= 0) continue;
for (int i = 0; i < weight; ++i)
{
weightedCities.push_back(city);
}
}
// Fallback if no valid cities
if (weightedCities.empty())
{
RandomTeleport(bot, fallbackLocs, true);
return;
}
// Pick a weighted city randomly, then a random banker in that city
// then teleport to that banker
CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)];
auto const& bankers = cityToBankers.at(selectedCity);
uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
if (locIt != bankerEntryToLocation.end())
{
std::vector<WorldLocation> teleportTarget = { locIt->second };
RandomTeleport(bot, teleportTarget, true);
return;
}
// Fallback if something went wrong
RandomTeleport(bot, *locs);
}
else
{
RandomTeleport(bot, *locs);
}
} }
void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot) void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
if (bot->InBattleground()) if (bot->InBattleground())
return; return;
uint32 level = bot->GetLevel(); std::vector<WorldLocation> locs = sTravelMgr.GetTeleportLocations(bot);
uint8 race = bot->getRace();
std::vector<WorldLocation>* locs = nullptr;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level];
else
locs = &locsPerLevelCache[level];
LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(), LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(),
bot->GetLevel(), locs->size()); bot->GetLevel(), locs.size());
RandomTeleport(bot, *locs); RandomTeleport(bot, locs);
} }
void RandomPlayerbotMgr::RandomTeleport(Player* bot) void RandomPlayerbotMgr::RandomTeleport(Player* bot)

View File

@ -164,25 +164,8 @@ public:
static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; }
void PrepareAddclassCache(); void PrepareAddclassCache();
void PrepareZone2LevelBracket();
void PrepareTeleportCache();
void Init(); void Init();
std::map<uint8, std::unordered_set<ObjectGuid>> addclassCache; std::map<uint8, std::unordered_set<ObjectGuid>> addclassCache;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> allianceStarterPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeStarterPerLevelCache;
struct LevelBracket {
uint32 low;
uint32 high;
bool InsideBracket(uint32 val) { return val >= low && val <= high; }
};
std::map<uint32, LevelBracket> zone2LevelBracket;
struct BankerLocation {
WorldLocation loc;
uint32 entry;
};
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
// Account type management // Account type management
void AssignAccountTypes(); void AssignAccountTypes();

View File

@ -1,39 +0,0 @@
#include "FlightMasterCache.h"
void FlightMasterCache::AddHordeFlightMaster(uint32 entry, WorldPosition pos)
{
hordeFlightMasterCache[entry] = pos;
}
void FlightMasterCache::AddAllianceFlightMaster(uint32 entry, WorldPosition pos)
{
allianceFlightMasterCache[entry] = pos;
}
Creature* FlightMasterCache::GetNearestFlightMaster(Player* bot)
{
std::map<uint32, WorldPosition>& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
float nearestDistance = std::numeric_limits<float>::max();
for (auto const& [entry, pos] : flightMasterCache)
{
if (pos.GetMapId() == bot->GetMapId())
{
float distance = bot->GetExactDist2dSq(pos);
if (distance < nearestDistance)
{
Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
if (flightMaster)
{
nearestDistance = distance;
nearestFlightMaster = flightMaster;
}
}
}
}
return nearestFlightMaster;
}

View File

@ -1,36 +0,0 @@
#ifndef _PLAYERBOT_FLIGHTMASTER_H
#define _PLAYERBOT_FLIGHTMASTER_H
#include "Creature.h"
#include "Player.h"
#include "TravelMgr.h"
class FlightMasterCache
{
public:
static FlightMasterCache& Instance()
{
static FlightMasterCache instance;
return instance;
}
Creature* GetNearestFlightMaster(Player* bot);
void AddHordeFlightMaster(uint32 entry, WorldPosition pos);
void AddAllianceFlightMaster(uint32 entry, WorldPosition pos);
private:
FlightMasterCache() = default;
~FlightMasterCache() = default;
FlightMasterCache(const FlightMasterCache&) = delete;
FlightMasterCache& operator=(const FlightMasterCache&) = delete;
FlightMasterCache(FlightMasterCache&&) = delete;
FlightMasterCache& operator=(FlightMasterCache&&) = delete;
std::map<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
};
#endif

View File

@ -66,6 +66,29 @@ private:
Player* bot; Player* bot;
}; };
class HasRelicBySubclassVisitor : public IterateItemsVisitor
{
public:
HasRelicBySubclassVisitor(uint32 subClass) : subClass(subClass) {}
bool Visit(Item* item) override
{
ItemTemplate const* proto = item->GetTemplate();
if (proto && proto->InventoryType == INVTYPE_RELIC && proto->SubClass == subClass)
{
found = true;
return false;
}
return true;
}
bool found = false;
private:
uint32 subClass;
};
class FindItemsByQualityVisitor : public IterateItemsVisitor class FindItemsByQualityVisitor : public IterateItemsVisitor
{ {
public: public:

View File

@ -8,6 +8,10 @@
#include <iomanip> #include <iomanip>
#include <numeric> #include <numeric>
#include "Creature.h"
#include "Log.h"
#include "ObjectAccessor.h"
#include "TravelNode.h"
#include "Talentspec.h" #include "Talentspec.h"
#include "ChatHelper.h" #include "ChatHelper.h"
#include "MMapFactory.h" #include "MMapFactory.h"
@ -22,6 +26,71 @@
#include "Corpse.h" #include "Corpse.h"
#include "CellImpl.h" #include "CellImpl.h"
// Navigation data
enum class CityId : uint8
{
STORMWIND,
IRONFORGE,
DARNASSUS,
EXODAR,
ORGRIMMAR,
UNDERCITY,
THUNDER_BLUFF,
SILVERMOON_CITY,
SHATTRATH_CITY,
DALARAN
};
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::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)
{
int weight = 0;
switch (city)
{
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;
}
return weight;
}
WorldPosition::WorldPosition(std::string const str) WorldPosition::WorldPosition(std::string const str)
{ {
std::vector<std::string> tokens = split(str, '|'); std::vector<std::string> tokens = split(str, '|');
@ -4287,3 +4356,434 @@ void TravelMgr::printObj(WorldObject* obj, std::string const type)
} }
} }
} }
void TravelMgr::Init()
{
if (sPlayerbotAIConfig.enabled)
{
PrepareZone2LevelBracket();
PrepareDestinationCache();
}
sTravelNodeMap.InitTaxiGraph();
LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built.");
}
Creature* TravelMgr::GetNearestFlightMaster(Player* bot)
{
std::map<uint32, WorldPosition>& flightMasterCache =
(bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache;
Creature* nearestFlightMaster = nullptr;
float nearestDistance = std::numeric_limits<float>::max();
for (auto const& [entry, pos] : flightMasterCache)
{
if (pos.GetMapId() != bot->GetMapId())
continue;
float distance = bot->GetExactDist2dSq(pos);
if (distance > nearestDistance)
continue;
Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry);
if (flightMaster)
{
nearestDistance = distance;
nearestFlightMaster = flightMaster;
}
}
return nearestFlightMaster;
}
ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot)
{
Creature* nearestFlightMaster = GetNearestFlightMaster(bot);
if (!nearestFlightMaster)
return ObjectGuid::Empty;
return nearestFlightMaster->GetGUID();
}
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)
return validDestinations;
uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(),
nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(),
bot->GetTeamId());
if (!fromNode)
return validDestinations;
std::vector<WorldLocation> candidateLocations;
if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
candidateLocations = GetCityLocations(bot);
std::vector<WorldLocation> hubLocations = GetTravelHubs(bot);
candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end());
for (auto const& loc : candidateLocations)
{
uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(),
loc.GetPositionZ(), loc.GetMapId(),
bot->GetTeamId());
if (!candidateNode)
continue;
std::vector<uint32> path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode);
if (!path.empty())
validDestinations.push_back(path);
}
return validDestinations;
}
const std::vector<WorldLocation> TravelMgr::GetTeleportLocations(Player* bot)
{
uint32 level = bot->GetLevel();
uint8 isAlliance = bot->GetTeamId() == TEAM_ALLIANCE;
if (sPlayerbotAIConfig.enableNewRpgStrategy)
return isAlliance ? allianceHubsPerLevelCache[level] : hordeHubsPerLevelCache[level];
return locsPerLevelCache[level];
}
const std::vector<WorldLocation> TravelMgr::GetTravelHubs(Player* bot)
{
std::vector<WorldLocation> locs = bot->GetTeamId() == TEAM_ALLIANCE
? allianceHubsPerLevelCache[bot->GetLevel()]
: hordeHubsPerLevelCache[bot->GetLevel()];
return locs;
}
std::vector<WorldLocation> TravelMgr::GetCityLocations(Player* bot)
{
uint32 level = bot->GetLevel();
std::vector<WorldLocation> fallbackLocations;
for (auto& bLoc : bankerLocsPerLevelCache[level])
fallbackLocations.push_back(bLoc.loc);
if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers)
return fallbackLocations;
TeamId botTeamId = bot->GetTeamId();
std::unordered_set<CityId> validBankerCities;
for (auto& loc : bankerLocsPerLevelCache[level])
{
auto cityIt = bankerToCity.find(loc.entry);
if (cityIt == bankerToCity.end())
continue;
TeamId cityTeamId = cityIt->second.second;
if (cityTeamId == botTeamId ||
(cityTeamId == TEAM_NEUTRAL)
)
validBankerCities.insert(cityIt->second.first);
}
// Fallback if no valid cities
if (validBankerCities.empty())
return fallbackLocations;
// Apply weights to valid cities
std::vector<CityId> weightedCities;
for (CityId city : validBankerCities)
{
int weight = GetCityWeight(city);
if (weight <= 0)
continue;
for (int i = 0; i < weight; ++i)
weightedCities.push_back(city);
}
// Fallback if no valid cities
if (weightedCities.empty())
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 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)];
auto locIt = bankerEntryToLocation.find(selectedBankerEntry);
if (locIt != bankerEntryToLocation.end())
return { locIt->second };
// Fallback if something went wrong
return fallbackLocations;
}
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 - 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 - 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 - 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 - 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
// 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
// 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
// Override with values from config
for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets)
zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second};
}
void TravelMgr::PrepareDestinationCache()
{
uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
uint32 flightMastersCount = 0;
uint32 innkeepersCount = 0;
uint32 bankerCount = 0;
LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel);
// Temporary map to group creatures by entry and area
std::map<std::tuple<uint16, int32, int32, int32>, std::vector<CreatureData>> tempLocsCache;
std::map<uint32, std::map<uint32, std::vector<WorldLocation>>> tempCreatureCache;
for (auto const& [guid, creatureData] : sObjectMgr->GetAllCreatureData())
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureData.id1);
if (!creatureTemplate)
continue;
uint16 mapId = creatureData.mapid;
if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId)
== sPlayerbotAIConfig.randomBotMaps.end())
continue;
float x = creatureData.posX;
float y = creatureData.posY;
float z = creatureData.posZ;
float orient = creatureData.orientation;
uint32 templateEntry = creatureData.id1;
Map* map = sMapMgr->FindMap(mapId, 0);
if (!map)
continue;
AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z));
if (!area)
continue;
uint32 areaId = area->zone ? area->zone : area->ID;
// CREATURES
if (creatureTemplate->npcflag == 0 &&
creatureTemplate->lootid != 0 &&
creatureTemplate->maxlevel - creatureTemplate->minlevel < 3 &&
creatureTemplate->Entry != 32820 && creatureTemplate->Entry != 24196 &&
creatureTemplate->Entry != 30627 && creatureTemplate->Entry != 30617 &&
creatureData.spawntimesecs < 1000 &&
creatureTemplate->faction != 11 && creatureTemplate->faction != 71 &&
creatureTemplate->faction != 79 && creatureTemplate->faction != 85 &&
creatureTemplate->faction != 188 && creatureTemplate->faction != 1575 &&
(creatureTemplate->unit_flags & 256) == 0 &&
(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;
tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData);
tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z));
}
// FLIGHT MASTERS
else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER ||
creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) &&
creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480)
{
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction);
bool forHorde = !(factionEntry->hostileMask & 4);
bool forAlliance = !(factionEntry->hostileMask & 2);
if (creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER)
{
WorldPosition pos(mapId, x, y, z, orient);
if (forHorde)
hordeFlightMasterCache[guid] = pos;
if (forAlliance)
allianceFlightMasterCache[guid] = pos;
flightMastersCount++;
}
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER)
{
if (zone2LevelBracket.find(areaId) == zone2LevelBracket.end())
continue;
LevelBracket bracket = zone2LevelBracket[areaId];
WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI);
for (int i = bracket.low; i <= bracket.high; i++)
{
if (forHorde)
hordeHubsPerLevelCache[i].push_back(loc);
if (forAlliance)
allianceHubsPerLevelCache[i].push_back(loc);
innkeepersCount++;
}
}
}
// === BANKERS ===
else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_BANKER &&
creatureTemplate->npcflag != 135298 &&
creatureTemplate->minlevel != 55 &&
creatureTemplate->minlevel != 65 &&
creatureTemplate->faction != 35 && creatureTemplate->faction != 474 &&
creatureTemplate->faction != 69 && creatureTemplate->faction != 57 &&
creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 &&
creatureTemplate->Entry != 29282)
{
BankerLocation bLoc;
bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI);
bLoc.entry = templateEntry;
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
for (int32 l = 1; l <= maxLevel; l++)
{
// Bots 1-60 go to base game bankers (all have minlevel 30 or 45)
if (l <=60 && level > 45)
continue;
// Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70)
if ((l >=61 && l <=70) && (level < 60 || level > 70))
continue;
// Bots 71+ go to Dalaran bankers (all have minlevel 75)
if ((l >=71) && level != 75)
continue;
bankerLocsPerLevelCache[(uint8)l].push_back(bLoc);
bankerEntryToLocation[bLoc.entry] = bLoc.loc;
}
bankerCount++;
}
}
// Process temporary caches
for (auto const& [gridTuple, creatureDataList] : tempLocsCache)
{
if (creatureDataList.size() > 2)
{
CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1);
uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2;
for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel;
l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++)
{
if (l < 1 || l > maxLevel)
continue;
locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple)));
}
}
}
for (auto const& [entry, areaMap] : tempCreatureCache)
{
for (auto const& [area, locList] : areaMap)
{
if (locList.size() > 3)
continue;
float totalX = 0, totalY = 0, totalZ = 0;
for (auto const& loc : locList)
{
totalX += loc.GetPositionX();
totalY += loc.GetPositionY();
totalZ += loc.GetPositionZ();
}
float avgX = totalX / locList.size();
float avgY = totalY / locList.size();
float avgZ = totalZ / locList.size();
creatureSpawnsByTemplate[entry].push_back(WorldLocation(locList[0].GetMapId(), avgX, avgY, avgZ, 0));
}
}
// Add travel hubs based on player start locations
for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++)
{
for (uint32 j = 1; j < MAX_CLASSES; j++)
{
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j);
if (!info)
continue;
WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
for (int32 l = 1; l <= 5; l++)
{
if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask())
allianceHubsPerLevelCache[(uint8)l].push_back(pos);
else
hordeHubsPerLevelCache[(uint8)l].push_back(pos);
}
break;
}
}
LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount);
}

View File

@ -7,6 +7,7 @@
#define _PLAYERBOT_TRAVELMGR_H #define _PLAYERBOT_TRAVELMGR_H
#include <boost/functional/hash.hpp> #include <boost/functional/hash.hpp>
#include <map>
#include <random> #include <random>
#include "AiObject.h" #include "AiObject.h"
@ -15,6 +16,7 @@
#include "GridDefines.h" #include "GridDefines.h"
#include "PlayerbotAIConfig.h" #include "PlayerbotAIConfig.h"
class Creature;
class GuidPosition; class GuidPosition;
class ObjectGuid; class ObjectGuid;
class Quest; class Quest;
@ -854,6 +856,16 @@ public:
void Clear(); void Clear();
void LoadQuestTravelTable(); void LoadQuestTravelTable();
// Navigation
void Init();
Creature* GetNearestFlightMaster(Player* bot);
ObjectGuid GetNearestFlightMasterGuid(Player* bot);
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);
const std::vector<WorldLocation>& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; }
template <class D, class W, class URBG> template <class D, class W, class URBG>
void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g) void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g)
{ {
@ -943,6 +955,37 @@ private:
TravelMgr(TravelMgr&&) = delete; TravelMgr(TravelMgr&&) = delete;
TravelMgr& operator=(TravelMgr&&) = delete; TravelMgr& operator=(TravelMgr&&) = delete;
// Navigation initialization
void PrepareZone2LevelBracket();
void PrepareDestinationCache();
// Internal types
struct LevelBracket
{
uint32 low;
uint32 high;
bool InsideBracket(uint32 val) const { return val >= low && val <= high; }
};
struct BankerLocation
{
WorldLocation loc;
uint32 entry;
};
// Navigation caches
std::map<uint32, WorldPosition> allianceFlightMasterCache;
std::map<uint32, WorldPosition> hordeFlightMasterCache;
std::map<uint8, std::vector<WorldLocation>> allianceHubsPerLevelCache;
std::map<uint8, std::vector<WorldLocation>> hordeHubsPerLevelCache;
std::map<uint8, std::vector<BankerLocation>> bankerLocsPerLevelCache;
std::unordered_map<uint32, WorldLocation> bankerEntryToLocation;
std::map<uint8, std::vector<WorldLocation>> locsPerLevelCache;
std::unordered_map<uint32, std::vector<WorldLocation>> creatureSpawnsByTemplate;
std::map<uint32, LevelBracket> zone2LevelBracket;
}; };
#define sTravelMgr TravelMgr::instance()
#endif #endif

View File

@ -7,6 +7,7 @@
#include <iomanip> #include <iomanip>
#include <regex> #include <regex>
#include <unordered_set>
#include "BudgetValues.h" #include "BudgetValues.h"
#include "PathGenerator.h" #include "PathGenerator.h"
@ -2447,3 +2448,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId)
return WorldPosition(mapId, 0, 0, 0, 0); return WorldPosition(mapId, 0, 0, 0, 0);
} }
// ============================================================
// TravelNodeMap taxi graph (BFS-based flight path lookup)
// ============================================================
void TravelNodeMap::InitTaxiGraph()
{
BuildTaxiGraph();
ComputeAllPaths();
}
std::vector<uint32> TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode)
{
if (fromNode == toNode)
return {};
TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode);
TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode);
if (!startNode || !endNode || startNode->map_id != endNode->map_id)
return {};
auto cacheItr = taxiPathCache.find(fromNode);
if (cacheItr == taxiPathCache.end())
return {};
auto toNodeItr = cacheItr->second.find(toNode);
if (toNodeItr == cacheItr->second.end())
return {};
return toNodeItr->second;
}
void TravelNodeMap::BuildTaxiGraph()
{
taxiGraph.clear();
std::unordered_map<uint32, std::unordered_set<uint32>> tempGraph;
for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i)
{
TaxiPathEntry const* path = sTaxiPathStore.LookupEntry(i);
if (!path)
continue;
if (path->to == 0 || path->to == uint32(-1))
continue;
tempGraph[path->from].insert(path->to);
tempGraph[path->to].insert(path->from);
}
for (auto const& [node, neighbors] : tempGraph)
taxiGraph[node] = std::vector<uint32>(neighbors.begin(), neighbors.end());
}
void TravelNodeMap::ComputeAllPaths()
{
std::set<uint32> allNodes;
for (auto const& [source, neighbors] : taxiGraph)
allNodes.insert(source);
for (uint32 source : allNodes)
{
auto parentMap = BFS(source);
for (uint32 target : allNodes)
{
if (source == target)
continue;
auto path = BuildPath(source, target, parentMap);
if (!path.empty())
taxiPathCache[source][target] = path;
}
}
}
std::unordered_map<uint32, uint32> TravelNodeMap::BFS(uint32 fromNode)
{
std::queue<uint32> workQueue;
std::unordered_set<uint32> visited;
std::unordered_map<uint32, uint32> parentMap;
workQueue.push(fromNode);
visited.insert(fromNode);
parentMap[fromNode] = 0;
while (!workQueue.empty())
{
uint32 current = workQueue.front();
workQueue.pop();
for (uint32 next : taxiGraph.at(current))
{
if (visited.count(next))
continue;
visited.insert(next);
parentMap[next] = current;
workQueue.push(next);
}
}
return parentMap;
}
std::vector<uint32> TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap)
{
if (!parentMap.count(toNode))
return {}; // unreachable
std::vector<uint32> path;
uint32 current = toNode;
while (current != fromNode)
{
path.push_back(current);
auto it = parentMap.find(current);
if (it == parentMap.end() || it->second == 0)
break;
current = it->second;
}
path.push_back(fromNode);
std::reverse(path.begin(), path.end());
return path;
}

View File

@ -580,6 +580,10 @@ public:
void calcMapOffset(); void calcMapOffset();
WorldPosition getMapOffset(uint32 mapId); WorldPosition getMapOffset(uint32 mapId);
// Taxi graph (BFS-based path lookup between taxi nodes)
void InitTaxiGraph();
std::vector<uint32> FindTaxiPath(uint32 fromNode, uint32 toNode);
std::shared_timed_mutex m_nMapMtx; std::shared_timed_mutex m_nMapMtx;
std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes; std::unordered_map<ObjectGuid, std::unordered_map<uint32, TravelNode*>> teleportNodes;
@ -593,6 +597,16 @@ private:
TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap(TravelNodeMap&&) = delete;
TravelNodeMap& operator=(TravelNodeMap&&) = delete; TravelNodeMap& operator=(TravelNodeMap&&) = delete;
// Taxi graph internals
void BuildTaxiGraph();
void ComputeAllPaths();
std::unordered_map<uint32, uint32> BFS(uint32 startNode);
std::vector<uint32> BuildPath(uint32 fromNode, uint32 toNode,
const std::unordered_map<uint32, uint32>& parentMap);
std::unordered_map<uint32, std::vector<uint32>> taxiGraph;
std::map<uint32, std::map<uint32, std::vector<uint32>>> taxiPathCache;
std::vector<TravelNode*> m_nodes; std::vector<TravelNode*> m_nodes;
std::vector<std::pair<uint32, WorldPosition>> mapOffsets; std::vector<std::pair<uint32, WorldPosition>> mapOffsets;

View File

@ -15,6 +15,7 @@
#include "RandomPlayerbotFactory.h" #include "RandomPlayerbotFactory.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "Talentspec.h" #include "Talentspec.h"
#include "TravelMgr.h"
template <class T> template <class T>
void LoadList(std::string const value, T& list) void LoadList(std::string const value, T& list)
@ -620,7 +621,10 @@ bool PlayerbotAIConfig::Initialize()
// SPP automation // SPP automation
freeMethodLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.FreeMethodLoot", false); freeMethodLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.FreeMethodLoot", false);
lootRollLevel = sConfigMgr->GetOption<int32>("AiPlayerbot.LootRollLevel", 1); lootNeedRollLevel = sConfigMgr->GetOption<int32>("AiPlayerbot.LootNeedRollLevel", 1);
lootRollRecipe = sConfigMgr->GetOption<bool>("AiPlayerbot.LootRollRecipe", false);
lootRollDisenchant = sConfigMgr->GetOption<bool>("AiPlayerbot.LootRollDisenchant", false);
lootGreedRollLevel = sConfigMgr->GetOption<bool>("AiPlayerbot.LootGreedRollLevel", false);
autoPickReward = sConfigMgr->GetOption<std::string>("AiPlayerbot.AutoPickReward", "yes"); autoPickReward = sConfigMgr->GetOption<std::string>("AiPlayerbot.AutoPickReward", "yes");
autoEquipUpgradeLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoEquipUpgradeLoot", true); autoEquipUpgradeLoot = sConfigMgr->GetOption<bool>("AiPlayerbot.AutoEquipUpgradeLoot", true);
equipUpgradeThreshold = sConfigMgr->GetOption<float>("AiPlayerbot.EquipUpgradeThreshold", 1.1f); equipUpgradeThreshold = sConfigMgr->GetOption<float>("AiPlayerbot.EquipUpgradeThreshold", 1.1f);
@ -688,6 +692,7 @@ bool PlayerbotAIConfig::Initialize()
{ {
PlayerbotDungeonRepository::instance().LoadDungeonSuggestions(); PlayerbotDungeonRepository::instance().LoadDungeonSuggestions();
} }
sTravelMgr.Init();
excludedHunterPetFamilies.clear(); excludedHunterPetFamilies.clear();
LoadList<std::vector<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies); LoadList<std::vector<uint32>>(sConfigMgr->GetOption<std::string>("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies);

View File

@ -346,7 +346,10 @@ public:
uint32 botActiveAloneSmartScaleWhenMaxLevel; uint32 botActiveAloneSmartScaleWhenMaxLevel;
bool freeMethodLoot; bool freeMethodLoot;
int32 lootRollLevel; int32 lootNeedRollLevel;
bool lootGreedRollLevel;
bool lootRollRecipe;
bool lootRollDisenchant;
std::string autoPickReward; std::string autoPickReward;
bool autoEquipUpgradeLoot; bool autoEquipUpgradeLoot;
float equipUpgradeThreshold; float equipUpgradeThreshold;

View File

@ -112,13 +112,10 @@ public:
if (sPlayerbotAIConfig.enabled || sPlayerbotAIConfig.randomBotAutologin) if (sPlayerbotAIConfig.enabled || sPlayerbotAIConfig.randomBotAutologin)
{ {
std::string roundedTime = std::string maxAllowedBotCount = std::to_string(sRandomPlayerbotMgr.GetMaxAllowedBotCount());
std::to_string(std::ceil((sPlayerbotAIConfig.maxRandomBots * 0.11 / 60) * 10) / 10.0);
roundedTime = roundedTime.substr(0, roundedTime.find('.') + 2);
ChatHandler(player->GetSession()).SendSysMessage( ChatHandler(player->GetSession()).SendSysMessage(
"|cff00ff00Playerbots:|r bot initialization at server startup takes about '" "|cff00ff00Playerbots:|r The server is configured with " + maxAllowedBotCount + " bots.");
+ roundedTime + "' minutes.");
} }
} }
} }